Define NDEBUG in Release and build it optimized - #211
Draft
doomedraven wants to merge 2 commits into
Draft
doomedraven wants to merge 2 commits into
doomedraven wants to merge 2 commits into
Conversation
Contributor
Author
|
Correction to the CI note in the description above: the diagnosis there was wrong. The MSBuild workflow is On top of that it targets the retired #212 fixes the workflow file for all three. Re-enabling the workflow in Settings → Actions is the part that has to be done by hand. |
This was referenced Sep 15, 2026
Release|Win32 has `_CRT_SECURE_NO_WARNINGSNDEBUG` in its preprocessor
definitions: a missing semicolon fused two macros, so NDEBUG is never
defined. Release|x64 never had NDEBUG at all. Result: every assert() in
capemon is live in Release, and the shipped DLL calls _wassert()/abort()
on a failed check inside a monitored process.
Fixing the typo alone is not safe, because three asserts are load-bearing.
This commit removes that dependency first, then defines NDEBUG:
alloc.c:85 - the guard page is created *inside* the assert argument:
assert(VirtualProtect(BaseAddress + RegionSize - 0x1000, 0x1000,
PAGE_NOACCESS, &oldprot));
Under NDEBUG the whole expression, VirtualProtect included, is deleted and
the allocator silently stops emitting guard pages. Now an unconditional
call. The result is deliberately not logged: this is the allocator the
logger itself runs on. Note this code is currently unreachable, alloc.h:29
defines USE_PRIVATE_HEAP and this cm_alloc lives in the #else branch, so
it is a latent defect rather than a live one.
hooking_32.c:267,378 and hooking_64.c:542,826 - the only overflow check on
hookdata->pre_tramp:
assert((ULONG_PTR)(p - h->hookdata->pre_tramp) < MAX_PRETRAMP_SIZE);
pre_tramp is a fixed 320-byte member of hook_data_t. Under NDEBUG the
check disappears and an over-long trampoline overruns it silently. Every
byte written by the four builders is memcpy'd out of function-local
fixed-size arrays, so the bound is a compile-time property: replaced with
C_ASSERT on the sum of the sizeofs. That is exact, strictly stronger than
the runtime check, and NDEBUG-immune. Each C_ASSERT is wrapped in its own
braces because it expands to a typedef and two in one scope is a
redefinition error. hook_create_pre_tramp_notail on x64 branches on
numargs, so it gets one bound per branch; the larger branch currently uses
307 of the 320 bytes.
hooking_32.c:597,606 and hooking_64.c:135,144 - assert(0) followed by
return 0 in get_near_rel_target/get_short_rel_target. The caller uses the
result as a jump target, so under NDEBUG an unhandled opcode silently
retargets the trampoline to address 0. Replaced with a DebugOutput of the
opcode and address; the return 0 is unchanged, but the failure is now
visible in the log instead of only in a debug build.
The remaining asserts (alloc.c:101,105,116) are pure predicates with no
side effects, so NDEBUG only removes the check.
TAG=agy
CONV=b3280e17-abe0-4fed-ad0b-c2e7f65da90f
…k us
Neither Release ItemDefinitionGroup sets <Optimization>, so cl.exe falls
back to its default of /Od and the shipped Release DLL is unoptimized. The
Debug groups both set <Optimization>Disabled explicitly, which suggests the
Release omission is an oversight rather than a decision. capemon sits in
the call path of every hooked API in the analysed process, so this is paid
on every call.
/O2 is not a drop-in here, because it is a bundle: /Og /Oi /Ot /Oy /Ob2
/GF /Gy. Two of those conflict with assumptions in the hooking engine, so
they are turned back off explicitly:
/Gy (function-level linking). hooking.c:202-211 declares
extern void start_transparent_hooks();
extern void end_transparent_hooks();
and addr_in_our_dll_range() exempts every address in the half-open range
[&start_transparent_hooks, &end_transparent_hooks) from recursion
detection. Those two are one-line functions in hook_special.c bracketing a
run of HOOKDEFs, so the range is a source-order-equals-address-order
assumption. /Gy puts each function in its own COMDAT, and /OPT:REF plus
/OPT:ICF are already enabled on both Release links, so the linker would be
free to reorder or fold the functions inside that range and the transparent
hooks would stop being transparent. <FunctionLevelLinking>false</> keeps
one code section per object file.
/Oy (frame pointer omission, x86 only). hooking_32.c:895-942
operate_on_backtrace walks the EBP chain, and capemon's own frames have to
appear in that walk for addr_in_our_dll_range to spot recursion. /Oy-
keeps them. x64 does not use EBP chains, so <OmitFramePointers> is only set
on Win32.
/GL is already on via <WholeProgramOptimization>true</> in both Release
property groups, so LTCG can already move code across object files; /O2
/Gy- does not make the source-order assumption any weaker than it is today.
It is worth noting that the assumption is fragile regardless, and a marker
section or explicit address capture would be a better long-term fix.
Split from the NDEBUG commit so it can be dropped independently: NDEBUG is
a correctness fix, this is a performance change and carries codegen risk.
TAG=agy
CONV=b3280e17-abe0-4fed-ad0b-c2e7f65da90f
doomedraven
force-pushed
the
fix/build-config-ndebug-o2
branch
from
September 26, 2026 17:10
247d6ee to
cb90208
Compare
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.
This is the build-configuration PR in the review series. Two commits, and
they are ordered deliberately.
What is broken
1.
NDEBUGis never defined in Release.capemon.vcxprojline 150, Release|Win32:A missing
;fused_CRT_SECURE_NO_WARNINGSandNDEBUGinto one macronamed
_CRT_SECURE_NO_WARNINGSNDEBUG. Line 186, Release|x64, never hadNDEBUGin the list at all.So every
assert()in capemon is live in the shipped Release DLL. A failedassert calls
_wassert(), which writes to stderr and callsabort()—inside the process being analysed, from inside a hook.
2. Release is built with
/Od.Neither Release
ItemDefinitionGroupsets<Optimization>, socl.exeuses its default, which is
/Od. Both Debug groups set<Optimization>Disabledexplicitly, so the Release omission reads as anoversight. capemon is in the call path of every hooked API in the analysed
process.
Are these related?
Yes, and that coupling is the reason this PR exists as its own unit rather
than being folded into the one-line-defects PR.
You cannot just add the missing semicolon. Three asserts in the tree are
load-bearing — defining
NDEBUGdeletes behaviour, not just checks:NDEBUGdeletesalloc.c:85assert(VirtualProtect(..., PAGE_NOACCESS, ...))— the guard page is created inside the assert argument. Whole expression vanishes; the allocator silently stops emitting guard pages.hooking_32.c:267,378,hooking_64.c:542,826assert((ULONG_PTR)(p - h->hookdata->pre_tramp) < MAX_PRETRAMP_SIZE)— the only overflow check on a fixed 320-byte buffer. Vanishes; an over-long trampoline overrunshook_data_tsilently.hooking_32.c:597,606,hooking_64.c:135,144assert(0); return 0;inget_near_rel_target/get_short_rel_target. The caller uses the result as a jump target, so an unhandled opcode silently retargets the trampoline to address 0.Commit 1 therefore removes the dependency first, then defines
NDEBUGin the same commit. Fixing the typo without the assert work would be a
regression; doing the assert work without the typo fix would be dead
effort. They have to land together.
Contrast this with the other PRs in the series (#206, #207, #208, #210):
those are batches of mutually independent one-site fixes, where each hunk
stands on its own and any subset can be dropped. This one does not
decompose.
Commit 1 — make the load-bearing asserts real, then define
NDEBUGalloc.c:85— theVirtualProtectcall is now unconditional. The resultis deliberately not logged: this is the allocator the logger runs on, and
calling
DebugOutputfrom insidecm_allocis a reentrancy hazard.Honest caveat: this code is currently unreachable.
alloc.h:29defines
USE_PRIVATE_HEAPand thiscm_allocis in the#elsebranch.It is a latent defect, not a live one — worth fixing because someone will
eventually flip that define, not because it is biting today.
The four
pre_trampbound asserts are replaced withC_ASSERTon the sumof the
sizeofs of the byte arrays. Every byte those builders write comesfrom
memcpy(p, pre_trampN, sizeof(pre_trampN))out of function-localfixed-size arrays —
emit_relpatches inside the arrays, not intopre_tramp— so the bound is a compile-time property. That makes theC_ASSERTexact, strictly stronger than the runtime check it replaces,and immune to
NDEBUG.C_ASSERTis wrapped in its own{ }because it expands totypedef char __C_ASSERT__[...], and two in one scope is a typedefredefinition error.
hook_create_pre_tramp_notailon x64 branches onh->numargs > 4, soit gets one bound per branch. The larger branch currently uses 307 of
the 320 bytes available, which is worth knowing before anyone adds
instructions to that stub.
The four
assert(0)sites become aDebugOutputof the offending opcodeand address.
return 0is unchanged — this does not alter control flow,it makes an already-silent failure visible in the log rather than only in
a debug build.
The remaining asserts (
alloc.c:101,105,116) are pure predicates with noside effects, so
NDEBUGonly removes the check. Left alone.Commit 2 — build Release with
/O2Split out so it can be dropped independently. Commit 1 is a correctness
fix; this one is a performance change and carries codegen risk.
/O2is a bundle:/Og /Oi /Ot /Oy /Ob2 /GF /Gy. Two of those conflictwith assumptions in the hooking engine, so they are explicitly turned back
off:
/Gy(function-level linking) — disabled on both platforms.hooking.c:202-211declaresand
addr_in_our_dll_range()exempts every address in[&start_transparent_hooks, &end_transparent_hooks)from recursiondetection. Both are one-line functions in
hook_special.cbracketing a runof
HOOKDEFs, so the range is a source-order-equals-address-orderassumption.
/Gygives every function its own COMDAT, and/OPT:REF+/OPT:ICFare already enabled on both Release links — the linker wouldthen be free to reorder or fold functions inside that range and the
transparent hooks would stop being transparent.
<FunctionLevelLinking>false</>keeps one code section per object file./Oy(frame pointer omission) — disabled on Win32 only.hooking_32.c:895-942operate_on_backtracewalks the EBP chain, andcapemon's own frames must show up in that walk for
addr_in_our_dll_rangeto detect recursion. x64 does not use EBP chains, so the setting is Win32
only.
Note that
/GLis already on via<WholeProgramOptimization>true</>inboth Release property groups, so LTCG can already move code across object
files.
/O2 /Gy-does not make the source-order assumption weaker than italready is. The assumption is fragile either way — a marker section or
capturing the addresses explicitly would be a better long-term fix, but
that is a separate change.
Testing
Not compiled. There is no MSVC on the machine this was written on, so
these two commits have been verified by inspection only. CI is the only
compile gate, and see the note below.
The
C_ASSERTs are the one part that will fail loudly and immediately ifthe reasoning above is wrong: a bad bound is a compile error, not a runtime
surprise. Hand-summing the arrays gives 36 / 73 bytes (x86) and 133 / 307 /
205 bytes (x64) against
MAX_PRETRAMP_SIZE320, so they should pass.Behaviour worth watching after
NDEBUGlands: anything that was previouslycrashing the analysed process via
_wassertwill now continue silently, soif a sample's behaviour changes shape, check the debugger log for the two
new
get_*_rel_targetmessages.CI
No checks have run on any PR in this series. Two candidates:
approval before they will schedule.
.github/workflows/msbuild.ymlusesruns-on: windows-2019, whichGitHub has retired. Even once approved it may fail to schedule.
If (2) is the problem, a prerequisite PR bumping
windows-2019→windows-2022andactions/checkout@v3/upload-artifact@v3→@v4would fix it. Say the word and I will open it.
Series