Skip to content

Commit e1d31f2

Browse files
authored
Use /proc/self/maps when available instead of std::env::current_exe (#488)
* standalone proc/self/maps parsing code to try instead of less reliable current_exe. (updated to only define the parse_running_mmaps module in cases where we are also defining libs_dl_iterate_phdr, since that is the only place it is currently needed.) * checkpoint regression test demonstrating failure of rust issue 101913. * fix code to work for cargo test --no-default-features . * fix test to ignore itself when it cannot invoke readelf. * Conditionalize 64-bit test. Add test that works for 32-bit targets. * placate cargo fmt. * switch to pure `Read` rather than `BufRead` based implementation, to save a bit on code size. ls -sk before: 1052 target/release/libbacktrace.rlib ls -sk after: 1044 target/release/libbacktrace.rlib * Avoid using `str::split_once` method, since the MSRV is 1.42 but that was added in 1.52. * placate cargo fmt. Co-authored-by: Felix Klock <[email protected]>
1 parent 8b83ba1 commit e1d31f2

File tree

7 files changed

+423
-11
lines changed

7 files changed

+423
-11
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,8 @@ edition = '2018'
130130
name = "concurrent-panics"
131131
required-features = ["std"]
132132
harness = false
133+
134+
[[test]]
135+
name = "current-exe-mismatch"
136+
required-features = ["std"]
137+
harness = false

src/symbolize/gimli.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ cfg_if::cfg_if! {
184184
))] {
185185
mod libs_dl_iterate_phdr;
186186
use libs_dl_iterate_phdr::native_libraries;
187+
#[path = "gimli/parse_running_mmaps_unix.rs"]
188+
mod parse_running_mmaps;
187189
} else if #[cfg(target_env = "libnx")] {
188190
mod libs_libnx;
189191
use libs_libnx::native_libraries;

src/symbolize/gimli/libs_dl_iterate_phdr.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ pub(super) fn native_libraries() -> Vec<Library> {
1717
return ret;
1818
}
1919

20+
fn infer_current_exe(base_addr: usize) -> OsString {
21+
if let Ok(entries) = super::parse_running_mmaps::parse_maps() {
22+
let opt_path = entries
23+
.iter()
24+
.find(|e| e.ip_matches(base_addr) && e.pathname().len() > 0)
25+
.map(|e| e.pathname())
26+
.cloned();
27+
if let Some(path) = opt_path {
28+
return path;
29+
}
30+
}
31+
env::current_exe().map(|e| e.into()).unwrap_or_default()
32+
}
33+
2034
// `info` should be a valid pointers.
2135
// `vec` should be a valid pointer to a `std::Vec`.
2236
unsafe extern "C" fn callback(
@@ -28,8 +42,12 @@ unsafe extern "C" fn callback(
2842
let libs = &mut *(vec as *mut Vec<Library>);
2943
let is_main_prog = info.dlpi_name.is_null() || *info.dlpi_name == 0;
3044
let name = if is_main_prog {
45+
// The man page for dl_iterate_phdr says that the first object visited by
46+
// callback is the main program; so the first time we encounter a
47+
// nameless entry, we can assume its the main program and try to infer its path.
48+
// After that, we cannot continue that assumption, and we use an empty string.
3149
if libs.is_empty() {
32-
env::current_exe().map(|e| e.into()).unwrap_or_default()
50+
infer_current_exe(info.dlpi_addr as usize)
3351
} else {
3452
OsString::new()
3553
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// Note: This file is only currently used on targets that call out to the code
2+
// in `mod libs_dl_iterate_phdr` (e.g. linux, freebsd, ...); it may be more
3+
// general purpose, but it hasn't been tested elsewhere.
4+
5+
use super::mystd::fs::File;
6+
use super::mystd::io::Read;
7+
use super::mystd::str::FromStr;
8+
use super::{OsString, String, Vec};
9+
10+
#[derive(PartialEq, Eq, Debug)]
11+
pub(super) struct MapsEntry {
12+
/// start (inclusive) and limit (exclusive) of address range.
13+
address: (usize, usize),
14+
/// The perms field are the permissions for the entry
15+
///
16+
/// r = read
17+
/// w = write
18+
/// x = execute
19+
/// s = shared
20+
/// p = private (copy on write)
21+
perms: [char; 4],
22+
/// Offset into the file (or "whatever").
23+
offset: usize,
24+
/// device (major, minor)
25+
dev: (usize, usize),
26+
/// inode on the device. 0 indicates that no inode is associated with the memory region (e.g. uninitalized data aka BSS).
27+
inode: usize,
28+
/// Usually the file backing the mapping.
29+
///
30+
/// Note: The man page for proc includes a note about "coordination" by
31+
/// using readelf to see the Offset field in ELF program headers. pnkfelix
32+
/// is not yet sure if that is intended to be a comment on pathname, or what
33+
/// form/purpose such coordination is meant to have.
34+
///
35+
/// There are also some pseudo-paths:
36+
/// "[stack]": The initial process's (aka main thread's) stack.
37+
/// "[stack:<tid>]": a specific thread's stack. (This was only present for a limited range of Linux verisons; it was determined to be too expensive to provide.)
38+
/// "[vdso]": Virtual dynamically linked shared object
39+
/// "[heap]": The process's heap
40+
///
41+
/// The pathname can be blank, which means it is an anonymous mapping
42+
/// obtained via mmap.
43+
///
44+
/// Newlines in pathname are replaced with an octal escape sequence.
45+
///
46+
/// The pathname may have "(deleted)" appended onto it if the file-backed
47+
/// path has been deleted.
48+
///
49+
/// Note that modifications like the latter two indicated above imply that
50+
/// in general the pathname may be ambiguous. (I.e. you cannot tell if the
51+
/// denoted filename actually ended with the text "(deleted)", or if that
52+
/// was added by the maps rendering.
53+
pathname: OsString,
54+
}
55+
56+
pub(super) fn parse_maps() -> Result<Vec<MapsEntry>, &'static str> {
57+
let mut v = Vec::new();
58+
let mut proc_self_maps =
59+
File::open("/proc/self/maps").map_err(|_| "Couldn't open /proc/self/maps")?;
60+
let mut buf = String::new();
61+
let _bytes_read = proc_self_maps
62+
.read_to_string(&mut buf)
63+
.map_err(|_| "Couldn't read /proc/self/maps")?;
64+
for line in buf.lines() {
65+
v.push(line.parse()?);
66+
}
67+
68+
Ok(v)
69+
}
70+
71+
impl MapsEntry {
72+
pub(super) fn pathname(&self) -> &OsString {
73+
&self.pathname
74+
}
75+
76+
pub(super) fn ip_matches(&self, ip: usize) -> bool {
77+
self.address.0 <= ip && ip < self.address.1
78+
}
79+
}
80+
81+
impl FromStr for MapsEntry {
82+
type Err = &'static str;
83+
84+
// Format: address perms offset dev inode pathname
85+
// e.g.: "ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]"
86+
// e.g.: "7f5985f46000-7f5985f48000 rw-p 00039000 103:06 76021795 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2"
87+
// e.g.: "35b1a21000-35b1a22000 rw-p 00000000 00:00 0"
88+
fn from_str(s: &str) -> Result<Self, Self::Err> {
89+
let mut parts = s
90+
.split(' ') // space-separated fields
91+
.filter(|s| s.len() > 0); // multiple spaces implies empty strings that need to be skipped.
92+
let range_str = parts.next().ok_or("Couldn't find address")?;
93+
let perms_str = parts.next().ok_or("Couldn't find permissions")?;
94+
let offset_str = parts.next().ok_or("Couldn't find offset")?;
95+
let dev_str = parts.next().ok_or("Couldn't find dev")?;
96+
let inode_str = parts.next().ok_or("Couldn't find inode")?;
97+
let pathname_str = parts.next().unwrap_or(""); // pathname may be omitted.
98+
99+
let hex = |s| usize::from_str_radix(s, 16).map_err(|_| "Couldn't parse hex number");
100+
let address = {
101+
// This could use `range_str.split_once('-')` once the MSRV passes 1.52.
102+
if let Some(idx) = range_str.find('-') {
103+
let (start, rest) = range_str.split_at(idx);
104+
let (_div, limit) = rest.split_at(1);
105+
(hex(start)?, hex(limit)?)
106+
} else {
107+
return Err("Couldn't parse address range");
108+
}
109+
};
110+
let perms: [char; 4] = {
111+
let mut chars = perms_str.chars();
112+
let mut c = || chars.next().ok_or("insufficient perms");
113+
let perms = [c()?, c()?, c()?, c()?];
114+
if chars.next().is_some() {
115+
return Err("too many perms");
116+
}
117+
perms
118+
};
119+
let offset = hex(offset_str)?;
120+
let dev = {
121+
// This could use `dev_str.split_once(':')` once the MSRV passes 1.52.
122+
if let Some(idx) = dev_str.find(':') {
123+
let (major, rest) = dev_str.split_at(idx);
124+
let (_div, minor) = rest.split_at(1);
125+
(hex(major)?, hex(minor)?)
126+
} else {
127+
return Err("Couldn't parse dev")?;
128+
}
129+
};
130+
let inode = hex(inode_str)?;
131+
let pathname = pathname_str.into();
132+
133+
Ok(MapsEntry {
134+
address,
135+
perms,
136+
offset,
137+
dev,
138+
inode,
139+
pathname,
140+
})
141+
}
142+
}
143+
144+
// Make sure we can parse 64-bit sample output if we're on a 64-bit target.
145+
#[cfg(target_pointer_width = "64")]
146+
#[test]
147+
fn check_maps_entry_parsing_64bit() {
148+
assert_eq!(
149+
"ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 \
150+
[vsyscall]"
151+
.parse::<MapsEntry>()
152+
.unwrap(),
153+
MapsEntry {
154+
address: (0xffffffffff600000, 0xffffffffff601000),
155+
perms: ['-', '-', 'x', 'p'],
156+
offset: 0x00000000,
157+
dev: (0x00, 0x00),
158+
inode: 0x0,
159+
pathname: "[vsyscall]".into(),
160+
}
161+
);
162+
163+
assert_eq!(
164+
"7f5985f46000-7f5985f48000 rw-p 00039000 103:06 76021795 \
165+
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2"
166+
.parse::<MapsEntry>()
167+
.unwrap(),
168+
MapsEntry {
169+
address: (0x7f5985f46000, 0x7f5985f48000),
170+
perms: ['r', 'w', '-', 'p'],
171+
offset: 0x00039000,
172+
dev: (0x103, 0x06),
173+
inode: 0x76021795,
174+
pathname: "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2".into(),
175+
}
176+
);
177+
assert_eq!(
178+
"35b1a21000-35b1a22000 rw-p 00000000 00:00 0"
179+
.parse::<MapsEntry>()
180+
.unwrap(),
181+
MapsEntry {
182+
address: (0x35b1a21000, 0x35b1a22000),
183+
perms: ['r', 'w', '-', 'p'],
184+
offset: 0x00000000,
185+
dev: (0x00, 0x00),
186+
inode: 0x0,
187+
pathname: Default::default(),
188+
}
189+
);
190+
}
191+
192+
// (This output was taken from a 32-bit machine, but will work on any target)
193+
#[test]
194+
fn check_maps_entry_parsing_32bit() {
195+
/* Example snippet of output:
196+
08056000-08077000 rw-p 00000000 00:00 0 [heap]
197+
b7c79000-b7e02000 r--p 00000000 08:01 60662705 /usr/lib/locale/locale-archive
198+
b7e02000-b7e03000 rw-p 00000000 00:00 0
199+
*/
200+
assert_eq!(
201+
"08056000-08077000 rw-p 00000000 00:00 0 \
202+
[heap]"
203+
.parse::<MapsEntry>()
204+
.unwrap(),
205+
MapsEntry {
206+
address: (0x08056000, 0x08077000),
207+
perms: ['r', 'w', '-', 'p'],
208+
offset: 0x00000000,
209+
dev: (0x00, 0x00),
210+
inode: 0x0,
211+
pathname: "[heap]".into(),
212+
}
213+
);
214+
215+
assert_eq!(
216+
"b7c79000-b7e02000 r--p 00000000 08:01 60662705 \
217+
/usr/lib/locale/locale-archive"
218+
.parse::<MapsEntry>()
219+
.unwrap(),
220+
MapsEntry {
221+
address: (0xb7c79000, 0xb7e02000),
222+
perms: ['r', '-', '-', 'p'],
223+
offset: 0x00000000,
224+
dev: (0x08, 0x01),
225+
inode: 0x60662705,
226+
pathname: "/usr/lib/locale/locale-archive".into(),
227+
}
228+
);
229+
assert_eq!(
230+
"b7e02000-b7e03000 rw-p 00000000 00:00 0"
231+
.parse::<MapsEntry>()
232+
.unwrap(),
233+
MapsEntry {
234+
address: (0xb7e02000, 0xb7e03000),
235+
perms: ['r', 'w', '-', 'p'],
236+
offset: 0x00000000,
237+
dev: (0x00, 0x00),
238+
inode: 0x0,
239+
pathname: Default::default(),
240+
}
241+
);
242+
}

tests/common/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/// Some tests only make sense in contexts where they can re-exec the test
2+
/// itself. Not all contexts support this, so you can call this method to find
3+
/// out which case you are in.
4+
pub fn cannot_reexec_the_test() -> bool {
5+
// These run in docker containers on CI where they can't re-exec the test,
6+
// so just skip these for CI. No other reason this can't run on those
7+
// platforms though.
8+
// Miri does not have support for re-execing a file
9+
cfg!(unix)
10+
&& (cfg!(target_arch = "arm")
11+
|| cfg!(target_arch = "aarch64")
12+
|| cfg!(target_arch = "s390x"))
13+
|| cfg!(miri)
14+
}

tests/concurrent-panics.rs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,11 @@ const PANICS: usize = 100;
99
const THREADS: usize = 8;
1010
const VAR: &str = "__THE_TEST_YOU_ARE_LUKE";
1111

12+
mod common;
13+
1214
fn main() {
13-
// These run in docker containers on CI where they can't re-exec the test,
14-
// so just skip these for CI. No other reason this can't run on those
15-
// platforms though.
16-
// Miri does not have support for re-execing a file
17-
if cfg!(unix)
18-
&& (cfg!(target_arch = "arm")
19-
|| cfg!(target_arch = "aarch64")
20-
|| cfg!(target_arch = "s390x"))
21-
|| cfg!(miri)
22-
{
15+
// If we cannot re-exec this test, there's no point in trying to do it.
16+
if common::cannot_reexec_the_test() {
2317
println!("test result: ok");
2418
return;
2519
}

0 commit comments

Comments
 (0)