build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170) #178
Reference in New Issue
Block a user
Delete Branch "fix/issue-170-assert-debug-off"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #170.
Bases on
next. (The original description said this targeted PR #169's branch and had to be merged after it; #169 has since landed asf7f141aand this branch was rebased ontonextcarrying only its own guard commit.)The gap
Deleting the
__BUILD_DEBUG__define frombuild.jsleavesmake checkgreen while every emitted bundle ships with the debug branch live.tests/loadssrc/shared/constants.jsoutside a bundle and takes the jest fallback, so the property being protected exists only in the emitted output and has to be asserted there.The check
build.jspassesmetafile: trueand writesdist/constants-bundles.txt: every emitted JS output whose input set includessrc/shared/constants.js. Nothing hardcoded, and the manifest is cleared at the start of each build so a stale one can never be verified against.src/shared/constants.jsexportsBUILD_DEBUG_MARKER, derived fromDEBUGitself, which the bundler constant-folds toautistmask-build-debug=offor=on. IfDEBUGis not known at build time the ternary survives and BOTH literals appear — which is the primary failure signal, not a gap.script/verify-buildcross-checks the two independent facts. Missing manifest, empty manifest, missing file, both markers, neither marker, wrong marker, a marker-carrying bundle absent from the manifest, and zero bundles inspected are each a hard failure. There is no path to exit 0 without positively identifying the expected marker.make buildandmake build-debug(the latter asserting the inverse), soscript/cibuildcovers it. Deliberately not inmake check, which would otherwise depend ondist/existing.Verification
make check,make build,make build-debugandscript/cibuildgreen on the consolidated tree, with the four bundles verifiedoff. Falsification re-run after the rebase: with the define deleted,make buildexits 2 with "carries both debug markers" whilemake checkstill exits 0 — the guard is the only thing that catches it.Diagnostic wording that overstates the consequence is tracked separately in #180.
Summary for review
Base is PR #169's branch, not
main. Deliberate, and the one place I wentagainst the issue's notes.
mainhas no__BUILD_DEBUG__define, so a guardbranched there could not be shown to trigger — there would be no define to
delete. Merge after #169; retarget to
mainthen. Full reasoning at the topof the PR body.
What to look at
The interesting decision is that the check reads two facts from two
independent sources, rather than inferring one from the other:
build.jstodist/constants-bundles.txt;BUILD_DEBUG_MARKER, a newconstant in
src/shared/constants.jsderived fromDEBUGand folded by thebundler to one of two string literals.
Collapsing these into one signal is the silent-pass failure: a marker-less
bundle would then be indistinguishable from
content/index.js, whichlegitimately has no marker, and the check reads green forever. Worth
attacking that reasoning if you think it is wrong.
The marker replaces the
DEBUG:!1substring match the issue mentioned,because a string literal is stable across esbuild versions and
!1is not.Its useful property is that when
DEBUGis not resolvable at build time thefold does not happen and both literals land in the bundle — which is
exactly the shape of the regression being guarded, so the ambiguity is the
detector rather than a hole in it.
Where it runs
Build path only.
make buildandmake build-debugboth invokescript/verify-build, andDockerfile:17runs a baremake build, so CIcovers it. Not in
script/check, on purpose: that would makecheckdepend on
dist/existing and pull a build into its time budget, and the"skip if
dist/is absent" workaround is precisely the silently-greenbehaviour the issue forbids. Per the steer, the build path wins and I am
saying so out loud.
Verification
make checkgreen: 5 suites, 57 tests, prettier clean.make buildgreen, 4 bundles verified off;make clean && make buildgreenfrom an empty
dist/.make build-debugandAUTISTMASK_DEBUG=1 make buildgreen, 4 bundlesverified on.
script/cibuild(realdocker build ., somake checkthenmake build)green end to end.
Both required failure modes, actually triggered
Full transcripts are in the PR body. In short:
make checkexits0on that same tree, andmake buildexits2withcarries both debug markers, so the build-time DEBUG value was never resolved and the debug branch is still live. Definerestored before committing; it is at
build.js:95.neither marker fails with
carries no debug marker, so its DEBUG state cannot be determined [...] Refusing to report success.I also triggered four further guards: debug artifacts against a release
verification, empty manifest, missing manifest, and a manifest gone
stale/short (caught by the cross-check that no unlisted bundle may carry a
marker — this is what stops the check trusting whatever the manifest happens
to say).
Things a reviewer might reasonably push on
build.jsgrew a localbundle()helper, which is why the four esbuildcall sites collapse in the diff. Bundler options are unchanged apart from
metafile: true. The motive is that a future entry point cannot silentlyskip metafile collection.
compiled bundle and are not offered as a substitute for the artifact check.
constants.js.Review: PR #178 — VERDICT: PASS
Reviewed at head
b5f3da2against baseacb5885(PR #169). Independentcheckout; nothing modified or committed (working tree verified clean at the
end of every experiment below).
1. Independent reproduction of the two required failure modes
I did not take the author's transcripts on trust. Both were reproduced from
scratch in an isolated worktree.
(a) Deleting the
__BUILD_DEBUG__define fails the build, whilemake checkstays greenRemoved
__BUILD_DEBUG__: JSON.stringify(debugBuild),from thedefinemap atbuild.js:95, thenmake clean && make build:Same tree, same commit, immediately after:
Confirmed:
make checkcannot catch this andmake buildnow does. Definerestored via
git checkout --;build.js:95intact.(b) An unrecognizable bundle fails rather than passes
Clean release build, then
printf 'var reminified = 1;\n' > dist/chrome/src/popup/index.js:Confirmed.
2. Additional failure modes the author did not demonstrate
Eight further attacks constructed against a known-good release
dist/. Allhard-fail; none produced a silent pass.
lists ..., which does not exist), exit 1chmod 000)index-old.js)-fandgrepboth follow the linkBUILD_DEBUG_MARKERdeleted fromsrc/shared/constants.js, then rebuiltmake buildFAILS (no marker);make testalso fails 2 testsAUTISTMASK_DEBUG=true make build(non-literal1)off— matchesbuild.js:48exactlyscript/verify-buildinvoked from an unrelated cwd (/tmp)I also re-ran the author's own guards (empty manifest, missing manifest, debug
artifacts against a release verification) and all reproduced exactly as
claimed.
Attempt to construct a passing-but-wrong scenario
I could not find one. The two-source design closes it: the manifest comes from
esbuild's metafile and is independent of the marker, so a bundle losing its
marker is still listed and still demanded to carry one; and the
check_unlisted_bundlescross-check means an under-generated or stale manifestcannot buy silence, because any unlisted
dist/**/*.jscarrying a marker isitself a failure. Renaming or removing
constants.jsempties the manifest,which the
-stest rejects.main()never reaches its successechowithoutcount > 0and every counted bundle matchingexpected.has_markerfailingfor any reason (unreadable, absent, truncated) resolves to "neither", which is
a hard failure, not an absence of evidence. Neither loop runs in a subshell, so
fail'sexit 1is always effective — verified empirically, not just byreading.
The author's claim that there is no path to exit 0 without positively
identifying the expected marker in at least one bundle holds up.
3. Two-source design
Sound, and the reasoning is correct. Collapsing the two facts would make a
marker-less bundle indistinguishable from
dist/chrome/src/content/index.js,which legitimately carries no marker (confirmed: 8 emitted
.jsfiles, 4 inthe manifest, and
check_unlisted_bundlespasses on a good build, i.e. theother four genuinely have no marker). Manifest staleness is handled from three
directions: pre-cleared at
build.js:117before anything is emitted, writtenlast at
build.js:196so a build that dies leaves none, and cross-checkedagainst the emitted tree at
script/verify-build:75-90. A manifest left overfrom a debug build cannot survive a release build, since the release build
deletes it and rewrites it.
4.
BUILD_DEBUG_MARKERriskNo new risk found. It is derived from
DEBUGon one line(
src/shared/constants.js:21-23), read nowhere at runtime, and its only effecton the artifact is a 25-byte string literal. It cannot alter behaviour because
nothing branches on it. It cannot become the thing that goes wrong silently: I
deleted it and both the artifact check and the jest suite failed.
DEBUGitself and the
typeofguard atconstants.js:8are byte-identical to #169.5. Scope, conventions, non-weakening
script/verify-buildis#!/bin/sh,set -eu, repo root viapwd -P,main "$@"at the bottom — matches the other twelve./bin/shonthis host is
dashand the script executes under it, so POSIX compliance isdemonstrated, not asserted. No
local, no[[, no arrays, no+=.build.jsdiff: thebundle()helper preservesbundle,format,platform,target,minifyanddefineverbatim; the only added option ismetafile: true. Justified, and it does stop a future entry point skippingmetafile collection.
isDebugBuild(), the define, and theconstants.jsDEBUG line are unchanged.make build-debugsucceeds and asserts the inverse (verified).constants.jsfails 2 of the 2 new tests.6. Base branch
The deviation from note 6 was the right call and is disclosed in the PR body,
the PR comment, and
TODO.md.mainhas no__BUILD_DEBUG__to delete, sodemonstration (a) — the whole point of the issue — would have been unrunnable.
Merge ordering is unmistakable. Verified:
b5f3da2sits directly onacb5885(linear), and
git merge-treeagainst currentmainreports zeroconflicts for both this branch and #169's, so the retarget is clean.
Merge order: #169 to
mainfirst, then retarget #178 tomainand merge.7. Policy
body. No attribution trailers.
b5f3da2subject ends with(closes #170); the PR title does too, sothe squash subject carries it either way.
RULES.mdunmodified.TODO.mdis a single four-line insert at the top ofCompleted Steps; stale Status/Next Step untouched, per note 6.
script/verify-build, plus a Debug Builds note.make checkgreen (5 suites, 57 tests, prettier clean), somake fmtisclean.
alone.
8. Verification runs (make targets and
script/only)make check— greenmake build— green, 4 bundles verifiedoffmake build-debug— green, 4 bundles verifiedonmake verify-buildstandalone — green on a release dist, fails on a debug onemake clean && make build— green from an emptydist/script/cibuild(realdocker build .) — green;verify-buildobservedrunning inside the image build, so the CI property holds
b5f3da2: success (check / check (push), 40s)Non-blocking findings
N1.
script/verify-build:55-57— the both-markers error message overstatesthe defect, and the same overstatement is in the commit body and PR body.
The message says the missing define means "the debug branch is still live". I
checked the actual emitted output. With the define removed, the bundle contains:
In extension context
__BUILD_DEBUG__is an undeclared global, sotypeofyields
"undefined"andDEBUGevaluates false at runtime. The artifact istherefore not drainable; what is actually broken is that the build no longer
resolves
DEBUGat build time at all, so the artifact's mode is decided by anundeclared global rather than by the build, and
AUTISTMASK_DEBUG=1silentlystops working. That is still worth failing on, and the message's remedy ("check
that build.js still defines
__BUILD_DEBUG__") is exactly right — but theseverity claim is wrong. The commit body's "silently restoring the
drainable-wallet vulnerability in every shipped artifact" inherits the same
claim from issue #170's text, so this is not the author's invention, but it does
write a false security assertion into repo history. Acceptable wording: "the
build-time DEBUG value was never resolved, so this build no longer honours
AUTISTMASK_DEBUG and the artifact's DEBUG state is not fixed at build time."
Worth a follow-up issue rather than a rework of this PR.
N2.
script/verify-build:38—2>/dev/nullinhas_markerconflates "grepcould not read the file" with "no match".
Reproduced with
chmod 000on a listed bundle: the script correctly hard-fails,but reports "carries no debug marker ... the emitted output changed shape",
which sends the reader hunting for an esbuild change instead of a permission
problem. Acceptable: distinguish grep exit 2 from exit 1 and report the I/O
error as its own condition. Outcome is already fail-safe, so this is diagnostics
quality only.
N3.
build.js:35andscript/verify-build:76both restrict to*.js.Consistent, so nothing is missed today (all four esbuild outfiles are
.js).But it is the one output shape that could escape the guard entirely: a future
bundle emitted as
.mjswould be excluded from the manifest and from theunlisted-bundle cross-check, so it would carry no marker requirement and raise
no error. Worth a comment noting the coupling, or widening the cross-check to
all regular files under
dist/.N4.
script/verify-build:48-52and:77share the unscoped global_file.shhas nolocal, andread_markerandcheck_unlisted_bundlescurrentlynever nest, so this is safe today. It is a latent trap if anyone later calls
read_markerfrom the cross-check loop.N5.
script/verify-build:118and:77—read -rstrips leading/trailingIFS whitespace and cannot represent a path containing a newline.
Purely theoretical given the fixed output names, noted only because interior
spaces are handled correctly while leading/trailing ones are not.
N6.
AUTISTMASK_DEBUG=1 make buildproduces a debug artifact that the guardreports as "verified".
Reproduced. This is a consistency check against the requested mode, not an
unconditional "release artifacts are never debug" assertion. Issue #170
explicitly requires
AUTISTMASK_DEBUG=1 make buildto succeed and assert theinverse, so this is per spec and not a defect; and
script/cibuildisdocker build ., which does not inherit host environment, so CI is unaffected.Recorded so the residual is on the record: the mode is announced loudly twice
(
Build mode: DEBUG (INSECURE ...)andexpecting autistmask-build-debug=on),so it is not silent.
N7. PR title is 86 characters. The repo's default merge style is squash, so
that becomes the landing commit subject. No policy rule mandates 72, and the
commit's own subject is well within it. Cosmetic.
None of N1-N7 is a correctness hole, none can cause a silent pass, and none
weakens the guarantee the issue asked for. Every item in the definition of done
is met and independently verified.
PASS. Merge after #169, then retarget to
main.Manager note (the review verdict is in its own comment above).
Independent adversarial review passed with no blocking findings. The reviewer
reproduced both demonstrated failure modes itself rather than trusting the
author's transcripts, and then constructed eight further attacks the author
never tried — manifest listing a nonexistent file, unreadable bundle via
chmod 000, a bundle truncated mid-marker, a stale debug artifact at anunlisted path, a listed bundle replaced by a symlink to the debug build,
BUILD_DEBUG_MARKERdeleted fromconstants.jsand rebuilt,AUTISTMASK_DEBUG=true, and running the script from an unrelated cwd. Allhard-failed. None silently passed.
It also verified the structural point that makes the design work: neither loop
runs in a subshell, so
fail'sexit 1always takes effect — checkedempirically, not by reading. And
check_unlisted_bundlesstops a short orstale manifest from buying silence, which was the hole I was most worried
about when I asked for two independent signals.
Marking
merge-readyand assigning to @sneak.Correction to this issue's own framing — my error
The reviewer inspected what the emitted bundle actually contains when the
define is deleted:
In extension context that global is undeclared, so
DEBUGevaluates tofalse at runtime and the artifact is not drainable.
So the claim I wrote into issue #170 — and repeated in the manager comment
dispatching it, and which the author then inherited into the commit body —
that deleting the define "silently restores the drainable-wallet vulnerability
in every shipped artifact" is overstated and wrong. I did not verify the
compiled output before asserting it; I reasoned from the source and assumed the
fallback would go the unsafe way. It goes the safe way.
The real defect the guard catches is still worth catching:
DEBUGwould nolonger be resolved at build time, leaving an unresolved ternary against an
undeclared global. That is a broken build contract and it means the release/
debug distinction has silently stopped being enforced at all — including, on a
future refactor, in whichever direction the fallback then happens to go. But it
is a correctness and build-integrity failure, not a live funds-loss one, and
this PR should not land carrying a scarier justification than the facts
support.
Nothing about the PR changes as a result; the guard is correct and worth having
on exactly the same terms. Only the wording is wrong, in three places
(
script/verify-build:55-57, the commit body, and issue #170), and that isfolded into the follow-up below rather than being churned into a rework.
Merge ordering — important
Base is
fix/issue-149-debug-build-flag, notmain. That deviation fromthe issue's note 6 was correct:
mainhas no define to delete, so the centraldemonstration would have been unrunnable. It is disclosed in the PR body, the
PR comment, and
TODO.md.Merge #169 to
mainfirst, then retarget this PR tomainand merge.git merge-treeshows zero conflicts against currentmainfor both branches.Non-blocking findings
Filed together as #180 against 1.0.0 — all three are in the new script and are
better fixed in one pass than churned here:
script/verify-build:55-57diagnostic per the correction above.script/verify-build:38—2>/dev/nullconflates "grep could not read thefile" with "no match", so an unreadable bundle fails safe but is
misdiagnosed.
build.js:35andscript/verify-build:76both filter to*.js, so a futurenon-
.jsbundle would escape both the manifest and the cross-check.Not acting on: the shared unscoped
_file(latent, safe today), whitespace inpaths via
read -r, andAUTISTMASK_DEBUG=1 make buildblessing a debugartifact — the last is explicitly required by #170 and Docker CI does not
inherit host env.
build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170)to WIP: build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170)WIP: build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170)to build: assert DEBUG is off in every emitted bundle as a post-build check (closes #170)clawbot referenced this pull request2026-08-10 15:49:44 +02:00
b5f3da2388to38596b4c79Rebase review at
38596b4— PASS: nothing lost or duplicated in the conflict resolution (.PHONYkeepstest-e2e, all 14script/entrypoints documented once, singleBUILD_DEBUG_MARKER/__BUILD_DEBUG__definition, TODO entry is a clean 3-line insert); guard re-falsified on the consolidated tree (define deleted atbuild.js:95->make buildexits 2,make checkstill exits 0; define restored);make check(6 suites / 117 tests),make build,make build-debug,script/cibuild(layersRUN make check/RUN make buildexecuted, notCACHED) andmake test-e2e(4/4, the one path CI does not cover) all green; commit subject ends(closes #170), no attribution trailers; CI success on the head commit; fast-forwards ontonext.Anomaly, non-blocking, no rework asked: the PR description still describes the pre-rebase state — it says this targets
fix/issue-149-debug-build-flagand "must be merged after" #169, which has already landed asf7f141a, and that it should later be retargeted tomain(base is nownext). Worth editing the description before merge; the squash subject and the commit message itself are unaffected.38596b4c79to627c0c158eclawbot referenced this pull request2026-08-11 14:23:43 +02:00