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
217
218
219
220
221
222
223
224
225
226
//! 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, amended bl-fd28)
//!
//! Building every child here and then letting each caller fork it by hand left
//! a second per-call contract open. [`spawn`], [`output`] and [`status`] are
//! therefore the crate's one fork — one place to reason about what a child
//! inherits, instead of a promise at every call site. [`exec`] joins them for a
//! different 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). `rules/no-bare-fork.yml` holds the
//! chokepoint.
//!
//! ## ETXTBSY is closed on the WRITE side, and the lock is gone (bl-fd28)
//!
//! The fork carried a second job for two rounds and no longer does. `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 this module took one
//! process-wide lock across the fork under `cfg(test)`: measured with 8
//! write-then-exec threads against an 8-thread fork storm, ~9,600 pairs,
//! unguarded forks cost 8.3% ETXTBSY and every fork through one lock cost zero
//! (bl-6397).
//!
//! **That zero was a CONDITION, not a property of the lock** (bl-6bf5). The
//! lock was a `cfg(test)` bracket around `Command::spawn` HERE, and a fork
//! performed by another crate in this same process never passed through it. yog
//! links its substrate — `balls`, `litany`, `brazen` — and each forks `git` 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 beat that drove one of
//! them in-process was an unlocked forker for as long as it ran. Measured over
//! one filter, 16 workers x 70 iterations on a 16-core box: the landing
//! repair's in-process `balls::substrate::found_landing` cost **8** ETXTBSY
//! failures, and `fan`'s attempt machinery another **2**. bl-6bf5 answered by
//! moving the beat out to a `tests/*.rs` of its own; `fan` could not follow,
//! its beats being unit tests of `pub(crate)` code no `tests/*.rs` can reach.
//!
//! bl-fd28 closed the hazard on the side that owns it instead. **Every
//! executable fixture in this crate is written by a CHILD** —
//! [`crate::test_support::write_exec`] for the lib binary,
//! `tests/support/write_exec.rs` for the integration ones, feeding the body to
//! `sh -c 'cat > "$1" && chmod 755 "$1"'` — so the write fd never exists in
//! this process and a peer fork, in ANY crate, locked or not, has nothing of
//! ours to copy. `rules/no-hand-chmod.yml` makes it structural rather than a
//! convention.
//!
//! **This module's older claim that "the victim's own care cannot save it" is
//! retired with the lock.** It was true of a write-side *bracket* — a lock that
//! only excludes forks agreeing to take it — and false of a write-side
//! *relocation*, which leaves no descriptor for anyone to inherit.
//! `tests/integration/support/mod.rs` had already reached that answer for the
//! reason that proves it: yog linked as a library is not `cfg(test)`, so no
//! lock of this module's was ever available to that binary, and writing the
//! fixture from `sh` was the only move it had. It measured ~1 run in 8 before
//! and none after.
//!
//! **So the lock was measured out.** Same recipe as above — `fan::` plus the
//! five fixture-exec families, 16 workers x 70 iterations, three runs each,
//! 3,360 test-binary runs per side: **0 / 0 / 0** with the lock still standing,
//! **0 / 0 / 0** with the guard removed. It was closing nothing once the write
//! fd was gone, so `SPAWN_LOCK` and `spawn_guard` are gone with it. The one
//! fork above stays for its own reasons; the ETXTBSY discipline now lives
//! entirely on the write side, where the fd is.
//!
//! One consequence is subtraction, not vigilance: bl-6bf5's placement rule —
//! *the lib test binary drives no embedded substrate in-process* — is no longer
//! load-bearing for ETXTBSY. The `tests/multiplex_*.rs` split keeps its OTHER
//! reason, which is [`INHERITED`]: a binary that runs its subject in-process
//! must scrub its own process env, no spawn boundary existing to do it for a
//! fork it does not perform.
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`). It takes no lock: the ETXTBSY
/// window it used to close is closed on the write side now (module doc,
/// bl-fd28).
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.
///
/// It is not an ETXTBSY party either way: that needs a fork to copy somebody's
/// write fd into a child, and an exec forks nothing. It could only ever be the
/// *victim*, and the write-side discipline above retired the fd 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.