Skip to content

std::env::vars and Command::spawn segfault in crt-static binaries on FreeBSD since 1.96 #158939

Description

@sbogomolov

Code

I tried this code, statically linked on x86_64-unknown-freebsd:

fn main() {
    println!("var_os(PATH) is_some: {}", std::env::var_os("PATH").is_some());
    println!("vars_os count: {}", std::env::vars_os().count());
}

I expected to see this happen: both lines print, as they do in a dynamically linked build:

$ rustc -O envtest.rs -o envtest-dyn && ./envtest-dyn
var_os(PATH) is_some: true
vars_os count: 52

Instead, this happened: the first call that iterates the environment segfaults:

$ rustc -O -C target-feature=+crt-static envtest.rs -o envtest-static && ./envtest-static
var_os(PATH) is_some: true
Segmentation fault (core dumped)

Command::spawn crashes the same way when it inherits the parent environment:

fn main() {
    let mut cmd = std::process::Command::new("/usr/bin/true");
    if std::env::args().nth(1).is_some() {
        cmd.env_clear();
    }
    println!("{:?}", cmd.status());
}

With the inherited environment this segfaults; with env_clear() it runs, because that path never reads environ.

Cause

PR #153718 changed the FreeBSD environ() accessor to resolve the symbol through dlsym(RTLD_DEFAULT, "environ") so that cdylibs linked with -Wl,--no-undefined can use it (#153451). A statically linked executable has no dynamic symbol table, so dlsym returns null, and both consumers dereference it:

  • sys::env::env() does let mut environ = *environ(); -- the null check two lines later guards the table contents, not the pointer environ() returned.
  • The posix_spawn path does envp.map(..).unwrap_or_else(|| *sys::env::environ() as *const _) for a spawn with unmodified env.

std::env::var(..) is unaffected (getenv).

A possible fix: when dlsym returns null, fall back to a weak reference (#[linkage = "extern_weak"], as already used for __dso_handle in sys/thread_local/destructors/linux_like.rs). A weak undefined symbol doesn't break -Wl,--no-undefined shared-library links, and in an executable it always resolves because crt1.o defines environ. A cfg(target_feature = "crt-static") gate wouldn't help: std ships precompiled without crt-static for this target, so the cfg would pick the dynamic branch no matter what the user builds with.

I hit this because my daemon reads its config from the environment at startup, so the shipped static FreeBSD binaries died on exec. The unit suite, also built with crt-static, kept passing: nothing in it iterates the real process environment.

Version it worked on

It most recently worked on: Rust 1.95 (its environ() is a plain extern "C" { static mut environ }, which links fine in a static executable because crt1.o provides the symbol; the dlsym accessor landed in 1.96.0 via #153718).

Version with regression

rustc --version --verbose:

rustc 1.96.1 (31fca3adb 2026-06-26) (built from a source tarball)

(FreeBSD 14.2-RELEASE amd64, rustc from the FreeBSD pkg; a binary built with pkg rust-1.96.0_1 crashes identically.)

Backtrace

Not a compiler crash; this is the produced binary's core, SEGV_MAPERR at address 0x0 in the environment iteration (unstripped release build of my daemon, whose first env access is in run):

Backtrace

(lldb) bt
* thread #1, name = 'reflector', stop reason = signal SIGSEGV
  * frame #0: 0x00000000002740d0 reflector`reflector::run::hbb44bf8ae677c0fe + 352
    frame #1: 0x0000000000248c43 reflector`reflector::main::he21a159ba8f19632 + 723
    frame #2: 0x0000000000248313 reflector`std::sys::backtrace::__rust_begin_short_backtrace::h1415ef89e14bc480 + 3
    frame #3: 0x0000000000249696 reflector`main + 1110
    frame #4: 0x00000000002f30a8 reflector`__libc_start1(argc=1, argv=0x000000082047d300, env=0x000000082047d310, cleanup=<unavailable>, mainX=(reflector`main)) at libc_start1.c:157:7
    frame #5: 0x00000000002482a0 reflector`_start at crt1_s.S:83

@rustbot label +C-bug +regression-from-stable-to-stable +O-freebsd +T-libs

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.O-freebsdOperating system: FreeBSDP-highHigh priorityT-libsRelevant to the library team, which will review and decide on the PR/issue.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions