CFGFast restarts inside an undecodable instruction on every architecture but x86 and ARM
THIS MESSAGE WAS GENERATED BY AN AUTOMATED PROCESS
When VEX cannot decode an instruction, CFGFast marks one byte as nodecode and leaves the rest of the instruction unoccupied, so the linear scan resumes inside it and seeds a block at an address that is not an instruction boundary.
cfg_fast.py:5770 sets nodecode_size = 1 as the default. The only exemptions are ud/ud0/ud1/ud2 on x86 and x86-64 (5772-5792) and ARM UND (5793-5828). Every other undecodable instruction on every other architecture falls to the one-byte default, and self._seg_list.occupy(real_addr + irsb_size, nodecode_size, "nodecode") at 5846/5853 marks only that byte.
Two concrete cases, both lifting to size=0/Ijk_NoDecode on current master while their neighbours lift normally:
AMD64 62 f1 7c 48 10 69 fd vmovups zmm5, [rcx-0xc0] size=0 Ijk_NoDecode
AMD64 f0 0f b1 0f lock cmpxchg size=4 Ijk_Boring
RISCV64 af b6 a9 18 sc.d a3, a0, (s3) size=0 Ijk_NoDecode
RISCV64 13 01 f1 00 addi size=4 Ijk_BoringWhat that does to recovery, on an ELF RISC-V 64 object:
0x40d9dc af b6 a9 18 sc.d a3, a0, (s3) angr: block size 0, Ijk_NoDecode
0x40d9de angr: block size 4, instrs [0x40d9de, 0x40d9e0]The second block starts two bytes into the sc.d and decodes the tail of one instruction plus the head of the next as if they were code. On an ELF x86-64 object the same thing happens two bytes into an EVEX vmovups.
Measured against .eh_frame/symbol evidence across a corpus of about 447,000 successfully analysed objects, this produces fabricated instruction boundaries in 1,467 objects, 61,154 addresses. Concentration: ELF RISC-V 27,766 over 658 objects, ELF x86-64 14,615 over 211, PE x86-64 10,551 over 56, ELF x86 6,925 over 432. On the RISC-V object sampled, 755 of 755 fabricated addresses are this mechanism; on the x86-64 one, 745 of 791.
The remaining 46 on that x86-64 object are worth mentioning because they are the opposite error — the oracle being wrong and angr right. glibc and musl lock elision emit cmp %fs:0x18,$0 ; je 1f ; lock ; 1: cmpxchg, where the je legitimately targets one byte into the lock cmpxchg. A real branch target that no linear decode can sit on. Any fix here should not try to make those go away.
Suggestion. Corrected — see #6929. I suggested that nodecode_size become the instruction width for fixed-width architectures, naming RISC-V, MIPS, PowerPC, AArch64 and SPARC. That was wrong twice over. Fixed-width architectures need no change at all: _next_code_addr_core already rounds the scan restart up to arch.instruction_alignment, which lands on the next real boundary by itself when that equals the instruction width — checked on AArch64, PowerPC and MIPS with a deliberately undecodable instruction, and the scan resumes correctly on all three. And RISC-V is not fixed-width, since it has 16-bit compressed instructions; it is affected precisely because its 2-byte alignment is smaller than a 32-bit instruction. The defect class is instruction_alignment < max_inst_bytes: x86, RISC-V, ARM (already special-cased) and s390x. #6929 fixes the RISC-V case using the length the ISA encodes in the instruction itself. x86 is the genuinely hard case, since the length cannot be recovered without decoding; there the honest options are to consult a length-only decoder or to accept the current behaviour and keep extending the special-case list.
Related, and not covering this: #6839 handles the scan restarting inside a ud2 specifically, which is one of the three architectures already exempted. #1386 tracks the AVX-512 lifting gap itself; nothing tracks the RISC-V atomics gap, and this issue is about the scan's response rather than the lifter's coverage — even with perfect lifting, the next unimplemented instruction reproduces it.
Verified on origin/master by reading cfg_fast.py and by lifting the instructions above directly. The corpus figures come from analysis runs on a slightly older revision, and I have not re-run CFGFast on master to reconfirm the fabricated addresses — the mechanism is unchanged there but the counts should be treated as from that revision. The objects are from a non-public corpus; counts and shapes only.
Source: angr/angr