mirror of
https://github.com/angr/angr
synced 2026-08-17 12:23:11 -04:00
* 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
112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
#!/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()
|