1use std::path::{Path, PathBuf};
5
6use r2smt_common::{Error, Result};
7use r2smt_ir::byte_patcher::BytePatcher;
8use tracing::{info, warn};
9
10use crate::digest::sha256_hex;
11use crate::manifest::{MANIFEST_VERSION, PatchManifest, PatchRecord};
12use crate::plan::PatchPlan;
13
14#[derive(Debug, Clone)]
20pub struct ApplyConfig {
21 pub binary_path: PathBuf,
24 pub backup_path: PathBuf,
26 pub r2smt_version: String,
28}
29
30pub fn apply_plan(
43 patcher: &mut dyn BytePatcher,
44 plan: &PatchPlan,
45 config: &ApplyConfig,
46) -> Result<PatchManifest> {
47 let binary_sha256_before = sha256_hex(&config.binary_path)?;
48 info!(
49 target: "r2smt::patch",
50 binary = %config.binary_path.display(),
51 ops = plan.operations.len(),
52 skipped = plan.skipped.len(),
53 sha256_before = %binary_sha256_before,
54 "starting patch run"
55 );
56
57 let mut records: Vec<PatchRecord> = Vec::with_capacity(plan.operations.len());
58 for op in &plan.operations {
59 let original = patcher.read_bytes(op.address, op.size)?;
60 if original.len() != op.new_bytes.len() {
61 warn!(
62 target: "r2smt::patch",
63 addr = %op.address,
64 original = original.len(),
65 new = op.new_bytes.len(),
66 "plan size disagreed with read; aborting"
67 );
68 return Err(r2smt_common::Error::parse(
69 "patch_apply",
70 format!(
71 "size mismatch at {addr}: original {orig}, new {new}",
72 addr = op.address,
73 orig = original.len(),
74 new = op.new_bytes.len(),
75 ),
76 ));
77 }
78 patcher.write_bytes(op.address, &op.new_bytes)?;
79 records.push(PatchRecord {
80 address: op.address,
81 strategy: op.strategy.as_str().to_string(),
82 kind: op.kind,
83 confidence: op.confidence,
84 original_bytes_hex: hex::encode(&original),
85 patched_bytes_hex: hex::encode(&op.new_bytes),
86 rationale: op.rationale.clone(),
87 });
88 }
89
90 let binary_sha256_after = sha256_hex(&config.binary_path)?;
91 info!(
92 target: "r2smt::patch",
93 applied = records.len(),
94 sha256_after = %binary_sha256_after,
95 "patch run completed"
96 );
97
98 Ok(PatchManifest {
99 manifest_version: MANIFEST_VERSION,
100 r2smt_version: config.r2smt_version.clone(),
101 binary: config.binary_path.display().to_string(),
102 binary_sha256_before,
103 binary_sha256_after,
104 backup_path: absolute_or_display(&config.backup_path),
105 operations: records,
106 })
107}
108
109fn absolute_or_display(path: &Path) -> String {
110 path.canonicalize()
111 .map_or_else(|_| path.display().to_string(), |p| p.display().to_string())
112}
113
114pub fn rollback_from_manifest(
134 patcher: &mut dyn BytePatcher,
135 manifest: &PatchManifest,
136) -> Result<()> {
137 info!(
138 target: "r2smt::patch",
139 ops = manifest.operations.len(),
140 binary = %manifest.binary,
141 "starting rollback"
142 );
143 for record in manifest.operations.iter().rev() {
144 let original = record.original_bytes()?;
145 let expected = record.patched_bytes()?;
146 if original.len() != expected.len() {
154 return Err(Error::parse(
155 "rollback",
156 format!(
157 "record at {addr} has mismatched byte lengths (original {orig}, \
158 patched {patched}); the manifest is malformed — refusing to restore",
159 addr = record.address,
160 orig = original.len(),
161 patched = expected.len(),
162 ),
163 ));
164 }
165 let current = patcher.read_bytes(record.address, expected.len())?;
166 if current != expected {
167 return Err(Error::parse(
168 "rollback",
169 format!(
170 "bytes at {addr} do not match the recorded patch \
171 ({current} vs {patched}); the target is not in the \
172 expected post-patch state — refusing to restore",
173 addr = record.address,
174 current = hex::encode(¤t),
175 patched = record.patched_bytes_hex,
176 ),
177 ));
178 }
179 patcher.write_bytes(record.address, &original)?;
180 }
181 info!(target: "r2smt::patch", "rollback completed");
182 Ok(())
183}
184
185#[cfg(test)]
186mod tests {
187 #![allow(clippy::unwrap_used)]
188
189 use std::fs;
190 use std::io::Write;
191
192 use r2smt_common::smt::SmtResult;
193 use r2smt_common::{Address, Arch};
194 use r2smt_core::{Confidence, Finding, FindingEvidence, FindingKind};
195 use r2smt_ir::testing::InMemoryBytePatcher;
196 use r2smt_report::PatchStrategy;
197 use r2smt_slicer::condition::BranchCondition;
198 use r2smt_slicer::slice::SliceStatus;
199 use tempfile::NamedTempFile;
200
201 use super::*;
202 use crate::plan::{PlanOperation, build_plan};
203
204 fn dead_branch_finding(address: u64, size: u64) -> Finding {
205 Finding {
206 address: Address(address),
207 function: Address(0x40_1000),
208 mnemonic: "jne".into(),
209 condition: BranchCondition::NotEqual,
210 formula: "ZF == 0".into(),
211 formula_pretty: "(ZF == 0)".into(),
212 formula_z3_pretty: None,
213 verdict: SmtResult::AlwaysFalse,
214 kind: FindingKind::DeadBranch,
215 confidence: Confidence::High,
216 taken_target: Some(Address(0x40_1080)),
217 fallthrough_target: Some(Address(address + size)),
218 operands: Vec::new(),
219 is_thumb: false,
220 evidence: FindingEvidence {
221 slice_status: SliceStatus::Complete,
222 statement_count: 0,
223 input_count: 0,
224 inputs: vec![],
225 unknown_count: 0,
226 upstream_resolved_to: None,
227 oracle_agreement: None,
228 },
229 pseudocode: None,
230 }
231 }
232
233 fn writable_temp_file_with_bytes(bytes: &[u8]) -> NamedTempFile {
234 let mut tmp = NamedTempFile::new().unwrap();
235 tmp.write_all(bytes).unwrap();
236 tmp.flush().unwrap();
237 tmp
238 }
239
240 #[test]
241 fn apply_records_original_and_new_bytes() {
242 let bytes = vec![0x75, 0x05, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90];
243 let tmp = writable_temp_file_with_bytes(&bytes);
244 let mut patcher = InMemoryBytePatcher::new(Address(0x40_1050), bytes);
245 let finding = dead_branch_finding(0x40_1050, 2);
246 let plan = build_plan(&[finding], Confidence::High, Arch::X86_64, &mut patcher).unwrap();
247 assert_eq!(plan.operations.len(), 1);
248
249 let config = ApplyConfig {
250 binary_path: tmp.path().to_path_buf(),
251 backup_path: tmp.path().with_extension("bak"),
252 r2smt_version: "test".into(),
253 };
254 let manifest = apply_plan(&mut patcher, &plan, &config).unwrap();
255
256 assert_eq!(manifest.operations.len(), 1);
257 let record = &manifest.operations[0];
258 assert_eq!(record.address, Address(0x40_1050));
259 assert_eq!(record.original_bytes_hex, "7505");
260 assert_eq!(record.patched_bytes_hex, "9090");
261 assert_eq!(record.strategy, PatchStrategy::NopJcc.as_str());
262 assert_eq!(&patcher.bytes[0..2], &[0x90, 0x90]);
265 }
266
267 #[test]
268 fn rollback_rejects_a_manifest_record_with_mismatched_byte_lengths() {
269 let mut patcher =
275 InMemoryBytePatcher::new(Address(0x40_1050), vec![0x90, 0x90, 0x00, 0x00]);
276 let manifest = PatchManifest {
277 manifest_version: MANIFEST_VERSION,
278 r2smt_version: "test".into(),
279 binary: "/x".into(),
280 binary_sha256_before: String::new(),
281 binary_sha256_after: String::new(),
282 backup_path: String::new(),
283 operations: vec![PatchRecord {
284 address: Address(0x40_1050),
285 strategy: PatchStrategy::NopJcc.as_str().to_string(),
286 kind: FindingKind::DeadBranch,
287 confidence: Confidence::High,
288 original_bytes_hex: "750500".into(), patched_bytes_hex: "9090".into(), rationale: "test".into(),
291 }],
292 };
293 let err = rollback_from_manifest(&mut patcher, &manifest).unwrap_err();
294 assert!(
295 format!("{err}").contains("mismatched byte lengths"),
296 "{err}"
297 );
298 assert_eq!(
299 patcher.bytes,
300 vec![0x90, 0x90, 0x00, 0x00],
301 "a rejected rollback must not write anything"
302 );
303 }
304
305 #[test]
306 fn rollback_restores_original_bytes() {
307 let bytes = vec![0x75, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
308 let tmp = writable_temp_file_with_bytes(&bytes);
309 let mut patcher = InMemoryBytePatcher::new(Address(0x40_1050), bytes.clone());
310 let finding = dead_branch_finding(0x40_1050, 2);
311 let plan = build_plan(&[finding], Confidence::High, Arch::X86_64, &mut patcher).unwrap();
312 let config = ApplyConfig {
313 binary_path: tmp.path().to_path_buf(),
314 backup_path: tmp.path().with_extension("bak"),
315 r2smt_version: "test".into(),
316 };
317 let manifest = apply_plan(&mut patcher, &plan, &config).unwrap();
318
319 assert_ne!(&patcher.bytes[0..2], &bytes[0..2]);
322
323 rollback_from_manifest(&mut patcher, &manifest).unwrap();
325 assert_eq!(&patcher.bytes[0..2], &bytes[0..2]);
326 }
327
328 #[test]
329 fn rollback_refuses_when_current_bytes_do_not_match_the_patch() {
330 let bytes = vec![0x75, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
336 let tmp = writable_temp_file_with_bytes(&bytes);
337 let mut patcher = InMemoryBytePatcher::new(Address(0x40_1050), bytes.clone());
338 let finding = dead_branch_finding(0x40_1050, 2);
339 let plan = build_plan(&[finding], Confidence::High, Arch::X86_64, &mut patcher).unwrap();
340 let config = ApplyConfig {
341 binary_path: tmp.path().to_path_buf(),
342 backup_path: tmp.path().with_extension("bak"),
343 r2smt_version: "test".into(),
344 };
345 let manifest = apply_plan(&mut patcher, &plan, &config).unwrap();
346
347 patcher.bytes[0] = 0xAB;
349
350 let err = rollback_from_manifest(&mut patcher, &manifest).unwrap_err();
351 assert!(err.to_string().contains("do not match"), "{err}");
352 assert_eq!(patcher.bytes[0], 0xAB);
354 }
355
356 #[test]
357 fn apply_aborts_when_patcher_write_fails() {
358 let bytes = vec![0x75, 0x05];
360 let tmp = writable_temp_file_with_bytes(&bytes);
361 let mut patcher = InMemoryBytePatcher::new(Address(0x40_1050), bytes);
362 let mut plan = PatchPlan::default();
363 plan.operations.push(PlanOperation {
364 address: Address(0x40_1050),
365 strategy: PatchStrategy::NopJcc,
366 kind: FindingKind::DeadBranch,
367 confidence: Confidence::High,
368 size: 2,
369 new_bytes: vec![0x90, 0x90],
370 rationale: "test".into(),
371 });
372 plan.operations.push(PlanOperation {
375 address: Address(0x40_1060),
376 strategy: PatchStrategy::NopJcc,
377 kind: FindingKind::DeadBranch,
378 confidence: Confidence::High,
379 size: 2,
380 new_bytes: vec![0x90, 0x90],
381 rationale: "test".into(),
382 });
383 let config = ApplyConfig {
384 binary_path: tmp.path().to_path_buf(),
385 backup_path: tmp.path().with_extension("bak"),
386 r2smt_version: "test".into(),
387 };
388 let err = apply_plan(&mut patcher, &plan, &config).unwrap_err();
389 let msg = err.to_string();
390 assert!(msg.contains("past end") || msg.contains("address"));
391 }
392
393 #[test]
394 fn apply_captures_sha256_from_disk_into_manifest() {
395 let bytes = vec![0x75, 0x05];
396 let tmp = writable_temp_file_with_bytes(&bytes);
397 let mut patcher = InMemoryBytePatcher::new(Address(0x40_1050), bytes);
398 let finding = dead_branch_finding(0x40_1050, 2);
399 let plan = build_plan(&[finding], Confidence::High, Arch::X86_64, &mut patcher).unwrap();
400 let config = ApplyConfig {
401 binary_path: tmp.path().to_path_buf(),
402 backup_path: tmp.path().with_extension("bak"),
403 r2smt_version: "test".into(),
404 };
405
406 let pre = sha256_hex(tmp.path()).unwrap();
412 let manifest = apply_plan(&mut patcher, &plan, &config).unwrap();
413 assert_eq!(manifest.binary_sha256_before, pre);
414 assert_eq!(manifest.binary_sha256_after, pre);
415
416 fs::write(tmp.path(), [0x90, 0x90]).unwrap();
421 let post = sha256_hex(tmp.path()).unwrap();
422 assert_ne!(pre, post, "rewriting the file must change its hash");
423 }
424}