#6837·angr

CFGFast restarts its linear scan inside an instruction it could not decode

Author: zardusCreated Aug 13, 2026Updated Sep 8, 2026

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:

python
# the default case
valid_ins = False
nodecode_size = 1

CFGFast'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:

  1. 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 UND branch a few lines below already reads raw bytes for a similar purpose, and Block.capstone is 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.
  2. Skip self.project.arch.instruction_alignment bytes 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.
  3. 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

python
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)       # True

Environment

angr, cle, pyvex, archinfo, claripy at master as of 2026-08-13 (angr 0c293dc0, archinfo da171ca0, claripy c0b78b21, cle b58ea02a, pypcode 559aacdc, pyvex 90e9094c).