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
Code
I tried this code, statically linked on x86_64-unknown-freebsd:
I expected to see this happen: both lines print, as they do in a dynamically linked build:
Instead, this happened: the first call that iterates the environment segfaults:
Command::spawncrashes the same way when it inherits the parent environment:With the inherited environment this segfaults; with
env_clear()it runs, because that path never readsenviron.Cause
PR #153718 changed the FreeBSD
environ()accessor to resolve the symbol throughdlsym(RTLD_DEFAULT, "environ")so that cdylibs linked with-Wl,--no-undefinedcan use it (#153451). A statically linked executable has no dynamic symbol table, so dlsym returns null, and both consumers dereference it:sys::env::env()doeslet mut environ = *environ();-- the null check two lines later guards the table contents, not the pointerenviron()returned.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_handleinsys/thread_local/destructors/linux_like.rs). A weak undefined symbol doesn't break-Wl,--no-undefinedshared-library links, and in an executable it always resolves because crt1.o definesenviron. Acfg(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 plainextern "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:(FreeBSD 14.2-RELEASE amd64, rustc from the FreeBSD pkg; a binary built with pkg
rust-1.96.0_1crashes 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
@rustbot label +C-bug +regression-from-stable-to-stable +O-freebsd +T-libs