Skip to content

Commit 03e5172

Browse files
loks0nclaude
andcommitted
proc: skip shared/deleted exec mappings in principal_binary
principal_binary picked the first executable mapping with a non-`[` path, which under Swoole is its executable shared-memory pool — an `r-xs` region backed by `/dev/zero (deleted)` mapped below the php text segment. attach() then tried to open that path as an on-disk ELF and failed ("opening /dev/zero (deleted): No such file or directory"), so every worker attach aborted with 0 samples. Require a private (`p`) file-backed mapping and exclude `/dev/*` and `(deleted)` paths so resolution lands on the real php ELF. Covers the `/memfd:… (deleted)` JIT-pool variant too. Regression test added from an observed Swoole 8.5 NTS maps layout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4858a14 commit 03e5172

1 file changed

Lines changed: 29 additions & 3 deletions

File tree

src/proc.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,19 @@ pub fn parse_maps(raw: &str) -> Result<Vec<MapEntry>> {
5656
/// Docker that link points at the translator binary, not the target ELF.
5757
pub fn principal_binary(maps: &[MapEntry]) -> Option<&MapEntry> {
5858
let exe_mapping = maps.iter().find(|m| {
59+
// A real ELF text segment is a *private* file-backed mapping
60+
// (`r-xp`). Skip *shared* executable mappings (`r-xs`) — Swoole and
61+
// other extensions place executable shared-memory pools backed by
62+
// `/dev/zero (deleted)` or `/memfd:… (deleted)` at low addresses,
63+
// which are not openable on-disk ELFs.
5964
m.perms.contains('x')
60-
&& m.path
61-
.as_deref()
62-
.is_some_and(|p| !p.starts_with('[') && !p.is_empty())
65+
&& m.perms.contains('p')
66+
&& m.path.as_deref().is_some_and(|p| {
67+
!p.starts_with('[')
68+
&& !p.is_empty()
69+
&& !p.starts_with("/dev/")
70+
&& !p.ends_with("(deleted)")
71+
})
6372
})?;
6473
let path = exe_mapping.path.as_deref()?;
6574
// The lowest-address mapping for that file (often a r--p header before
@@ -109,6 +118,23 @@ mod tests {
109118
assert_eq!(p.start, 0x555555554000);
110119
}
111120

121+
#[test]
122+
fn principal_binary_skips_swoole_shared_exec_mapping() {
123+
// Swoole maps an executable *shared* memory pool backed by
124+
// `/dev/zero (deleted)` at a lower address than the php text segment.
125+
// The principal binary must still resolve to the real php ELF.
126+
let swoole = "\
127+
5649acc00000-5649c4c00000 rw-s 00000000 00:01 4586 /dev/zero (deleted)
128+
5649c4c00000-5649ccc00000 r-xs 18000000 00:01 4586 /dev/zero (deleted)
129+
5649ccc00000-5649ccd69000 r--p 00000000 fe:01 656824 /usr/local/bin/php
130+
5649cce00000-5649cd601000 r-xp 00200000 fe:01 656824 /usr/local/bin/php
131+
";
132+
let maps = parse_maps(swoole).unwrap();
133+
let p = principal_binary(&maps).unwrap();
134+
assert_eq!(p.path.as_deref(), Some("/usr/local/bin/php"));
135+
assert_eq!(p.start, 0x5649ccc00000);
136+
}
137+
112138
#[test]
113139
fn principal_binary_handles_only_anon() {
114140
let only_anon = "\

0 commit comments

Comments
 (0)