angr/tests/knowledge_plugins/functions/test_function_parser_fakeret.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

113 lines
3.7 KiB
Python
Raw Permalink Normal View History

function_parser: derive fake_return is_outside from cmsg.blocks membership (defense-in-depth for #6414) (#6417) * function_parser: derive fake_return outside flag from cmsg.blocks parse_from_cmessage's call-edge handler called obj._call_to(..., return_to_outside=fake_ret_edge is None) which only checked whether a matching fake_return edge exists, not whether its destination was external at save time. _call_to then called _register_node(is_local=True, ret_node) and added the destination to _local_block_addrs even when it was serialized as an external_block. This is a defense-in-depth follow-up to #6416. With that fix landed, no newly-saved record should contain a fake_return edge with is_outside=False whose dst is in cmsg.external_blocks. But: * legacy LMDB records written before #6416 still have this shape * other call sites (notably _add_fakeret_to(confirmed=None)) can create the same in-memory inconsistency, which then makes the parser inflate the block set on every roundtrip Fix: derive return_to_outside / to_outside from cmsg.blocks membership (the authoritative 'was local at save' signal) rather than from the edge attribute. The same logic applies to the subsequent obj._fakeret_to call, which previously used the edge attribute directly. Adds tests/knowledge_plugins/functions/test_function_parser_fakeret.py which round-trips a function with the inconsistent edge shape and asserts _local_block_addrs is preserved. * test_function_parser_fakeret: assert func is not None for pyright * test_function_parser_fakeret: drop __package__ override
2026-05-20 05:49:54 -04:00
#!/usr/bin/env python3
# pylint: disable=missing-class-docstring
"""Regression test for serialize/parse round-trip non-idempotence
when a Function has a fake_return edge whose destination is external
(in cmsg.external_blocks at save time) but whose edge attribute
outside=False.
Before the fix, parse_from_cmessage's call-edge handler trusted the
saved edge attribute and called _call_to(return_to_outside=False),
which made _call_to register the destination as a local block via
_register_node. Each roundtrip added one entry to
_local_block_addrs.
This shape arises organically when CFGFast calls
kb.functions._add_fakeret_to(..., confirmed=None) -- the underlying
Function._fakeret_to(confirmed=None) adds the edge with
outside=False but does NOT call _register_node for the to_node
(the `if confirmed:` branch is skipped). At save the dst goes to
cmsg.external_blocks; the edge keeps is_outside=False; the loader
then disagrees.
"""
from __future__ import annotations
import tempfile
import unittest
import angr
from angr.codenode import BlockNode, FuncNode
from angr.knowledge_plugins.functions.function import Function
class TestFunctionParserFakeret(unittest.TestCase):
def test_serialize_parse_roundtrip_with_external_fakeret(self):
blob = bytes.fromhex("ffc86b060000000000e86b060001")
addr = 0x100077547
fakeret_dst_addr = addr + 14
call_dst_addr = 0x101077BC0
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
f.write(blob)
blob_path = f.name
proj = angr.Project(
blob_path,
main_opts={
"backend": "blob",
"base_addr": addr,
"arch": "AMD64",
"entry_point": addr,
},
auto_load_libs=False,
)
fm = proj.kb.functions
func = fm.function(addr=addr, create=True)
assert func is not None
local_block = BlockNode(addr, 14, bytestr=blob[:14])
ext_block = BlockNode(fakeret_dst_addr, 4, bytestr=b"\x00" * 4)
func._register_node(True, local_block)
# Synthesize the call + fake_return edge pair that CFGFast can
# leave behind via _add_fakeret_to(confirmed=None).
call_target = FuncNode(call_dst_addr)
func.transition_graph.add_node(call_target)
func.transition_graph.add_edge(
local_block,
call_target,
type="call",
outside=False,
ins_addr=addr + 9,
stmt_idx=None,
)
func.transition_graph.add_node(ext_block)
func.transition_graph.add_edge(
local_block,
ext_block,
type="fake_return",
outside=False,
confirmed=True,
ins_addr=addr + 9,
stmt_idx=None,
)
pre = set(func._local_block_addrs)
cmsg = func.serialize_to_cmessage()
# Sanity: cmsg shape that exposes the bug.
self.assertEqual([b.ea for b in cmsg.blocks], [addr])
fakeret_edges = [e for e in cmsg.graph.edges if e.dst_ea == fakeret_dst_addr]
self.assertEqual(len(fakeret_edges), 1)
self.assertFalse(
bool(fakeret_edges[0].is_outside),
"test setup: edge must have is_outside=False",
)
loaded = Function.parse_from_cmessage(
cmsg,
function_manager=fm,
project=proj,
)
post = set(loaded._local_block_addrs)
self.assertEqual(
pre,
post,
f"round-trip is not idempotent: pre={sorted(hex(a) for a in pre)} post={sorted(hex(a) for a in post)}",
)
if __name__ == "__main__":
unittest.main()