CFGFast restarts its linear scan inside an instruction it could not decode
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
Description
When _generate_cfgnode cannot lift an instruction, it marks a single byte as nodecode in
the segment list:
# the default case
valid_ins = False
nodecode_size = 1CFGFast's linear scan then takes the next unoccupied address, which is one or two bytes into
the instruction that failed, and decodes from there. Every block it builds after that starts in
the middle of a real instruction, and each one becomes a function of its own because nothing
reaches it.
The immediate cause is usually an encoding VEX does not support, so the block itself is a correct outcome. The scan restarting inside that instruction is not: the number of bytes to skip is knowable, and angr already has a disassembler that will say how many.
binaries/tests/x86_64/langdetect_gcc_dwarf is a plain glibc-static x86-64 binary whose string
routines use AVX-512, which #1386 tracks. CFGFast(normalize=True, data_references=False, resolve_indirect_jumps=True, force_complete_scan=False) produces, among 471 like them:
| instruction | encoding | block angr starts |
|---|---|---|
0x42045d vmovups zmm5, [rcx-0xc0] |
62 f1 7c 48 10 69 fd |
0x42045f, 2 bytes in |
0x424cdc vpcmpeqb k0, ymm16, [rsi] |
62 f3 7d 20 3f 06 00 |
0x424cde, 2 bytes in |
0x424cfa kmovd edx, k1 |
c5 fb 93 d1 |
0x424cfb, 1 byte in |
0x42b1ea vptestnmb k0, ymm19, ymm19 |
62 b2 66 20 26 c3 |
0x42b1eb, 1 byte in |
Each of those addresses is inside a STT_FUNC symbol whose linear disassembly consumes the
symbol's size exactly, so the real boundaries are not in doubt.
The same shape appears wherever an encoding is missing rather than only on AVX-512: incsspq
on CET binaries, and until angr/vex#VEX_PR every sc.w/sc.d/feq/flt on RISC-V, where the
minimum two-byte alignment still lands halfway through a four-byte instruction.
Over a 43k-object corpus sweep this was the single largest source of blocks that begin mid-instruction.
Options
Which of these is right is a maintainer's call, which is why this is an issue and not a pull request:
- Ask capstone for the length of the instruction at the failing address and skip that many
bytes, falling back to the current single byte when capstone will not decode it either. The
ARM
UNDbranch a few lines below already reads raw bytes for a similar purpose, andBlock.capstoneis used elsewhere in the same function. The risk is that capstone and VEX disagree and the skip hides a byte that VEX would have decoded. - Skip
self.project.arch.instruction_alignmentbytes rather than one. Correct for fixed-width ISAs and free, but no help on x86 and only half a fix on RISC-V with compressed instructions. - Do not let the linear scan start inside a range already marked
nodecode-adjacent -- treat a decode failure as covering the rest of the function's extent when the function's bounds are known from a symbol or an FDE.
Steps to reproduce the bug
import angr
proj = angr.Project("binaries/tests/x86_64/langdetect_gcc_dwarf", auto_load_libs=False)
cfg = proj.analyses.CFGFast(normalize=True, data_references=False,
resolve_indirect_jumps=True, force_complete_scan=False)
print(cfg.model.get_any_node(0x42045F)) # block 2 bytes into the vmovups at 0x42045d
print(0x42045F in cfg.kb.functions) # TrueEnvironment
angr, cle, pyvex, archinfo, claripy at master as of 2026-08-13
(angr 0c293dc0, archinfo da171ca0, claripy c0b78b21, cle b58ea02a, pypcode 559aacdc,
pyvex 90e9094c).
Source: angr/angr