Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
status: open
kind: tooling
opened: 2026-09-29
---

# A pipe made on macOS can leak into a sibling's spawn and red the tether tests

`src/tether.rs`'s `Owner` judges its tethered children by the end of the
owner's stderr pipe: the end is every holder exited. On macOS, std makes that
pipe with `pipe()` and marks it close-on-exec afterwards
(`library/std/src/sys/pipe/unix.rs`: `pipe2` is only used on the targets that
have it, and macOS does not). A process another thread of the same test binary
spawns in that window inherits the write end and holds it until it exits.

Both arms run beside sibling spawns: `a_tethered_child_dies_with_its_owner`
in `cargo test -p toyos-build --lib`, whose other tests spawn processes, and
`guest_dies_with_its_harness`, a `Sched::Parallel` test in the harness, beside
compiles and other guests' QEMUs. Such a leak turns a working tether into a
red: the pipe does not end within `WITHIN`, the owner's group is killed, and
the refusal says a holder outside that group still ran.
`toyos-tmpdir/tests/reclaim.rs` serialises its own spawns (`SPAWNING`) against
exactly this, and nothing here does.

Owner: whoever next changes `src/tether.rs`. Exit condition: the verdict no
longer depends on the end of a pipe made on macOS without close-on-exec — the
pipe made atomically close-on-exec, or every spawn in each process that runs an
`Owner` serialised against its making, or a verdict read off something other
than a pipe's end.
2 changes: 2 additions & 0 deletions issues/build/the-build-system-does-not-compile-on-windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ there. Every other crate in the graph, first-party and third-party, checked
clean. The `#[cfg(unix)]` at `src/ci.rs:489` is still the only conditional
compilation in the build system.

`src/tether.rs` is a fourth: `std::os::unix` and a pseudo-terminal per child, behind a Linux and macOS `cfg` pair with no Windows arm; `portability-windows` in run 36351950439 fails on it.

## The judge, and it needs no Windows host and no download

```
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub mod soundfont;
pub mod sourcegate;
pub mod sysroot;
pub mod testargs;
pub mod tether;
pub mod tiers;
pub mod toolchain;
pub mod userlandhost;
Expand Down
12 changes: 9 additions & 3 deletions src/sourcegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,9 +629,15 @@ const HOST_SPAWNS: &[Spawn] = &[
},
Spawn {
arg: "std::env::current_exe().unwrap()",
sites: &[("src/buildlock.rs", 1), ("toyos-tmpdir/tests/reclaim.rs", 1)],
why: "a test binary re-running itself: the build system under the lock, and a \
scratch holder whose death is what is judged",
sites: &[
("src/buildlock.rs", 1),
("src/tether.rs", 1),
("tests/common/orphan.rs", 1),
("toyos-tmpdir/tests/reclaim.rs", 1),
],
why: "a test binary re-running itself: the build system under the lock, and an \
owner whose death is what is judged — of its scratch, its tethered child and \
its guest",
},
Spawn {
arg: "env!(\"CARGO_BIN_EXE_toyos-ld\")",
Expand Down
26 changes: 26 additions & 0 deletions src/testargs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ declare_flags!(pub SUITE = {
/// machine is not touched**: the run builds the images and writes down what
/// to run on them, or judges readbacks a driver already left there.
pub METAL_READBACK = "--metal-readback", Next;
/// The owner `guest_dies_with_its_harness` kills: the image it names,
/// booted and held until stdin ends. Alone on its line.
pub HOLD = "--hold", Next;
});

/// Validate the harness's argv and return the run's filter.
Expand All @@ -183,6 +186,7 @@ pub fn parse(args: &[String]) -> Result<Option<&str>, String> {
if let Some(refusal) = line.malformed() {
return Err(refusal);
}
let flags = line.seen.len();

let mut filter: Option<&str> = None;
for word in line.positionals {
Expand Down Expand Up @@ -236,6 +240,13 @@ pub fn parse(args: &[String]) -> Result<Option<&str>, String> {
}
}
}
if has(&HOLD) && (flags != 1 || filter.is_some()) {
return Err(
"--hold boots the image it names and holds it, and reads nothing else on the line; \
every other word would be dropped in silence"
.to_string(),
);
}
Ok(filter)
}

Expand Down Expand Up @@ -510,6 +521,8 @@ mod tests {
vec!["--debug"],
vec!["--metal"],
vec!["--metal", "--metal-readback", "target/metal"],
vec!["--hold", "boot.img"],
vec!["--hold=boot.img"],
] {
assert!(parse_owned(&argv).is_ok(), "{argv:?}");
}
Expand All @@ -525,4 +538,17 @@ mod tests {
let refusal = parse_owned(&["--metal", "--audio-gate", "30"]).unwrap_err();
assert!(refusal.contains("cannot be combined"), "{refusal}");
}

#[test]
fn hold_is_alone_on_its_line() {
for argv in [
&["--hold", "boot.img", "boot"][..],
&["--hold", "boot.img", "--nightly"],
&["-j", "2", "--hold", "boot.img"],
&["--hold"],
] {
let refusal = parse_owned(argv).unwrap_err();
assert!(refusal.contains("--hold"), "{argv:?}: {refusal}");
}
}
}
Loading
Loading