Skip to content

fix(chart): stop init-writable-data skipping the chmod when the chown fails (#672) - #689

Merged
shujaatTracebloc merged 5 commits into
developfrom
fix/672-init-writable-data-independent-chmod
Aug 13, 2026
Merged

fix(chart): stop init-writable-data skipping the chmod when the chown fails (#672)#689
shujaatTracebloc merged 5 commits into
developfrom
fix/672-init-writable-data-independent-chmod

Conversation

@shujaatTracebloc

@shujaatTraceblocshujaatTracebloc commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Fixes#672.

The bug

init-writable-data chained the two repairs:

chown 1000:1000 "$d"&& chmod "$m""$d"||echo"… could not adjust $d …; leaving as-is"

A refused chown short-circuits, so the chmod never runs — and the message says
"leaving as-is", which reads as "nothing could be done" when the one operation that
mattered was never attempted.

Why it's worse than a cosmetic ordering nit

The mode, not the ownership, is what makes these trees usable. kubelet ignores
fsGroup on hostPath (kubernetes/kubernetes#138411), so /data/shared has to be
other-writable for the ingestion Job (uid 65534, or HOST_UID) and the CLI
staging/teardown pod (uid 65532) — neither is 1000, and neither shares a group with it.
The chown to 1000 is cosmetic next to that.

It is also the call most likely to fail: on a Windows/Docker-Desktop bind mount or an
NFS root_squash export, chown is exactly what gets refused while chmod would have
succeeded. So the least important failure was cancelling the most important repair, which
silently nullified the 2777/3777 split from #667on the platform that split was written
for
. Losing the chmod is strictly worse than losing the chown. Symptom is #653's
mkdir: can't create directory '/data/shared/.tracebloc-staging/': Permission denied.

The fix

chown and chmod are now separate best-effort statements, each recording whether it
failed, and the per-dir verdict is graded on the mode observed afterwards (ls -ldn)
rather than on either exit status — a bind mount can accept a chmod and ignore it, so an
exit code is not evidence. A partial result is reported as one:

init-writable-data: OK /data/shared other-writable, want 2777 got drwxrwsrwx uid 0 (chown failed; other-writable regardless)
init-writable-data: FAIL /data/shared want 2777 got drwxr-xr-x uid 0 (chown+chmod failed)

The verdict grades other-writability alone — that is what decides whether those uids can
work, and failing a mount that granted other-write while dropping S_ISGID would cry wolf on
a working install. So the label names the one thing it verified and always prints want vs
got, rather than a bare "OK" that could be read as "the whole mode landed".

stderr from the failing call is deliberately not suppressed, so the real errno
(Operation not permitted vs Read-only file system) lands in kubectl logs next to the
verdict. That is the one intentional divergence from Get-ReleaseDirsPrepCommand, which
suppresses it because its output is a user-facing installer line.

Unchanged: per-dir modes (/data/shared 2777 setgid-no-sticky, /data/logs 3777
setgid+sticky), per-dir independence, non-fatal behaviour, POSIX sh for busybox. Kept
diffable by eye against the installer's Get-ReleaseDirsPrepCommand, which already uses
the same path:mode spec and the same ls -ldn verification.

Verified by executing the rendered command, not by reading it

Every run below uses command[2] extracted from helm template output, in busybox:1.35.

ScenarioResult
sh -n, dash -n, bash --posix -nclean
busybox as root (happy path)/data/shareddrwxrwsrwx, /data/logsdrwxrwsrwt, uid 1000, exit 0
--cap-drop CHOWN (chown refused, chmod permitted)modes still landdrwxrwsrwx / drwxrwsrwt, exit 0
…same conditions, old commandboth dirs stay drwxr-xr-x while logging "leaving as-is"
/data/shared read-only (both calls fail)FAIL with the real errno; /data/logs still fixed; exit 0

End-to-end on a shared volume, after a refused chown: uid 65534 creates
.tracebloc-staging/ and writes /data/logs; uid 65532 unlinks uid 65534's entries in
/data/shared (no sticky) but is correctly refused in /data/logs (sticky). Both halves
of the #667 split still hold.

Tests

New #672 case in client/tests/jobs_manager_test.yaml, checked in both directions: it
fails against the old command and passes against the fix. Existing assertions kept and
updated for the multi-line command ((?s) where a pattern now spans lines).

One trap worth flagging for review: the obvious comment-scoped guard
^[^#\n]*chown.*&&.*chmod is silently vacuous here — the loop body legitimately
contains a # (${e#*:}) before the chown, so [^#\n]* can never reach it and the
assertion passes against the very one-liner it exists to reject. I hit this and confirmed
it by running the assertion against the old command. The guard is therefore unscoped, and
the template describes the old shape in words ("chained with a logical AND") rather than in
code so prose can't trip it.

  • helm unittest client -f 'tests/jobs_manager_test.yaml': 35 passed (was 34)
  • full suite: 380 passed (was 379); develop's baseline of 5 failed / 5 errored / 4 failed
    suites is unchanged — verified against a clean origin/develop checkout, not assumed
  • helm lint: unchanged (pre-existing clientId / clientPasswordminLength errors only)
  • scripts/chart-version-guard.sh with BASE_SHA=origin/develop: ✓ (1.9.33 → 1.9.34,
    version + appVersion in lockstep)

Second commit: two overclaims in my own reporting

Found by running the failure paths, not reading them — same family as the bug being fixed, so
worth calling out rather than burying:

  1. The pass label was a bare OK, which reads as "the whole mode landed" when only
    other-writability was checked. Now OK <dir> other-writable, want … got ….

  2. Worse: the partial-result note said "mode applied anyway" whenever a call failed. On a
    dir that was already other-writable where both calls were refused, that is false —
    nothing the container did applied anything. Reproduced in busybox:1.35 (pre-set 1777,
    run as a non-owner uid so both calls are refused):

    want 2777 got drwxrwxrwt uid 0 (chown+chmod failed; mode applied anyway)
    

    Now reads (chown+chmod failed; other-writable regardless) — the observation, not a causal
    claim it cannot support. A notMatchRegex pins the old wording out.

Notes

🤖 Generated with Claude Code


Note

Medium Risk
Changes privileged init behavior on every hostPath jobs-manager rollout; impact is positive for common mount types but alters logging and permission repair semantics at cluster bootstrap.

Overview
Fixes#672: on hostPath installs, init-writable-data no longer chains chown and chmod with &&, so a refused chown (bind mounts, NFS root_squash, etc.) no longer skips the load-bearing chmod that makes /data/shared and /data/logs usable for non-1000 writers.

The init script is now a multi-line shell block: separate best-effortchown and chmod, per-dir OK/FAIL lines graded on observed mode via ls -ldn (other-write only, with explicit want vs got), always exit 0, and stderr left visible in pod logs. Template comments document the control-flow bug and alignment with the PowerShell installer prep path.

Chart bumps to 1.9.37 (version / appVersion). Tests update existing #611 assertions for the new script shape and add a dedicated #672 case (no chown…&&…chmod, ls -ldn check, no mode applied anyway). .cursor/BUGBOT.md adds a standing rule to flag independent repairs joined by && when the second operation matters.

Reviewed by Cursor Bugbot for commit 56171e8. Bugbot is set up for automated code reviews on this repo. Configure here.

… fails (#672)
init-writable-data ran `chown 1000:1000 "$d" && chmod "$m" "$d" || echo …`, so a
refused chown short-circuited the chmod and the mode was never applied — while the
message said "leaving as-is", implying nothing could be done.
That inverts the priority. kubelet ignores fsGroup on hostPath
(kubernetes/kubernetes#138411), so the MODE is what makes these trees usable:
/data/shared must be other-writable for the ingestion Job (uid 65534, or HOST_UID)
and the CLI staging/teardown pod (uid 65532), neither of which is 1000 nor shares a
group with it. The chown is cosmetic next to that, and it is also the call most
likely to be refused — on a Windows/Docker-Desktop bind mount or an NFS root_squash
export it is precisely what fails. So the failure that mattered least was cancelling
the one that mattered most, silently nullifying the 2777/3777 split from #667 on the
platform that split was written for. Symptom: #653's
`mkdir: can't create directory '/data/shared/.tracebloc-staging/': Permission denied`.
The chown and the chmod are now separate best-effort statements, each recording
whether it failed, and the per-dir verdict is graded on the mode OBSERVED afterwards
via `ls -ldn` rather than on either exit status — a bind mount can accept a chmod and
ignore it, so an exit code is not evidence. A partial result is reported as such
("chown failed; mode applied anyway") instead of implied. Unchanged: per-dir modes,
per-dir independence, non-fatal behaviour, POSIX sh for busybox. Kept diffable by eye
against the installer's Get-ReleaseDirsPrepCommand, which already does it this way.
Verified by executing the helm-rendered command[2], not by reading it:
- sh -n, dash -n, bash --posix -n all clean
- busybox:1.35 as root: /data/shared drwxrwsrwx, /data/logs drwxrwsrwt, exit 0
- busybox:1.35 with --cap-drop CHOWN (chown refused, chmod permitted): modes STILL
land drwxrwsrwx / drwxrwsrwt; the old command leaves both at drwxr-xr-x
- /data/shared read-only (both calls fail): FAIL reported with the real errno,
/data/logs still fixed, exit 0
- end-to-end on a shared volume after a refused chown: uid 65534 creates
.tracebloc-staging and writes /data/logs; uid 65532 unlinks uid 65534's entries in
/data/shared (no sticky) but not in /data/logs (sticky) — both splits intact
Tests: the new #672 case fails against the old command and passes against the fix.
The obvious comment-scoped guard (`^[^#\n]*chown.*&&.*chmod`) is silently VACUOUS —
`${e#*:}` puts a '#' before the chown — so the guard is unscoped and the template
describes the old shape in words instead. Existing assertions kept, updated for the
multi-line command. jobs_manager_test.yaml 34 -> 35 passing; full suite 379 -> 380
passing with develop's 5 failed / 5 errored baseline unchanged.
Also adds the recurring-finding rule to .cursor/BUGBOT.md per CLAUDE.md.
Refs #672, #667, #653, #654
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
shujaatTraceblocand others added 2 commits August 12, 2026 16:20
Two overclaims in the first commit's own reporting, both found by running the
failure paths rather than reading them — the same family as the bug being fixed.
1. The verdict grades other-writability alone (correctly: that is what decides
whether uid 65534/65532 can work, and failing a setgid-stripped-but-writable
mount would cry wolf on a working install). But it labelled that bare "OK",
which reads as "the whole mode landed". Now says "OK <dir> other-writable" and
always prints want vs got, so a mount that granted other-write while dropping
S_ISGID is visible instead of implied.
2. Worse: the partial-result note said "mode applied anyway" whenever any call
failed. On a dir that was ALREADY other-writable and where BOTH calls were
refused, that is simply false — nothing this container did applied anything.
Reproduced in busybox:1.35 (pre-set 1777, run as a non-owner uid so chown and
chmod are both refused):
want 2777 got drwxrwxrwt uid 0 (chown+chmod failed; mode applied anyway)
Now reads "(chown+chmod failed; other-writable regardless)" — it claims the
observation, not a causal link it cannot support.
Re-verified on the helm-rendered command[2]: sh -n / dash -n / bash --posix -n
clean; root happy path lands drwxrwsrwx + drwxrwsrwt; chown-refused still lands
both modes; already-1777 with both calls refused now reports truthfully; read-only
/data/shared still FAILs with the real errno while /data/logs is still fixed;
exit 0 throughout. Tests pin both strings, including a notMatchRegex on the old
"mode applied anyway" wording. 35 passing, full suite 380 with develop's
5 failed / 5 errored baseline unchanged.
Refs #672
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…672)
The long "why other-writability alone is the pass condition" prose was inside the
script passed to `sh -c`, so it shipped in the pod spec and showed up in every
`kubectl get deploy -o yaml`. It belongs in the YAML comment above, which does not.
Left a two-line pointer where a script editor will see it.
Also records the one intentional divergence from the installer's
Get-ReleaseDirsPrepCommand: the chart does not redirect chown/chmod stderr to
/dev/null, so the real errno (Operation not permitted vs Read-only file system)
lands in `kubectl logs` next to the verdict. The installer suppresses it because its
output is a user-facing progress line; an init container's log is a debugging surface,
and hiding the errno there would remove the evidence a reader needs.
Comment-only inside command[2]: re-rendered and re-ran the chown-refused path in
busybox:1.35 to confirm byte-identical output and modes (drwxrwsrwx / drwxrwsrwt,
exit 0). 35 passing; full suite 380 passing, baseline unchanged.
Refs #672
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@shujaatTracebloc

Copy link
Copy Markdown
ContributorAuthor

bugbot run

…ed (#672)
The rule said "for two releases". Verified against git instead: the
`chown … && chmod …` shape entered in chart 1.9.20 (#611/#612, commit a07f76b) and
survived every version through 1.9.33 — thirteen chart versions, not two. #667
(7852f02) rewrote the modes on that exact line and left the chain untouched, which
is the more useful half of the lesson: the line was re-read for its modes and not
for its control flow. Also corrects the issue's attribution of the chain to #667.
Refs #672, #667, #611
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ae7471a. Configure here.

@shujaatTracebloc

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 4489aba. Configure here.

@shujaatTracebloc
shujaatTracebloc marked this pull request as ready for review August 12, 2026 15:38
@LukasWodka
LukasWodka requested review from saqlainsyed007 and removed request for saadqbalAugust 13, 2026 06:10
…ble-data-independent-chmod
# Conflicts:
#	client/Chart.yaml
@shujaatTracebloc

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 56171e8. Configure here.

@shujaatTracebloc
shujaatTracebloc merged commit 9e3f50d into developAug 13, 2026
22 checks passed
@shujaatTracebloc
shujaatTracebloc deleted the fix/672-init-writable-data-independent-chmod branch August 13, 2026 07:26
@shujaatTracebloc

Copy link
Copy Markdown
ContributorAuthor

Two corrections to the description above (post-merge)

Leaving the body as-written rather than editing it — the verification lines record what actually ran at the time, and rewriting them would falsify that record. Correcting here instead.

1. The blast radius is wider than stated. The Notes say the chain "shipped in every version through 1.9.33 — thirteen chart versions". That was true when written; develop had moved by merge time. Re-derived just now: the chain entered at a07f76b (chart 1.9.20, #611/#612) and shipped through 1.9.36, so it is seventeen chart versions, and this fix lands in 1.9.37 (not 1.9.34 as the version-guard line records). Any hostPath edge installed on 1.9.20–1.9.36 whose mount refused the chown has had a root:root 0755/data/shared the whole time — that's the range to check against, not 1.9.20–1.9.33.

2. The "Out of scope … Worth its own ticket" note is obsolete — please don't file that ticket. It flags the dirs the ingest creates inside/data/shared coming out drwxr-sr-x and attributes it to the ingest's umask. Both halves have since been superseded:

  • The ticket was filed — backend#1833 — and it corrected the diagnosis: the ingestor sets umask(0o002), so the mode was never the problem. The real cause is DEST_PATH inheriting group 1000 from the setgid mount plus a silent S_ISGID strip (the process has no CAP_FSETID), landing it at 0775 group-1000 — where the teardown pod (uid 65532) falls through to other = r-x and cannot unlink anything directly inside. Also noted there: /data/shared/.tracebloc-staging/<table> is written by the CLI stage pod at the same uid as teardown, so it was never at risk — the failure is on the ingestor's FinalDestPrefix tree.
  • And it was already fixed before that ticket was filed — closed as a duplicate of data-ingestors#477 (9e20849, DEST_DIR_MODE0o27750o2777), tracked as Windows: first data ingest fails with Permission denied on /data/shared — installer never pre-creates the hostPath PV dirs (bash does) #653.

So the follow-up this PR asks for is done; acting on the note as written would duplicate a closed ticket using a diagnosis that's been disproven.

Neither correction affects the change that merged — the chart fix itself is unchanged and stands.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

init-writable-data skips the chmod whenever the chown fails (chown && chmod)

3 participants

@shujaatTracebloc@saadqbal@LukasWodka