1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//! The ambient-git-environment scrub: one list, one constructor, every child.
//!
//! `git` exports `GIT_DIR`, `GIT_INDEX_FILE` and friends into every process it
//! starts — hooks above all. Those variables OUTRANK `-C <repo>` and
//! `current_dir`, so any `git` yog forks while such a variable is set is
//! silently re-aimed at the *outer* repo: a fixture the caller just built is
//! read straight past, and a production read of a workspace answers about
//! whatever repo the hook was committing to.
//!
//! **The scrub belongs to every child, not just to `git`** (bl-916a). Scrubbing
//! only yog's own `git` forks left the larger half open: `bl`, `litany`, `bz`,
//! an `$EDITOR` shim and the fake substrate scripts the suite drives all fork
//! `git` *of their own accord*, and they inherit whatever yog handed them. A
//! hook-run suite therefore still committed onto the branch being committed —
//! reproduced: the fake `litany new` arm's `git commit -m 'config: init
//! [config/default]'` landed on the outer work branch and replaced its tree.
//! Scrubbing at yog's spawn boundary clears the variables from the whole
//! descendant process tree at once, so no descendant needs to remember.
//!
//! The cure is not per-call vigilance — a spawn site that forgets is a defect
//! nobody sees until a hook runs it. It is that **no child is spawned by
//! hand**: [`command`] is the crate's one [`Command`] constructor ([`git`] is
//! its `git` spelling), it scrubs, and a caller cannot opt out because there is
//! nothing else to call. Enforced by `rules/no-bare-command.yml`.
//!
//! # The fork is the boundary too (bl-6397)
//!
//! Building every child here and then letting each caller fork it by hand left
//! a second per-call contract open, and it cost the suite a recurring flake.
//! `fs::write` on a fixture script holds a write fd; a `fork` in ANOTHER thread
//! copies that fd into a child that keeps it until its own `exec` completes; an
//! `exec` of the script inside that window is **ETXTBSY**. So a test that never
//! spawns anything can still redden a test three modules away, and the victim's
//! own care cannot save it.
//!
//! [`spawn`], [`output`] and [`status`] are therefore the crate's one fork, and
//! in `cfg(test)` they take one process-wide lock across the fork — the whole of
//! the discipline, in one place nobody has to remember. [`exec`] joins them for
//! the same reason and not the same hazard: it forks nothing, it *replaces*,
//! and the two process-global effects a RETURNING one leaves are its own
//! (bl-3792's `SIGPIPE` reset, which it repairs, and bl-419d's freed `environ`
//! copy, which nothing can — read [`exec`] there). Measured on this box
//! with 8 write-then-exec threads against an 8-thread fork storm, ~9,600 pairs
//! each: unguarded forks, 8.3% ETXTBSY; every fork through one lock, **zero** —
//! and zero *with the writes left entirely unguarded*, which is why the write
//! side needs no contract at all. Releasing the lock the instant the fork
//! returns is enough (a child's inherited fds are gone by then), so a child is
//! never waited on under it and the suite's subprocesses still run concurrently.
//!
//! ## The lock covers yog's forks, and the binary must contain no others
//!
//! **That "zero" was measured with every fork in the process going through the
//! lock — a CONDITION, not a property of the lock** (bl-6bf5). The lock is a
//! `cfg(test)` bracket around `Command::spawn` here; a fork performed by
//! another crate in this same process never passes through it. yog links its
//! substrate — `balls`, `litany`, `brazen` — and each forks on its own account
//! (this module says so below, at [`exec`]: *"the linked balls' own `git`
//! forks, which take no lock of yog's"*). A lib test that drives one of them
//! **in-process** puts an unlocked forker back in the binary, and every
//! write-then-exec fixture in it is a victim again.
//!
//! So the condition to keep is: **the lib test binary drives no embedded
//! substrate in-process.** A beat that must belongs in a `tests/*.rs` process
//! of its own — `tests/multiplex_bl.rs`, `tests/multiplex_litany.rs`,
//! `tests/multiplex_landing.rs` — each carrying the note that says why it may
//! not come back. That placement is also what lets those files scrub their own
//! process env of [`INHERITED`]: no spawn boundary exists to do it for a fork
//! they do not perform.
//!
//! Measured both ways, one filter over the lib test binary (`multiplex` plus
//! the five fixture-exec families), 16 workers x 70 iterations on a 16-core
//! box: with the landing repair's in-process `balls::substrate::found_landing`
//! still in the lib binary, **8 ETXTBSY failures**; with it moved out, **0** —
//! and 0 for the same victims with no substrate beat in the filter at all.
//!
//! **One unlocked forker remains and is not a test's to move** (bl-6bf5, filed
//! on as bl-fd28): `fan`'s production path opens balls' attempts, which forks `git` inside
//! balls, and `fan`'s beats are unit tests of `pub(crate)` code no `tests/*.rs`
//! can reach. Same filter with `fan::` in place of `multiplex`, same volume:
//! 2 ETXTBSY failures. Adding an in-process substrate drive here is therefore not "one
//! more like fan" — it re-opens a hole that is already costing the suite.
use Path;
use ;
/// The variables `git` exports into a hook's (or any child's) environment that
/// re-aim a child `git` at another repository. Public because a few callers
/// scrub the *process* environment rather than one child's — the multiplex
/// integration tests run their subject in-process — and that list must be this
/// list.
pub const INHERITED: & = &;
/// A command running `program` with every [`INHERITED`] variable removed from
/// the child's environment. The only lawful way to build a [`Command`] in this
/// crate — see the module doc for why there is no unscrubbed alternative.
/// [`command`] for `git` itself — the crate's git constructor.
/// Fork + exec `cmd` — **the crate's one fork**, and the only lawful way to
/// start a child here (`rules/no-bare-fork.yml`). Under `cfg(test)` the fork
/// happens under the binary-wide spawn lock; see the module doc for the race
/// that buys.
pub
/// [`spawn`] then read the child to EOF — [`Command::output`]'s behavior, with
/// its default stdio spelled out, so the lock covers the fork alone and never
/// the child's whole life.
pub
/// [`spawn`] then wait — [`Command::status`]'s behavior (stdio inherited unless
/// the caller said otherwise), with the lock over the fork alone.
pub
/// Replace this process image with `cmd` — **the crate's one `exec`**, and the
/// only lawful way to spend an `execve` baton here (`rules/no-bare-fork.yml`).
/// It returns only on failure, carrying that failure, because an `execvp` that
/// works never comes back.
///
/// **The return is the whole reason it lives here** (bl-3792).
/// `CommandExt::exec` does not fork: std's `do_exec` runs in THIS process and
/// resets `SIGPIPE` to `SIG_DFL` on its way to `execvp`, so what a failed exec
/// hands back is a live process in which the next write to a reader that went
/// away is a *death* and no longer a `BrokenPipe` error. Under `cargo test`
/// that process is the whole test binary and the writer is any peer thread, so
/// the death lands nowhere near the exec and reports no failing test — a
/// `signal: 13, SIGPIPE` on roughly one parallel run in four. Putting the
/// disposition back is therefore not the caller's errand any more than the git
/// scrub is: a contract a caller has to remember is a defect nobody sees until
/// it fires.
///
/// No spawn lock, and the asymmetry is the point: ETXTBSY needs a fork to copy
/// somebody's write fd into a child, and an exec forks nothing. It can only be
/// ETXTBSY's *victim*, and the discipline above already retired the party that
/// makes one.
///
/// # A returning exec has a SECOND global effect, and it cannot be repaired
///
/// `SIGPIPE` above is repairable because the damage outlives the call. The
/// other one does not (bl-419d). A `Command` that carries any env delta — and
/// [`command`] gives every command in this crate seven — makes std capture the
/// environment into a `CStringArray`, and `do_exec` points the process's own
/// `environ` at it on the way to `execvp`. A failed `execvp` restores the old
/// pointer, and then FREES that array as `exec` returns. std holds only the env
/// **read** lock across all of it, so a peer thread's env read runs
/// concurrently by design: it can be walking that array at the moment it is
/// freed, and what it hands back is freed memory — entries with interior NUL
/// bytes, which surface as `InvalidInput: "nul byte found in provided data"` at
/// that peer's next spawn, in a module the exec is nowhere near. Measured on
/// this box: 6 reader threads against a failing-exec loop, 73,435 torn entries
/// in 21.2M reads; zero with the env delta removed, which is the leg that
/// proves the swap is the party.
///
/// **The torn read has two faces, and the quiet one buys a wrong diagnosis**
/// (bl-2f8b). The NUL above is the loud one. The other is a peer whose spawn
/// cannot find its *program*: the `PATH` it read out of that array was gone or
/// garbage, `execvp` answers ENOENT, and the spawn fails
/// `NotFound: "No such file or directory"` — which reads as a missing
/// DIRECTORY, so the reader goes hunting a path bug. It was seen as a `git
/// status` refusing a landing checkout that existed and had just been
/// committed to, and it was filed against a `/var` vs `/private/var`
/// canonicalization split that the failing log itself refutes: both halves of
/// that path carry one spelling, and a split needs two. Both sightings landed
/// in `multiplex::landing`, which is simply the densest git-forker in the lib
/// binary and therefore the likeliest victim, never the author. That it showed
/// on macOS and not on Linux is the allocator: glibc's `free` leaves the bytes
/// readable, so the same race there usually reads intact.
///
/// Nothing here can fix it — the window is inside std, and the victim is any
/// env reader in the process, including the linked balls' own `git` forks,
/// which take no lock of yog's. So the discipline is placement, not repair:
/// **a returning exec belongs only in a process with no peer threads.** In
/// production that is where it already stands (`main.rs`, above eframe). Its
/// proof is `tests/exec_return.rs`, an integration binary with exactly one
/// `#[test]` — the `tests/multiplex_bl.rs` precedent — and that is why this
/// verb is `pub` while [`spawn`]/[`output`]/[`status`] are not. The lib suite
/// must never reach `execvp`: `multiplex::litany`'s unit test hands the arm a
/// command std refuses ABOVE `do_exec`, so it proves the arm without spending
/// either effect.