Skip to content

Commit e013075

Browse files
committed
fix: scan only direct aggregate members
AccessPlanner previously walked the full aggregate DIE subtree when resolving a member chain. That let members of nested C++ types match as if they were direct members of the parent aggregate, so invalid accesses such as `o.shadow` could compile instead of failing. Switch member lookup to direct DW_TAG_member children only, and add an e2e regression that accepts `o.nested.shadow` while rejecting `o.shadow`.
1 parent 5cef595 commit e013075

3 files changed

Lines changed: 100 additions & 4 deletions

File tree

e2e-tests/tests/cpp_script_execution.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ mod common;
44

55
use common::{init, FIXTURES};
66

7+
const CPP_NESTED_MEMBER_TRACE_LINE: u32 = 44;
8+
9+
async fn compile_cpp_complex_script(
10+
script: &str,
11+
) -> anyhow::Result<ghostscope_compiler::CompilationResult> {
12+
let binary_path = FIXTURES.get_test_binary("cpp_complex_program")?;
13+
let mut analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path)
14+
.await
15+
.map_err(|e| anyhow::anyhow!("failed to load DWARF for cpp_complex_program: {e}"))?;
16+
let compile_options = ghostscope_compiler::CompileOptions {
17+
binary_path_hint: Some(binary_path.to_string_lossy().into_owned()),
18+
..Default::default()
19+
};
20+
21+
ghostscope_compiler::compile_script(script, &mut analyzer, None, Some(1), &compile_options)
22+
.map_err(|e| anyhow::anyhow!("compile_script failed: {e}"))
23+
}
24+
725
async fn run_ghostscope_with_script_for_target(
826
script_content: &str,
927
timeout_secs: u64,
@@ -31,6 +49,56 @@ async fn spawn_cpp_complex_program() -> anyhow::Result<common::targets::TargetHa
3149
Ok(target)
3250
}
3351

52+
#[tokio::test]
53+
async fn test_cpp_nested_type_direct_child_member_access_is_not_recursive() -> anyhow::Result<()> {
54+
init();
55+
56+
let binary_path = FIXTURES.get_test_binary("cpp_complex_program")?;
57+
let source_path = binary_path
58+
.parent()
59+
.ok_or_else(|| anyhow::anyhow!("cpp_complex_program has no parent directory"))?
60+
.join("main.cpp");
61+
62+
let valid_script = format!(
63+
r#"
64+
trace {}:{CPP_NESTED_MEMBER_TRACE_LINE} {{
65+
print o.nested.shadow;
66+
}}
67+
"#,
68+
source_path.display()
69+
);
70+
let valid = compile_cpp_complex_script(&valid_script).await?;
71+
assert!(
72+
!valid.uprobe_configs.is_empty(),
73+
"expected valid o.nested.shadow to compile; target_info={} failed_targets={:?}",
74+
valid.target_info,
75+
valid.failed_targets
76+
);
77+
78+
let invalid_script = format!(
79+
r#"
80+
trace {}:{CPP_NESTED_MEMBER_TRACE_LINE} {{
81+
print o.shadow;
82+
}}
83+
"#,
84+
source_path.display()
85+
);
86+
if let Ok(invalid) = compile_cpp_complex_script(&invalid_script).await {
87+
assert!(
88+
invalid.uprobe_configs.is_empty(),
89+
"expected o.shadow to be rejected because shadow is only a member of o.nested; target_info={} failed_targets={:?}",
90+
invalid.target_info,
91+
invalid.failed_targets
92+
);
93+
assert!(
94+
!invalid.failed_targets.is_empty(),
95+
"expected at least one failed target for invalid o.shadow access"
96+
);
97+
}
98+
99+
Ok(())
100+
}
101+
34102
#[tokio::test]
35103
async fn test_cpp_script_print_globals() -> anyhow::Result<()> {
36104
init();

e2e-tests/tests/fixtures/cpp_complex_program/main.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <string>
33
#include <thread>
44
#include <chrono>
5+
#include <cstdint>
56

67
int g_counter = 0;
78
const char* g_msg = "hello cpp";
@@ -10,6 +11,17 @@ static int s_internal = 123;
1011
namespace ns1 {
1112
struct Point { int x; int y; };
1213

14+
struct Outer {
15+
struct Nested {
16+
int shadow;
17+
int payload;
18+
};
19+
20+
int tag;
21+
Nested nested;
22+
int tail;
23+
};
24+
1325
class Foo {
1426
public:
1527
static int s_val;
@@ -22,6 +34,17 @@ int Foo::s_val = 7;
2234
__attribute__((noinline)) int add(int a, int b) { return a + b; }
2335
__attribute__((noinline)) int add(double a, double b) { return (int)(a + b); }
2436

37+
__attribute__((noinline)) int nested_member_probe(int v) {
38+
volatile Outer outer = {
39+
101,
40+
{202 + v, 303},
41+
404,
42+
};
43+
Outer* o = (Outer*)&outer;
44+
volatile std::uintptr_t sink = (std::uintptr_t)o + (std::uintptr_t)o->nested.shadow;
45+
return (int)sink;
46+
}
47+
2548
// Variables purposely ending with ::h and ::h264 to validate demangled leaf handling
2649
int h = 5;
2750
int h264 = 7;
@@ -40,6 +63,7 @@ int main() {
4063
acc += f.bar(i);
4164
acc += ns1::add(i, i+1);
4265
acc += ns1::add(1.5, 2.5);
66+
acc += ns1::nested_member_probe(i);
4367
touch_globals();
4468
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
4569
}

ghostscope-dwarf/src/objfile/access_planner.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,15 @@ impl<'dwarf> AccessPlanner<'dwarf> {
159159
let header_now2 = self.dwarf.unit_header(current_cu_off)?;
160160
let unit_now2 = self.dwarf.unit(header_now2)?;
161161
let def_die = unit_now2.entry(def_off)?;
162-
// Scan members for the field
163-
let mut entries = unit_now2.entries_at_offset(def_die.offset())?;
164-
let _ = entries.next_entry()?; // self
162+
// Only direct DW_TAG_member children belong to this aggregate.
163+
// Nested class/struct DIEs may appear under a C++ aggregate, but
164+
// their members are not direct members of the parent type.
165+
let mut tree = unit_now2.entries_tree(Some(def_die.offset()))?;
166+
let root = tree.root()?;
167+
let mut children = root.children();
165168
let mut found_member = false;
166-
while let Some(e) = entries.next_dfs()? {
169+
while let Some(child) = children.next()? {
170+
let e = child.entry();
167171
if e.tag() == gimli::DW_TAG_member {
168172
if let Some(attr) = e.attr(gimli::DW_AT_name) {
169173
if let Ok(s) = self.dwarf.attr_string(&unit_now2, attr.value()) {

0 commit comments

Comments
 (0)