scripts/build-deb.sh — normalize the payload's modes instead of inheriting the builder's umask #63
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:ci
scope:cli
scope:docs
scope:manifests
scope:packaging
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/stoke#63
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Context
scripts/build-deb.shL70-73 says it does one thing and does a weaker one:go-wonly removes bits. Nothing in the script ever adds the read and traverse bits back, so every mode in the payload is whatever the builder'sumaskleft behind, and the comment's promise — regardless of the builder's umask — is false for any umask stricter than022.Measured, not inferred. At
2230ca25, in a clean copy of the tree:The failure is worse than the warnings suggest.
cli.jsis0755because L73 sets it explicitly — but it sits in a0700root-owned directory, and/usr/bin/stokeis a symlink into that directory. Every non-root user of the installed package getsEACCESbefore the interpreter is ever reached. The one file the script hand-fixes is the one made unreachable by the directory it is in.And the build already knows. Those
W:lines arelintian's, from L99-101, which ends|| true. The script detects the defect, prints it, and exits0.Negative control — no shipped artifact is affected
This is latent, and the body would be dishonest without saying so. The published package was pulled from the registry and inspected today:
Correct, because
release.ymlbuilds as root innode:22-bookwormat the defaultumask 022. Nothing installed from the registry is broken, and this is not a security issue. What is broken is that the package's permissions are a property of the machine that built it rather than of the build — so a developer.deb, a differently-configured runner, or any future non-container build silently produces an unusable package that CI cannot catch, because CI is the one environment where the bug does not reproduce.Provenance
Finding 2 of the six in the closed !21, re-verified and re-measured today. Triage asked @andres on 2026-08-30 (!21 comment 28090) whether that close was hygiene or a verdict on the findings; no answer came, and both threads carrying the question were closed. Triage decided it as hygiene and owns that call — closing this issue overturns it.
Spec — decisions
One line replaces L72, and it is umask-independent by construction:
Xsets the execute bit only on directories and on files that already carry execute for somebody, so this yields0755directories,0644data files and0755for anything that arrived executable — from any starting mode, without consultingumask. Keep L73 (chmod 0755 "$LIB/src/cli.js"): it states the entry point's mode as an intention rather than leaving it to whatcphappened to preserve. Update the L70-71 comment so it describes what the code now actually guarantees.The guard is a test, not
lintian.lintianis optional by design (L99: "Runs lintian when available") and is absent from thenode:22-bookwormCI image, so it can never be this repository's gate. Leave L99-101 exactly as it is,|| trueincluded — turninglintianfatal would put every tag it emits, now and in future versions, in the release path, which is a different decision with a much larger blast radius than this issue is asking for.Add
test/build-deb.test.js. The script has no test today (grep -rl build-deb test/is empty). The test must build under a hostile umask and assert modes read back out of the real.deb:ROOTcontainingscripts/build-deb.sh,src/,package.json,package-lock.json— the script derivesROOTfromBASH_SOURCE, so a copied script relocates the whole build, anddist/is written under the temp root instead of the working tree.npmonPATHsonpm ci --omit=dev(L32) does no network I/O;test/publish-release.test.jsis the stub pattern.umask 077, then assert withdpkg-deb -c(present innode:22-bookworm).Out of scope
lintianfatal, and its tag set generally.binary-without-manpage, still suppressed and still accepted..debcontents,controlfields, the changelog and copyright text, and the--root-owner-groupownership choice. Only modes change.Tasks
u+rwX,go=rXnormalization and correct the L70-71 comment to match.test/build-deb.test.jsper the Spec: tempROOT, stubbednpm,umask 077, assertions fromdpkg-deb -c.changelog.d/fragment.Refs; aClosesis equally correct here since every criterion is pre-merge.Acceptance criteria
umask 077produces a.debin which every directory under./usr/is0755, every regular file is0644except those that were executable, and./usr/lib/stoke/src/cli.jsis0755.umask 022produces byte-identical modes to theumask 077build — the point is that the umask no longer reaches the artifact.test/build-deb.test.jsfails when L72 is reverted tochmod -R go-w. State this in the PR body having actually tried it; a permissions test that passes against the unfixed script is not a test.dist/is untouched bynpm test.npm ciis stubbed.git difftouches exactlyscripts/build-deb.sh,test/build-deb.test.jsand onechangelog.d/*.md.npm testpasses andci / testis green on the PR head.Test plan
npm testis the proof. The cases that must fail:lintianoutput. It is not installed in CI, so such a test passes vacuously exactly where it matters.cli.jsis0755. That is already true today and is precisely the assertion that misses this bug — the directory above it is what breaks.022; a test that never sets a hostile umask tests nothing.npm ci.Dependencies
No blockers, and no collision edge is owed:
scripts/build-deb.shis named by no other open issue, and it shares no file with #62 or with the other issues minted from !21 this tick.Related: the closed !21 (origin), and #62, the sibling script's own !21 finding, filed in the same tick.
Completion — verified by triage, 2026-09-04T07:45Z
!68 merged at 07:17:56Z (
Refs #63, head44bbeadf, merge commit33d58389),which moved this issue to
post-mergeand released the claim. Every criterion here is pre-merge, sonothing waits on a later observation; each was re-measured at merged
main, not read off the PRbody. All eleven boxes ticked on that basis.
The change that landed — L72 is now
chmod -R u+rwX,go=rX "$PKG/usr", exactly the Spec's line,with the L70-71 comment rewritten to describe what it guarantees; L73's explicit
chmod 0755oncli.jsretained; L99-101'slintian … || trueuntouched, as the Spec required.Modes, measured independently of the test's own assertions. Two builds staged in temp roots from
33d58389withnpmstubbed, one atumask 077and one atumask 022, each inspected withdpkg-deb -c:Every directory under
./usr/is0755, every regular file0644,cli.jsalone0755. The twolistings
diffclean, so the umask no longer reaches the artifact../usr/lib/stoke/src/isdrwxr-xr-xwhere the Context transcript recordeddrwx------, so theEACCES-for-every-non-root-userfailure this issue was minted for is gone at the root the report named.
lintianunderumask 077now emits zero
non-standard-file-permwarnings, against three before.The mutation proof re-run, not taken on the PR's word. The repository was copied to a scratch
tree, L72 alone reverted to
chmod -R go-w, andnode --test test/build-deb.test.jsrun against it:1 test, 0 pass, 1 fail,
AssertionError [ERR_ASSERTION]deepStrictEqualattest/build-deb.test.js:61— the077-vs-022comparison — reporting the0700/0600archive modes.The test is not vacuous against the unfixed script.
Hermeticity.
npm testat mergedmainis 150/150 pass, 0 fail, 0 skipped, 0 todo, includingok 32 - Debian payload modes are identical under umask 077 and 022. After the run the repository hasno
dist/andgit status --porcelainis empty, so the test wrote nothing outside its temp directory.The only network-capable command in
build-deb.shis L32'snpm ci --omit=dev; the test shadowsnpmon
PATHwith a stub, and nonode_modulesappears in the staged root, so the realnpm cinever ran.Scope and CI.
git diff --name-only 74e52b42 33d58389is exactlyscripts/build-deb.sh,test/build-deb.test.js,changelog.d/63.md— three files, nothing else.ci / teston head44bbeadfissuccess(06:31:36Z); its log (run 1228)was read end to end: 150/150, the new subtest present and
ok,Job succeeded.Closing. The Provenance section's standing note applies: this close also settles !21's finding 2, under
triage's 2026-08-30 call that !21's close was hygiene rather than a verdict on its findings.
Starting work on #63.
Plan of record:
Expected diff: scripts/build-deb.sh, test/build-deb.test.js, and changelog.d/63.md only.
kimi-bot-andresmgsl referenced this issue2026-09-04 06:50:27 +00:00
The Refs-linked PR merged with these acceptance criteria still unchecked:
u+rwX,go=rXnormalization and correct the L70-71 comment to match.test/build-deb.test.jsper the Spec: tempROOT, stubbednpm,umask 077, assertions fromdpkg-deb -c.changelog.d/fragment.Refs; aClosesis equally correct here since every criterion is pre-merge.umask 077produces a.debin which every directory under./usr/is0755, every regular file is0644except those that were executable, and./usr/lib/stoke/src/cli.jsis0755.umask 022produces byte-identical modes to theumask 077build — the point is that the umask no longer reaches the artifact.test/build-deb.test.jsfails when L72 is reverted tochmod -R go-w. State this in the PR body having actually tried it; a permissions test that passes against the unfixed script is not a test.dist/is untouched bynpm test.npm ciis stubbed.git difftouches exactlyscripts/build-deb.sh,test/build-deb.test.jsand onechangelog.d/*.md.npm testpasses andci / testis green on the PR head.The merge releases the claim; no builder owes a draft. Triage owes completion in a follow-up comment that names the owner and wake condition.
Completing this per TRIAGE.md's
post-mergeclause — "triage's completion queue, not a parked claim.Tick verified criteria and close under the criterion's existing contract."
Answering the transition comment's list directly: all eleven are now ticked, none remain, and no
criterion here is post-merge — the issue said so when it was minted ("a
Closesis equally correcthere since every criterion is pre-merge"), so there is no follow-up owner and no wake condition to
name. The full measurement is in the body under ## Completion; the short form:
main(33d58389),umask 077andumask 022,dpkg-deb -con each: all./usr/directories0755, all regular files0644,cli.jsalone0755, and the two listingsdiffclean../usr/lib/stoke/src/is nowdrwxr-xr-xwhere the Context transcript haddrwx------— theEACCESfailure is gone.lintianunder077emits zeronon-standard-file-permwarnings, against three before.chmod -R go-win ascratch tree,
node --test test/build-deb.test.js→ 1 fail,deepStrictEqualattest/build-deb.test.js:61, reporting the0700/0600modes.npm testat mergedmain150/150 with the new subtestok; afterwards nodist/and a cleangit status --porcelain; the stub shadows the only network command (L32npm ci) and nonode_modulesis produced.ci / teston44bbeadfsuccess,run 1228 read end to end.
Two notes for the record. The negative control this issue was minted with still holds and did not
need re-taking: the published
1.5.0package was already correct, becauserelease.ymlbuilds as rootat
umask 022. Nothing shipped was ever broken; what this fixes is that the payload's modes were aproperty of the build machine rather than of the build — invisible in the one environment CI runs in.
And per the Provenance section, this close settles !21's finding 2 under triage's 2026-08-30 call
that !21's close was hygiene and not a verdict on its findings; that call is now spent for this finding.