refactor(dynamic): add recursive dependency resolution for C receivers, enhance harness generation logic, and expand test coverage

This commit is contained in:
elipeter 2026-05-25 00:03:40 -05:00
parent 6e9cc0b607
commit 680fc6bd28
4 changed files with 286 additions and 3 deletions

View file

@ -450,7 +450,8 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
// free function whose name is the entry symbol (often
// `Class_method` by convention) and calls it with the payload.
if let crate::evidence::EntryKind::ClassMethod { class, method } = &spec.entry_kind {
return Ok(emit_class_method_harness(class, method));
let entry_src = std::fs::read_to_string(&spec.entry_file).unwrap_or_default();
return Ok(emit_class_method_harness(class, method, &entry_src));
}
let shape = detect_shape(spec);
@ -482,9 +483,24 @@ pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
/// `entry_name` field; this fallback keeps the build path uniform
/// for the Phase 19 acceptance harness even though the class /
/// method projection collapses to a free-function call in C.
fn emit_class_method_harness(class: &str, method: &str) -> HarnessSource {
fn emit_class_method_harness(class: &str, method: &str, entry_src: &str) -> HarnessSource {
let shim = probe_shim();
let symbol = format!("{class}_{method}");
let receiver = c_receiver_plan(entry_src, class, &symbol);
let (receiver_setup, invocation) = if let Some(plan) = receiver {
(
format!(" {}\n", plan.setup_lines.join("\n ")),
format!(
"{symbol}(&{name}, payload, strlen(payload));",
name = plan.root_name
),
)
} else {
(
String::new(),
format!("{symbol}(payload, strlen(payload));"),
)
};
let body = format!(
r#"/* Nyx dynamic harness — class method (Phase 19 / Track M.1). */
#include <stddef.h>
@ -502,7 +518,7 @@ int main(int argc, char *argv[]) {{
char *payload = nyx_payload();
if (!payload) payload = (char*)"";
__nyx_install_crash_guard("{symbol}");
{symbol}(payload, strlen(payload));
{receiver_setup} {invocation}
puts("__NYX_SINK_HIT__");
return 0;
}}
@ -516,6 +532,8 @@ static char *nyx_payload(void) {{
}}
"#,
symbol = symbol,
receiver_setup = receiver_setup,
invocation = invocation,
);
HarnessSource {
source: body,
@ -526,6 +544,184 @@ static char *nyx_payload(void) {{
}
}
#[derive(Debug, Clone)]
struct CReceiverPlan {
root_name: String,
setup_lines: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CStructDef {
name: String,
fields: Vec<CStructField>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CStructField {
ty: String,
name: String,
pointer: bool,
}
fn c_receiver_plan(entry_src: &str, class: &str, symbol: &str) -> Option<CReceiverPlan> {
if !c_symbol_has_receiver(entry_src, symbol, class) {
return None;
}
let structs = c_struct_defs(entry_src);
let mut setup_lines = Vec::new();
let root_name = "nyx_receiver".to_owned();
c_receiver_init(class, &structs, &root_name, 3, &mut setup_lines);
Some(CReceiverPlan {
root_name,
setup_lines,
})
}
fn c_symbol_has_receiver(entry_src: &str, symbol: &str, class: &str) -> bool {
let Some(params) = c_function_params(entry_src, symbol) else {
return false;
};
let first = params
.split(',')
.next()
.map(str::trim)
.unwrap_or_default()
.replace('\n', " ");
first.contains('*') && c_bare_type(&first) == class
}
fn c_function_params(entry_src: &str, symbol: &str) -> Option<String> {
let needle = format!("{symbol}(");
let start = entry_src.find(&needle)? + needle.len();
let mut depth = 1usize;
let mut end = start;
for (offset, ch) in entry_src[start..].char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
end = start + offset;
break;
}
}
_ => {}
}
}
(end > start).then(|| entry_src[start..end].to_owned())
}
fn c_receiver_init(
ty: &str,
structs: &[CStructDef],
var_name: &str,
depth: usize,
lines: &mut Vec<String>,
) {
let Some(def) = structs.iter().find(|def| def.name == ty) else {
lines.push(format!("{ty} {var_name} = {{0}};"));
return;
};
if depth == 0 {
lines.push(format!("{ty} {var_name} = {{0}};"));
return;
}
let mut initializers = Vec::new();
for field in &def.fields {
if !c_has_struct_type(structs, &field.ty) {
continue;
}
let child = format!("nyx_{}_{}", field.name, lines.len());
c_receiver_init(&field.ty, structs, &child, depth - 1, lines);
if field.pointer {
initializers.push(format!(".{} = &{child}", field.name));
} else {
initializers.push(format!(".{} = {child}", field.name));
}
}
if initializers.is_empty() {
lines.push(format!("{ty} {var_name} = {{0}};"));
} else {
lines.push(format!(
"{ty} {var_name} = {{ {} }};",
initializers.join(", ")
));
}
}
fn c_has_struct_type(structs: &[CStructDef], ty: &str) -> bool {
structs.iter().any(|def| def.name == ty)
}
fn c_struct_defs(entry_src: &str) -> Vec<CStructDef> {
let mut out = Vec::new();
for chunk in entry_src.split("typedef struct").skip(1) {
let Some(open) = chunk.find('{') else {
continue;
};
let Some(close_rel) = chunk[open + 1..].find('}') else {
continue;
};
let body = &chunk[open + 1..open + 1 + close_rel];
let after = &chunk[open + 1 + close_rel + 1..];
let name = after
.split(';')
.next()
.unwrap_or_default()
.split_whitespace()
.last()
.unwrap_or_default()
.trim();
if name.is_empty() {
continue;
}
let fields = body
.split(';')
.filter_map(c_struct_field)
.collect::<Vec<_>>();
out.push(CStructDef {
name: name.to_owned(),
fields,
});
}
out
}
fn c_struct_field(raw: &str) -> Option<CStructField> {
let field = raw.trim();
if field.is_empty() || field.contains('(') || field.contains(')') {
return None;
}
let name = field
.split_whitespace()
.last()?
.trim()
.trim_start_matches('*')
.to_owned();
if name.is_empty() {
return None;
}
let before_name = field
.strip_suffix(field.split_whitespace().last()?)?
.trim()
.to_owned();
let pointer = field.contains('*');
let ty = c_bare_type(&before_name);
(!ty.is_empty()).then_some(CStructField { ty, name, pointer })
}
fn c_bare_type(raw: &str) -> String {
raw.replace('*', " ")
.replace("const", " ")
.replace("struct", " ")
.split_whitespace()
.find(|part| !matches!(*part, "volatile" | "restrict"))
.unwrap_or_default()
.to_owned()
}
/// Generate the harness `main.c` for the resolved shape.
fn generate_main_c(spec: &HarnessSpec, shape: CShape) -> String {
let invocation = invoke_for_shape(spec, shape);

View file

@ -241,6 +241,31 @@ fn class_method_c_collapses_to_class_underscore_method_symbol() {
assert!(h.source.contains("UserService_run"));
}
#[test]
fn class_method_c_builds_recursive_receiver_pointer() {
let mut spec = make_spec(Lang::C);
spec.entry_file = "tests/dynamic_fixtures/class_method/c_recursive_deps/vuln.c".into();
spec.sink_file = spec.entry_file.clone();
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("ShellRunner nyx_shell_0 = {0};"));
assert!(
h.source
.contains("CommandRunner nyx_runner_0 = { .shell = &nyx_shell_0 };")
);
assert!(
h.source
.contains("UserService nyx_receiver = { .runner = &nyx_runner_0 };")
);
assert!(
h.source
.contains("UserService_run(&nyx_receiver, payload, strlen(payload));")
);
assert!(
!h.source
.contains("UserService_run(payload, strlen(payload));")
);
}
#[test]
fn class_method_cpp_constructs_default_then_calls_method() {
let spec = make_spec(Lang::Cpp);
@ -477,6 +502,17 @@ mod e2e_phase_19 {
cap: Cap::CODE_EXEC,
bins: &["cc"],
},
Case {
lang: Lang::C,
fixture_dir: "c_recursive_deps",
vuln_file: "vuln.c",
benign_file: "benign.c",
vuln_class: "UserService",
benign_class: "UserService",
method: "run",
cap: Cap::CODE_EXEC,
bins: &["cc"],
},
Case {
lang: Lang::Cpp,
fixture_dir: "cpp",

View file

@ -0,0 +1,25 @@
/* Benign control for the recursive C receiver fixture. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct ShellRunner {
int enabled;
} ShellRunner;
typedef struct CommandRunner {
ShellRunner *shell;
} CommandRunner;
typedef struct UserService {
CommandRunner *runner;
} UserService;
void UserService_run(UserService *self, const char *input, size_t len) {
(void)input;
(void)len;
if (!self || !self->runner || !self->runner->shell) {
return;
}
system("true");
}

View file

@ -0,0 +1,26 @@
/* ClassMethod C fixture with a receiver pointer and recursive struct deps. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct ShellRunner {
int enabled;
} ShellRunner;
typedef struct CommandRunner {
ShellRunner *shell;
} CommandRunner;
typedef struct UserService {
CommandRunner *runner;
} UserService;
void UserService_run(UserService *self, const char *input, size_t len) {
(void)len;
if (!self || !self->runner || !self->runner->shell) {
return;
}
char buf[512];
snprintf(buf, sizeof(buf), "true %s", input ? input : "");
system(buf);
}