Skip to content

Commit fde7929

Browse files
committed
fix(hook-augment): cache the build fingerprint so the startup deadline is met
Every process start SHA-256s its own ~295 MB executable to build the build-identity fingerprint (~2.3 s, software path), which pushes the PreToolUse hook past its default deadline on large graphs so it emits 0 bytes and exits 0. - Cache the fingerprint keyed by (device, inode, size, mtime, ctime) at runtime_process_image_reference_acquire, covering both the self- fingerprint and the per-peer rendezvous; a rebuilt binary rolls the key. - Back the streaming SHA-256 with CommonCrypto on Apple (hardware path), dropping even a cache miss to ~0.25 s; asserted bit-identical to scalar. - A missed deadline is now recorded on BOTH the timeouts log and stderr (never a silent 0-byte exit 0); CBM_HOOK_DEADLINE_MS still honored; and `daemon <start|stop|status>` is documented in --help. Fixes #1335 Signed-off-by: Brian McNaboe <bmcnaboe@gmail.com>
1 parent 4bbc978 commit fde7929

8 files changed

Lines changed: 505 additions & 36 deletions

File tree

‎src/cli/hook_augment.c‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,31 @@ static size_t g_ha_crumb_len = 0;
9090

9191
static void ha_deadline_exit(int sig) {
9292
(void)sig;
93-
if (g_ha_crumb_fd >= 0 && g_ha_crumb_len > 0) {
94-
ssize_t w = write(g_ha_crumb_fd, g_ha_crumb_msg, g_ha_crumb_len);
95-
(void)w;
93+
/* async-signal-safe only: write() the prepared breadcrumb to the timeouts
94+
* log (when it opened) and ALWAYS to stderr, so a fired deadline is a
95+
* visible event, never a silent 0-byte exit(0). */
96+
if (g_ha_crumb_len > 0) {
97+
if (g_ha_crumb_fd >= 0) {
98+
ssize_t w = write(g_ha_crumb_fd, g_ha_crumb_msg, g_ha_crumb_len);
99+
(void)w;
100+
}
101+
ssize_t e = write(STDERR_FILENO, g_ha_crumb_msg, g_ha_crumb_len);
102+
(void)e;
96103
}
97104
_exit(0);
98105
}
99106

100107
static void ha_open_crumb_log(int deadline_ms) {
108+
/* Pre-format the breadcrumb unconditionally: the handler may only write() a
109+
* prepared buffer, and formatting it before the log path is resolved means
110+
* stderr still gets the line even when the file cannot be opened (no HOME,
111+
* unwritable cache dir) — the "never silent" guarantee holds regardless. */
112+
int n = snprintf(g_ha_crumb_msg, sizeof(g_ha_crumb_msg),
113+
"hook-augment: deadline_exceeded ms=%d pid=%ld (raise via "
114+
"CBM_HOOK_DEADLINE_MS)\n",
115+
deadline_ms, (long)getpid());
116+
g_ha_crumb_len = (n > 0 && n < (int)sizeof(g_ha_crumb_msg)) ? (size_t)n : 0;
117+
101118
const char *override = getenv("CBM_HOOK_TIMEOUT_LOG"); /* tests + power users */
102119
char path[CBM_SZ_1K];
103120
if (override && override[0]) {
@@ -113,14 +130,6 @@ static void ha_open_crumb_log(int deadline_ms) {
113130
snprintf(path, sizeof(path), "%s/hook-augment-timeouts.log", dir);
114131
}
115132
g_ha_crumb_fd = open(path, O_WRONLY | O_CREAT | O_APPEND, 0644);
116-
if (g_ha_crumb_fd < 0) {
117-
return;
118-
}
119-
int n = snprintf(g_ha_crumb_msg, sizeof(g_ha_crumb_msg),
120-
"hook-augment: deadline_exceeded ms=%d pid=%ld (raise via "
121-
"CBM_HOOK_DEADLINE_MS)\n",
122-
deadline_ms, (long)getpid());
123-
g_ha_crumb_len = (n > 0 && n < (int)sizeof(g_ha_crumb_msg)) ? (size_t)n : 0;
124133
}
125134

126135
void cbm_hook_augment_arm_deadline(void) {

‎src/daemon/runtime.c‎

Lines changed: 245 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,239 @@ static bool runtime_posix_stat_same_image(const struct stat *first, const struct
695695

696696
#endif
697697

698+
/* ── Executable-image fingerprint cache ──────────────────────────────────────
699+
* The build-identity fingerprint is a SHA-256 over the entire ~295 MB
700+
* executable (cbm_daemon_build_fingerprint_native_file) — ~2.3 s of pure CPU on
701+
* every process start and every peer rendezvous, and the direct cause of the
702+
* hook-augment startup deadline being missed on large graphs. The mapped image
703+
* is immutable for a build's lifetime, so the digest is cached keyed by the file
704+
* identity the acquire path already verifies as stable across the hash (device,
705+
* inode, size, mtime, ctime — the same tuple runtime_*_stat_same compares). A
706+
* rebuilt or replaced binary lands on a new inode/mtime and misses, so the cache
707+
* can never return a stale digest and build-cohort admission keeps its exact
708+
* meaning. A cache hit does not weaken the identity proof: the acquire chain
709+
* still brackets the held image with its before/after stat and process-maps
710+
* checks, so the returned digest is bound to that same verified image — only the
711+
* re-read of bytes whose identity is unchanged is skipped. */
712+
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__) || defined(__FreeBSD__)
713+
714+
typedef struct {
715+
bool valid;
716+
uint64_t device;
717+
uint64_t inode;
718+
uint64_t size;
719+
int64_t mtime_seconds;
720+
int64_t mtime_nanoseconds;
721+
int64_t ctime_seconds;
722+
int64_t ctime_nanoseconds;
723+
} runtime_fingerprint_key_t;
724+
725+
#define RUNTIME_FINGERPRINT_CACHE_CAP 8
726+
727+
static atomic_flag runtime_fingerprint_cache_lock = ATOMIC_FLAG_INIT;
728+
static struct {
729+
runtime_fingerprint_key_t key;
730+
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
731+
} runtime_fingerprint_cache[RUNTIME_FINGERPRINT_CACHE_CAP];
732+
static size_t runtime_fingerprint_cache_count;
733+
static size_t runtime_fingerprint_cache_victim;
734+
735+
#ifdef CBM_ENABLE_TEST_SEAMS
736+
static atomic_int runtime_fingerprint_hash_call_count;
737+
static atomic_bool runtime_fingerprint_hash_stub_active;
738+
static char runtime_fingerprint_hash_stub_digest[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
739+
#endif
740+
741+
static bool runtime_fingerprint_key_equal(const runtime_fingerprint_key_t *a,
742+
const runtime_fingerprint_key_t *b) {
743+
return a->valid && b->valid && a->device == b->device && a->inode == b->inode &&
744+
a->size == b->size && a->mtime_seconds == b->mtime_seconds &&
745+
a->mtime_nanoseconds == b->mtime_nanoseconds && a->ctime_seconds == b->ctime_seconds &&
746+
a->ctime_nanoseconds == b->ctime_nanoseconds;
747+
}
748+
749+
static void runtime_fingerprint_cache_enter(void) {
750+
while (
751+
atomic_flag_test_and_set_explicit(&runtime_fingerprint_cache_lock, memory_order_acquire)) {}
752+
}
753+
754+
static void runtime_fingerprint_cache_leave(void) {
755+
atomic_flag_clear_explicit(&runtime_fingerprint_cache_lock, memory_order_release);
756+
}
757+
758+
static bool runtime_fingerprint_cache_lookup(const runtime_fingerprint_key_t *key,
759+
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
760+
if (!key->valid) {
761+
return false;
762+
}
763+
bool hit = false;
764+
runtime_fingerprint_cache_enter();
765+
for (size_t index = 0; index < runtime_fingerprint_cache_count; index++) {
766+
if (runtime_fingerprint_key_equal(&runtime_fingerprint_cache[index].key, key)) {
767+
memcpy(out, runtime_fingerprint_cache[index].fingerprint,
768+
CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
769+
hit = true;
770+
break;
771+
}
772+
}
773+
runtime_fingerprint_cache_leave();
774+
return hit;
775+
}
776+
777+
static void runtime_fingerprint_cache_store(
778+
const runtime_fingerprint_key_t *key,
779+
const char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
780+
if (!key->valid || fingerprint[0] == '\0') {
781+
return;
782+
}
783+
runtime_fingerprint_cache_enter();
784+
size_t slot = RUNTIME_FINGERPRINT_CACHE_CAP;
785+
for (size_t index = 0; index < runtime_fingerprint_cache_count; index++) {
786+
if (runtime_fingerprint_key_equal(&runtime_fingerprint_cache[index].key, key)) {
787+
slot = index;
788+
break;
789+
}
790+
}
791+
if (slot == RUNTIME_FINGERPRINT_CACHE_CAP) {
792+
if (runtime_fingerprint_cache_count < RUNTIME_FINGERPRINT_CACHE_CAP) {
793+
slot = runtime_fingerprint_cache_count++;
794+
} else {
795+
slot = runtime_fingerprint_cache_victim;
796+
runtime_fingerprint_cache_victim =
797+
(runtime_fingerprint_cache_victim + 1) % RUNTIME_FINGERPRINT_CACHE_CAP;
798+
}
799+
}
800+
runtime_fingerprint_cache[slot].key = *key;
801+
memcpy(runtime_fingerprint_cache[slot].fingerprint, fingerprint,
802+
CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
803+
runtime_fingerprint_cache_leave();
804+
}
805+
806+
/* The single point where the expensive image hash is paid; a test seam counts
807+
* calls and can substitute a stub digest so the cache's hit/miss behaviour is
808+
* observable without a real 295 MB read. */
809+
static bool runtime_fingerprint_hash_native_file(uintptr_t native_file,
810+
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
811+
#ifdef CBM_ENABLE_TEST_SEAMS
812+
atomic_fetch_add_explicit(&runtime_fingerprint_hash_call_count, 1, memory_order_relaxed);
813+
if (atomic_load_explicit(&runtime_fingerprint_hash_stub_active, memory_order_acquire)) {
814+
memcpy(out, runtime_fingerprint_hash_stub_digest, CBM_DAEMON_BUILD_FINGERPRINT_SIZE);
815+
return true;
816+
}
817+
#endif
818+
return cbm_daemon_build_fingerprint_native_file(native_file, out);
819+
}
820+
821+
/* Resolve the fingerprint of the held native image, consulting the cache before
822+
* paying for the full-image hash and populating it on a miss. */
823+
static bool runtime_build_fingerprint_cached(uintptr_t native_file,
824+
const runtime_fingerprint_key_t *key,
825+
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
826+
if (runtime_fingerprint_cache_lookup(key, fingerprint)) {
827+
return true;
828+
}
829+
if (!runtime_fingerprint_hash_native_file(native_file, fingerprint)) {
830+
return false;
831+
}
832+
runtime_fingerprint_cache_store(key, fingerprint);
833+
return true;
834+
}
835+
836+
#ifdef _WIN32
837+
static int64_t runtime_fingerprint_filetime(FILETIME value) {
838+
return (int64_t)(((uint64_t)value.dwHighDateTime << 32) | value.dwLowDateTime);
839+
}
840+
841+
static bool runtime_build_fingerprint_cached_windows(
842+
uintptr_t native_file, const BY_HANDLE_FILE_INFORMATION *information, const LARGE_INTEGER *size,
843+
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
844+
runtime_fingerprint_key_t key;
845+
memset(&key, 0, sizeof(key));
846+
key.valid = information != NULL && size != NULL && size->QuadPart >= 0;
847+
if (key.valid) {
848+
key.device = information->dwVolumeSerialNumber;
849+
key.inode = ((uint64_t)information->nFileIndexHigh << 32) | information->nFileIndexLow;
850+
key.size = (uint64_t)size->QuadPart;
851+
key.mtime_seconds = runtime_fingerprint_filetime(information->ftLastWriteTime);
852+
key.ctime_seconds = runtime_fingerprint_filetime(information->ftCreationTime);
853+
}
854+
return runtime_build_fingerprint_cached(native_file, &key, fingerprint);
855+
}
856+
#else
857+
static bool runtime_build_fingerprint_cached_posix(
858+
uintptr_t native_file, const struct stat *status,
859+
char fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
860+
runtime_fingerprint_key_t key;
861+
memset(&key, 0, sizeof(key));
862+
key.valid = status != NULL && S_ISREG(status->st_mode);
863+
if (key.valid) {
864+
key.device = (uint64_t)status->st_dev;
865+
key.inode = (uint64_t)status->st_ino;
866+
key.size = (uint64_t)status->st_size;
867+
#ifdef __APPLE__
868+
key.mtime_seconds = status->st_mtimespec.tv_sec;
869+
key.mtime_nanoseconds = status->st_mtimespec.tv_nsec;
870+
key.ctime_seconds = status->st_ctimespec.tv_sec;
871+
key.ctime_nanoseconds = status->st_ctimespec.tv_nsec;
872+
#else
873+
key.mtime_seconds = status->st_mtim.tv_sec;
874+
key.mtime_nanoseconds = status->st_mtim.tv_nsec;
875+
key.ctime_seconds = status->st_ctim.tv_sec;
876+
key.ctime_nanoseconds = status->st_ctim.tv_nsec;
877+
#endif
878+
}
879+
return runtime_build_fingerprint_cached(native_file, &key, fingerprint);
880+
}
881+
#endif
882+
883+
#ifdef CBM_ENABLE_TEST_SEAMS
884+
void cbm_daemon_runtime_fingerprint_cache_reset_for_testing(void) {
885+
runtime_fingerprint_cache_enter();
886+
memset(runtime_fingerprint_cache, 0, sizeof(runtime_fingerprint_cache));
887+
runtime_fingerprint_cache_count = 0;
888+
runtime_fingerprint_cache_victim = 0;
889+
runtime_fingerprint_cache_leave();
890+
atomic_store_explicit(&runtime_fingerprint_hash_call_count, 0, memory_order_relaxed);
891+
atomic_store_explicit(&runtime_fingerprint_hash_stub_active, false, memory_order_release);
892+
memset(runtime_fingerprint_hash_stub_digest, 0, sizeof(runtime_fingerprint_hash_stub_digest));
893+
}
894+
895+
void cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(const char *digest) {
896+
if (!digest) {
897+
atomic_store_explicit(&runtime_fingerprint_hash_stub_active, false, memory_order_release);
898+
return;
899+
}
900+
memset(runtime_fingerprint_hash_stub_digest, 0, sizeof(runtime_fingerprint_hash_stub_digest));
901+
size_t length = strlen(digest);
902+
if (length >= CBM_DAEMON_BUILD_FINGERPRINT_SIZE) {
903+
length = CBM_DAEMON_BUILD_FINGERPRINT_SIZE - 1;
904+
}
905+
memcpy(runtime_fingerprint_hash_stub_digest, digest, length);
906+
atomic_store_explicit(&runtime_fingerprint_hash_stub_active, true, memory_order_release);
907+
}
908+
909+
int cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(void) {
910+
return atomic_load_explicit(&runtime_fingerprint_hash_call_count, memory_order_relaxed);
911+
}
912+
913+
bool cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(
914+
uint64_t device, uint64_t inode, uint64_t size, int64_t mtime_seconds,
915+
int64_t mtime_nanoseconds, int64_t ctime_seconds, int64_t ctime_nanoseconds,
916+
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]) {
917+
runtime_fingerprint_key_t key = {.valid = true,
918+
.device = device,
919+
.inode = inode,
920+
.size = size,
921+
.mtime_seconds = mtime_seconds,
922+
.mtime_nanoseconds = mtime_nanoseconds,
923+
.ctime_seconds = ctime_seconds,
924+
.ctime_nanoseconds = ctime_nanoseconds};
925+
return runtime_build_fingerprint_cached((uintptr_t)0, &key, out);
926+
}
927+
#endif
928+
929+
#endif /* supported platform: executable-image fingerprint cache */
930+
698931
/* Acquire one process instance's mapped image as a native object. Supplying a
699932
* fingerprint hashes that same held object; NULL performs metadata-only
700933
* acquisition for the HELLO fast path. Every platform brackets the process
@@ -736,7 +969,8 @@ static bool runtime_process_image_reference_acquire(
736969
: INVALID_HANDLE_VALUE;
737970
free(open_path);
738971
ok = ok && runtime_windows_file_snapshot(file, &file_before, &size_before) &&
739-
(!fingerprint || cbm_daemon_build_fingerprint_native_file((uintptr_t)file, fingerprint)) &&
972+
(!fingerprint || runtime_build_fingerprint_cached_windows((uintptr_t)file, &file_before,
973+
&size_before, fingerprint)) &&
740974
runtime_windows_file_snapshot(file, &file_after, &size_after) &&
741975
runtime_windows_file_snapshot_same(&file_before, &size_before, &file_after, &size_after) &&
742976
runtime_windows_process_image_snapshot(process, &process_after) &&
@@ -772,7 +1006,8 @@ static bool runtime_process_image_reference_acquire(
7721006
struct stat file_after;
7731007
ok = ok && fd >= 0 && fstat(fd, &file_before) == 0 && S_ISREG(file_before.st_mode) &&
7741008
runtime_mac_process_maps_file_executable(pid, &file_before) &&
775-
(!fingerprint || cbm_daemon_build_fingerprint_native_file((uintptr_t)fd, fingerprint)) &&
1009+
(!fingerprint ||
1010+
runtime_build_fingerprint_cached_posix((uintptr_t)fd, &file_before, fingerprint)) &&
7761011
fstat(fd, &file_after) == 0 && runtime_mac_stat_same(&file_before, &file_after) &&
7771012
runtime_mac_process_maps_file_executable(pid, &file_after) &&
7781013
runtime_mac_process_instance(pid, &process_after) &&
@@ -795,12 +1030,12 @@ static bool runtime_process_image_reference_acquire(
7951030
int image_fd = process_fd >= 0 ? openat(process_fd, "exe", O_RDONLY | O_CLOEXEC) : -1;
7961031
struct stat image_before;
7971032
struct stat image_after;
798-
bool ok = image_fd >= 0 && fstat(image_fd, &image_before) == 0 &&
799-
S_ISREG(image_before.st_mode) &&
800-
(!fingerprint ||
801-
cbm_daemon_build_fingerprint_native_file((uintptr_t)image_fd, fingerprint)) &&
802-
fstat(image_fd, &image_after) == 0 &&
803-
runtime_posix_stat_same_image(&image_before, &image_after);
1033+
bool ok =
1034+
image_fd >= 0 && fstat(image_fd, &image_before) == 0 && S_ISREG(image_before.st_mode) &&
1035+
(!fingerprint ||
1036+
runtime_build_fingerprint_cached_posix((uintptr_t)image_fd, &image_before, fingerprint)) &&
1037+
fstat(image_fd, &image_after) == 0 &&
1038+
runtime_posix_stat_same_image(&image_before, &image_after);
8041039
int verify_fd = ok ? openat(process_fd, "exe", O_RDONLY | O_CLOEXEC) : -1;
8051040
struct stat verify_status;
8061041
ok = ok && verify_fd >= 0 && fstat(verify_fd, &verify_status) == 0 &&
@@ -834,8 +1069,8 @@ static bool runtime_process_image_reference_acquire(
8341069
struct stat image_before;
8351070
struct stat image_after;
8361071
ok = image_fd >= 0 && fstat(image_fd, &image_before) == 0 && S_ISREG(image_before.st_mode) &&
837-
(!fingerprint ||
838-
cbm_daemon_build_fingerprint_native_file((uintptr_t)image_fd, fingerprint)) &&
1072+
(!fingerprint || runtime_build_fingerprint_cached_posix((uintptr_t)image_fd, &image_before,
1073+
fingerprint)) &&
8391074
fstat(image_fd, &image_after) == 0 &&
8401075
runtime_posix_stat_same_image(&image_before, &image_after);
8411076
if (ok) {

‎src/daemon/runtime.h‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,18 @@ void cbm_daemon_runtime_force_peer_image_unverified_for_testing(bool force);
419419
* i.e. the tamper case that must still be rejected after unverifiable images
420420
* became admissible. */
421421
void cbm_daemon_runtime_force_peer_image_mismatch_for_testing(bool force);
422+
/* Executable-image fingerprint-cache seams: reset the cache and hash counter
423+
* between cases, substitute a stub for the expensive image hash so cache
424+
* hit/miss/key-roll are observable without a real 295 MB read, and resolve a
425+
* synthetic file identity through the exact cached path the acquire fingerprint
426+
* sites use. */
427+
void cbm_daemon_runtime_fingerprint_cache_reset_for_testing(void);
428+
void cbm_daemon_runtime_fingerprint_cache_set_hash_stub_for_testing(const char *digest);
429+
int cbm_daemon_runtime_fingerprint_hash_call_count_for_testing(void);
430+
bool cbm_daemon_runtime_fingerprint_cache_resolve_for_testing(
431+
uint64_t device, uint64_t inode, uint64_t size, int64_t mtime_seconds,
432+
int64_t mtime_nanoseconds, int64_t ctime_seconds, int64_t ctime_nanoseconds,
433+
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]);
422434
#endif
423435

424436
#endif /* CBM_DAEMON_RUNTIME_H */

0 commit comments

Comments
 (0)