Skip to content

Commit 3b80fc3

Browse files
basilclaude
andauthored
fix: Resolve environ symbol in PIE executables and glibc's dynamic linker (#108)
Skip LOCAL-binding symbols during symbol lookup, as they are file-scoped and should never match external lookups like `environ`. Recognize glibc's dynamic linker (`ld-linux-*.so`) as a C runtime module so its `environ` symbol is found when no copy relocation exists (PIE executables, Rust programs, etc.). Add integration tests verifying that `penv` and `pargs -e` reflect runtime `setenv` changes read from the live `environ` symbol. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b2bdb43 commit 3b80fc3

File tree

4 files changed

+143
-9
lines changed

4 files changed

+143
-9
lines changed

examples/penv_setenv.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// Copyright (c) 2026 Basil Crow
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
//! Test helper that modifies its environment at runtime via `set_var`.
18+
//!
19+
//! Used by integration tests to verify that `penv` and `pargs -e` reflect
20+
//! runtime environment changes (read from the `environ` symbol) rather than
21+
//! the static `/proc/pid/environ` snapshot.
22+
23+
use std::env;
24+
use std::fs::File;
25+
use std::thread;
26+
use std::time::Duration;
27+
28+
fn allow_ptrace_for_tests() {
29+
// On Linux with Yama ptrace_scope=1 (the Ubuntu default), only a
30+
// process's descendants may ptrace it. Since the test harness spawns
31+
// this process and the ptool as siblings, we opt in to tracing by any
32+
// process so the tests work without elevated privileges.
33+
unsafe {
34+
nix::libc::prctl(
35+
nix::libc::PR_SET_PTRACER,
36+
nix::libc::PR_SET_PTRACER_ANY,
37+
0,
38+
0,
39+
0,
40+
);
41+
}
42+
}
43+
44+
fn main() {
45+
allow_ptrace_for_tests();
46+
47+
// Add a new environment variable at runtime. This will only be visible
48+
// via the `environ` symbol (process_vm_readv), not via /proc/pid/environ.
49+
env::set_var("PTOOLS_TEST_SETENV_VAR", "runtime_value");
50+
51+
// Also overwrite an existing variable to verify updates are reflected.
52+
env::set_var("PTOOLS_TEST_OVERWRITE_VAR", "after");
53+
54+
let signal_path =
55+
env::var("PTOOLS_TEST_READY_FILE").expect("PTOOLS_TEST_READY_FILE must be set");
56+
57+
// Signal parent process (the test process) that this process is ready to be observed by the
58+
// ptool being tested.
59+
File::create(signal_path).unwrap();
60+
61+
// Wait for the parent to finish running the ptool and then kill us.
62+
loop {
63+
thread::sleep(Duration::from_millis(100));
64+
}
65+
}

src/dw/dwfl/module.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ impl ModuleRef {
165165
if sym_name.is_null() {
166166
continue;
167167
}
168+
// Skip LOCAL-binding symbols. LOCAL symbols are
169+
// file-scoped and not visible outside their object
170+
// file, so they should never match external lookups.
171+
if (sym.st_info >> 4) == 0 {
172+
continue;
173+
}
168174
if let Some(t) = sym_type {
169175
if (sym.st_info & 0xf) != t {
170176
continue;

src/source/dw.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,26 +111,31 @@ pub(super) fn find_environ_symbol(
111111
}
112112
Ok(())
113113
})?;
114-
// Prefer the non-libc copy (typically the main executable). On
115-
// dynamically linked executables the linker emits a copy relocation
116-
// for `environ`, placing the live storage in the executable's BSS.
117-
// The libc address may remain at its initial (zero) value because
118-
// all runtime updates go through the copy-relocated slot.
114+
// Prefer the non-libc copy (typically the main executable). Non-PIE
115+
// executables that reference `extern char **environ` get a copy
116+
// relocation, placing the live storage in the executable's BSS; in
117+
// that case the libc copy is stale because all runtime updates go
118+
// through the copy-relocated slot. When no copy relocation exists
119+
// (PIE executables, Rust programs, etc.) only the libc symbol is
120+
// found and used directly.
119121
Ok(other_addr.or(libc_addr))
120122
}
121123

122-
/// Returns `true` if the module name looks like it belongs to libc.
124+
/// Returns `true` if the module name looks like it belongs to the C runtime.
123125
///
124-
/// Matches glibc (`libc.so.6`, `libc-2.31.so`) and musl (`ld-musl-x86_64.so.1`,
125-
/// which is musl's combined dynamic-linker/libc).
126+
/// Matches glibc's libc (`libc.so.6`, `libc-2.31.so`), glibc's dynamic
127+
/// linker (`ld-linux-x86-64.so.2`, `ld-linux-aarch64.so.1`), and musl
128+
/// (`ld-musl-x86_64.so.1`, which is musl's combined dynamic-linker/libc).
126129
fn is_libc_module(module: &ModuleRef) -> bool {
127130
module.name().is_some_and(|name| {
128131
let bytes = name.to_bytes();
129132
let filename = match bytes.iter().rposition(|&b| b == b'/') {
130133
Some(pos) => &bytes[pos + 1..],
131134
None => bytes,
132135
};
133-
filename.starts_with(b"libc") || filename.starts_with(b"ld-musl")
136+
filename.starts_with(b"libc")
137+
|| filename.starts_with(b"ld-linux")
138+
|| filename.starts_with(b"ld-musl")
134139
})
135140
}
136141

tests/pargs_penv_test.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,64 @@ fn pargs_x_alias_matches_pauxv_output() {
282282
);
283283
}
284284

285+
#[test]
286+
fn penv_reflects_runtime_setenv() {
287+
let output = common::run_ptool(
288+
"penv",
289+
&[],
290+
"examples/penv_setenv",
291+
&[],
292+
&[("PTOOLS_TEST_OVERWRITE_VAR", "before")],
293+
false,
294+
);
295+
let stdout = common::assert_success_and_get_stdout_allow_warnings(output);
296+
297+
// PTOOLS_TEST_SETENV_VAR was added at runtime via setenv() and should
298+
// only be visible when reading the environ symbol (not /proc/pid/environ).
299+
assert!(
300+
stdout.contains("PTOOLS_TEST_SETENV_VAR=runtime_value"),
301+
"Runtime setenv variable not found in penv output:\n\n{stdout}\n\n"
302+
);
303+
304+
// PTOOLS_TEST_OVERWRITE_VAR was passed as "before" at spawn time and then
305+
// overwritten to "after" via setenv().
306+
assert!(
307+
stdout.contains("PTOOLS_TEST_OVERWRITE_VAR=after"),
308+
"Overwritten env variable should show new value in penv output:\n\n{stdout}\n\n"
309+
);
310+
assert!(
311+
!stdout.contains("PTOOLS_TEST_OVERWRITE_VAR=before"),
312+
"Overwritten env variable should not show old value in penv output:\n\n{stdout}\n\n"
313+
);
314+
}
315+
316+
#[test]
317+
fn pargs_e_reflects_runtime_setenv() {
318+
let output = common::run_ptool(
319+
"pargs",
320+
&["-e"],
321+
"examples/penv_setenv",
322+
&[],
323+
&[("PTOOLS_TEST_OVERWRITE_VAR", "before")],
324+
false,
325+
);
326+
let stdout = common::assert_success_and_get_stdout_allow_warnings(output);
327+
328+
assert!(
329+
stdout.contains("PTOOLS_TEST_SETENV_VAR=runtime_value"),
330+
"Runtime setenv variable not found in pargs -e output:\n\n{stdout}\n\n"
331+
);
332+
333+
assert!(
334+
stdout.contains("PTOOLS_TEST_OVERWRITE_VAR=after"),
335+
"Overwritten env variable should show new value in pargs -e output:\n\n{stdout}\n\n"
336+
);
337+
assert!(
338+
!stdout.contains("PTOOLS_TEST_OVERWRITE_VAR=before"),
339+
"Overwritten env variable should not show old value in pargs -e output:\n\n{stdout}\n\n"
340+
);
341+
}
342+
285343
#[test]
286344
fn pargs_x_prints_auxv_entries() {
287345
let output = common::run_ptool("pargs", &["-x"], "examples/pargs_penv", &[], &[], false);

0 commit comments

Comments
 (0)