mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-12 09:47:20 +08:00
Microsoft's ML flagged the rc.1 release binaries. The decisive evidence is that the SAME sha256 went from 0/62 clean to Microsoft-detected in about an hour with no byte change, so the verdict lives partly in scanner-side state and no code change can promise a clean result. What code CAN do is stop shipping things that have no business in a release artifact, which is worth doing on its own merits and incidentally widens the classifier margin. Every claim below is verified against a built binary by the new gate, not by reading source. Executable stack (the worst of the findings). vendored/nomic/code_vectors_blob.S is the only assembly in the build and carried no .note.GNU-stack. An unannotated object makes ld assume the worst for the whole link, so EVERY Linux release we have ever shipped had GNU_STACK RWE. Adds the note (cause) plus ELF-only -Wl,-z,noexecstack (outcome); the gate fails the release if it returns. Test seams are now opt-in, never opt-out. TEST_SEAMS=1 defines CBM_ENABLE_TEST_SEAMS; without it the crash-orphan probe -- which forks a child that ignores SIGTERM and loops forever, then writes its pid to a caller-supplied path -- and the lease-ownership marker compile to trivial stubs, so call sites are untouched and the binary holds no fork, no signal handler and no env-var string. Opt-IN is the point: forgetting the flag yields a clean binary rather than a leaky one. scripts/test.sh requests it in the leg that consumes it, and tests/test_worker_watchdog.sh now asserts the capability up front instead of dying later with an opaque "Killed: 9". The daemon's background version check is gone. It spawned curl against api.github.com/repos/.../releases/latest on the first eligible session of every run to say "a newer version exists" -- a release URL and an outbound request in every shipped binary, for something the install scripts already report. The INJECTABLE SEAM survives: update_ops is still honoured, the fakes in tests/test_daemon_application.c still cover notice/ownership/cancellation/replay, and with no provider application_update_subscribe_locked returns early so no generation ever starts. "No network request by default" is now structural. Dead capability out of release builds. The tar.gz/zip extraction block (gzip_decompress through cbm_extract_binary_from_zip, plus its cli.h declarations) moves under CBM_CLI_ENABLE_TEST_API -- verified self-contained, zero uses of any helper outside it, only callers the excluded updater and tests/test_cli.c. Downloading an archive, decompressing it, picking an executable out of it and marking it executable is the canonical dropper composite; it is now absent rather than merely unreachable. SQLite is built with -DSQLITE_OMIT_LOAD_EXTENSION (no caller of load_extension anywhere in src/ or internal/), removing that API surface and part of the dlopen/dlsym surface. Temp files and environment scanning (S2/S3). Predictable paths in mcp.c, artifact.c and diagnostics.c are created privately and exclusively and written through the returned descriptor; pass_envscan.c no longer descends symlinked directories out of the project root, and its fixed 512-byte path buffers no longer truncate into pointer arithmetic that could land outside the buffer. Build-time entropy. mimalloc's version banner baked __DATE__/__TIME__ into every binary, so two builds of identical source seconds apart could never share a hash and no release could inherit a false-positive determination made about its predecessor. Local patch removes it (marked to survive refreshes), -Wdate-time makes any future use a build error, and -Wl,--no-insert-timestamp stops the PE header carrying the link clock. scripts/ci/check-binary-composition.sh is the proof that each removal stays removed, wired into package-release.sh after strip so the local artifact-flow smoke enforces exactly what CI does. It asserts absences plus a CANARY string, so handing it a compressed, stubbed or empty file fails instead of passing vacuously, and a missing tool is a hard error -- a skipped assertion must never look like a satisfied one. Two build-system traps found by that gate, both of which had silently defeated a fix: the product binary is compiled in one shot from sources, so a flag flip did not rebuild it (now tracked by a .build-config stamp that also removes the binary, making it independent of mtime granularity); and prod_sqlite3.o / prod_mimalloc.o depended on a single named source, so SQLITE_OMIT_LOAD_EXTENSION and the mimalloc patch BOTH compiled to nothing on the first incremental build. Source review would have called them done. Deliberately NOT changed. Three seams stay in release artifacts because scripts/smoke-test.sh runs against the real artifact and needs them: CBM_TEST_CRASH_ON and CBM_TEST_HANG_ON inject the faults that prove supervisor recovery, and CBM_TEST_WINDOWS_USER_PATH_RUN_ID is what keeps the PATH smoke from writing the tester's actual PATH. The gate treats those as an allowlist, so a NOVEL seam still fails. The true no-UI standard build is deferred rather than rushed: src/ui/* is in PROD_SRCS and four files outside src/ui reference UI symbols, including the daemon that serves the UI, so that assertion reports instead of failing until the split lands -- a gate everyone knows is red teaches people to ignore gates. No grammar is removed. ObjectScript accounts for essentially all binary growth since the last provably-clean release (+21.4MB rodata, +1.1MB text from two four-line shims), which made it the obvious ablation candidate, but a dry run performed twelve real Defender endpoint scans across standard/UI and amd64/arm64 with ObjectScript, the daemon and the expanded hooks all present and every scan was clean. Nothing there is a deterministic trigger, so cutting a community-contributed language would spend a real feature on unproven margin. Lean is not a candidate either: at 99.6MB of source it is by far the largest grammar, but it shipped in v0.9.0 which scanned 20/20 clean, so removing it would produce a novel unscanned profile instead of restoring a known-good one. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
714 lines
29 KiB
C
714 lines
29 KiB
C
/* ----------------------------------------------------------------------------
|
|
Copyright (c) 2018-2025, Microsoft Research, Daan Leijen
|
|
This is free software; you can redistribute it and/or modify it under the
|
|
terms of the MIT license. A copy of the license can be found in the file
|
|
"LICENSE" at the root of this distribution.
|
|
-----------------------------------------------------------------------------*/
|
|
#include "mimalloc.h"
|
|
#include "mimalloc/internal.h"
|
|
#include "mimalloc/atomic.h"
|
|
#include "mimalloc/prim.h" // mi_prim_out_stderr
|
|
|
|
#include <stdio.h> // stdin/stdout
|
|
#include <stdlib.h> // abort
|
|
|
|
static long mi_max_error_count = 16; // stop outputting errors after this (use < 0 for no limit)
|
|
static long mi_max_warning_count = 16; // stop outputting warnings after this (use < 0 for no limit)
|
|
|
|
static void mi_add_stderr_output(void);
|
|
|
|
int mi_version(void) mi_attr_noexcept {
|
|
return MI_MALLOC_VERSION;
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------
|
|
// Options
|
|
// These can be accessed by multiple threads and may be
|
|
// concurrently initialized, but an initializing data race
|
|
// is ok since they resolve to the same value.
|
|
// --------------------------------------------------------
|
|
|
|
|
|
#define MI_OPTION(opt) mi_option_##opt, #opt, NULL
|
|
#define MI_OPTION_LEGACY(opt,legacy) mi_option_##opt, #opt, #legacy
|
|
|
|
// Some options can be set at build time for statically linked libraries
|
|
// (use `-DMI_EXTRA_CPPDEFS="opt1=val1;opt2=val2"`)
|
|
//
|
|
// This is useful if we cannot pass them as environment variables
|
|
// (and setting them programmatically would be too late)
|
|
|
|
#ifndef MI_DEFAULT_VERBOSE
|
|
#define MI_DEFAULT_VERBOSE 0
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_ARENA_EAGER_COMMIT
|
|
#define MI_DEFAULT_ARENA_EAGER_COMMIT 2
|
|
#endif
|
|
|
|
// in KiB
|
|
#ifndef MI_DEFAULT_ARENA_RESERVE
|
|
#if (MI_INTPTR_SIZE>4)
|
|
#define MI_DEFAULT_ARENA_RESERVE 1024L*1024L
|
|
#else
|
|
#define MI_DEFAULT_ARENA_RESERVE 128L*1024L
|
|
#endif
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_ARENA_MAX_OBJECT_SIZE
|
|
#define MI_DEFAULT_ARENA_MAX_OBJECT_SIZE ((MI_SIZE_BITS * MI_ARENA_MAX_CHUNK_OBJ_SIZE)/MI_KiB) /* 2 GiB (or 256 MiB on 32-bit), larger than this is alloc'd by the OS */
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_DISALLOW_ARENA_ALLOC
|
|
#define MI_DEFAULT_DISALLOW_ARENA_ALLOC 0
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_ALLOW_LARGE_OS_PAGES
|
|
#define MI_DEFAULT_ALLOW_LARGE_OS_PAGES 0
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_RESERVE_HUGE_OS_PAGES
|
|
#define MI_DEFAULT_RESERVE_HUGE_OS_PAGES 0
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_RESERVE_OS_MEMORY
|
|
#define MI_DEFAULT_RESERVE_OS_MEMORY 0
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_GUARDED_SAMPLE_RATE
|
|
#if MI_GUARDED && !MI_DEBUG
|
|
#define MI_DEFAULT_GUARDED_SAMPLE_RATE 4000
|
|
#else
|
|
#define MI_DEFAULT_GUARDED_SAMPLE_RATE 0
|
|
#endif
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_PAGEMAP_COMMIT
|
|
#if defined(__APPLE__) // when overloading malloc, we still get mixed pointers sometimes on macOS; this avoids a bad access
|
|
#define MI_DEFAULT_PAGEMAP_COMMIT 1
|
|
#else
|
|
#define MI_DEFAULT_PAGEMAP_COMMIT 0
|
|
#endif
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_PAGE_MAX_RECLAIM
|
|
#define MI_DEFAULT_PAGE_MAX_RECLAIM (-1) // unlimited
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_PAGE_CROSS_THREAD_MAX_RECLAIM
|
|
#define MI_DEFAULT_PAGE_CROSS_THREAD_MAX_RECLAIM 32
|
|
#endif
|
|
|
|
#ifndef MI_DEFAULT_ALLOW_THP
|
|
#if defined(__ANDROID__)
|
|
#define MI_DEFAULT_ALLOW_THP 0
|
|
#else
|
|
#define MI_DEFAULT_ALLOW_THP 1
|
|
#endif
|
|
#endif
|
|
|
|
// Static options
|
|
static mi_option_desc_t mi_options[_mi_option_last] =
|
|
{
|
|
// stable options
|
|
#if MI_DEBUG || defined(MI_SHOW_ERRORS)
|
|
{ 1, MI_OPTION_UNINIT, MI_OPTION(show_errors) },
|
|
#else
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(show_errors) },
|
|
#endif
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(show_stats) },
|
|
{ MI_DEFAULT_VERBOSE, MI_OPTION_UNINIT, MI_OPTION(verbose) },
|
|
|
|
// some of the following options are experimental and not all combinations are allowed.
|
|
{ 1, MI_OPTION_UNINIT, MI_OPTION(deprecated_eager_commit) },
|
|
{ MI_DEFAULT_ARENA_EAGER_COMMIT,
|
|
MI_OPTION_UNINIT, MI_OPTION_LEGACY(arena_eager_commit,eager_region_commit) }, // eager commit arena's? 2 is used to enable this only on an OS that has overcommit (i.e. linux)
|
|
{ 1, MI_OPTION_UNINIT, MI_OPTION_LEGACY(purge_decommits,reset_decommits) }, // purge decommits memory (instead of reset) (note: on linux this uses MADV_DONTNEED for decommit)
|
|
{ MI_DEFAULT_ALLOW_LARGE_OS_PAGES,
|
|
MI_OPTION_UNINIT, MI_OPTION_LEGACY(allow_large_os_pages,large_os_pages) }, // use large OS pages, use only with eager commit to prevent fragmentation of VMA's
|
|
{ MI_DEFAULT_RESERVE_HUGE_OS_PAGES,
|
|
MI_OPTION_UNINIT, MI_OPTION(reserve_huge_os_pages) }, // per 1GiB huge pages
|
|
{-1, MI_OPTION_UNINIT, MI_OPTION(reserve_huge_os_pages_at) }, // reserve huge pages at node N
|
|
{ MI_DEFAULT_RESERVE_OS_MEMORY,
|
|
MI_OPTION_UNINIT, MI_OPTION(reserve_os_memory) }, // reserve N KiB OS memory in advance (use `option_get_size`)
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(deprecated_segment_cache) }, // cache N segments per thread
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(deprecated_page_reset) }, // reset page memory on free
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(deprecated_abandoned_page_purge) },
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(deprecated_segment_reset) }, // reset segment memory on free (needs eager commit)
|
|
{ 1, MI_OPTION_UNINIT, MI_OPTION(deprecated_eager_commit_delay) },
|
|
{ 1000,MI_OPTION_UNINIT, MI_OPTION_LEGACY(purge_delay,reset_delay) }, // purge delay in milli-seconds
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(use_numa_nodes) }, // 0 = use available numa nodes, otherwise use at most N nodes.
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION_LEGACY(disallow_os_alloc,limit_os_alloc) }, // 1 = do not use OS memory for allocation (but only reserved arenas)
|
|
{ 100, MI_OPTION_UNINIT, MI_OPTION(os_tag) }, // only apple specific for now but might serve more or less related purpose
|
|
{ 32, MI_OPTION_UNINIT, MI_OPTION(max_errors) }, // maximum errors that are output
|
|
{ 32, MI_OPTION_UNINIT, MI_OPTION(max_warnings) }, // maximum warnings that are output
|
|
{ 10, MI_OPTION_UNINIT, MI_OPTION(deprecated_max_segment_reclaim)}, // max. percentage of the abandoned segments to be reclaimed per try.
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(destroy_on_exit)}, // release all OS memory on process exit; careful with dangling pointer or after-exit frees!
|
|
{ MI_DEFAULT_ARENA_RESERVE, MI_OPTION_UNINIT, MI_OPTION(arena_reserve) }, // reserve memory N KiB at a time (=1GiB) (use `option_get_size`)
|
|
{ 1, MI_OPTION_UNINIT, MI_OPTION(arena_purge_mult) }, // purge delay multiplier for arena's
|
|
{ 1, MI_OPTION_UNINIT, MI_OPTION_LEGACY(deprecated_purge_extend_delay, decommit_extend_delay) },
|
|
{ MI_DEFAULT_DISALLOW_ARENA_ALLOC, MI_OPTION_UNINIT, MI_OPTION(disallow_arena_alloc) }, // 1 = do not use arena's for allocation (except if using specific arena id's)
|
|
{ 400, MI_OPTION_UNINIT, MI_OPTION(retry_on_oom) }, // windows only: retry on out-of-memory for N milli seconds (=400), set to 0 to disable retries.
|
|
#if defined(MI_VISIT_ABANDONED)
|
|
{ 1, MI_OPTION_INITIALIZED, MI_OPTION(visit_abandoned) }, // allow visiting theap blocks in abandoned segments; requires taking locks during reclaim.
|
|
#else
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(visit_abandoned) },
|
|
#endif
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(guarded_min) }, // only used when building with MI_GUARDED: minimal rounded object size for guarded objects
|
|
{ MI_GiB, MI_OPTION_UNINIT, MI_OPTION(guarded_max) }, // only used when building with MI_GUARDED: maximal rounded object size for guarded objects
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(guarded_precise) }, // disregard minimal alignment requirement to always place guarded blocks exactly in front of a guard page (=0)
|
|
{ MI_DEFAULT_GUARDED_SAMPLE_RATE,
|
|
MI_OPTION_UNINIT, MI_OPTION(guarded_sample_rate)}, // 1 out of N allocations in the min/max range will be guarded (=4000)
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(guarded_sample_seed)},
|
|
{ 10000, MI_OPTION_UNINIT, MI_OPTION(generic_collect) }, // collect theaps every N (=10000) generic allocation calls
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION_LEGACY(page_reclaim_on_free, abandoned_reclaim_on_free) },// reclaim abandoned (small) pages on a free: -1 = disable completely, 0 = only reclaim into the originating theap, 1 = reclaim on free across theaps
|
|
{ 2, MI_OPTION_UNINIT, MI_OPTION(page_full_retain) }, // number of (small) pages to retain in the free page queues
|
|
{ 4, MI_OPTION_UNINIT, MI_OPTION(page_max_candidates) }, // max search to find a best page candidate
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(max_vabits) }, // max virtual address space bits
|
|
{ MI_DEFAULT_PAGEMAP_COMMIT,
|
|
MI_OPTION_UNINIT, MI_OPTION(pagemap_commit) }, // commit the full pagemap upfront?
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(page_commit_on_demand) }, // commit pages on-demand (2 disables this only on overcommit systems (like Linux))
|
|
{ MI_DEFAULT_PAGE_MAX_RECLAIM,
|
|
MI_OPTION_UNINIT, MI_OPTION(page_max_reclaim) }, // don't reclaim (small) pages of the same originating theap if we already own N pages in that size class
|
|
{ MI_DEFAULT_PAGE_CROSS_THREAD_MAX_RECLAIM,
|
|
MI_OPTION_UNINIT, MI_OPTION(page_cross_thread_max_reclaim) }, // don't reclaim (small) pages across threads if we already own N pages in that size class
|
|
{ MI_DEFAULT_ALLOW_THP,
|
|
MI_OPTION_UNINIT, MI_OPTION(allow_thp) }, // allow transparent huge pages? (=1) (on Android =0 by default). Set to 0 to disable THP for the process.
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(minimal_purge_size) }, // set minimal purge size (in KiB) (=0). Using 0 resolves to either 64 (or 2048 if `mi_option_allow_thp==2`).
|
|
{ MI_DEFAULT_ARENA_MAX_OBJECT_SIZE,
|
|
MI_OPTION_UNINIT, MI_OPTION(arena_max_object_size) }, // set maximal object size that can be allocated in an arena (in KiB) (=2GiB on 64-bit).
|
|
{ 0, MI_OPTION_UNINIT, MI_OPTION(arena_is_numa_local) }, // associate local numa node with an initial arena allocation
|
|
};
|
|
|
|
static void mi_option_init(mi_option_desc_t* desc);
|
|
|
|
static bool mi_option_has_size_in_kib(mi_option_t option) {
|
|
return (option == mi_option_reserve_os_memory || option == mi_option_arena_reserve ||
|
|
option == mi_option_minimal_purge_size || option == mi_option_arena_max_object_size);
|
|
}
|
|
|
|
void _mi_options_init(void) {
|
|
// called on process load
|
|
for(int i = 0; i < _mi_option_last; i++ ) {
|
|
mi_option_t option = (mi_option_t)i;
|
|
long l = mi_option_get(option); MI_UNUSED(l); // initialize
|
|
}
|
|
mi_max_error_count = mi_option_get(mi_option_max_errors);
|
|
mi_max_warning_count = mi_option_get(mi_option_max_warnings);
|
|
#if MI_GUARDED
|
|
if (mi_option_get(mi_option_guarded_sample_rate) > 0) {
|
|
if (mi_option_is_enabled(mi_option_allow_large_os_pages)) {
|
|
mi_option_disable(mi_option_allow_large_os_pages);
|
|
_mi_warning_message("option 'allow_large_os_pages' is disabled to allow for guarded objects\n");
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// called at actual process load, it should be safe to print now
|
|
void _mi_options_post_init(void) {
|
|
mi_add_stderr_output(); // now it safe to use stderr for output
|
|
if (mi_option_is_enabled(mi_option_verbose)) { mi_options_print(); }
|
|
}
|
|
|
|
#define mi_stringifyx(str) #str // and stringify
|
|
#define mi_stringify(str) mi_stringifyx(str) // expand
|
|
|
|
mi_decl_export void mi_options_print_out(mi_output_fun* out, void* arg) mi_attr_noexcept
|
|
{
|
|
// show version
|
|
const int vermajor = MI_MALLOC_VERSION/10000;
|
|
const int verminor = (MI_MALLOC_VERSION%10000)/100;
|
|
const int verpatch = (MI_MALLOC_VERSION%100);
|
|
/* CBM LOCAL PATCH (keep across mimalloc refreshes): upstream appends
|
|
* "(built on __DATE__, __TIME__)" here. mimalloc is linked into every cbm
|
|
* binary, so those two macros made every build byte-unique — two compilations
|
|
* of identical source seconds apart could never produce the same hash. That
|
|
* defeats reproducible builds, and it also means a released artifact can never
|
|
* inherit an antivirus false-positive determination made about its
|
|
* predecessor, because every rebuild is a brand-new file to a reputation
|
|
* system. The banner keeps the version; the wall clock is not diagnostics.
|
|
* -Wdate-time in CFLAGS_COMMON makes any new use a build failure. */
|
|
_mi_fprintf(out, arg, "v%i.%i.%i%s%s\n", vermajor, verminor, verpatch,
|
|
#if defined(MI_CMAKE_BUILD_TYPE)
|
|
", " mi_stringify(MI_CMAKE_BUILD_TYPE)
|
|
#else
|
|
""
|
|
#endif
|
|
,
|
|
#if defined(MI_GIT_DESCRIBE)
|
|
", git " mi_stringify(MI_GIT_DESCRIBE)
|
|
#else
|
|
""
|
|
#endif
|
|
);
|
|
|
|
// show options
|
|
for (int i = 0; i < _mi_option_last; i++) {
|
|
mi_option_t option = (mi_option_t)i;
|
|
long l = mi_option_get(option); MI_UNUSED(l); // possibly initialize
|
|
mi_option_desc_t* desc = &mi_options[option];
|
|
_mi_fprintf(out, arg, "option '%s': %ld %s\n", desc->name, desc->value, (mi_option_has_size_in_kib(option) ? "KiB" : ""));
|
|
}
|
|
|
|
// show build configuration
|
|
_mi_fprintf(out, arg, "debug level : %d\n", MI_DEBUG );
|
|
_mi_fprintf(out, arg, "secure level: %d\n", MI_SECURE );
|
|
_mi_fprintf(out, arg, "mem tracking: %s\n", MI_TRACK_TOOL);
|
|
#if MI_GUARDED
|
|
_mi_fprintf(out, arg, "guarded build: %s\n", mi_option_get(mi_option_guarded_sample_rate) != 0 ? "enabled" : "disabled");
|
|
#endif
|
|
#if MI_TSAN
|
|
_mi_fprintf(out, arg, "thread santizer enabled\n");
|
|
#endif
|
|
}
|
|
|
|
mi_decl_export void mi_options_print(void) mi_attr_noexcept {
|
|
mi_options_print_out(NULL, NULL);
|
|
}
|
|
|
|
long _mi_option_get_fast(mi_option_t option) {
|
|
mi_assert(option >= 0 && option < _mi_option_last);
|
|
mi_option_desc_t* desc = &mi_options[option];
|
|
mi_assert(desc->option == option); // index should match the option
|
|
//mi_assert(desc->init != MI_OPTION_UNINIT);
|
|
return desc->value;
|
|
}
|
|
|
|
|
|
mi_decl_nodiscard long mi_option_get(mi_option_t option) {
|
|
mi_assert(option >= 0 && option < _mi_option_last);
|
|
if (option < 0 || option >= _mi_option_last) return 0;
|
|
mi_option_desc_t* desc = &mi_options[option];
|
|
mi_assert(desc->option == option); // index should match the option
|
|
if mi_unlikely(desc->init == MI_OPTION_UNINIT) {
|
|
mi_option_init(desc);
|
|
}
|
|
return desc->value;
|
|
}
|
|
|
|
mi_decl_nodiscard long mi_option_get_clamp(mi_option_t option, long min, long max) {
|
|
long x = mi_option_get(option);
|
|
return (x < min ? min : (x > max ? max : x));
|
|
}
|
|
|
|
mi_decl_nodiscard size_t mi_option_get_size(mi_option_t option) {
|
|
const long x = mi_option_get(option);
|
|
size_t size = (x < 0 ? 0 : (size_t)x);
|
|
if (mi_option_has_size_in_kib(option)) {
|
|
if (mi_mul_overflow(size, MI_KiB, &size)) {
|
|
size = MI_MAX_ALLOC_SIZE;
|
|
}
|
|
}
|
|
return size;
|
|
}
|
|
|
|
void mi_option_set(mi_option_t option, long value) {
|
|
mi_assert(option >= 0 && option < _mi_option_last);
|
|
if (option < 0 || option >= _mi_option_last) return;
|
|
mi_option_desc_t* desc = &mi_options[option];
|
|
mi_assert(desc->option == option); // index should match the option
|
|
desc->value = value;
|
|
desc->init = MI_OPTION_INITIALIZED;
|
|
// ensure min/max range; be careful to not recurse.
|
|
if (desc->option == mi_option_guarded_min && _mi_option_get_fast(mi_option_guarded_max) < value) {
|
|
mi_option_set(mi_option_guarded_max, value);
|
|
}
|
|
else if (desc->option == mi_option_guarded_max && _mi_option_get_fast(mi_option_guarded_min) > value) {
|
|
mi_option_set(mi_option_guarded_min, value);
|
|
}
|
|
}
|
|
|
|
void mi_option_set_default(mi_option_t option, long value) {
|
|
mi_assert(option >= 0 && option < _mi_option_last);
|
|
if (option < 0 || option >= _mi_option_last) return;
|
|
mi_option_desc_t* desc = &mi_options[option];
|
|
if (desc->init != MI_OPTION_INITIALIZED) {
|
|
desc->value = value;
|
|
}
|
|
}
|
|
|
|
mi_decl_nodiscard bool mi_option_is_enabled(mi_option_t option) {
|
|
return (mi_option_get(option) != 0);
|
|
}
|
|
|
|
void mi_option_set_enabled(mi_option_t option, bool enable) {
|
|
mi_option_set(option, (enable ? 1 : 0));
|
|
}
|
|
|
|
void mi_option_set_enabled_default(mi_option_t option, bool enable) {
|
|
mi_option_set_default(option, (enable ? 1 : 0));
|
|
}
|
|
|
|
void mi_option_enable(mi_option_t option) {
|
|
mi_option_set_enabled(option,true);
|
|
}
|
|
|
|
void mi_option_disable(mi_option_t option) {
|
|
mi_option_set_enabled(option,false);
|
|
}
|
|
|
|
static void mi_cdecl mi_out_stderr(const char* msg, void* arg) {
|
|
MI_UNUSED(arg);
|
|
if (msg != NULL && msg[0] != 0) {
|
|
_mi_prim_out_stderr(msg);
|
|
}
|
|
}
|
|
|
|
// Since an output function can be registered earliest in the `main`
|
|
// function we also buffer output that happens earlier. When
|
|
// an output function is registered it is called immediately with
|
|
// the output up to that point.
|
|
#ifndef MI_MAX_DELAY_OUTPUT
|
|
#define MI_MAX_DELAY_OUTPUT ((size_t)(16*1024))
|
|
#endif
|
|
static char out_buf[MI_MAX_DELAY_OUTPUT+1];
|
|
static _Atomic(size_t) out_len;
|
|
static mi_lock_t out_buf_lock = MI_LOCK_INITIALIZER;
|
|
|
|
static void mi_cdecl mi_out_buf(const char* msg, void* arg) {
|
|
MI_UNUSED(arg);
|
|
if (msg==NULL) return;
|
|
if (mi_atomic_load_acquire(&out_len)>=MI_MAX_DELAY_OUTPUT) return;
|
|
size_t n = _mi_strlen(msg);
|
|
if (n==0 || n >= MI_MAX_DELAY_OUTPUT) return;
|
|
// copy msg into the buffer
|
|
mi_lock(&out_buf_lock) {
|
|
const size_t start = mi_atomic_add_acq_rel(&out_len, n);
|
|
if (start < MI_MAX_DELAY_OUTPUT) {
|
|
// check bound
|
|
if (start+n >= MI_MAX_DELAY_OUTPUT) {
|
|
n = MI_MAX_DELAY_OUTPUT-start-1;
|
|
}
|
|
_mi_memcpy(&out_buf[start], msg, n);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void mi_out_buf_flush(mi_output_fun* out, bool no_more_buf, void* arg) {
|
|
if (out==NULL) return;
|
|
// claim (if `no_more_buf == true`, no more output will be added after this point)
|
|
mi_lock(&out_buf_lock) {
|
|
size_t count = mi_atomic_add_acq_rel(&out_len, (no_more_buf ? MI_MAX_DELAY_OUTPUT : 1));
|
|
// and output the current contents
|
|
if (count>MI_MAX_DELAY_OUTPUT) count = MI_MAX_DELAY_OUTPUT;
|
|
out_buf[count] = 0;
|
|
out(out_buf,arg);
|
|
if (!no_more_buf) {
|
|
out_buf[count] = '\n'; // if continue with the buffer, insert a newline
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Once this module is loaded, switch to this routine
|
|
// which outputs to stderr and the delayed output buffer.
|
|
static void mi_cdecl mi_out_buf_stderr(const char* msg, void* arg) {
|
|
mi_out_stderr(msg,arg);
|
|
mi_out_buf(msg,arg);
|
|
}
|
|
|
|
|
|
|
|
// --------------------------------------------------------
|
|
// Default output handler
|
|
// --------------------------------------------------------
|
|
|
|
// The program should only install a single output handler from a single thread
|
|
// since otherwise the argument and output function may not match.
|
|
static _Atomic(void*) mi_out_default; // = // is `mi_output_fun*` (but some platforms don't support atomic function pointers)
|
|
static _Atomic(void*) mi_out_arg; // = NULL
|
|
|
|
static mi_output_fun* mi_out_get_default(void** parg) {
|
|
mi_output_fun* const out = (mi_output_fun*)mi_atomic_load_ptr_acquire(void,&mi_out_default);
|
|
if (parg != NULL) { *parg = mi_atomic_load_ptr_acquire(void,&mi_out_arg); }
|
|
return (out == NULL ? &mi_out_buf : out);
|
|
}
|
|
|
|
void mi_register_output(mi_output_fun* out, void* arg) mi_attr_noexcept {
|
|
mi_atomic_store_ptr_release(void,&mi_out_default, (void*)(out == NULL ? &mi_out_stderr : out)); // stop using the delayed output buffer
|
|
mi_atomic_store_ptr_release(void,&mi_out_arg, arg);
|
|
if (out!=NULL) { mi_out_buf_flush(out,true,arg); } // output all the delayed output now
|
|
}
|
|
|
|
// add stderr to the delayed output after the module is loaded
|
|
static void mi_add_stderr_output(void) {
|
|
mi_assert_internal(mi_out_default == NULL);
|
|
mi_out_buf_flush(&mi_out_stderr, false, NULL); // flush current contents to stderr
|
|
mi_atomic_store_ptr_release(void,&mi_out_default,(void*)&mi_out_buf_stderr); // and add stderr to the delayed output
|
|
mi_atomic_store_ptr_release(void,&mi_out_arg,NULL);
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Messages, all end up calling `_mi_fputs`.
|
|
// --------------------------------------------------------
|
|
static _Atomic(size_t) error_count; // = 0; // when >= max_error_count stop emitting errors
|
|
static _Atomic(size_t) warning_count; // = 0; // when >= max_warning_count stop emitting warnings
|
|
|
|
// When overriding malloc, we may recurse into mi_vfprintf if an allocation
|
|
// inside the C runtime causes another message.
|
|
// In some cases (like on macOS) the loader already allocates which
|
|
// calls into mimalloc; if we then access thread locals (like `recurse`)
|
|
// this may crash as the access may call _tlv_bootstrap that tries to
|
|
// (recursively) invoke malloc again to allocate space for the thread local
|
|
// variables on demand. This is why we use a _mi_preloading test on such
|
|
// platforms. However, C code generator may move the initial thread local address
|
|
// load before the `if` and we therefore split it out in a separate function.
|
|
static mi_decl_thread bool recurse = false;
|
|
|
|
static mi_decl_noinline bool mi_recurse_enter_prim(void) {
|
|
if (recurse) return false;
|
|
recurse = true;
|
|
return true;
|
|
}
|
|
|
|
static mi_decl_noinline void mi_recurse_exit_prim(void) {
|
|
recurse = false;
|
|
}
|
|
|
|
static bool mi_recurse_enter(void) {
|
|
#if defined(__APPLE__) || defined(__ANDROID__) || defined(MI_TLS_RECURSE_GUARD)
|
|
if (_mi_preloading()) return false;
|
|
#endif
|
|
return mi_recurse_enter_prim();
|
|
}
|
|
|
|
static void mi_recurse_exit(void) {
|
|
#if defined(__APPLE__) || defined(__ANDROID__) || defined(MI_TLS_RECURSE_GUARD)
|
|
if (_mi_preloading()) return;
|
|
#endif
|
|
mi_recurse_exit_prim();
|
|
}
|
|
|
|
void _mi_fputs(mi_output_fun* out, void* arg, const char* prefix, const char* message) {
|
|
if (out==NULL || (void*)out==(void*)stdout || (void*)out==(void*)stderr) { // TODO: use mi_out_stderr for stderr?
|
|
if (!mi_recurse_enter()) return;
|
|
out = mi_out_get_default(&arg);
|
|
if (prefix != NULL) out(prefix, arg);
|
|
out(message, arg);
|
|
mi_recurse_exit();
|
|
}
|
|
else {
|
|
if (prefix != NULL) out(prefix, arg);
|
|
out(message, arg);
|
|
}
|
|
}
|
|
|
|
// Define our own limited `fprintf` that avoids memory allocation.
|
|
// We do this using `_mi_vsnprintf` with a limited buffer.
|
|
static void mi_vfprintf( mi_output_fun* out, void* arg, const char* prefix, const char* fmt, va_list args ) {
|
|
char buf[992];
|
|
if (fmt==NULL) return;
|
|
if (!mi_recurse_enter()) return;
|
|
_mi_vsnprintf(buf, sizeof(buf)-1, fmt, args);
|
|
mi_recurse_exit();
|
|
_mi_fputs(out,arg,prefix,buf);
|
|
}
|
|
|
|
void _mi_fprintf( mi_output_fun* out, void* arg, const char* fmt, ... ) {
|
|
va_list args;
|
|
va_start(args,fmt);
|
|
mi_vfprintf(out,arg,NULL,fmt,args);
|
|
va_end(args);
|
|
}
|
|
|
|
static void mi_vfprintf_thread(mi_output_fun* out, void* arg, const char* prefix, const char* fmt, va_list args) {
|
|
if (prefix != NULL && _mi_strnlen(prefix,33) <= 32 && !_mi_is_main_thread()) {
|
|
char tprefix[64];
|
|
_mi_snprintf(tprefix, sizeof(tprefix), "%sthread 0x%tx: ", prefix, (uintptr_t)_mi_thread_id());
|
|
mi_vfprintf(out, arg, tprefix, fmt, args);
|
|
}
|
|
else {
|
|
mi_vfprintf(out, arg, prefix, fmt, args);
|
|
}
|
|
}
|
|
|
|
void _mi_raw_message(const char* fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
mi_vfprintf(NULL, NULL, NULL, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void _mi_message(const char* fmt, ...) {
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
mi_vfprintf_thread(NULL, NULL, "mimalloc: ", fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void _mi_trace_message(const char* fmt, ...) {
|
|
if (mi_option_get(mi_option_verbose) <= 1) return; // only with verbose level 2 or higher
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
mi_vfprintf_thread(NULL, NULL, "mimalloc: ", fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
void _mi_verbose_message(const char* fmt, ...) {
|
|
if (!mi_option_is_enabled(mi_option_verbose)) return;
|
|
va_list args;
|
|
va_start(args,fmt);
|
|
mi_vfprintf(NULL, NULL, "mimalloc: ", fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
static void mi_show_error_message(const char* fmt, va_list args) {
|
|
if (!mi_option_is_enabled(mi_option_verbose)) {
|
|
if (!mi_option_is_enabled(mi_option_show_errors)) return;
|
|
if (mi_max_error_count >= 0 && (long)mi_atomic_increment_acq_rel(&error_count) > mi_max_error_count) return;
|
|
}
|
|
mi_vfprintf_thread(NULL, NULL, "mimalloc: error: ", fmt, args);
|
|
}
|
|
|
|
void _mi_warning_message(const char* fmt, ...) {
|
|
if (!mi_option_is_enabled(mi_option_verbose)) {
|
|
if (!mi_option_is_enabled(mi_option_show_errors)) return;
|
|
if (mi_max_warning_count >= 0 && (long)mi_atomic_increment_acq_rel(&warning_count) > mi_max_warning_count) return;
|
|
}
|
|
va_list args;
|
|
va_start(args,fmt);
|
|
mi_vfprintf_thread(NULL, NULL, "mimalloc: warning: ", fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
|
|
#if MI_DEBUG
|
|
mi_decl_noreturn mi_decl_cold void _mi_assert_fail(const char* assertion, const char* fname, unsigned line, const char* func ) mi_attr_noexcept {
|
|
_mi_fprintf(NULL, NULL, "mimalloc: assertion failed: at \"%s\":%u, %s\n assertion: \"%s\"\n", fname, line, (func==NULL?"":func), assertion);
|
|
abort();
|
|
}
|
|
#endif
|
|
|
|
// --------------------------------------------------------
|
|
// Errors
|
|
// --------------------------------------------------------
|
|
|
|
static mi_error_fun* volatile mi_error_handler; // = NULL
|
|
static _Atomic(void*) mi_error_arg; // = NULL
|
|
|
|
static void mi_error_default(int err) {
|
|
MI_UNUSED(err);
|
|
#if (MI_DEBUG>0)
|
|
if (err==EFAULT) {
|
|
#ifdef _MSC_VER
|
|
__debugbreak();
|
|
#endif
|
|
abort();
|
|
}
|
|
#endif
|
|
#if (MI_SECURE>0)
|
|
if (err==EFAULT) { // abort on serious errors in secure mode (corrupted meta-data)
|
|
abort();
|
|
}
|
|
#endif
|
|
#if defined(MI_XMALLOC)
|
|
if (err==ENOMEM || err==EOVERFLOW) { // abort on memory allocation fails in xmalloc mode
|
|
abort();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void mi_register_error(mi_error_fun* fun, void* arg) {
|
|
mi_error_handler = fun; // can be NULL
|
|
mi_atomic_store_ptr_release(void,&mi_error_arg, arg);
|
|
}
|
|
|
|
void _mi_error_message(int err, const char* fmt, ...) {
|
|
// show detailed error message
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
mi_show_error_message(fmt, args);
|
|
va_end(args);
|
|
// and call the error handler which may abort (or return normally)
|
|
if (mi_error_handler != NULL) {
|
|
mi_error_handler(err, mi_atomic_load_ptr_acquire(void,&mi_error_arg));
|
|
}
|
|
else {
|
|
mi_error_default(err);
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------
|
|
// Initialize options by checking the environment
|
|
// --------------------------------------------------------
|
|
|
|
// TODO: implement ourselves to reduce dependencies on the C runtime
|
|
#include <stdlib.h> // strtol
|
|
#include <string.h> // strstr
|
|
|
|
|
|
static void mi_option_init(mi_option_desc_t* desc) {
|
|
// Read option value from the environment
|
|
char s[64 + 1];
|
|
char buf[64+1];
|
|
_mi_strlcpy(buf, "mimalloc_", sizeof(buf));
|
|
_mi_strlcat(buf, desc->name, sizeof(buf));
|
|
bool found = _mi_getenv(buf, s, sizeof(s));
|
|
if (!found && desc->legacy_name != NULL) {
|
|
_mi_strlcpy(buf, "mimalloc_", sizeof(buf));
|
|
_mi_strlcat(buf, desc->legacy_name, sizeof(buf));
|
|
found = _mi_getenv(buf, s, sizeof(s));
|
|
if (found) {
|
|
_mi_warning_message("environment option \"mimalloc_%s\" is deprecated -- use \"mimalloc_%s\" instead.\n", desc->legacy_name, desc->name);
|
|
}
|
|
}
|
|
|
|
if (found) {
|
|
size_t len = _mi_strnlen(s, sizeof(buf) - 1);
|
|
for (size_t i = 0; i < len; i++) {
|
|
buf[i] = _mi_toupper(s[i]);
|
|
}
|
|
buf[len] = 0;
|
|
if (buf[0] == 0 || _mi_streq(buf,"1") || _mi_streq(buf,"TRUE") || _mi_streq(buf,"YES") || _mi_streq(buf,"ON")) {
|
|
desc->value = 1;
|
|
desc->init = MI_OPTION_INITIALIZED;
|
|
}
|
|
else if (_mi_streq(buf,"0") || _mi_streq(buf,"FALSE") || _mi_streq(buf,"NO") || _mi_streq(buf,"OFF")) {
|
|
desc->value = 0;
|
|
desc->init = MI_OPTION_INITIALIZED;
|
|
}
|
|
else {
|
|
char* end = buf;
|
|
long value = strtol(buf, &end, 10);
|
|
if (mi_option_has_size_in_kib(desc->option)) {
|
|
// this option is interpreted in KiB to prevent overflow of `long` for large allocations
|
|
// (long is 32-bit on 64-bit windows, which allows for 4TiB max.)
|
|
size_t size = (value < 0 ? 0 : (size_t)value);
|
|
bool overflow = false;
|
|
if (*end == 'K') { end++; }
|
|
else if (*end == 'M') { overflow = mi_mul_overflow(size,MI_KiB,&size); end++; }
|
|
else if (*end == 'G') { overflow = mi_mul_overflow(size,MI_MiB,&size); end++; }
|
|
else if (*end == 'T') { overflow = mi_mul_overflow(size,MI_GiB,&size); end++; }
|
|
else { size = (size + MI_KiB - 1) / MI_KiB; }
|
|
if (end[0] == 'I' && end[1] == 'B') { end += 2; } // KiB, MiB, GiB, TiB
|
|
else if (*end == 'B') { end++; } // Kb, Mb, Gb, Tb
|
|
if (overflow || size > (MI_MAX_ALLOC_SIZE / MI_KiB)) { size = (MI_MAX_ALLOC_SIZE / MI_KiB); }
|
|
value = (size > LONG_MAX ? LONG_MAX : (long)size);
|
|
}
|
|
if (*end == 0) {
|
|
mi_option_set(desc->option, value);
|
|
}
|
|
else {
|
|
// set `init` first to avoid recursion through _mi_warning_message on mimalloc_verbose.
|
|
desc->init = MI_OPTION_DEFAULTED;
|
|
if (desc->option == mi_option_verbose && desc->value == 0) {
|
|
// if the 'mimalloc_verbose' env var has a bogus value we'd never know
|
|
// (since the value defaults to 'off') so in that case briefly enable verbose
|
|
desc->value = 1;
|
|
_mi_warning_message("environment option mimalloc_%s has an invalid value.\n", desc->name);
|
|
desc->value = 0;
|
|
}
|
|
else {
|
|
_mi_warning_message("environment option mimalloc_%s has an invalid value.\n", desc->name);
|
|
}
|
|
}
|
|
}
|
|
mi_assert_internal(desc->init != MI_OPTION_UNINIT);
|
|
}
|
|
else if (!_mi_preloading()) {
|
|
desc->init = MI_OPTION_DEFAULTED;
|
|
}
|
|
}
|