variable_recovery: don't add call-arg type constraints from guessed prototypes' integer params

Integer parameter types in guessed prototypes are size-based defaults, not
inferred types. Using them as call-site constraints can conflict with pointer
constraints on the same variable from other call sites, collapsing the type
solution to bottom and degrading recovered pointer parameters to plain
integers. Skip them; pointer/struct/float parameters and non-guessed
prototypes are unaffected.
This commit is contained in:
atipriya 2026-07-15 06:12:38 +00:00
parent 36587c513a
commit 17373ff40c
2 changed files with 82 additions and 3 deletions

View file

@ -249,7 +249,14 @@ class SimEngineVRAIL(
isinstance(expr.target, (ailment.Expr.Const, str))
or expr.tags.get("is_prototype_guessed", True) is False
) and expr.args is not None:
self._call_add_arg_based_type_constraints(prototype, prototype_libname, args, list(expr.args))
prototype_guessed = (
func.is_prototype_guessed
if func is not None
else expr.tags.get("is_prototype_guessed", True) is not False
)
self._call_add_arg_based_type_constraints(
prototype, prototype_libname, args, list(expr.args), prototype_guessed=prototype_guessed
)
# handle return type
if not expr.tags.get("is_prototype_guessed", True):
return_ty = self.type_lifter.lift(prototype.returnty) # type: ignore
@ -322,7 +329,14 @@ class SimEngineVRAIL(
isinstance(stmt.expr.target, (ailment.Expr.Const, str))
or stmt.tags.get("is_prototype_guessed", True) is False
):
self._call_add_arg_based_type_constraints(prototype, prototype_libname, args, stmt.expr.args)
prototype_guessed = (
func.is_prototype_guessed
if func is not None
else stmt.tags.get("is_prototype_guessed", True) is not False
)
self._call_add_arg_based_type_constraints(
prototype, prototype_libname, args, stmt.expr.args, prototype_guessed=prototype_guessed
)
# handle return type
return_ty = self.type_lifter.lift(prototype.returnty) # type: ignore
ret_ty = self.tv_manager.new_tv()
@ -362,7 +376,12 @@ class SimEngineVRAIL(
)
def _call_add_arg_based_type_constraints(
self, prototype: SimTypeFunction, prototype_libname: str | None, args: list, arg_atoms: list
self,
prototype: SimTypeFunction,
prototype_libname: str | None,
args: list,
arg_atoms: list,
prototype_guessed: bool = False,
) -> None:
# add type constraints
if not args:
@ -373,6 +392,12 @@ class SimEngineVRAIL(
continue
arg_type = dereference_simtype_by_lib(arg_type, prototype_libname) if prototype_libname else arg_type
arg_ty = self.type_lifter.lift(arg_type)
if prototype_guessed and isinstance(arg_ty, typeconsts.Int) and not isinstance(arg_ty, typeconsts.Pointer):
# plain integer parameter types in guessed prototypes are usually size-based defaults and carry
# no real type information. adding them as constraints can conflict with precise (e.g., pointer)
# constraints from other call sites and degrade the solution to the bottom type. skip them;
# informative types (pointers, structs, floats) from guessed prototypes are still used.
continue
if arg.typevar is not None and isinstance(
arg_ty, (typeconsts.TypeConstant, typevars.TypeVariable, typevars.DerivedTypeVariable)
):

View file

@ -7,8 +7,16 @@ import logging
import os
import unittest
import archinfo
import angr
from angr.analyses.typehoon import typeconsts
from angr.analyses.typehoon.translator import TypeTranslator
from angr.analyses.typehoon.typevars import Subtype, TypeVariable
from angr.analyses.variable_recovery.engine_ail import SimEngineVRAIL
from angr.analyses.variable_recovery.engine_base import RichR
from angr.knowledge_plugins.variables import VariableType
from angr.sim_type import SimTypeChar, SimTypeFunction, SimTypeInt, SimTypeLongLong, SimTypePointer
from angr.sim_variable import SimRegisterVariable, SimStackVariable
from tests.common import bin_location, print_decompilation_result
@ -448,6 +456,52 @@ class TestVariableRecovery(unittest.TestCase):
False,
)
def test_guessed_prototype_int_args_do_not_constrain_caller_types(self):
"""
Plain integer parameter types in guessed callee prototypes are size-based defaults and must not
generate type constraints for the caller's argument expressions: a spurious int64 upper bound
conflicts with precise pointer constraints from other call sites (SInt64 and Pointer64 are
siblings in the base type lattice), collapsing the solution to the bottom type and degrading
recovered pointer arguments (e.g., a char* format string) to plain integers.
Informative parameter types (pointers, structs) from guessed prototypes must still be used, and
non-guessed prototypes (e.g., libc SimProcedures) must constrain all arguments as before.
"""
arch = archinfo.arch_from_id("AMD64")
engine = object.__new__(SimEngineVRAIL)
engine.type_lifter = TypeTranslator(arch)
constraints = []
class FakeState:
@staticmethod
def get_stack_offset(_data):
return None
@staticmethod
def add_type_constraint(con):
constraints.append(con)
engine.state = FakeState()
tv_int = TypeVariable(name="arg_int")
tv_ptr = TypeVariable(name="arg_ptr")
args = [RichR(None, typevar=tv_int), RichR(None, typevar=tv_ptr)]
prototype = SimTypeFunction([SimTypeLongLong(), SimTypePointer(SimTypeChar())], SimTypeInt()).with_arch(arch)
# guessed prototype: the plain-int parameter adds no constraint; the pointer parameter still does
engine._call_add_arg_based_type_constraints(prototype, None, args, [None, None], prototype_guessed=True)
assert len(constraints) == 1
(con,) = constraints
assert isinstance(con, Subtype)
assert con.sub_type is tv_ptr
assert isinstance(con.super_type, typeconsts.Pointer)
# non-guessed prototype: both parameters constrain the arguments, as before
constraints.clear()
engine._call_add_arg_based_type_constraints(prototype, None, args, [None, None], prototype_guessed=False)
assert len(constraints) == 2
assert {con.sub_type for con in constraints if isinstance(con, Subtype)} == {tv_int, tv_ptr}
def test_format_string_type_hints_sscanf(self):
"""
Test that VariableRecoveryFast extracts type hints from format strings.