The execve failures that said nothing
Status: closed. Every exec refusal is now named, counted and asserted.
How it surfaced
A six-boot run came back 4/6, with both failures reporting
stress_seed_not_reported,stress_run_did_not_finish. Every memory counter was
clean — no leaks, no double allocations, no poison hits, no rmap mismatch — and
SVC_STRS.ELF appeared in the log only as a line of ls output. The service
had never run.
Following it took three steps and none of them should have been necessary:
- init printed
SVC_START svc-stress 10, so the fork succeeded. - The scheduler printed
task pid=0xa exited code=0x7f— 127. - Reading
user/prog/init.cshowed where 127 comes from: the child callsexecveand, when it returns, exits 127. execve had failed.
And the kernel said nothing about it. The [EXEC] … refused: tally showed
not-found=6, the same six a healthy boot shows.
What was wrong
hw_exec_refuse prints a line and increments a per-reason tally, and the boot
gate asserts the set of reasons seen contains only not-found — which is a
much stronger check than a count, and was already right.
Three paths never reached it. The copies from user memory — the path string,
the argv vector, the envp vector — returned a bare -EFAULT, -E2BIG or
-EINVAL. No line, no tally, no reason. An execve that failed on one of them
was indistinguishable, from outside, from an execve that was never called.
That is the same defect this subsystem already carried a comment about, in the place where it had been fixed:
Counted as well as printed. This one had a message of its own, which is exactly how it stayed outside the tally: a sentence in the log is not a reason anything can assert on.
The not-found path was fixed with that reasoning. The three beside it were
not, and nothing noticed for as long as they never fired.
The fix
A new reason, VIBEOS_EXEC_BAD_ARGS — "the kernel could not read the path, the
argv vector or a string in it" — and the three paths routed through
hw_exec_refuse, with a detail saying which vector: a program with too many
arguments and a program handed a bad environment pointer are not the same bug.
Confirmed by breaking it. Setting VIBEOS_HW_MAX_ARGV to 1 makes init's
two-entry argv too large:
[EXEC] refused reason=bad-args path=EFI/BOOT/SELFTEST.ELF at=argv
[EXEC] refused reason=bad-args path=EFI/BOOT/SVC_OK.ELF at=argv
reason=missing:VIBEOS_SELFTEST_DONE verdict=guest_wedged
The boot goes red and says why. Before this change the identical sabotage produced services that quietly did not start.
A note on that sabotage, because it nearly passed for a different reason: the
first attempt used sed against 32u while the constant reads 16, so the
edit never applied and the run came back green having proved nothing. It was
caught only because the script prints the constant before and after. CLAUDE.md's
rule about a sabotage run that proves nothing is worth applying to the edit
itself, not only to the harness.
It named itself on the first occurrence
The eight-boot run after the fix hit it once, and the failure that used to read
reason=invariant_failed:stress_seed_not_reported,stress_run_did_not_finish
now reads
reason=invariant_failed:exec_unexpected_refusal=bad-args,stress_seed_not_reported,...
[EXEC] refused reason=bad-args path=EFI/BOOT/SVC_STRS.ELF at=argv
with the chain complete and legible in three lines:
SVC_START svc-stress 10
[EXEC] refused reason=bad-args path=EFI/BOOT/SVC_STRS.ELF at=argv
SVC_EXIT svc-stress 10 127
Two things narrow it a long way. The path copied fine - it is in the
message - so the failure is specific to the argv vector, not to reading user
memory in general. And svc_argv lives in init's .bss: the child of the fork
writes argv[0] and argv[1] into it, which is a copy-on-write write, and
execve then reads that same page back.
So the shape is a page the child has just faulted copy-on-write, read by the kernel a moment later.
What is still open
Which of the range check's reasons applies. "bad-args" says a vector could not
be read and not why, and the check has a why for exactly this - "not
accessible without a reason is a dead end" is already written down beside it.
The refusal now carries it: the detail reads argv:pdpt_absent_or_not_user or
argv:leaf_not_writable or whichever level of the walk failed, because an
absent PML4 entry and a leaf that is present but not user-accessible are
completely different bugs.
That distinction is what decides between a stale TLB entry after the copy-on-write fault and a page-table level that was never installed. The next occurrence answers it.