1use r2smt_common::smt::SmtResult;
9use r2smt_common::{Address, Arch, Error, Result};
10use r2smt_core::{Confidence, Finding, FindingKind};
11use r2smt_ir::byte_patcher::BytePatcher;
12use r2smt_report::PatchStrategy;
13use tracing::{debug, warn};
14
15use crate::arm_encoding::{
16 ARM_INSTRUCTION_BYTES, THUMB_HALFWORD_BYTES, arm_nop_buffer, thumb_nop_buffer,
17};
18use crate::x86_encoding::{nop_buffer, patch_cmovcc_to_mov, patch_setcc};
19
20mod aarch64;
21use aarch64::{
22 classify_aarch64_mnemonic, plan_aarch64_cs_arith, plan_aarch64_csel, plan_aarch64_cset,
23};
24
25const X86_NOP_BYTE: u8 = 0x90;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct PlanOperation {
31 pub address: Address,
33 pub strategy: PatchStrategy,
35 pub kind: FindingKind,
37 pub confidence: Confidence,
39 pub size: usize,
41 pub new_bytes: Vec<u8>,
43 pub rationale: String,
45}
46
47#[derive(Debug, Clone, Default, PartialEq, Eq)]
49pub struct PatchPlan {
50 pub operations: Vec<PlanOperation>,
53 pub skipped: Vec<(Address, String)>,
56}
57
58pub const MAX_INSTRUCTION_SIZE: usize = 16;
62
63pub fn build_plan(
88 findings: &[Finding],
89 min_confidence: Confidence,
90 arch: Arch,
91 patcher: &mut dyn BytePatcher,
92) -> Result<PatchPlan> {
93 let mut operations: Vec<PlanOperation> = Vec::new();
94 let mut skipped: Vec<(Address, String)> = Vec::new();
95
96 for finding in findings {
97 match consider_finding(finding, min_confidence, arch, patcher)? {
98 FindingDecision::Plan(op) => operations.push(op),
99 FindingDecision::Skip(reason) => skipped.push((finding.address, reason)),
100 }
101 }
102
103 Ok(PatchPlan {
104 operations,
105 skipped,
106 })
107}
108
109enum FindingDecision {
110 Plan(PlanOperation),
111 Skip(String),
112}
113
114fn consider_finding(
115 finding: &Finding,
116 min_confidence: Confidence,
117 arch: Arch,
118 patcher: &mut dyn BytePatcher,
119) -> Result<FindingDecision> {
120 if !finding.is_actionable() {
121 return Ok(FindingDecision::Skip(format!(
122 "kind {:?} is not actionable",
123 finding.kind
124 )));
125 }
126 if finding.confidence > min_confidence {
127 return Ok(FindingDecision::Skip(format!(
128 "confidence {:?} below threshold {:?}",
129 finding.confidence, min_confidence,
130 )));
131 }
132
133 let mnemonic = finding.mnemonic.to_ascii_lowercase();
134 let kind = classify_mnemonic(&mnemonic, arch);
135 if kind == MnemonicKind::Other {
136 return Ok(FindingDecision::Skip(format!(
137 "{mnemonic} not a recognised branch / setcc / cmovcc for {arch:?} — no rewrite strategy"
138 )));
139 }
140
141 let size = match measure_instruction_size(finding, patcher) {
147 Ok(size) => size,
148 Err(reason) => return Ok(FindingDecision::Skip(reason.to_string())),
149 };
150 if arch_is_arm(arch) && !finding.is_thumb && size % ARM_INSTRUCTION_BYTES != 0 {
151 return Ok(FindingDecision::Skip(format!(
152 "ARM instruction at {addr} has non-4-byte size {size} (Thumb mode?)",
153 addr = finding.address,
154 )));
155 }
156 if finding.is_thumb && size % THUMB_HALFWORD_BYTES != 0 {
157 return Ok(FindingDecision::Skip(format!(
158 "Thumb instruction at {addr} has odd size {size}",
159 addr = finding.address,
160 )));
161 }
162
163 match kind {
164 MnemonicKind::Jcc => plan_jcc(finding, size, arch, patcher),
165 MnemonicKind::SetCc => plan_setcc(finding, size, patcher),
166 MnemonicKind::CMovCc => plan_cmovcc(finding, size, patcher),
167 MnemonicKind::Cset { all_ones } => plan_aarch64_cset(finding, size, patcher, all_ones),
168 MnemonicKind::Csel => plan_aarch64_csel(finding, size, patcher),
169 MnemonicKind::CsArith { op, aliased } => {
170 plan_aarch64_cs_arith(finding, size, patcher, op, aliased)
171 }
172 MnemonicKind::Other => unreachable!(),
173 }
174}
175
176fn arch_is_arm(arch: Arch) -> bool {
177 matches!(arch, Arch::Aarch64 | Arch::Arm)
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181enum MnemonicKind {
182 Jcc,
183 SetCc,
184 CMovCc,
185 Cset {
188 all_ones: bool,
189 },
190 Csel,
192 CsArith {
198 op: CsArithOp,
199 aliased: bool,
200 },
201 Other,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208enum CsArithOp {
209 Csinc,
211 Csinv,
213 Csneg,
215}
216
217fn classify_mnemonic(mnemonic: &str, arch: Arch) -> MnemonicKind {
218 match arch {
219 Arch::X86 | Arch::X86_64 => classify_x86_mnemonic(mnemonic),
220 Arch::Aarch64 => classify_aarch64_mnemonic(mnemonic),
221 Arch::Arm => classify_aarch32_mnemonic(mnemonic),
222 _ => MnemonicKind::Other,
223 }
224}
225
226fn classify_x86_mnemonic(mnemonic: &str) -> MnemonicKind {
227 if mnemonic.starts_with("cmov") {
228 MnemonicKind::CMovCc
229 } else if mnemonic.starts_with("set") {
230 MnemonicKind::SetCc
231 } else if mnemonic.starts_with('j') && mnemonic != "jmp" {
232 MnemonicKind::Jcc
233 } else {
234 MnemonicKind::Other
235 }
236}
237
238fn classify_aarch32_mnemonic(mnemonic: &str) -> MnemonicKind {
239 const COND_SUFFIXES: &[&str] = &[
243 "eq", "ne", "cs", "hs", "cc", "lo", "mi", "pl", "vs", "vc", "hi", "ls", "ge", "lt", "gt",
244 "le",
245 ];
246 if let Some(suffix) = mnemonic.strip_prefix('b')
247 && COND_SUFFIXES.contains(&suffix)
248 {
249 return MnemonicKind::Jcc;
250 }
251 MnemonicKind::Other
252}
253
254fn plan_jcc(
255 finding: &Finding,
256 size: usize,
257 arch: Arch,
258 patcher: &mut dyn BytePatcher,
259) -> Result<FindingDecision> {
260 let strategy = jcc_strategy(finding)?;
261 let new_bytes = match strategy {
262 PatchStrategy::NopJcc => nop_bytes_for(arch, size, finding.is_thumb)?,
263 PatchStrategy::ReplaceJccWithJmp => {
264 let Some(target) = finding.taken_target else {
265 return Ok(FindingDecision::Skip(
266 "AlwaysTrue jcc has no resolved taken target".into(),
267 ));
268 };
269 let assembled =
270 patcher.assemble(finding.address, &unconditional_branch_asm(arch, target))?;
271 if assembled.len() > size {
272 warn!(
273 target: "r2smt::patch",
274 addr = %finding.address,
275 asm_size = assembled.len(),
276 original_size = size,
277 "assembled branch larger than original — skipping"
278 );
279 return Ok(FindingDecision::Skip(format!(
280 "assembled branch is {asm} bytes, original instruction is {orig}",
281 asm = assembled.len(),
282 orig = size,
283 )));
284 }
285 if arch_is_arm(arch) && assembled.len() != size {
286 return Ok(FindingDecision::Skip(format!(
292 "ARM assembled branch is {asm} bytes, original instruction is {orig} — refusing to pad",
293 asm = assembled.len(),
294 orig = size,
295 )));
296 }
297 pad_to_size(arch, assembled, size)?
298 }
299 _ => {
300 return Ok(FindingDecision::Skip(format!(
301 "{strategy:?} not applicable to jcc"
302 )));
303 }
304 };
305
306 debug!(
307 target: "r2smt::patch",
308 addr = %finding.address,
309 size,
310 strategy = strategy.as_str(),
311 "planned jcc operation"
312 );
313
314 Ok(FindingDecision::Plan(PlanOperation {
315 address: finding.address,
316 strategy,
317 kind: finding.kind,
318 confidence: finding.confidence,
319 size,
320 new_bytes,
321 rationale: rationale_for(finding, strategy),
322 }))
323}
324
325fn plan_setcc(
326 finding: &Finding,
327 size: usize,
328 patcher: &mut dyn BytePatcher,
329) -> Result<FindingDecision> {
330 let value = match finding.verdict {
331 SmtResult::AlwaysTrue => true,
332 SmtResult::AlwaysFalse => false,
333 _ => {
334 return Err(Error::parse(
335 "patch_plan",
336 format!(
337 "{:?} verdict at {addr} cannot drive a setcc rewrite",
338 finding.verdict,
339 addr = finding.address,
340 ),
341 ));
342 }
343 };
344 let original = patcher.read_bytes(finding.address, size)?;
345 let new_bytes = match patch_setcc(&original, value) {
346 Ok(bytes) => bytes,
347 Err(err) => {
348 return Ok(FindingDecision::Skip(format!(
349 "setcc byte rewrite failed: {err}"
350 )));
351 }
352 };
353 let strategy = PatchStrategy::ReplaceSetCcWithMovConst;
354 debug!(
355 target: "r2smt::patch",
356 addr = %finding.address,
357 size,
358 strategy = strategy.as_str(),
359 value,
360 "planned setcc operation"
361 );
362 Ok(FindingDecision::Plan(PlanOperation {
363 address: finding.address,
364 strategy,
365 kind: finding.kind,
366 confidence: finding.confidence,
367 size,
368 new_bytes,
369 rationale: setcc_rationale(finding, value),
370 }))
371}
372
373fn plan_cmovcc(
374 finding: &Finding,
375 size: usize,
376 patcher: &mut dyn BytePatcher,
377) -> Result<FindingDecision> {
378 let always_true = match finding.verdict {
379 SmtResult::AlwaysTrue => true,
380 SmtResult::AlwaysFalse => false,
381 _ => {
382 return Err(Error::parse(
383 "patch_plan",
384 format!(
385 "{:?} verdict at {addr} cannot drive a cmovcc rewrite",
386 finding.verdict,
387 addr = finding.address,
388 ),
389 ));
390 }
391 };
392
393 let new_bytes = if always_true {
394 let original = patcher.read_bytes(finding.address, size)?;
395 match patch_cmovcc_to_mov(&original) {
396 Ok(bytes) => bytes,
397 Err(err) => {
398 return Ok(FindingDecision::Skip(format!(
399 "cmovcc byte rewrite failed: {err}"
400 )));
401 }
402 }
403 } else {
404 nop_buffer(size)
407 };
408
409 let strategy = PatchStrategy::ReplaceCMovCcWithMovOrNop;
410 debug!(
411 target: "r2smt::patch",
412 addr = %finding.address,
413 size,
414 strategy = strategy.as_str(),
415 always_true,
416 "planned cmovcc operation"
417 );
418 Ok(FindingDecision::Plan(PlanOperation {
419 address: finding.address,
420 strategy,
421 kind: finding.kind,
422 confidence: finding.confidence,
423 size,
424 new_bytes,
425 rationale: cmovcc_rationale(finding, always_true),
426 }))
427}
428
429fn setcc_rationale(finding: &Finding, value: bool) -> String {
430 let target = i32::from(value);
431 format!(
432 "{mnem} at {addr} always sets its destination to {target} ({formula} is always {value})",
433 mnem = finding.mnemonic,
434 addr = finding.address,
435 formula = finding.formula,
436 value = if value { "true" } else { "false" },
437 )
438}
439
440fn cmovcc_rationale(finding: &Finding, always_true: bool) -> String {
441 if always_true {
442 format!(
443 "{mnem} at {addr} always moves ({formula} is always true) — rewritten as unconditional MOV",
444 mnem = finding.mnemonic,
445 addr = finding.address,
446 formula = finding.formula,
447 )
448 } else {
449 format!(
450 "{mnem} at {addr} never moves ({formula} is always false) — NOPed",
451 mnem = finding.mnemonic,
452 addr = finding.address,
453 formula = finding.formula,
454 )
455 }
456}
457
458fn jcc_strategy(finding: &Finding) -> Result<PatchStrategy> {
459 match finding.verdict {
460 SmtResult::AlwaysFalse => Ok(PatchStrategy::NopJcc),
461 SmtResult::AlwaysTrue => Ok(PatchStrategy::ReplaceJccWithJmp),
462 _ => Err(Error::parse(
463 "patch_plan",
464 format!(
465 "{:?} verdict at {addr} cannot drive a jcc rewrite",
466 finding.verdict,
467 addr = finding.address,
468 ),
469 )),
470 }
471}
472
473fn nop_bytes_for(arch: Arch, size: usize, is_thumb: bool) -> Result<Vec<u8>> {
480 if is_thumb {
481 return thumb_nop_buffer(size);
482 }
483 if arch_is_arm(arch) {
484 arm_nop_buffer(arch, size)
485 } else {
486 Ok(vec![X86_NOP_BYTE; size])
487 }
488}
489
490fn pad_to_size(arch: Arch, mut bytes: Vec<u8>, size: usize) -> Result<Vec<u8>> {
495 if arch_is_arm(arch) {
496 if bytes.len() != size {
499 return Err(Error::parse(
500 "patch_plan.pad",
501 format!(
502 "ARM assembled length {} mismatched target size {}",
503 bytes.len(),
504 size
505 ),
506 ));
507 }
508 return Ok(bytes);
509 }
510 while bytes.len() < size {
511 bytes.push(X86_NOP_BYTE);
512 }
513 Ok(bytes)
514}
515
516fn unconditional_branch_asm(arch: Arch, target: Address) -> String {
517 if arch_is_arm(arch) {
518 format!("b {target}")
519 } else {
520 format!("jmp {target}")
521 }
522}
523
524fn rationale_for(finding: &Finding, strategy: PatchStrategy) -> String {
525 match strategy {
526 PatchStrategy::NopJcc => format!(
527 "{mnem} at {addr} is never taken ({formula} is always false)",
528 mnem = finding.mnemonic,
529 addr = finding.address,
530 formula = finding.formula,
531 ),
532 PatchStrategy::ReplaceJccWithJmp => format!(
533 "{mnem} at {addr} is always taken ({formula} is always true)",
534 mnem = finding.mnemonic,
535 addr = finding.address,
536 formula = finding.formula,
537 ),
538 _ => finding.formula.clone(),
539 }
540}
541
542fn measure_instruction_size(finding: &Finding, patcher: &mut dyn BytePatcher) -> Result<usize> {
543 if let Some(ft) = finding.fallthrough_target {
553 let raw = ft.get().saturating_sub(finding.address.get());
554 if raw > 0 {
555 if let Ok(size) = usize::try_from(raw) {
556 if size <= MAX_INSTRUCTION_SIZE {
557 let _ = patcher.read_bytes(finding.address, size)?;
560 return Ok(size);
561 }
562 }
563 }
564 }
565 Err(Error::parse(
566 "patch_plan",
567 format!(
568 "could not determine size of instruction at {addr}",
569 addr = finding.address,
570 ),
571 ))
572}
573
574#[cfg(test)]
575mod tests;