lookup: return NULL on allocation failure, keep payloads pointer-aligned - #217
Merged
kevoreilly merged 1 commit intoSep 25, 2026
Merged
Conversation
lookup_add() dereferenced the result of calloc() without checking it, so an allocation failure faulted inside the lookup code even for callers that already handle NULL (SetSoftwareBreakpoint, __handle_duplicate, add_force_hook_thread_func) or never use the result (add_file_to_log_tracking, file_write, add_ignored_thread, the CLR JIT tracking in hook_clr.c, LOOKUP_MARK_SEEN). It now returns NULL. lookup_get_or_create() passes that NULL through instead of memsetting it. The memset was redundant anyway: calloc here is cm_calloc, i.e. HeapAlloc(HEAP_ZERO_MEMORY), so the payload is already zero. Dropping it also removes the write to an entry that is already visible to other threads. entry_t put data[] at offset 20 on x64 (8 + 8 + 4), so every pointer in every payload - hook_info_t, file_record_t, SOFTBP - sat at 4 mod 8. x64 tolerates that for plain loads and stores, but the Interlocked*Pointer and 64-bit Interlocked functions require natural alignment, and LOOKUP_THREAD / LOOKUP_SHARED are now meant to carry per-thread and shared state. size is now ULONG_PTR, which moves data[] to offset 24 on x64 and leaves x86 at 12. sizeof(entry_t) is 24 on x64 before and after (the four bytes were tail padding), so no allocation grows. A C_ASSERT pins the alignment. Callers that dereference the result without checking - cache_file and hook_info - are unchanged in effect: they faulted inside lookup_add() before and fault in their own code now. TAG=agy CONV=b3f21014-0e97-49b9-aa93-0601a28982fe
kevoreilly
marked this pull request as ready for review
September 25, 2026 15:24
doomedraven
added a commit
to doomedraven/capemon
that referenced
this pull request
Sep 25, 2026
Replace the TlsAlloc/TlsGetValue/TlsSetValue context with LOOKUP_THREAD (lookup.h, 8f40674), the idiom added to replace per-thread state reached through the Tls APIs. It is the same thread-id-keyed scheme hook_info() uses for g_hook_info. g_bson and g_istr now expand to a local ctx. loq() and each serializing helper fetch it once, so a record costs one table walk in loq() plus one per helper call, rather than one TLS read per field access. Contexts are never freed. A thread given a dead thread's id inherits its context, which is safe because loq() starts every record with bson_init() (zeroes the whole bson struct). Retained memory is bounded by distinct thread ids. sizeof(log_context_t) is 312 bytes on x64 and 168 on x86; the old comment's "40-something bytes" missed the bson struct's 32-entry size_t stack. With kevoreilly#217, allocation failure on a thread's first log returns NULL and loq() drops the record, as before; without it, lookup_add() faults on the NULL calloc() result. TAG=agy CONV=b3f21014-0e97-49b9-aa93-0601a28982fe
doomedraven
added a commit
to doomedraven/capemon
that referenced
this pull request
Sep 25, 2026
Replace the TlsAlloc/TlsGetValue/TlsSetValue context with LOOKUP_THREAD (lookup.h, 8f40674), the idiom added to replace per-thread state reached through the Tls APIs. It is the same thread-id-keyed scheme hook_info() uses for g_hook_info. g_bson and g_istr now expand to a local ctx. loq() and each serializing helper fetch it once, so a record costs one table walk in loq() plus one per helper call, rather than one TLS read per field access. Contexts are never freed. A thread given a dead thread's id inherits its context, which is safe because loq() starts every record with bson_init() (zeroes the whole bson struct). Retained memory is bounded by distinct thread ids. sizeof(log_context_t) is 312 bytes on x64 and 168 on x86; the old comment's "40-something bytes" missed the bson struct's 32-entry size_t stack. With kevoreilly#217, allocation failure on a thread's first log returns NULL and loq() drops the record, as before; without it, lookup_add() faults on the NULL calloc() result. TAG=agy CONV=b3f21014-0e97-49b9-aa93-0601a28982fe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
lookup_add()doesn't check whatcalloc()returns, and on x64 every lookup payload starts only 4-byte aligned. TheLOOKUP_THREADversion of #216's scratch pool suggested there would be the macro's first user, so both are fixed here as a separate change (9 lines added, 6 removed).Changes
lookup_add()returns NULL when the allocation fails. Previously it wrote through the NULL result insidelookup.c, which crashed even callers that already handle NULL.lookup_get_or_create()passes that NULL through. It no longer memsets the result. The memset was redundant anyway:callochere iscm_calloc, i.e.HeapAlloc(g_heap, HEAP_ZERO_MEMORY, ...), so the payload is already zero. Dropping it also removes a write to an entry that other threads can already see.entry_t.sizeis nowULONG_PTRinstead ofunsigned int. On x64,data[]sat at offset 20 (8 + 8 + 4), so every pointer in every payload (hook_info_t,file_record_t,SOFTBP) was at 4 mod 8. x64 tolerates that for plain loads and stores, although it is still undefined behaviour in C and UBSan reports it (see Testing). TheInterlocked*Pointerand 64-bitInterlocked*functions, however, require natural alignment, andLOOKUP_THREAD/LOOKUP_SHAREDare meant to carry exactly that kind of state. With the change,data[]moves to offset 24 on x64 and stays at 12 on x86.sizeof(entry_t)is 24 on x64 both before and after, since those four bytes were tail padding, so no allocation grows. AC_ASSERTpins the alignment.Callers when the allocation fails
SetSoftwareBreakpoint,__handle_duplicate,add_force_hook_thread_funclookup_adddespite their own NULL checkadd_file_to_log_tracking,file_write,add_ignored_thread,hook_clr.cJIT tracking,LOOKUP_MARK_SEENlookup_addcache_file,hook_infolookup_addI left the last two alone. Deciding what
hook_info()should return when it has nothing is a design decision, not a hardening fix.Deliberately not in this PR
LOOKUP_SHAREDfirst-use race.lookup_get_or_create()does the lookup and the add as two separate steps. For a shared key, two threads arriving first can each add an entry, and the later one hides the earlier. A thread-id key cannot hit this, and nothing usesLOOKUP_SHAREDyet, so the fix (insert-if-absent under a CAS) can wait until something does.file_handle_terminate()(hook_file.c:333) reads pastg_files. The loop starts from(entry_t *)&g_files.root, a fake list head, and readsp->idfrom it. That field lies beyond the one-pointerlookup_t, so it reads the adjacent global. This change doesn't movenextorid, so the behaviour is the same before and after. It is noted here only so it isn't lost.lookup_get()'s plain loads racinglookup_add()'s CAS publish. This is the long-standing lock-free pattern. It is fine on x86/x64 because nothing is ever freed, and it is unchanged here.Testing
Not compiled with MSVC and not run in a detonation. There is no MSVC here, and the
MSBuildworkflow isdisabled_manually(see #212).Instead,
lookup.c/lookup.hwere compiled verbatim against a small Win32 shim and checked as follows:gcc -Wall -Wextra -Werrorbuilds cleanly for both-m64and-m32. TheC_ASSERTholds on both.C_ASSERTfails the build as intended (size of array '__C_ASSERT__' is negative).lookup_add,lookup_get_or_createandLOOKUP_THREADreturn NULL and the table is left untouched.LOOKUP_MARK_SEENdoes not fault.lookup_get, and a secondget_or_createreturns the same entry.offsetof(entry_t, data)is 24 on LP64 (was 20) and 12 on ILP32 (unchanged).sizeof(entry_t)is unchanged on both.LOOKUP_THREADversion of Stop allocating 64 KB per path on the hooked call paths #216's pool was run against both headers: 300 threads over 32 recycled thread ids, ASan+UBSan, default sanitizer options.member access within misaligned address ... for type 'struct path_scratch_t', which requires 8 byte alignmentat 10 distinct source locations. LeakSanitizer reports all 136 retained 64 KB slots as leaked, because it only scans pointer-aligned words and every payload pointer was at 4 mod 8.Are these related?
No. This change is self-contained and can be merged, reverted or deferred on its own.
#216 benefits from it if it moves to
LOOKUP_THREAD: allocation failure on a thread's first path buffer then falls back to the heap instead of faulting inlookup_add. #216 does not require it, and neither PR changes the other's files.Contrast with the
NDEBUGcase in #211, which genuinely is coupled: theassert()removals there are only correct because the same PR definesNDEBUG.Textual conflicts: none expected. No other PR in the series touches
lookup.corlookup.h.Series
#206, #207, #208, #209, #210, #211, #212, #213, #214, #215, #164, #216, this one.