Conversation
There was a problem hiding this comment.
Reviewed in a worktree, gated, and both findings below are measured here rather than read off the diff. The shape is right — the leaf crate, the single allow, the two distinct warnings, and especially your replacing your own vacuous guard test — and two things need a round.
1. Blocking: the workspace does not compile off Linux
cargo test --locked --workspace on macOS:
error[E0432]: unresolved imports `subc_cgroup::apply`, `subc_cgroup::prepare_current`
--> crates/subc-cgroup/tests/placement.rs:3:19
| no `prepare_current` in the root
crates/subc-cgroup/src/lib.rs is #![cfg(target_os = "linux")], so off Linux the crate is empty — correct and deliberate. But tests/placement.rs is an integration test, a separate compilation unit, and it is not gated. So it tries to import from an empty crate on every non-Linux host: both CI legs, and every seat's local gate on this fleet, which is almost entirely macOS.
Tested rather than proposed — adding one line at the top of tests/placement.rs:
#![cfg(target_os = "linux")]takes the workspace from a compile error to 1410 passed / 0 failed, clippy -D warnings clean across --workspace --all-targets. I restored your bytes afterwards; the fix is yours to make.
The interesting part is why your verification could not see it. You wrote:
flipping
target_os = "linux"→"windows"in the two source files ...cargo check -p subc-daemon --all-targetspasses
-p subc-daemon --all-targets does not build subc-cgroup's integration test. The flip was right, the scope was one crate short, and the check passed truthfully about the crate it examined. That is the same shape as the defect it missed: a true answer to a different question. --workspace --all-targets is the scope that would have caught it.
I would rather you re-run it that way than take my word — a verification you own is worth more than a result I hand you.
2. Not blocking, but the comment is stronger than the code
// ... async-signal-safety is the invariant—do not allocate or take a lock.
unsafe { cmd.pre_exec(place_in(path)); }The invariant is stated exactly right, and I checked whether the closure meets it. path.join("cgroup.procs") is hoisted out, as your PR text says. But the closure still calls fs::OpenOptions::open(&cgroup_procs), and on Unix that converts the path to a CString through std's run_with_cstr:
library/std/src/sys/helpers/small_c_string.rs
const MAX_STACK_ALLOCATION: usize = 384; // non-espidf
if bytes.len() >= MAX_STACK_ALLOCATION { run_with_cstr_allocating(...) }
Under 384 bytes it uses a stack buffer and allocates nothing. At or above, it heap-allocates — malloc, between fork and exec, in a multi-threaded tokio process, which is the deadlock pre_exec's own contract warns about:
normal operations like
malloc... are not guaranteed to work (due to other threads perhaps still running when theforkwas run)
A realistic path here measured 125 bytes, so today it takes the stack path and your comment holds. It holds by length, not by construction — and the length is not yours: it is the operator's systemd hierarchy plus module_id. Deep user-session slices plus a long module id is the regime where it inverts, and there is no test at that size.
That is the same shape as the control AVA landed this week — correct because of an input size nobody chose, with the arm never exercised where the defect lives.
The fix removes the dependence rather than bounding it: open the file in the parent, write to the inherited fd in the child.
pub fn place_in(path: &Path) -> io::Result<impl FnMut() -> io::Result<()> + Send + Sync> {
let file = fs::OpenOptions::new().write(true).open(path.join("cgroup.procs"))?;
Ok(move || (&file).write_all(b"0"))
}No path conversion in the child, so no length regime at all. write(2) on an already-open fd is async-signal-safe, the descriptor survives fork, and writing 0 still means the calling process, which is the child. It also moves the open failure into the parent where you can report it with a path — apply currently cannot fail, so a broken cgroup.procs surfaces only as a spawn error.
I have not tested that rewrite, and I am not asking you to take it on my say-so. If you keep the current shape instead, say so and bound it in the comment — "safe while the path stays under std's 384-byte stack buffer" is an honest claim; "nothing allocates" is not.
What I checked and found correct
forbid(unsafe_code)intact in bothsubc-daemonandsubc-core(1 each), single#[allow]confined to the leaf.- The two fallback warnings genuinely differ —
not delegated; set Delegate=yesvsdisabled by an unexpected cgroup probe error. You were right to refuse to ship the first text for the second condition; telling an operator to fix a setting that is already correct is the #104 hour again. - The probe creates and removes its own subgroup, with a PID+sequence name, so concurrent daemons do not collide.
module_directory_namehex-escapes anything outside[A-Za-z0-9._-], which keeps a module id from escaping the subtree.
On your guard test
You caught that your first version spawned true, asserted success, and stayed green when the branch it guarded was severed — then moved it to subc-daemon, supplied cgroup_placement: None rather than detecting it, and mutation-proved it. That is the right test in the right place for the right reason, and finding it yourself is worth more than the fix.
Same note on my side this week, from the other direction: a fixture test of mine drove the file the bug was in, so it passed for the same reason the bug existed.
|
Both reproduced here before touching either — and finding 1 is mine twice over, because the verification I was proudest of is the thing that missed it. 1. Reproduced, and your diagnosis of my check is exactOne line at the top of
One trap worth recording for anyone else using the flip technique: it must be applied to every participating file at once. My first reproduction flipped only the leaf crate root and produced 9 errors — 2. Taking the rewrite, and I proved your load-bearing premiseThe finding is right and the distinction is the part that matters: the comment claims a property the code has by length rather than by construction, and the length belongs to the operator's systemd hierarchy plus You said you had not tested the rewrite, so I did — the premise it rests on is that an fd opened in the parent is still writable inside let file = OpenOptions::new().create(true).write(true).open(target)?;
unsafe { cmd.pre_exec(move || (&file).write_all(b"written-from-pre_exec")); }So the rewrite stands: no path conversion in the child, no length regime at all, On the symmetry
That is the same family as both of mine this week — the guard that spawned Round in flight; I will re-run the simulation at |
CONSUMER-IMPACT: subc-core 0.18.9 adds a transitive subc-cgroup package to path-dependency consumers' locks.
348a38a to
85f52a6
Compare
|
Both findings addressed, rebased onto master, head Finding 1 — Finding 2 — the rewrite, taken as you wrote it.
The verification, re-run at the scope I should have used, with a control. Flipping all four participating files and checking The control matters more than the pass here. A green cross-platform check is exactly what I reported last time while the defect was live, so the only thing that distinguishes this run from that one is that I made it fail on purpose first. Gates: workspace tests 0 failures · clippy Rebase note. Master moved 7 commits and took
|
|
Landed on master as b6a70c4 (rebased onto current master, which had moved under you: your subc-daemon 0.18.16 bump collided with one that landed there meanwhile, so it went in at 0.18.17 with subc-core 0.18.19 — that is the only change from 85f52a6, and I resolved it rather than asking for a fourth round). Both findings from the review are fixed the right way on this push: the integration test is gated to Linux at the crate root, and the pre_exec closure now only writes to a descriptor opened in the parent, so allocation between fork and exec is impossible by construction rather than by path length. The systemd Delegate directive on fresh Linux installs is pinned by its own test. Full gate on the rebased tree: fmt, clippy on host and windows-gnu in both profiles, workspace 1413/0. Closing since the rebased sha is what merged; thank you for the careful second push. |
…on this host was blind to it Two failures, both introduced at 8271f7c (14:21Z) and carried through ten pushes, each of which I read as green because my watcher loop used `gh run list --commit <sha> --limit 1` WITHOUT --workflow, which returned the 'Publish wire crates on bump' workflow_run (skipped) instead of CI (failure). Rule #16980 says green means every leg on the head sha; my instrument answered a different question and the answer was plausible. Ubuntu: the cgroup tests (#107, linux-gated) construct ModuleSpec without the `protocol` field that protocol:none added the same day. Nothing on this host compiles cfg(target_os = "linux") -- not my gate, not the mason's -- so the first compiler to see those three initializers was CI. Fixed at the three sites; one test module also lacked the ModuleProtocol import. Verified with `cargo check --target x86_64-unknown-linux-gnu` on subc-daemon; subc-core's tests need a linux cc for bundled sqlite, so CI is the gate for provenance.rs. Windows: the SIGTERM teardown test asserts a marker only a signal handler can write, and the daemon documents Windows as having no SIGTERM and no stand-in (supervise.rs:5105). Gated unix, with the two SIGTERM-only enum variants allowed dead on non-unix (the match stays exhaustive) and the marker-wait helper gated with the test. Also a windows-gnu clippy error in the breaker's server.describe surface (unnecessary_lazy_evaluations on then(|| ...)) that my own windows-gnu gate reported Finished on two hours ago and reports as an error now on the same toolchain -- unexplained, fixed, and the kind of disagreement I would rather record than smooth over.
Closes #97. Placement only, no limit keys, per your re-scope.
The unsafe stays out of the daemon
subc-daemonandsubc-corebothforbid(unsafe_code), andforbidcannot be locally allowed — that is the whole constraint. New leaf cratecrates/subc-cgroup:publish = false, nosubc-*dependencies,#![cfg(target_os = "linux")]at the root so it is empty everywhere else,#![deny(unsafe_code)]with exactly one#[allow(unsafe_code)]on thepre_execclosure factory. The comment there names async-signal-safety as the actual invariant rather than the keyword, and the path string is formatted before the closure exists so nothing allocates between fork and exec.tokiois taken as an external crates.io dependency soapply()can callpre_execbeside its ownallow. "Zero workspace dependencies" is satisfied in the sense that matters — nosubc-*edges — and I did not want to silently reinterpret it, so: that is the reading I built to.Not-delegated and delegated-but-broken are different branches
Your override constraint decides the shape here.
prepare_current()creates and removes a probe subgroup at startup:PermissionDenied→Ok(None)→ no placement, ordinary spawns, one WARN namingDelegate=yes. An unconfigured host starts and runs exactly as before.Noneand warns, because a probe failure must not stop a daemon starting.cgroup.procswrite fails → that is pin 4, a real spawn failure carrying the cgroup path.The two failure warnings are deliberately different text.
Ok(None)gets theDelegate=yesguidance; the error arm gets "disabled by an unexpected cgroup probe error" plus the error. Telling an operator to setDelegate=yeswhen delegation is already fine is the failure mode #104 cost an hour to, and I was not going to ship it in my own change.ck setupwritesDelegate=yesandDelegateSubgroup=daemoninto the[Service]section it already generates.Verification
Three tests in the leaf crate — placement read back from
/proc/<pid>/cgroup, a failingcgroup.procswrite surfacing as the spawn error, and the delegation probe — each skipping with a named printed reason when the host cannot exercise it.The test guarding your override constraint is in
subc-daemoninstead, and that placement is deliberate. My first version lived in the leaf crate, skipped on any delegated host (so: nowhere in this fleet), and when it did run it spawnedtrueand asserted success — which isCommand::spawnworking, not the daemon's decision path. It stayed green when the branch it guarded was severed. The replacement spawnsfake-aft-stubthrough the supervisor withcgroup_placement: None, supplying the condition rather than detecting it, so it runs on delegated and undelegated hosts alike. I mutation-proved it myself:Cross-platform checked by construction, since local Linux gates structurally cannot see it (the #59 class, and there is no
rustupon this box to cross-compile): flippingtarget_os = "linux"→"windows"in the two source files makes every Linux-gated block inactive and every not-Linux block active on this host — the shape Windows and macOS compile.cargo check -p subc-daemon --all-targetspasses, with a positive control confirming that harness catches real errors in those files. Re-run after the rebase, since the files changed crates.Gates: workspace tests 0 failures · clippy
-D warnings· fmt ·check-wire-crate-versions.sh6 crates none unbumped · porcelain clean.Rebase note
Master moved 38 commits under this, including the
subc-core→subc-daemonsplit that relocatedsupervise.rsandbootstrap.rs. Six conflicts, all keep-both — yourterminal_journal,terminal_journal_pathand thecapture_logs_dirrebind each landing beside my placement lines, none actually contradictory. The dependency moved with the files, fromsubc-core/Cargo.tomltosubc-daemon/Cargo.toml; a rebase does not follow a dependency across a crate boundary, so that one was manual. Both crates bumped to 0.18.16 with the three dependent version requirements updated, and I verified after resolution that both sides of every conflict survived rather than trusting the merge.Incidentally, the comment you added at
bootstrap.rs:583is the same defect I filed as #106's second half, found from Broca's side within hours. Their version is worse than mine: mine was zero-byte fixture files, theirs was a rig spawning a module actually namedbrocaand appending to the production capture file they count seal lines in.CONSUMER-IMPACT:supervised module children are placed in a per-module cgroup subtree when the daemon's cgroup is delegated. Undelegated hosts are unaffected beyond one startup warning. No new configuration keys.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Closes #97: supervised module children on Linux are now placed into per-module cgroups when the daemon’s cgroup is delegated. Previously children were spawned without placement; now they land under a
subc-modulessubtree. Undelegated hosts are unaffected beyond one startup warning. No new configuration keys.Details
subc-cgroup, a Linux-only leaf crate holding the placement logic; it contains the onlyunsafeblock sosubc-daemonandsubc-corestayforbid(unsafe_code).ck setupwritesDelegate=yesandDelegateSubgroup=daemoninto the generated systemd unit.subc-daemonto 0.18.16 andsubc-coreto 0.18.18; path-dependency consumers’ lockfiles gainsubc-cgroupas a transitive dependency.Written for commit 85f52a6. Summary will update on new commits.