refactor(dynamic): add recursive dependency resolution for Java, Go, and Ruby receivers, expand corresponding tests

This commit is contained in:
elipeter 2026-05-24 21:45:54 -05:00
parent 0e8c900078
commit acec041676
10 changed files with 366 additions and 10 deletions

View file

@ -1876,11 +1876,78 @@ func nyxBuildReceiver(structName string) (reflect.Value, error) {{
// the auto-generated file references the target type by name and
// the Go compiler enforces the contract.
if r, ok := entry.NyxAutoReceivers[structName]; ok {{
return reflect.ValueOf(r), nil
return nyxPopulateReceiver(reflect.ValueOf(r), 3), nil
}}
return reflect.Value{{}}, fmt.Errorf("class not found: %s", structName)
}}
func nyxPopulateReceiver(v reflect.Value, depth int) reflect.Value {{
seen := map[reflect.Type]bool{{}}
return nyxPopulateValue(v, depth, seen)
}}
func nyxPopulateValue(v reflect.Value, depth int, seen map[reflect.Type]bool) reflect.Value {{
if !v.IsValid() || depth < 0 {{
return v
}}
if v.Kind() == reflect.Pointer {{
if v.IsNil() {{
if v.Type().Elem().Kind() != reflect.Struct {{
return v
}}
v = reflect.New(v.Type().Elem())
}}
nyxPopulateStruct(v.Elem(), depth, seen)
return v
}}
if v.Kind() == reflect.Struct {{
out := reflect.New(v.Type()).Elem()
out.Set(v)
nyxPopulateStruct(out, depth, seen)
return out
}}
return v
}}
func nyxPopulateStruct(v reflect.Value, depth int, seen map[reflect.Type]bool) {{
if !v.IsValid() || v.Kind() != reflect.Struct || depth < 0 {{
return
}}
t := v.Type()
if seen[t] {{
return
}}
seen[t] = true
defer delete(seen, t)
for i := 0; i < v.NumField(); i++ {{
field := v.Field(i)
if !field.CanSet() {{
continue
}}
dep := nyxBuildValueForType(field.Type(), depth-1, seen)
if dep.IsValid() && dep.Type().AssignableTo(field.Type()) {{
field.Set(dep)
}}
}}
}}
func nyxBuildValueForType(t reflect.Type, depth int, seen map[reflect.Type]bool) reflect.Value {{
if depth < 0 {{
return reflect.Value{{}}
}}
if t.Kind() == reflect.Pointer && t.Elem().Kind() == reflect.Struct {{
ptr := reflect.New(t.Elem())
nyxPopulateStruct(ptr.Elem(), depth, seen)
return ptr
}}
if t.Kind() == reflect.Struct {{
value := reflect.New(t).Elem()
nyxPopulateStruct(value, depth, seen)
return value
}}
return reflect.Value{{}}
}}
func nyxPayload() string {{
if v := os.Getenv("NYX_PAYLOAD"); v != "" {{
return v

View file

@ -3313,6 +3313,9 @@ fn emit_class_method_harness(
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Set;
public class NyxHarness {{
{probe}
@ -3322,6 +3325,14 @@ public class NyxHarness {{
{mock_log}
static Object nyxBuildReceiver(Class<?> cls) throws Exception {{
return nyxBuildReceiver(cls, 3, new HashSet<Class<?>>());
}}
static Object nyxBuildReceiver(Class<?> cls, int depth, Set<Class<?>> seen) throws Exception {{
if (cls == null || seen.contains(cls)) {{
return null;
}}
seen.add(cls);
// Preferred path: zero-arg ctor.
try {{
Constructor<?> c = cls.getDeclaredConstructor();
@ -3335,22 +3346,28 @@ public class NyxHarness {{
Class<?>[] params = c.getParameterTypes();
Object[] args = new Object[params.length];
for (int i = 0; i < params.length; i++) {{
args[i] = nyxStubForType(params[i]);
args[i] = nyxValueForType(params[i], depth - 1, new HashSet<Class<?>>(seen));
}}
try {{ return c.newInstance(args); }} catch (Exception ignore) {{}}
}}
return null;
}}
static Object nyxStubForType(Class<?> t) {{
String n = t.getName().toLowerCase();
if (n.contains("http") || n.contains("client")) return new MockHttpClient();
if (n.contains("database") || n.contains("connection") || n.contains("session") || n.contains("repository")) return new MockDatabaseConnection();
if (n.contains("logger") || n.contains("log")) return new MockLogger();
static Object nyxValueForType(Class<?> t, int depth, Set<Class<?>> seen) {{
if (t.equals(String.class)) return "";
if (t.equals(int.class) || t.equals(Integer.class)) return 0;
if (t.equals(long.class) || t.equals(Long.class)) return 0L;
if (t.equals(boolean.class) || t.equals(Boolean.class)) return false;
if (depth >= 0 && !t.isPrimitive() && !t.isInterface() && !Modifier.isAbstract(t.getModifiers())) {{
try {{
Object receiver = nyxBuildReceiver(t, depth, seen);
if (receiver != null) return receiver;
}} catch (Throwable ignore) {{}}
}}
String n = t.getName().toLowerCase();
if (n.contains("http") || n.contains("client")) return new MockHttpClient();
if (n.contains("database") || n.contains("connection") || n.contains("session") || n.contains("repository")) return new MockDatabaseConnection();
if (n.contains("logger") || n.contains("log")) return new MockLogger();
return null;
}}
@ -3380,10 +3397,13 @@ public class NyxHarness {{
Class<?>[] params = match.getParameterTypes();
Object[] mArgs = new Object[params.length];
for (int i = 0; i < params.length; i++) {{
mArgs[i] = params[i].equals(String.class) ? payload : nyxStubForType(params[i]);
mArgs[i] = params[i].equals(String.class) ? payload : nyxValueForType(params[i], 2, new HashSet<Class<?>>());
}}
match.invoke(instance, mArgs);
Object result = match.invoke(instance, mArgs);
System.out.println("__NYX_SINK_HIT__");
if (result != null) {{
System.out.println(result.toString());
}}
}} catch (InvocationTargetException ite) {{
Throwable cause = ite.getCause() == null ? ite : ite.getCause();
System.err.println("NYX_EXCEPTION: " + cause.getClass().getName() + ": " + cause.getMessage());

View file

@ -578,11 +578,47 @@ unless Object.const_defined?(cls_name)
end
cls = Object.const_get(cls_name)
def _nyx_build_receiver(cls)
def _nyx_known_mock_for(name)
n = name.to_s.downcase
return MockHttpClient.new if n.include?('http') || n.include?('client')
return MockDatabaseConnection.new if n.include?('db') || n.include?('conn') || n.include?('repo') || n.include?('session')
return MockLogger.new if n.include?('log')
nil
end
def _nyx_const_for_param(name)
raw = name.to_s
camel = raw.split('_').reject(&:empty?).map {{ |part| part[0].upcase + part[1..].to_s }}.join
[camel, raw].each do |candidate|
next if candidate.empty?
if Object.const_defined?(candidate, false)
value = Object.const_get(candidate)
return value if value.is_a?(Class)
end
end
nil
end
def _nyx_build_receiver(cls, depth = 3, seen = {{}})
return nil if seen[cls]
seen = seen.merge(cls => true)
begin
return cls.new
rescue ArgumentError
end
begin
init = cls.instance_method(:initialize)
deps = init.parameters.map do |_kind, name|
dep = nil
if depth > 0 && name
dep_cls = _nyx_const_for_param(name)
dep = _nyx_build_receiver(dep_cls, depth - 1, seen) if dep_cls && dep_cls != cls
end
dep || _nyx_known_mock_for(name)
end
return cls.new(*deps)
rescue StandardError
end
begin
return cls.new(MockHttpClient.new, MockDatabaseConnection.new, MockLogger.new)
rescue StandardError

View file

@ -182,14 +182,26 @@ fn class_method_java_emits_reflective_dispatch() {
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("Class.forName"));
assert!(h.source.contains("nyxBuildReceiver"));
assert!(h.source.contains("nyxValueForType(params[i], depth - 1"));
assert!(h.source.contains("Object result = match.invoke"));
assert!(h.source.contains("UserRepository"));
}
#[test]
fn class_method_ruby_dispatch_builds_recursive_receiver() {
let spec = make_spec(Lang::Ruby);
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("_nyx_build_receiver(cls, depth = 3"));
assert!(h.source.contains("_nyx_const_for_param"));
assert!(h.source.contains("depth - 1"));
}
#[test]
fn class_method_go_uses_reflect_receivers_registry() {
let spec = make_spec(Lang::Go);
let h = lang::emit(&spec).expect("emit ok");
assert!(h.source.contains("entry.NyxAutoReceivers"));
assert!(h.source.contains("nyxPopulateReceiver"));
assert!(h.source.contains("MethodByName"));
let registry = h
.extra_files
@ -284,6 +296,17 @@ mod e2e_phase_19 {
cap: Cap::CODE_EXEC,
bins: &["ruby"],
},
Case {
lang: Lang::Ruby,
fixture_dir: "ruby_recursive_deps",
vuln_file: "vuln.rb",
benign_file: "benign.rb",
vuln_class: "UserService",
benign_class: "UserService",
method: "run",
cap: Cap::CODE_EXEC,
bins: &["ruby"],
},
Case {
lang: Lang::JavaScript,
fixture_dir: "javascript",
@ -361,6 +384,17 @@ mod e2e_phase_19 {
cap: Cap::CODE_EXEC,
bins: &["java", "javac"],
},
Case {
lang: Lang::Java,
fixture_dir: "java_recursive_deps",
vuln_file: "Vuln.java",
benign_file: "Benign.java",
vuln_class: "Vuln$UserService",
benign_class: "Benign$UserService",
method: "run",
cap: Cap::CODE_EXEC,
bins: &["java", "javac"],
},
Case {
lang: Lang::Go,
fixture_dir: "go",
@ -372,6 +406,17 @@ mod e2e_phase_19 {
cap: Cap::CODE_EXEC,
bins: &["go"],
},
Case {
lang: Lang::Go,
fixture_dir: "go_recursive_deps",
vuln_file: "vuln.go",
benign_file: "benign.go",
vuln_class: "UserService",
benign_class: "UserService",
method: "Run",
cap: Cap::CODE_EXEC,
bins: &["go"],
},
Case {
lang: Lang::Rust,
fixture_dir: "rust",

View file

@ -0,0 +1,32 @@
// Benign control for recursively populated Go struct dependencies.
package entry
import "strings"
type ShellRunner struct{}
func (ShellRunner) Run(command string) string {
return strings.ReplaceAll(command, "NYX_PWN_CMDI", "")
}
type UserRepository struct {
Runner *ShellRunner
}
func (r UserRepository) Find(input string) string {
if r.Runner == nil {
return ""
}
return r.Runner.Run(input)
}
type UserService struct {
Repository *UserRepository
}
func (s UserService) Run(input string) string {
if s.Repository == nil {
return ""
}
return s.Repository.Find(input)
}

View file

@ -0,0 +1,33 @@
// Class-method fixture with recursively populated Go struct dependencies.
package entry
import "os/exec"
type ShellRunner struct{}
func (ShellRunner) Run(command string) string {
out, _ := exec.Command("sh", "-c", "true "+command).Output()
return string(out)
}
type UserRepository struct {
Runner *ShellRunner
}
func (r UserRepository) Find(input string) string {
if r.Runner == nil {
return ""
}
return r.Runner.Run(input)
}
type UserService struct {
Repository *UserRepository
}
func (s UserService) Run(input string) string {
if s.Repository == nil {
return ""
}
return s.Repository.Find(input)
}

View file

@ -0,0 +1,32 @@
// Benign control for recursively constructed Java dependencies.
public class Benign {
public static class ShellRunner {
public String run(String command) {
return command.replace("NYX_PWN_CMDI", "");
}
}
public static class UserRepository {
private final ShellRunner shellRunner;
public UserRepository(ShellRunner shellRunner) {
this.shellRunner = shellRunner;
}
public String find(String input) {
return shellRunner.run(input);
}
}
public static class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String run(String input) {
return userRepository.find(input);
}
}
}

View file

@ -0,0 +1,39 @@
// Class-method fixture with recursively constructed Java dependencies.
import java.io.InputStream;
public class Vuln {
public static class ShellRunner {
public String run(String command) throws Exception {
Process p = new ProcessBuilder("sh", "-c", "true " + command)
.redirectErrorStream(true)
.start();
try (InputStream in = p.getInputStream()) {
return new String(in.readAllBytes());
}
}
}
public static class UserRepository {
private final ShellRunner shellRunner;
public UserRepository(ShellRunner shellRunner) {
this.shellRunner = shellRunner;
}
public String find(String input) throws Exception {
return shellRunner.run(input);
}
}
public static class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String run(String input) throws Exception {
return userRepository.find(input);
}
}
}

View file

@ -0,0 +1,26 @@
# Benign control for recursively constructed Ruby dependencies.
class ShellRunner
def run(command)
command.gsub('NYX_PWN_CMDI', '')
end
end
class UserRepository
def initialize(shell_runner)
@shell_runner = shell_runner
end
def find(input)
@shell_runner.run(input)
end
end
class UserService
def initialize(user_repository)
@user_repository = user_repository
end
def run(input)
@user_repository.find(input)
end
end

View file

@ -0,0 +1,26 @@
# Class-method fixture with recursively constructed Ruby dependencies.
class ShellRunner
def run(command)
`true #{command}`
end
end
class UserRepository
def initialize(shell_runner)
@shell_runner = shell_runner
end
def find(input)
@shell_runner.run(input)
end
end
class UserService
def initialize(user_repository)
@user_repository = user_repository
end
def run(input)
@user_repository.find(input)
end
end