Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions scripts/sof-crash-decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,61 @@
import os
import json
import shlex
import ast
import operator


# Largest shift we accept: linker addresses/sizes fit well under 2**64, so a
# bigger shift is nonsensical and only serves to build a huge integer.
_MAX_SHIFT = 64


def _trunc_div(a, b):
# truncate toward zero (C semantics); Python's // floors instead
q = abs(a) // abs(b)
return -q if (a < 0) != (b < 0) else q


def _lshift(a, b):
# reject absurd shift counts to avoid building a massive integer
if b < 0 or b > _MAX_SHIFT:
raise ValueError("shift count out of range")
return a << b


# Operators allowed when evaluating arithmetic from an untrusted linker script.
_SAFE_OPS = {
ast.Add: operator.add, ast.Sub: operator.sub,
ast.Mult: operator.mul, ast.Div: _trunc_div,
ast.Mod: operator.mod, ast.LShift: _lshift,
ast.RShift: operator.rshift, ast.BitOr: operator.or_,
ast.BitAnd: operator.and_, ast.BitXor: operator.xor,
ast.USub: operator.neg, ast.UAdd: operator.pos,
}
Comment on lines +61 to +68

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed — ast.Div now maps to a truncate-toward-zero helper (C/linker semantics) instead of Python's floor //. e.g. -7 / 2 evaluates to -3, not -4.



def safe_eval_int(expr):
"""Evaluate an integer arithmetic expression without executing code.

A crash bundle's linker.cmd is attacker-controllable, so its MEMORY
expressions must never be passed to eval(). Only integer literals and
basic arithmetic operators are accepted; anything else raises ValueError.
"""
def _eval(node):
if isinstance(node, ast.Expression):
return _eval(node.body)
if isinstance(node, ast.Constant):
# reject bool (a subclass of int) and everything non-integer
if type(node.value) is int:
return node.value
raise ValueError("non-integer constant")
Comment on lines +81 to +85

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the constant check is now type(node.value) is int, so booleans (a subclass of int) are rejected as non-integers.

if isinstance(node, ast.BinOp) and type(node.op) in _SAFE_OPS:
return _SAFE_OPS[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in _SAFE_OPS:
return _SAFE_OPS[type(node.op)](_eval(node.operand))
Comment on lines +86 to +89

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a guard — left shifts are now capped at 64 bits (_MAX_SHIFT), which rejects things like 1 << 100000000 before a huge integer is built. Linker addresses/sizes are well under 2**64, so this doesn't restrict valid input. Deep nesting / large literals are bounded in practice by the small regex-extracted org/len expressions, and any RecursionError/MemoryError is caught by the caller (see below).

raise ValueError("unsupported expression")

return _eval(ast.parse(expr, mode='eval'))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caller already handles this: parse_linker_cmd() wraps both safe_eval_int() calls in try: ... except Exception: pass, so a SyntaxError from ast.parse(), a ZeroDivisionError, or a RecursionError just causes that MEMORY region to be skipped rather than aborting the tool. So the simple "invalid expression -> ignore" failure mode is already in place.


XTENSA_EXCCAUSE = {
0: "No Error (or IllegalInstruction)",
Expand Down Expand Up @@ -151,8 +206,8 @@ def parse_linker_cmd(filepath):
org_expr = m_org.group(1).strip()
len_expr = m_len.group(1).strip()
try:
org_val = eval(org_expr)
len_val = eval(len_expr)
org_val = safe_eval_int(org_expr)
len_val = safe_eval_int(len_expr)
# Ignore debug regions
if not (name.startswith('.debug') or name.startswith('.stab')):
regions.append({'name': name, 'start': org_val, 'end': org_val + len_val})
Expand Down
Loading