Skip to content

Add an x86-64 lowered-switch fixture whose head is reached through a range check - #227

Open
zardus wants to merge 1 commit into
masterfrom
feature/man2html-lowered-switch
Open

zardus wants to merge 1 commit into
masterfrom
feature/man2html-lowered-switch

Conversation

@zardus

@zardus zardus commented Sep 7, 2026 •

Copy link
Copy Markdown
Member

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Problem

LoweredSwitchSimplifier builds a switch head out of the first comparison node that emits a case, then removes the comparison nodes the switch subsumes. When one of those redundant comparisons sits upstream of the head, removing it leaves the head with no in-edges, and the same walk takes the head and every case body hanging off it. The pass reads one of those nodes back and raises, which sends the whole function to the basic decompilation preset. The search below found no object already tracked here that reproduces that shape, so the consumer cannot assert on it.

Root cause

The shape needs a lowered switch whose first case-emitting comparison is reached through a range-splitting comparison, which is a narrow codegen accident. With angr at 854269112a5ce5b1b356cf6d67bff9abaf986b55, every function of at least eight basic blocks in fifteen tracked x86-64 binaries -- file at two optimization levels, plus cat, cvs, dir, elfedit, grep, gzip, lighttpd, luac, minigzip, mv, sort, tar and tiffinfo, 3,409 functions in total -- was decompiled, and the pass raised on none of them. The same scan reports one hit when pointed at this fixture, so its silence on the fifteen is a result rather than a broken instrument.

Fix

tests/x86_64/man2html_gcc11.4.0_O2 is bash 5.2's support/man2html.c, whose scan_request has the shape. It is taken byte for byte from the public, ungated HuggingFace dataset noelo-lab/decbench-dataset at commit 4b42a0dc6158913db0648a9123e76d6ddd9ab9cf, path binaries/O2/bash/man2html, and downloads anonymously from there:

$ curl -sL -o m2h -w '%{http_code} %{size_download}\n' https://huggingface.co/datasets/noelo-lab/decbench-dataset/resolve/4b42a0dc6158913db0648a9123e76d6ddd9ab9cf/binaries/O2/bash/man2html
200 165992
$ sha256sum m2h
49e20152077b12b3aa33efd065450d7a42729045e1d6f9c85046ecbe4a41c1a1  m2h

DW_AT_producer records the flags -- -mtune=generic -march=x86-64 -g -O2 -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection -fcf-protection -- and .comment records the compiler, GCC: (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0. Its .interp is /lib64/ld-linux-x86-64.so.2 and it declares no RUNPATH or RPATH. It carries seven absolute paths in all: that interpreter, a DW_AT_comp_dir of /tmp/tmp53t3gycz/bash-5.2/support, and five DWARF include directories under /usr.

Testing

This repository runs no CI. Loaded with angr at master 854269112a5ce5b1b356cf6d67bff9abaf986b55, decompiling scan_request at 0x4051d0 leaves Decompiler.errors holding NetworkXError: The node <AILBlock 0x406572> is not in the digraph and returns 2,515 lines with 136 gotos from the fallback preset. With the consumer's change the errors list is empty and the same function is 2,473 lines with 135 gotos. That is the assertion the consumer gains.

Validation: #227 (comment)

🤖 Generated with Claude Code

session: sharpen

@zardus

zardus commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Validation record for head 44a93d4f867e670fb593bbc3b151a0dc04474daa against baseline fc07821c89535534979b02760e7e1bfc35faf690.

What it adds. One object, tests/x86_64/man2html_gcc11.4.0_O2, mode 100755, 165,992 bytes, blob ee7738c8db25a5191bc5db2199950341b1356105, sha256 49e20152077b12b3aa33efd065450d7a42729045e1d6f9c85046ecbe4a41c1a1. No other path changes.

Provenance, checked anonymously. The HuggingFace dataset noelo-lab/decbench-dataset answers an unauthenticated API call with HTTP 200 and private: false, gated: false, disabled: false. The file downloads from it without credentials at a pinned revision and is byte-identical to what is committed here:

$ curl -sL -o m2h -w 'http=%{http_code} size=%{size_download}\n' https://huggingface.co/datasets/noelo-lab/decbench-dataset/resolve/4b42a0dc6158913db0648a9123e76d6ddd9ab9cf/binaries/O2/bash/man2html
http=200 size=165992
$ sha256sum m2h
49e20152077b12b3aa33efd065450d7a42729045e1d6f9c85046ecbe4a41c1a1  m2h

Build, read out of the object. readelf --debug-dump=info gives one DW_AT_producer, GNU C17 11.4.0 -mtune=generic -march=x86-64 -g -O2 -fno-builtin -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection -fcf-protection, and one DW_AT_comp_dir, /tmp/tmp53t3gycz/bash-5.2/support. readelf -p .comment gives GCC: (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0. The source is bash 5.2's support/man2html.c.

No workspace or store paths. readelf -p .interp is /lib64/ld-linux-x86-64.so.2, matching the tracked x86-64 objects rather than a nix store path, and readelf -d reports no RUNPATH or RPATH. strings -a … | command grep -E '^/' | sort -u gives seven absolute paths and no more: that interpreter, the comp_dir above, and /usr/include, /usr/include/x86_64-linux-gnu/bits, /usr/include/x86_64-linux-gnu/bits/types, /usr/include/x86_64-linux-gnu/sys, /usr/lib/gcc/x86_64-linux-gnu/11/include. Over the whole 1,034-string dump, man2html matches 10 and GCC: matches 1 as positive controls, while /home/ and decbench match 0.

The search found nothing that reproduces it. With angr at 854269112a5ce5b1b356cf6d67bff9abaf986b55, every function of at least eight basic blocks -- the scanner also skips SimProcedures, PLT stubs and alignment functions -- in fifteen tracked x86-64 binaries was decompiled and its Decompiler.errors inspected for a frame in lowered_switch_simplifier.py: cat_gcc17.0.0_O2 27, cvs 737, dir_gcc_-O0 170, elfedit_gcc17_O0 34, file_gcc13.3.0_O2 137, file_gcc17_O0 229, grep_gcc17.0.0_O2 156, gzip_gcc13.3.0_O2 70, lighttpd_gcc17.0.0_O2 416, luac_gcc13.3.0_O2 304, minigzip_gcc17.0.0_O2 93, mv_-O2 115, sort_gcc17_O0 148, tar_gcc17_O2 468, tiffinfo_gcc17_O0 305 -- 3,409 functions, zero hits.

The scanner was checked against a known positive. Pointed at this fixture at the same angr revision it reports one hit, scan_request at 0x4051d0, 846 blocks, NetworkXError from lowered_switch_simplifier.py:412. So the silence over the fifteen is a measurement rather than an instrument that cannot fire.

Before and after. Posted as the output comment on this pull request. Both arms are angr master 854269112a5ce5b1b356cf6d67bff9abaf986b55, the second with the consumer change applied, whole-binary CFGFast rather than a scoped one, run from the angr checkout root where ../binaries is the sibling checkout.

Suites. This repository has no .github/workflows at this head and runs no CI, so no check will appear and there is nothing to wait for. check-test-inputs.py exempts this repository by design and reports so. The consumer's own gate is recorded on the pull request that references this one.

@zardus

zardus commented Sep 7, 2026

Copy link
Copy Markdown
Member Author

THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS

Complete output of the recorded command for scan_request at 0x4051d0 in this fixture, before and after the consumer change in the pull request that references this one. Both arms are angr master 854269112a5ce5b1b356cf6d67bff9abaf986b55. Run from the angr checkout root, where ../binaries is the sibling checkout. Paths inside the traceback are shortened; nothing else is edited.

import logging

import angr

logging.getLogger("angr").setLevel(logging.ERROR)
logging.getLogger("cle").setLevel(logging.ERROR)
logging.getLogger("pyvex").setLevel(logging.ERROR)

proj = angr.Project("../binaries/tests/x86_64/man2html_gcc11.4.0_O2", auto_load_libs=False)
cfg = proj.analyses.CFGFast(normalize=True, data_references=True)
proj.analyses.CompleteCallingConventions(recover_variables=True, cfg=cfg.model)
dec = proj.analyses.Decompiler(cfg.functions[0x4051D0], cfg=cfg.model)
print("errors:", [str(e.exc_value) for e in dec.errors])
print("lines:", dec.codegen.text.count("\n"), "gotos:", dec.codegen.text.count("goto "))

Before — LoweredSwitchSimplifier raises, Analysis._resilience catches it, and the function comes back from the basic preset:

angr master
ERROR    | 2026-09-07 09:23:37,656 | angr.analyses.analysis | Caught and logged NetworkXError with resilience: The node <AILBlock 0x406572> is not in the digraph.
Traceback (most recent call last):
  File "site-packages/networkx/classes/digraph.py", line 937, in successors
    return iter(self._succ[n])
                ~~~~~~~~~~^^^
KeyError: <AILBlock 0x406572 of 4 statements>

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "angr/analyses/analysis.py", line 313, in _resilience
    yield
  File "angr/analyses/decompiler/decompiler.py", line 302, in __init__
    self._decompile_with_cache()
  File "angr/analyses/decompiler/decompiler.py", line 377, in _decompile_with_cache
    self._decompile()
  File "angr/utils/timing.py", line 73, in timed_func
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "angr/analyses/decompiler/decompiler.py", line 581, in _decompile
    clinic.graph, self.region_identifier = self._run_region_simplification_passes(
                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "angr/utils/timing.py", line 73, in timed_func
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "angr/analyses/decompiler/decompiler.py", line 815, in _run_region_simplification_passes
    a = pass_(
        ^^^^^^
  File "angr/utils/timing.py", line 73, in timed_func
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "angr/analyses/decompiler/optimization_passes/lowered_switch_simplifier.py", line 190, in __init__
    self.analyze()
  File "angr/analyses/decompiler/optimization_passes/optimization_pass.py", line 545, in analyze
    self._analyze_and_verify()
  File "angr/analyses/decompiler/optimization_passes/optimization_pass.py", line 577, in _analyze_and_verify
    updates = self._analyze(cache=cache)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "angr/analyses/decompiler/optimization_passes/lowered_switch_simplifier.py", line 412, in _analyze
    node_successors = list(graph_copy.successors(succ_node))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "site-packages/networkx/classes/digraph.py", line 939, in successors
    raise NetworkXError(f"The node {n} is not in the digraph.") from err
networkx.exception.NetworkXError: The node <AILBlock 0x406572> is not in the digraph.
errors: ['The node <AILBlock 0x406572> is not in the digraph.']
lines: 2515 gotos: 136

After — no caught exception, so the function keeps the full preset:

angr master with the consumer change applied
errors: []
lines: 2473 gotos: 135

…range check

`scan_request` in this binary has a lowered switch-case whose first
case-emitting comparison is reached through a range-splitting comparison.
LoweredSwitchSimplifier treats that splitter as redundant and removes it, which
orphans the switch head it has just built and takes the head and every case body
with it; the pass then reads one of those nodes back and raises.

No object already tracked here was found to have that shape. With angr at
854269112a5ce5b1b356cf6d67bff9abaf986b55, every function of at least eight basic
blocks in fifteen tracked x86-64 binaries -- `file` at two optimization levels,
plus cat, cvs, dir, elfedit, grep, gzip, lighttpd, luac, minigzip, mv, sort, tar
and tiffinfo, 3,409 functions in total -- was decompiled, and the pass raised on
none of them. The same scan reports one hit when pointed at this fixture.

`man2html` is bash 5.2's `support/man2html.c`, built for x86-64 by GCC 11.4.0 on
Ubuntu 22.04 with `-mtune=generic -march=x86-64 -g -O2 -fno-builtin
-fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection
-fcf-protection`, taken from the public HuggingFace dataset
`noelo-lab/decbench-dataset` at `4b42a0dc6158913db0648a9123e76d6ddd9ab9cf`, path
`binaries/O2/bash/man2html`. The bytes here are that file: 165,992 bytes, sha256
49e20152077b12b3aa33efd065450d7a42729045e1d6f9c85046ecbe4a41c1a1.
@zardus
zardus force-pushed the feature/man2html-lowered-switch branch from 44a93d4 to 0d17cb2 Compare September 23, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant