Skip to main content

r2smt_patch/
x86_encoding.rs

1//! Byte-level rewrites for `setcc` / `cmovcc` on x86 / `x86_64`.
2//!
3//! All functions in this module take the raw byte sequence of the
4//! original instruction (as read via [`r2smt_ir::BytePatcher::read_bytes`])
5//! and produce a same-length replacement that preserves any prefix
6//! bytes, ModR/M, SIB, and displacement bytes. Same-length output is
7//! enforced because the patcher writes back over the original
8//! footprint; a longer or shorter instruction would either clobber
9//! the next instruction or leave invalid bytes behind.
10//!
11//! Encoding references:
12//!
13//! - `SETcc r/m8` — `[REX]? 0F 9x /0 r/m8`
14//! - MOV r/m8, imm8 — `[REX]? C6 /0 r/m8 imm8`
15//! - `CMOVcc r, r/m` — `[REX]? 0F 4x /r`
16//! - MOV r, r/m — `[REX]? 8B /r`
17//!
18//! Both pairs share their addressing-byte layout (ModR/M + optional
19//! SIB + optional displacement), so the rewrites only swap opcode
20//! bytes and either append `imm8` (setcc) or a `nop` (cmovcc).
21
22use r2smt_common::{Error, Result};
23
24const NOP: u8 = 0x90;
25
26/// Single-byte NOP opcode used for padding patched instructions.
27#[must_use]
28pub const fn nop_byte() -> u8 {
29    NOP
30}
31
32/// Rewrite a `SETcc` instruction so it unconditionally writes
33/// `value as u8` to its destination operand.
34///
35/// `original` is the verbatim instruction byte sequence (typically
36/// 3 bytes for `setcc r/m8` or 4 bytes with a REX prefix; longer for
37/// memory operands with SIB / displacement). The returned vector is
38/// the same length and contains the equivalent `MOV r/m8, imm8`.
39///
40/// # Errors
41///
42/// Returns [`Error::Parse`] if the byte sequence does not look like
43/// a `SETcc` encoding (no `0F 9x` opcode pair, REG field of ModR/M not
44/// `/0`, or the buffer is too short).
45pub fn patch_setcc(original: &[u8], value: bool) -> Result<Vec<u8>> {
46    if original.len() < 3 {
47        return Err(Error::parse(
48            "x86_encoding.setcc",
49            format!("setcc must be at least 3 bytes, got {}", original.len()),
50        ));
51    }
52
53    let mut idx = 0usize;
54    let mut out = Vec::with_capacity(original.len());
55
56    // Optional REX prefix (0x40-0x4F).
57    if (original[idx] & 0xF0) == 0x40 {
58        out.push(original[idx]);
59        idx += 1;
60        if idx + 2 >= original.len() {
61            return Err(Error::parse(
62                "x86_encoding.setcc",
63                "buffer too short after REX prefix",
64            ));
65        }
66    }
67
68    // Expect the SETcc opcode pair `0F 9x`.
69    if original[idx] != 0x0F || (original[idx + 1] & 0xF0) != 0x90 {
70        return Err(Error::parse(
71            "x86_encoding.setcc",
72            format!(
73                "not a SETcc opcode at offset {idx}: {:02x} {:02x}",
74                original[idx],
75                original[idx + 1]
76            ),
77        ));
78    }
79    idx += 2;
80
81    // ModR/M follows the opcode. SETcc encodes /0, i.e. the REG bits
82    // (5-3) of ModR/M must be zero.
83    if idx >= original.len() {
84        return Err(Error::parse("x86_encoding.setcc", "missing ModR/M byte"));
85    }
86    let modrm = original[idx];
87    if (modrm >> 3) & 0x7 != 0 {
88        return Err(Error::parse(
89            "x86_encoding.setcc",
90            format!("SETcc ModR/M REG field is non-zero: 0x{modrm:02x}"),
91        ));
92    }
93
94    // Build `[REX]? C6 ModR/M [SIB] [disp] imm8`. MOV r/m8, imm8
95    // uses the same /0 ModR/M and the same addressing bytes, so we
96    // simply copy everything from the ModR/M onward and append imm8.
97    out.push(0xC6);
98    out.extend_from_slice(&original[idx..]);
99    out.push(u8::from(value));
100
101    if out.len() != original.len() {
102        return Err(Error::parse(
103            "x86_encoding.setcc",
104            format!(
105                "size mismatch: original {}, patched {}",
106                original.len(),
107                out.len()
108            ),
109        ));
110    }
111    Ok(out)
112}
113
114/// Rewrite a `CMOVcc` instruction as an unconditional `MOV r, r/m`.
115///
116/// The opcode byte pair `0F 4x` becomes a single-byte `8B`; the
117/// remaining ModR/M, SIB, and displacement bytes are preserved
118/// verbatim. The freed byte at the end of the instruction is replaced
119/// with a `nop` so the total footprint stays identical.
120///
121/// # Errors
122///
123/// Returns [`Error::Parse`] if the input does not match a
124/// `[REX]? 0F 4x ModR/M ...` layout.
125pub fn patch_cmovcc_to_mov(original: &[u8]) -> Result<Vec<u8>> {
126    let mut idx = 0usize;
127    let mut out = Vec::with_capacity(original.len());
128
129    // Optional 16-bit operand-size override (Intel SDM §2.1.1). The
130    // matching `MOV r16, r/m16` form is `66 8B /r`, so we simply copy
131    // the prefix through verbatim.
132    if original.first() == Some(&0x66) {
133        out.push(0x66);
134        idx += 1;
135    }
136
137    // Optional REX prefix (Intel SDM §2.2.1). Any byte in `0x40..=0x4F`
138    // counts. We never reinterpret REX.W/R/X/B here — both `0F 4x /r`
139    // (cmovcc) and `8B /r` (mov) honour the same prefix verbatim.
140    if let Some(&b) = original.get(idx)
141        && (b & 0xF0) == 0x40
142    {
143        out.push(b);
144        idx += 1;
145    }
146
147    // 2-byte opcode (`0F 4x`) + at least one ModR/M byte must follow.
148    if idx + 2 >= original.len() {
149        return Err(Error::parse(
150            "x86_encoding.cmovcc",
151            format!(
152                "cmovcc body must be ≥3 bytes after prefixes at offset {idx}, got {}",
153                original.len(),
154            ),
155        ));
156    }
157
158    if original[idx] != 0x0F || (original[idx + 1] & 0xF0) != 0x40 {
159        return Err(Error::parse(
160            "x86_encoding.cmovcc",
161            format!(
162                "not a CMOVcc opcode at offset {idx}: {:02x} {:02x}",
163                original[idx],
164                original[idx + 1]
165            ),
166        ));
167    }
168    idx += 2;
169
170    // `8B /r` swallows the same ModR/M-driven addressing bytes — and
171    // the same REX/operand-size prefixes — as `0F 4x /r`. After
172    // copying ModR/M+SIB+displacement verbatim the encoding is one
173    // byte shorter than the original; pad the tail with NOPs so the
174    // overall footprint matches.
175    out.push(0x8B);
176    out.extend_from_slice(&original[idx..]);
177    while out.len() < original.len() {
178        out.push(NOP);
179    }
180    debug_assert_eq!(
181        out.len(),
182        original.len(),
183        "cmovcc rewrite produced wrong length",
184    );
185    Ok(out)
186}
187
188/// Return a buffer of `len` NOPs (`0x90`). Used both for fully-NOPed
189/// `setcc` / `cmovcc` instructions (always-false outcome) and for
190/// NOP-padding shorter replacements.
191#[must_use]
192pub fn nop_buffer(len: usize) -> Vec<u8> {
193    vec![NOP; len]
194}
195
196#[cfg(test)]
197mod tests {
198    #![allow(clippy::unwrap_used, clippy::panic)]
199
200    use super::*;
201
202    // setcc tests
203
204    #[test]
205    fn setcc_reg_no_rex_value_true() {
206        // sete al — 0F 94 C0
207        let bytes = [0x0F, 0x94, 0xC0];
208        let patched = patch_setcc(&bytes, true).unwrap();
209        // mov al, 1 — C6 C0 01
210        assert_eq!(patched, vec![0xC6, 0xC0, 0x01]);
211    }
212
213    #[test]
214    fn setcc_reg_no_rex_value_false() {
215        // setne bl — 0F 95 C3
216        let bytes = [0x0F, 0x95, 0xC3];
217        let patched = patch_setcc(&bytes, false).unwrap();
218        // mov bl, 0 — C6 C3 00
219        assert_eq!(patched, vec![0xC6, 0xC3, 0x00]);
220    }
221
222    #[test]
223    fn setcc_reg_with_rex_preserves_prefix() {
224        // sete sil — 40 0F 94 C6
225        let bytes = [0x40, 0x0F, 0x94, 0xC6];
226        let patched = patch_setcc(&bytes, true).unwrap();
227        // mov sil, 1 — 40 C6 C6 01
228        assert_eq!(patched, vec![0x40, 0xC6, 0xC6, 0x01]);
229    }
230
231    #[test]
232    fn setcc_memory_operand_preserves_addressing() {
233        // sete byte ptr [rbp - 4] — 0F 94 45 FC
234        // mod = 01, reg = 000, r/m = 101  → ModR/M = 0x45
235        // disp8 = 0xFC (-4)
236        let bytes = [0x0F, 0x94, 0x45, 0xFC];
237        let patched = patch_setcc(&bytes, true).unwrap();
238        // mov byte ptr [rbp - 4], 1 — C6 45 FC 01
239        assert_eq!(patched, vec![0xC6, 0x45, 0xFC, 0x01]);
240    }
241
242    #[test]
243    fn setcc_memory_sib_operand_preserves_layout() {
244        // sete byte ptr [rax + rcx*1] — 0F 94 04 08
245        // mod=00, reg=000, r/m=100 → ModR/M = 0x04; SIB = 0x08
246        let bytes = [0x0F, 0x94, 0x04, 0x08];
247        let patched = patch_setcc(&bytes, false).unwrap();
248        // mov byte ptr [rax+rcx], 0 — C6 04 08 00
249        assert_eq!(patched, vec![0xC6, 0x04, 0x08, 0x00]);
250    }
251
252    #[test]
253    fn setcc_size_is_preserved() {
254        let bytes = [0x0F, 0x94, 0xC0];
255        let patched = patch_setcc(&bytes, true).unwrap();
256        assert_eq!(patched.len(), bytes.len());
257    }
258
259    #[test]
260    fn setcc_rejects_too_short() {
261        let bytes = [0x0F, 0x94];
262        assert!(patch_setcc(&bytes, true).is_err());
263    }
264
265    #[test]
266    fn setcc_rejects_wrong_opcode() {
267        // 0F 80 = JO (near jump) — not SETcc.
268        let bytes = [0x0F, 0x80, 0x00];
269        assert!(patch_setcc(&bytes, true).is_err());
270    }
271
272    #[test]
273    fn setcc_rejects_non_zero_reg_field() {
274        // ModR/M with reg = 010 (non-zero) — invalid SETcc encoding.
275        // 0F 94 D0 — reg field = 010
276        let bytes = [0x0F, 0x94, 0xD0];
277        assert!(patch_setcc(&bytes, true).is_err());
278    }
279
280    // cmovcc tests
281
282    #[test]
283    fn cmovcc_reg_no_rex_to_mov() {
284        // cmove eax, ebx — 0F 44 C3
285        let bytes = [0x0F, 0x44, 0xC3];
286        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
287        // mov eax, ebx — 8B C3 + NOP padding
288        assert_eq!(patched, vec![0x8B, 0xC3, NOP]);
289    }
290
291    #[test]
292    fn cmovcc_reg_with_rex_w_preserves_prefix() {
293        // cmove rax, rbx — 48 0F 44 C3
294        let bytes = [0x48, 0x0F, 0x44, 0xC3];
295        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
296        // mov rax, rbx — 48 8B C3 + NOP
297        assert_eq!(patched, vec![0x48, 0x8B, 0xC3, NOP]);
298    }
299
300    #[test]
301    fn cmovcc_memory_with_disp8() {
302        // cmove eax, dword ptr [rbp - 4] — 0F 44 45 FC
303        let bytes = [0x0F, 0x44, 0x45, 0xFC];
304        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
305        // mov eax, dword ptr [rbp - 4] — 8B 45 FC + NOP
306        assert_eq!(patched, vec![0x8B, 0x45, 0xFC, NOP]);
307    }
308
309    #[test]
310    fn cmovcc_size_is_preserved() {
311        let bytes = [0x0F, 0x44, 0xC3];
312        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
313        assert_eq!(patched.len(), bytes.len());
314        let _ = patched.iter().last().unwrap();
315    }
316
317    #[test]
318    fn cmovcc_rejects_wrong_opcode() {
319        // 0F 84 = JZ near — not CMOVcc.
320        let bytes = [0x0F, 0x84, 0x00, 0x00, 0x00, 0x00];
321        assert!(patch_cmovcc_to_mov(&bytes).is_err());
322    }
323
324    #[test]
325    fn cmovcc_rejects_too_short() {
326        let bytes = [0x0F, 0x44];
327        assert!(patch_cmovcc_to_mov(&bytes).is_err());
328    }
329
330    #[test]
331    fn cmovcc_disp8_memory_operand() {
332        // cmove rax, [rbx + 0x10] — 48 0F 44 43 10
333        //   ModR/M = 0x43 (mod=01, reg=000, r/m=011 → [rbx + disp8])
334        let bytes = [0x48, 0x0F, 0x44, 0x43, 0x10];
335        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
336        // mov rax, [rbx + 0x10] — 48 8B 43 10 + NOP
337        assert_eq!(patched, vec![0x48, 0x8B, 0x43, 0x10, NOP]);
338    }
339
340    #[test]
341    fn cmovcc_sib_disp32_memory_operand() {
342        // cmove rax, [rbx + rcx*4 + 0x12345678]
343        //   REX.W=1, opcode=0F 44, ModR/M=0x84 (mod=10, reg=000,
344        //   r/m=100 → SIB-with-disp32), SIB=0x8B (scale=10, idx=001,
345        //   base=011), disp32 little-endian
346        let bytes = [0x48, 0x0F, 0x44, 0x84, 0x8B, 0x78, 0x56, 0x34, 0x12];
347        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
348        assert_eq!(
349            patched,
350            vec![0x48, 0x8B, 0x84, 0x8B, 0x78, 0x56, 0x34, 0x12, NOP],
351        );
352    }
353
354    #[test]
355    fn cmovcc_rex_b_extended_source() {
356        // cmove rax, r11 — 49 0F 44 C3 (REX.B picks r11 as r/m)
357        let bytes = [0x49, 0x0F, 0x44, 0xC3];
358        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
359        assert_eq!(patched, vec![0x49, 0x8B, 0xC3, NOP]);
360    }
361
362    #[test]
363    fn cmovcc_rex_r_extended_dest() {
364        // cmove r8, rbx — 4C 0F 44 C3 (REX.R picks r8 as reg)
365        let bytes = [0x4C, 0x0F, 0x44, 0xC3];
366        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
367        assert_eq!(patched, vec![0x4C, 0x8B, 0xC3, NOP]);
368    }
369
370    #[test]
371    fn cmovcc_rex_rb_both_extended() {
372        // cmove r8, r11 — 4D 0F 44 C3 (REX.R + REX.B)
373        let bytes = [0x4D, 0x0F, 0x44, 0xC3];
374        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
375        assert_eq!(patched, vec![0x4D, 0x8B, 0xC3, NOP]);
376    }
377
378    #[test]
379    fn cmovcc_operand_size_16bit() {
380        // cmove ax, bx — 66 0F 44 C3. The 16-bit operand-size
381        // override prefix `66` must be preserved verbatim; the
382        // matching `MOV r16, r/m16` form is `66 8B /r`.
383        let bytes = [0x66, 0x0F, 0x44, 0xC3];
384        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
385        assert_eq!(patched, vec![0x66, 0x8B, 0xC3, NOP]);
386    }
387
388    #[test]
389    fn cmovcc_32bit_no_rex() {
390        // cmove eax, ebx — 0F 44 C3
391        let bytes = [0x0F, 0x44, 0xC3];
392        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
393        assert_eq!(patched, vec![0x8B, 0xC3, NOP]);
394    }
395
396    #[test]
397    fn cmovcc_64bit_with_rex_w() {
398        // cmove rax, rbx — 48 0F 44 C3 (REX.W = 1)
399        let bytes = [0x48, 0x0F, 0x44, 0xC3];
400        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
401        assert_eq!(patched, vec![0x48, 0x8B, 0xC3, NOP]);
402    }
403
404    #[test]
405    fn cmovcc_condition_code_variety() {
406        // Loop over every legal condition code (`0F 40` .. `0F 4F`).
407        // All of them must rewrite identically to `8B C3 NOP`.
408        for cond in 0x40u8..=0x4Fu8 {
409            let bytes = [0x0F, cond, 0xC3];
410            let patched = patch_cmovcc_to_mov(&bytes)
411                .unwrap_or_else(|e| panic!("cond byte {cond:#x} failed: {e}"));
412            assert_eq!(patched, vec![0x8B, 0xC3, NOP], "cond byte {cond:#x}");
413        }
414    }
415
416    #[test]
417    fn cmovcc_rip_relative() {
418        // cmove rax, [rip + 0x12345678] — 48 0F 44 05 78 56 34 12
419        //   ModR/M=0x05 (mod=00, reg=000, r/m=101 → RIP-relative
420        //   in 64-bit mode), disp32 little-endian.
421        let bytes = [0x48, 0x0F, 0x44, 0x05, 0x78, 0x56, 0x34, 0x12];
422        let patched = patch_cmovcc_to_mov(&bytes).unwrap();
423        assert_eq!(patched, vec![0x48, 0x8B, 0x05, 0x78, 0x56, 0x34, 0x12, NOP],);
424    }
425
426    #[test]
427    fn cmovcc_too_short_no_prefix() {
428        // Single byte cannot contain even the opcode pair.
429        let bytes = [0x0F];
430        assert!(patch_cmovcc_to_mov(&bytes).is_err());
431    }
432
433    #[test]
434    fn cmovcc_too_short_after_rex() {
435        // REX + 1 opcode byte — missing the condition code byte.
436        let bytes = [0x48, 0x0F];
437        assert!(patch_cmovcc_to_mov(&bytes).is_err());
438    }
439
440    #[test]
441    fn cmovcc_wrong_first_byte() {
442        // 0x90 (NOP) followed by garbage — must fail.
443        let bytes = [0x90, 0x44, 0xC3];
444        assert!(patch_cmovcc_to_mov(&bytes).is_err());
445    }
446
447    #[test]
448    fn cmovcc_wrong_second_nibble() {
449        // `0F 90 C3` is `seto`, not a cmovcc. Must reject.
450        let bytes = [0x0F, 0x90, 0xC3];
451        assert!(patch_cmovcc_to_mov(&bytes).is_err());
452    }
453
454    #[test]
455    fn cmovcc_invalid_rex_followed_by_garbage() {
456        // 48 (REX.W) AA BB CC — second byte is not 0F so opcode
457        // detection must reject.
458        let bytes = [0x48, 0xAA, 0xBB, 0xCC];
459        assert!(patch_cmovcc_to_mov(&bytes).is_err());
460    }
461
462    // nop_buffer
463
464    #[test]
465    fn nop_buffer_emits_requested_length() {
466        let buf = nop_buffer(7);
467        assert_eq!(buf, vec![NOP; 7]);
468    }
469}