diff --git a/angr/knowledge_plugins/functions/function_parser.py b/angr/knowledge_plugins/functions/function_parser.py index 38a8a545b..725349787 100644 --- a/angr/knowledge_plugins/functions/function_parser.py +++ b/angr/knowledge_plugins/functions/function_parser.py @@ -216,7 +216,7 @@ class FunctionParser: obj.info = json.loads(cmsg.info.decode("utf-8")) if cmsg.info else {} obj.is_default_name = cmsg.is_default_name obj.ran_cca = cmsg.ran_cca - obj.previous_names = cmsg.previous_names + obj.previous_names = list(cmsg.previous_names) # signature matched? if cmsg.matched_from == function_pb2.Function.UNMATCHED: diff --git a/tests/knowledge_plugins/functions/test_function_parser_previous_names.py b/tests/knowledge_plugins/functions/test_function_parser_previous_names.py new file mode 100644 index 000000000..57274e0a0 --- /dev/null +++ b/tests/knowledge_plugins/functions/test_function_parser_previous_names.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# pylint: disable=missing-class-docstring +"""Regression test: parse_from_cmsg assigned the protobuf RepeatedScalarContainer +directly to Function.previous_names instead of converting it to a plain list. +This breaks pickling of Function objects. +""" + +from __future__ import annotations + +import pickle +import tempfile +import unittest + +import angr +from angr.codenode import BlockNode +from angr.knowledge_plugins.functions.function import Function + + +class TestFunctionParserPreviousNames(unittest.TestCase): + def test_parsed_previous_names_is_plain_list_and_picklable(self): + blob = bytes.fromhex("c3") # ret + addr = 0x400000 + + 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 + func._register_node(True, BlockNode(addr, 1, bytestr=blob)) + func.name = "original_name" + func.name = "renamed" # records "original_name" in previous_names + self.assertIn("original_name", func.previous_names) + expected = list(func.previous_names) + + cmsg = func.serialize_to_cmessage() + loaded = Function.parse_from_cmessage(cmsg, function_manager=fm, project=proj) + + # previous_names must be a plain list, not a live protobuf container + self.assertIs(type(loaded.previous_names), list) + self.assertEqual(loaded.previous_names, expected) + + # mutating the parsed function must not alias back into the cmsg + loaded.previous_names.append("another_name") + self.assertEqual(list(cmsg.previous_names), expected) + + # the parsed function must round-trip through pickle + unpickled = pickle.loads(pickle.dumps(loaded)) + self.assertEqual(unpickled.previous_names, [*expected, "another_name"]) + + +if __name__ == "__main__": + unittest.main()