diff --git a/src/dwarffi/instances.py b/src/dwarffi/instances.py index c1bbecd..a5795e5 100644 --- a/src/dwarffi/instances.py +++ b/src/dwarffi/instances.py @@ -4,7 +4,7 @@ from .backend import LiveMemoryProxy from .dtyping import FlatFieldsDict, MemoryBuffer, StructLike, TypeAccessor, TypeInfoDict, Vtype -from .types import VtypeBaseType, VtypeEnum, VtypeUserType +from .types import VtypeBaseType, VtypeEnum, VtypeFunction, VtypeParameter, VtypeUserType def _wrap_integer(value: int, size_bytes: int, signed: bool) -> int: @@ -1017,7 +1017,44 @@ def points_to_type_name(self) -> str: return str(self._subtype_info.name) # If it's a raw ISF dictionary, use .get() - return str(self._subtype_info.get("name", "void")) + if isinstance(self._subtype_info, dict): + if self._subtype_info.get("kind") == "function": + return "function" + return str(self._subtype_info.get("name", "void")) + + return "void" + + @property + def signature(self) -> Optional[VtypeFunction]: + """ + Returns the function signature (as a VtypeFunction) if this pointer + points to a function, or None otherwise. + """ + if isinstance(self._subtype_info, dict) and self._subtype_info.get("kind") == "function": + ret_info = self._subtype_info.get("return_type", {"kind": "void"}) + params_info = self._subtype_info.get("parameters", []) + + params = [] + for p in params_info: + if isinstance(p, dict) and "type" in p: + p_name = p.get("name", "") + p_type = p["type"] + else: + p_name = "" + p_type = p + + param = VtypeParameter(name=p_name, type_info=p_type, _dffi=self._vtype_accessor) + params.append(param) + + func = VtypeFunction( + name=self._subtype_info.get("name", ""), + return_type_info=ret_info, + parameters=params, + _dffi=self._vtype_accessor + ) + return func + + return None def deref(self) -> Any: """ diff --git a/tests/test_e2e_edge_cases.py b/tests/test_e2e_edge_cases.py index ac61ae1..b6ea7f3 100644 --- a/tests/test_e2e_edge_cases.py +++ b/tests/test_e2e_edge_cases.py @@ -322,4 +322,4 @@ def test_e2e_function_returning_function_pointer(compiler): assert inst.get_handler.address == 0xCAFEBABE # The string representation should gracefully handle the nested type info - assert "void" in inst.get_handler.points_to_type_name \ No newline at end of file + assert "function" in inst.get_handler.points_to_type_name \ No newline at end of file diff --git a/tests/test_e2e_functions.py b/tests/test_e2e_functions.py index 5ddc5ed..46efda9 100644 --- a/tests/test_e2e_functions.py +++ b/tests/test_e2e_functions.py @@ -343,4 +343,67 @@ def test_e2e_deep_function_signature_resolution(compiler): # Validate the size calculation recursively handles the inner mac_addr_t arrays # 6 bytes (src) + 6 bytes (dst) + 2 bytes (short) = 14 bytes - assert ffi.sizeof(pkt_struct) == 14 \ No newline at end of file + assert ffi.sizeof(pkt_struct) == 14 + +@pytest.mark.parametrize("compiler", AVAILABLE_COMPILERS) +def test_e2e_struct_function_pointer_member(compiler): + """ + Tests dynamic extraction of function signatures from members of a struct + using a simulated file_operations jump table. + """ + ffi = DFFI() + ffi.cdef( + """ + typedef long long loff_t; + struct file { + loff_t f_pos; + }; + + struct file_operations { + loff_t (*llseek) (struct file *, loff_t, int); + long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); + }; + + void __attribute__((used)) _force_keep(struct file_operations f) {} + """, + compiler=compiler + ) + + if not ffi.functions and not ffi.types.get("file_operations"): + pytest.skip("System dwarf2json does not support custom function signatures or type resolution failed. Skipping.") + + inst = ffi.new("struct file_operations") + + # 1. Inspect llseek signature + llseek_ptr = inst.llseek + assert llseek_ptr.points_to_type_name == "function" + + sig1 = llseek_ptr.signature + if not sig1: + pytest.skip("dwarf2json version does not output function signatures for pointers. Skipping.") + + # Check if this dwarf2json version outputs full signature details + if not sig1.args: + pytest.skip("dwarf2json version does not output function signature parameters for pointers. Skipping.") + + assert sig1.return_type_info.get("name") in ("loff_t", "long long", "long long int") + assert len(sig1.args) == 3 + + # Arg 0: struct file * + assert sig1.args[0].type_info["kind"] == "pointer" + assert sig1.args[0].type_info["subtype"]["name"] == "file" + + # Arg 1: loff_t + assert sig1.args[1].type_info["name"] in ("loff_t", "long long", "long long int") + + # Arg 2: int + assert sig1.args[2].type_info["name"] == "int" + + # 2. Inspect unlocked_ioctl signature + ioctl_ptr = inst.unlocked_ioctl + sig2 = ioctl_ptr.signature + assert sig2 is not None + assert sig2.return_type_info.get("name") in ("long", "long int") + assert len(sig2.args) == 3 + assert sig2.args[1].type_info["name"] == "unsigned int" + assert sig2.args[2].type_info["name"] in ("unsigned long", "long unsigned int") \ No newline at end of file diff --git a/tests/test_struct_function_pointers.py b/tests/test_struct_function_pointers.py new file mode 100644 index 0000000..5ce9b1b --- /dev/null +++ b/tests/test_struct_function_pointers.py @@ -0,0 +1,155 @@ +import pytest + +from dwarffi import DFFI + + +@pytest.fixture +def mock_isf_with_function_pointer(): + """A mock ISF dictionary containing a struct with an anonymous function pointer.""" + return { + "metadata": {"format": "6.2.0", "producer": {"name": "test", "version": "1.0"}}, + "base_types": { + "int": {"size": 4, "signed": True, "kind": "int", "endian": "little"}, + "loff_t": {"size": 8, "signed": True, "kind": "int", "endian": "little"}, + "pointer": {"size": 8, "signed": False, "kind": "pointer", "endian": "little"} + }, + "user_types": { + "file": { + "kind": "struct", + "size": 16, + "fields": { + "f_pos": {"offset": 0, "type": {"kind": "base", "name": "loff_t"}} + } + }, + "file_operations": { + "kind": "struct", + "size": 8, + "fields": { + "proc_lseek": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "function", + "return_type": {"kind": "base", "name": "loff_t"}, + "parameters": [ + {"kind": "pointer", "subtype": {"kind": "struct", "name": "file"}}, + {"kind": "base", "name": "loff_t"}, + {"name": "whence", "type": {"kind": "base", "name": "int"}} + ] + } + } + } + } + } + }, + "enums": {}, + "symbols": {}, + "functions": {} + } + +def test_struct_function_pointer_introspection(mock_isf_with_function_pointer): + """Verify that function pointers inside structs can be correctly introspected.""" + ffi = DFFI(mock_isf_with_function_pointer) + + # Create an instance + fops = ffi.new("struct file_operations") + ptr = fops.proc_lseek + + # Verify points_to_type_name + assert ptr.points_to_type_name == "function" + assert repr(ptr) == "" + + # Verify signature extraction + sig = ptr.signature + assert sig is not None + assert sig.return_type_info["name"] == "loff_t" + + # Verify parameters + assert len(sig.args) == 3 + + # First argument has no name, but valid type + assert sig.args[0].name == "" + assert sig.args[0].type_info["kind"] == "pointer" + + # Second argument has no name, but valid type + assert sig.args[1].name == "" + assert sig.args[1].type_info["name"] == "loff_t" + + # Third argument has both name and type + assert sig.args[2].name == "whence" + assert sig.args[2].type_info["name"] == "int" + +def test_struct_function_pointer_legacy_compat(): + """Verify that an older ISF without return_type or parameters doesn't break.""" + isf = { + "metadata": {"format": "6.2.0"}, + "base_types": { + "pointer": {"size": 8, "signed": False, "kind": "pointer", "endian": "little"} + }, + "user_types": { + "legacy_struct": { + "kind": "struct", + "size": 8, + "fields": { + "old_func": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "function" + } + } + } + } + } + }, + "enums": {}, + "symbols": {}, + } + ffi = DFFI(isf) + inst = ffi.new("struct legacy_struct") + + ptr = inst.old_func + assert ptr.points_to_type_name == "function" + + sig = ptr.signature + assert sig is not None + assert sig.return_type_info == {"kind": "void"} + assert len(sig.args) == 0 + +def test_non_function_pointer_signature(): + """Verify that a non-function pointer returns None for signature.""" + isf = { + "metadata": {"format": "6.2.0"}, + "base_types": { + "int": {"size": 4, "signed": True, "kind": "int", "endian": "little"}, + "pointer": {"size": 8, "signed": False, "kind": "pointer", "endian": "little"} + }, + "user_types": { + "my_struct": { + "kind": "struct", + "size": 8, + "fields": { + "ptr": { + "offset": 0, + "type": { + "kind": "pointer", + "subtype": { + "kind": "base", + "name": "int" + } + } + } + } + } + }, + "enums": {}, + "symbols": {}, + } + ffi = DFFI(isf) + inst = ffi.new("struct my_struct") + + ptr = inst.ptr + assert ptr.points_to_type_name == "int" + assert ptr.signature is None