Skip to content

fix: regressions of the 1.1.21–1.1.25 hardening, invalid-JSON emitters, FwUsers GC, small correctness batch (1.1.27) - #230

Merged
click0 merged 1 commit into
mainfrom
claude/analyze-test-coverage-nCOJW
Sep 14, 2026
Merged

click0 merged 1 commit into
mainfrom
claude/analyze-test-coverage-nCOJW

Conversation

@click0

@click0 click0 commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

Step 2 of the agreed plan: regressions introduced by our own 1.1.21–1.1.25 hardening (found by an adversarial self-review of those diffs), plus two invalid-JSON emitters, the FwUsers GC gap, and a small correctness batch.

Regressions of our own fixes

  • cron/user rejected legitimate dotted usernames — lib/run_pure.cpp (MED). 1.1.22's validator allowed [A-Za-z0-9_-] only, but FreeBSD pw(8) accepts . — a spec with user: john.doe aborted crate run. . is now allowed (shell-inert; / excluded, ./.. reserved → no traversal). The 1.1.22 comment "empty is accepted unchanged (caller's default handling)" was false — no such handling existed, so user: "" reached /var/cron/tabs/ (a directory). run.cpp now maps empty → root before validating; the validator rejects empty.
  • validateStackIp never checked the CIDR suffix — lib/stack_pure.cpp. 10.0.0.5/999, /abc, / all passed. A / now requires a bare decimal prefix within the family's range.
  • crate migrate refused artifacts its own server produced — lib/migrate_pure.cpp. 1.1.21 rejected any .. substring, stricter than the daemon's validateArtifactName (allows . freely). app..v2 exported fine and then failed to migrate. With / excluded a single component cannot traverse; check dropped (exact ./.. stay reserved).
  • Trailing-slash prefix in datasetOwned/pathOwnedlib/privops_authz_pure.cpp (latent). Same class as the 1.1.25 safePath fix; both now share one guarded helper.
  • CHANGELOG correction: 1.1.25 said validateStackName enforces "no .." — it reserves only the exact ./...

Other

  • Two invalid-JSON emitters. hub/scheduling_pure.cpp jsonQuote wrote "\u" << std::hex << c with no width/fill (0x01 → \u1, stream left in hex mode) and compared a signed char (every UTF-8 byte "escaped"). daemon/routes.cpp log endpoint escaped only " \ \n \r \t (an ESC from a colour code went out raw). Both now emit \u00XX for all control bytes.
  • FwUsers had no dead-pid GC — lib/ctx.cpp, lib/clean.cpp (MED). FwSlots always GC'd; FwUsers did not, and crate clean's comment claimed it did while only locking/unlocking. A crate run killed without teardown left its pid forever → isEmpty() never true → shared NAT rule + ip.forwarding never restored. New FwUsers::garbageCollect (mirrors FwSlots), called from del() and from crate clean.
  • Small batch. Util::Fs::writeFile(data, fd) no longer closes the caller's fd on error (double close). S_ISREG/S_ISDIR instead of st_mode & S_IF* (a UNIX socket matched S_IFDIRdirectory_iterator threw out of findElfFiles, aborting crate create). gmtime_r in the snapshot route. socat proxy fork() failure now errors.

Tests

Updated/added in run_pure_test, stack_test, migrate_pure_test, privops_authz_pure_test (incl. hub_scheduling_pure_test re-run). All five pass locally. ctx/clean/util/routes changes are runtime-only → FreeBSD lite compile gate.

Deferred (E13)

The jail-name max-length drift (200 in privops_pure vs 64 in warm/backup/migrate vs 63 in vmwrap) is deliberately not patched here — it needs one shared constant across four modules and belongs to the checkSafeName refactor (step 4).

Version

Bumps to 1.1.27; CHANGELOG.md + docs/trust-model.{md,uk.md} + TODO updated.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK


Generated by Claude Code

…x, migrate '..', pathOwned trailing slash), invalid-JSON emitters, FwUsers GC, small correctness batch (1.1.27)

Regressions of our own fixes, found by an adversarial self-review:

- run_pure.cpp validateCronUser: allow '.' (FreeBSD pw(8) accepts it;
  1.1.22 rejected `john.doe` specs). Reject empty; run.cpp maps an
  empty spec value to the documented "root" default first — the 1.1.22
  claim of "caller's default handling" was false and `user: ""` reached
  /var/cron/tabs/ (a directory).
- stack_pure.cpp validateStackIp: validate the CIDR suffix (1.1.25 let
  10.0.0.5/999, /abc, / through).
- migrate_pure.cpp validateArtifactFile: drop the any-".."-substring
  rejection — stricter than the server that produces the name, and with
  '/' excluded a single component cannot traverse.
- privops_authz_pure.cpp datasetOwned/pathOwned: trailing-slash prefix
  guard (same class as the 1.1.25 safePath fix), shared helper.

Other:

- hub/scheduling_pure.cpp jsonQuote emitted `\u1` (no width) and left
  the stream in hex mode; compared signed char (UTF-8 bytes "escaped").
  daemon/routes.cpp log endpoint escaped only " \ \n \r \t. Both now
  emit \u00XX for every control byte.
- ctx.cpp/ctx.h/clean.cpp: FwUsers::garbageCollect (mirrors FwSlots),
  called from del() and from `crate clean` — stale pids from SIGKILLed
  runs kept the shared NAT rule + ip.forwarding alive forever.
- util.cpp writeFile(data, fd) no longer closes the caller's fd on
  error (double close); S_ISREG/S_ISDIR instead of `& S_IF*` (a UNIX
  socket matched S_IFDIR and aborted `crate create`); routes.cpp
  gmtime_r; run_services.cpp errors on socat fork() failure.

Tests updated in run_pure/migrate_pure/privops_authz_pure/stack. Bumps
to 1.1.27; CHANGELOG (incl. a correction to the 1.1.25 wording) +
trust-model; TODO updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK
@click0
click0 merged commit 52f2168 into main Sep 14, 2026
2 checks passed
Sign up for free to 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.

2 participants