Skip to content

fix(linux): _start never passed envp, and getenv could not even link - #60

Open
zemo-g wants to merge 1 commit into
masterfrom
fix/linux-envp-not-passed
Open

zemo-g wants to merge 1 commit into
masterfrom
fix/linux-envp-not-passed

Conversation

@zemo-g

@zemo-g zemo-g commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Two commits. The second was found by following the first.

1. _start never passed envp

It computed argc into x0 and argv into x1 and left x2 untouched.
_main's prologue then stored that garbage into _rail_envp, and
_rail_shell handed it to execve. Every Linux Rail program that shelled
out ran with no environment.

One probe, both targets:

HOME env vars cd ~ cat ~/.fleet/token
Linux before empty 1 /home/zemog/~ empty
Linux after /home/zemog 17 /home/zemog reads
macOS /Users/… 43 ok reads

macOS always worked because its prologue receives envp in x2 under the
main(argc, argv, envp) convention. A static Linux binary with its own
_start never gets that, and nothing computed it. The Linux initial stack
is argc, argv[…], NULL, envp[…], so envp is argv + (argc+1)*8.

2. getenv could not link, and _getenv was a stub

foreign getenv did not work on Linux, and not subtly: the FFI
string-return path calls _strdup to copy a borrowed C string onto the
Rail heap, and linux_libc.s had no _strdup at all. Anything using
getenv would not build for the Pi.

_getenv was separately a stub returning 0, justified by envp not being
populated at arena-init time, which commit 1 fixes. It now walks envp
comparing NAME=value against the name up to the =, so HOME does
not match HOMEBREW_PREFIX.

Verified on the Pi against macOS:

getenv HOME  ->  /home/zemog     (macOS: /Users/ledaticempire)
getenv USER  ->  zemog           (macOS: ledaticempire)
getenv NOPE  ->  empty           (macOS: empty)
HOMEBREW_PREFIX set, HOME=/correct/home  ->  /correct/home

One claim retracted

The first commit said storing envp in _start unblocks RAIL_ARENA_MB on
Linux. It does not. The Linux _rail_arena_init never consults
_getenv; it uses a statically reserved BSS heap of fixed size, so that
variable would need an mmap path to have any effect there. Corrected in the
second commit. What the envp fix actually unblocks is _getenv itself.

How it surfaced

The long way round. The fleet agent on the Pi could not read
~/.fleet/token, because ~ will not expand without HOME under dash. That
left its auth token empty, which hit a fail-open branch, which meant the
control plane accepted any token or none. Patched at the caller first
(security/fleet-v2-logs-jobs-injection); this is the cause underneath, and
it affects every Linux Rail program that shells out or reads the
environment.

Verification

  • Probes above, run on the Pi before and after.
  • macOS untouched: 189/189.
  • Self-compile fixed point re-verified byte-identical after each change.
  • No Linux regression test exists in the macOS suite; the x86_64
    conformance harness would be the place for one.

@zemo-g zemo-g changed the title fix(linux): _start never passed envp, so every shell() ran with no environment fix(linux): _start never passed envp, and getenv could not even link Aug 29, 2026
Two faults in the Linux target, the second found by following the first.

**_start never passed envp.** It computed argc into x0 and argv into x1 and
left x2 untouched. _main's prologue then stored that garbage into
_rail_envp, and _rail_shell handed it to execve, so every Linux Rail program
that shelled out ran with no environment. Measured with one probe compiled
for both targets: shell() saw ONE variable and no HOME on a Pi whose parent
had a full environment, against 43 on macOS. macOS always worked because its
prologue receives envp in x2 under the main(argc,argv,envp) convention,
which a static Linux binary with its own _start never gets.

The Linux initial stack is argc, argv[0..argc-1], NULL, envp[0]..., so envp
is argv + (argc+1)*8. Now computed in _start, stored to _rail_envp directly
so it is live during _rail_arena_init, and left in x2 for the prologue.

**getenv could not link.** The FFI string-return path calls _strdup to copy
a borrowed C string onto the Rail heap, and linux_libc.s had no _strdup at
all, so anything using `foreign getenv` failed to build for Linux rather
than misbehaving at runtime. _getenv was separately a stub returning 0,
justified by envp being unpopulated, which the above fixes. It now walks
envp comparing NAME=value against the name up to the '=', so HOME does not
match HOMEBREW_PREFIX. _strdup is built from the _strlen/_malloc/_strcpy
already present, with NULL in giving an empty string out rather than
faulting, which is the shape getenv's not-found case needs.

This does NOT unblock RAIL_ARENA_MB on Linux, despite the natural guess:
that arena_init never consults _getenv, it uses a statically reserved BSS
heap of fixed size and would need an mmap path to gain the variable.

Verified on the Pi against macOS: HOME, USER and a missing name all agree,
including the prefix-collision case. macOS untouched at 190/190, and the
rebuilt seed still reproduces itself byte-identically.

How it surfaced: the fleet agent on the Pi could not read ~/.fleet/token,
because `~` will not expand without HOME under dash. That left its auth
token empty, hit a fail-open branch, and left the control plane accepting
any token or none. Patched at the caller first; this is the cause
underneath, and it affects every Linux Rail program that shells out or
reads the environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EzjMXh1wxgasCH81MJfTCk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant