What 0xD5033FFF Taught Me About Decoder Validity
How one AArch64 decoding disagreement exposed the difference between decodable, architecturally valid, and invalid instructions.
I stumbled across 0xD5033FFF while working on SILICA, a tool that asks several independent oracles whether each possible 32-bit AArch64 word is valid. One result immediately stood out:
| Oracle | Result |
|---|---|
| Arm-derived spec oracle | Valid |
| LLVM | Valid |
| Capstone 5.0.7 | Invalid |
| Unicorn | Invalid |
That looked simple at first. LLVM could decode the word and Capstone could not. I wanted to find out which one was wrong.
That was the wrong question.
What I eventually found was a case that sits between ordinary validity and complete decode failure. LLVM and newer Capstone can both represent that middle state. SILICA had flattened it away.
Reproducing the disagreement
AArch64 instructions are 32 bits wide. The word I was investigating was:
0xD5033FFF
On a little-endian system, those bytes appear in memory as:
FF 3F 03 D5
I first passed those exact bytes to LLVM:
printf '0xff 0x3f 0x03 0xd5\n' | \
llvm-mc --triple=aarch64 --disassemble
LLVM printed:
msr S0_3_C3_C15_7, xzr
Capstone 5.0.7 returned no instruction for the same four bytes. That reproduced SILICA’s result independently, but it still did not prove that LLVM had found an ordinary valid MSR instruction.
LLVM round-tripped it exactly
My next test was to give LLVM its own output and ask it to assemble the instruction again:
printf 'msr S0_3_C3_C15_7, xzr\n' | \
llvm-mc --triple=aarch64 --show-encoding
The output was the original byte sequence:
msr S0_3_C3_C15_7, xzr
// encoding: [0xff,0x3f,0x03,0xd5]
So LLVM’s interpretation was internally consistent:
FF 3F 03 D5
-> msr S0_3_C3_C15_7, xzr
-> FF 3F 03 D5
At this point I knew the output was intentional. I still did not know whether the architecture assigned that encoding to a valid system-register access.
Pulling apart the generic name
LLVM was using its generic AArch64 system-register syntax:
S<op0>_<op1>_C<CRn>_C<CRm>_<op2>
Extracting the fields from 0xD5033FFF gave me:
word = 11010101000000110011111111111111
op0 = 0
op1 = 3
CRn = 3
CRm = 15
op2 = 7
Rt = 31
Those values reconstruct LLVM’s text exactly. Rt = 31 gives xzr, and the remaining fields give S0_3_C3_C15_7.
My first assumption was that I was looking at an unusual MSR (register) encoding. Manually checking the fixed bits changed that. The architectural MSR (register) form uses system-register op0 values 2 or 3. This word did not belong to that class.
The generic spelling was useful because LLVM could preserve and print the decoded fields. It was not evidence that S0_3_C3_C15_7 was an architecturally assigned register.
It was almost SB
Once I stopped treating the output text as the instruction class, the fixed bits put the word in the barrier and system-control region. That region includes instructions such as CLREX, DSB, DMB, ISB, and SB.
Comparing the candidate with the canonical encoding for the AArch64 Speculation Barrier made the relationship clear:
| Word | CRm bits 11:8 | Result |
|---|---|---|
0xD50330FF | 0000 | Canonical SB |
0xD5033FFF | 1111 | Candidate |
Only the CRm field changed. Arm’s SB definition requires the FEAT_SB feature, and the normal encoding fixes these four bits to zero. The candidate had all four set.
That changed my hypothesis again. The question was no longer why LLVM accepted an odd MSR. It was why LLVM reached the generic printer by default for something structurally related to SB.
FEAT_SB was the turning point
I had two LLVM versions available. Ubuntu provided LLVM 18.1.3, while SILICA’s environment used LLVM 22.1.8. I initially suspected a version difference, so I tested the canonical SB and the candidate under the same feature configurations in both versions.
With default target features, LLVM printed:
msr S0_3_C3_C0_7, xzr
msr S0_3_C3_C15_7, xzr
Then I enabled FEAT_SB explicitly:
printf '0xff 0x30 0x03 0xd5\n0xff 0x3f 0x03 0xd5\n' | \
llvm-mc --triple=aarch64 --disassemble --mattr=+sb
LLVM printed both words as sb, but warned on the candidate:
<stdin>:2:1: warning: potentially undefined instruction encoding
.text
sb
sb
LLVM 18.1.3 and 22.1.8 behaved the same. Default or --mattr=-sb produced the generic msr spelling. --mattr=+sb selected sb, with a warning only for 0xD5033FFF.
This ruled out the LLVM-version theory. The missing variable was the target feature profile.
LLVM had three answers, not two
The warning led me into LLVM’s decoder API. MCDisassembler::DecodeStatus is ternary:
| Status | Meaning |
|---|---|
Success | The instruction decoded normally. |
SoftFail | It is disassemblable but architecturally incorrect. |
Fail | No valid instruction was decoded. |
That middle state was the key. llvm-mc warns about a potentially undefined encoding when decoding returns SoftFail, but it still prints the instruction.
The LLVM AArch64 instruction definitions describe SB with the HasSB predicate and mark differences in bits 11:8 as unpredictable. The relevant rule is effectively:
def SB ... {
let Inst{20-5} = 0b0001100110000111;
let Unpredictable{11-8} = 0b1111;
let Predicates = [HasSB];
}
The 1111 there is a mask identifying which mismatched bits trigger SoftFail. It is not the expected CRm value.
That gave me the exact classification:
0xD50330FF + FEAT_SB -> Success -> sb
0xD5033FFF + FEAT_SB -> SoftFail -> sb plus warning
The candidate was recognizable as SB, but it was not an ordinary valid SB.
Capstone 5 and 6 made the same distinction differently
I next repeated the test in two isolated Capstone environments.
Capstone 5.0.7 behaved like the original SILICA run:
0xd50330ff [('sb', '')]
0xd5033fff INVALID
Capstone 6.0.0 returned an instruction for both words and exposed an illegal flag:
0xd50330ff [('sb', '', False)]
0xd5033fff [('sb', '', True)]
Capstone’s version 6 release guide explains that illegal=True means the instruction decoded but is illegal under the ISA definitions. That is close to the distinction LLVM represents with SoftFail.
Capstone 5 had collapsed this particular case into decode failure. Capstone 6 could preserve the middle state.
What SILICA had actually measured
At this point the decoders made sense, so I went back to SILICA’s oracle code.
Its LLVM oracle creates a disassembler for aarch64-unknown-linux-gnu without requesting +sb. It then calls LLVMDisasmInstruction and treats any nonzero consumed-byte count as valid. That C API path tells SILICA whether it got an instruction back, but SILICA was not preserving LLVM’s Success versus SoftFail distinction.
The Capstone 5 oracle had a similar binary rule. If cs_disasm returned an instruction, SILICA recorded valid. If it returned none, SILICA recorded invalid.
For 0xD5033FFF, the comparison was really:
LLVM default context -> generic fallback returned -> VALID
Capstone 5 -> no instruction returned -> INVALID
That disagreement was real at the API-output level. It did not establish an architectural correctness bug in LLVM or Capstone.
The problem was in the question SILICA asked. It treated decoder validity as a boolean when the underlying tools had more information:
VALID / SOFTFAIL_OR_ILLEGAL / INVALID
It also needed to record the target feature profile. A decoder result without that context was incomplete.
What I proved
The final result was narrower than the bug I initially thought I might have found, but much more useful for SILICA:
- LLVM 18.1.3 and 22.1.8 behave the same for these words.
- Without
FEAT_SB, LLVM uses a generic system-register spelling. - With
FEAT_SB, canonical0xD50330FFisSuccess, while0xD5033FFFisSoftFailand produces a warning. - Capstone 5.0.7 rejects the candidate, while Capstone 6.0.0 decodes it as
sbwithillegal=True. - The candidate differs from canonical
SBonly inCRm, but that does not make it an ordinary validSB.
I did not prove how every AArch64 implementation would execute this word, and decoder output alone cannot prove that. I also did not find a reportable current LLVM or Capstone bug.
What I found was a classification problem in my own system. SILICA had taken several different questions and forced them into VALID or INVALID: Can the decoder recognize the bit pattern? Is the required feature enabled? Is the encoding architecturally legal? Does the API expose that distinction?
Those are not the same question. The real fix is to stop pretending they have one binary answer.