diff --git a/src/dynamic/lang/c.rs b/src/dynamic/lang/c.rs index c5b010d9..06e019a9 100644 --- a/src/dynamic/lang/c.rs +++ b/src/dynamic/lang/c.rs @@ -450,7 +450,8 @@ pub fn emit(spec: &HarnessSpec) -> Result { // 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 { /// `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 @@ -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, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct CStructDef { + name: String, + fields: Vec, +} + +#[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 { + 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 { + 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, +) { + 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 { + 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::>(); + out.push(CStructDef { + name: name.to_owned(), + fields, + }); + } + out +} + +fn c_struct_field(raw: &str) -> Option { + 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); diff --git a/tests/class_method_corpus.rs b/tests/class_method_corpus.rs index fa6fa6df..cdfdbec5 100644 --- a/tests/class_method_corpus.rs +++ b/tests/class_method_corpus.rs @@ -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", diff --git a/tests/dynamic_fixtures/class_method/c_recursive_deps/benign.c b/tests/dynamic_fixtures/class_method/c_recursive_deps/benign.c new file mode 100644 index 00000000..0b5ab18f --- /dev/null +++ b/tests/dynamic_fixtures/class_method/c_recursive_deps/benign.c @@ -0,0 +1,25 @@ +/* Benign control for the recursive C receiver fixture. */ +#include +#include +#include + +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"); +} diff --git a/tests/dynamic_fixtures/class_method/c_recursive_deps/vuln.c b/tests/dynamic_fixtures/class_method/c_recursive_deps/vuln.c new file mode 100644 index 00000000..c6aa446c --- /dev/null +++ b/tests/dynamic_fixtures/class_method/c_recursive_deps/vuln.c @@ -0,0 +1,26 @@ +/* ClassMethod C fixture with a receiver pointer and recursive struct deps. */ +#include +#include +#include + +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); +}