1use std::path::PathBuf;
21
22use object::SymbolKind;
23use subsecond_types::{AddressMap, JumpTable};
24
25use super::symbol_table::SymbolTable;
26
27#[derive(Debug, Clone, Default, PartialEq, Eq)]
31pub struct DiffReport {
32 pub removed: Vec<String>,
35 pub added: Vec<String>,
38 pub weak: Vec<String>,
42}
43
44#[derive(Debug, Clone)]
47pub struct PatchPlan {
48 pub table: JumpTable,
49 pub report: DiffReport,
50}
51
52pub fn build_jump_table(
58 old: &SymbolTable,
59 new: &SymbolTable,
60 new_lib: PathBuf,
61 aslr_reference: u64,
62 new_base_address: u64,
63) -> PatchPlan {
64 let mut map = AddressMap::default();
65 let mut report = DiffReport::default();
66
67 for (name, new_sym) in &new.by_name {
68 let old_sym = match old.by_name.get(name) {
69 Some(s) => s,
70 None => {
71 report.added.push(name.clone());
72 continue;
73 }
74 };
75
76 if !is_patchable(old_sym.kind) || !is_patchable(new_sym.kind) {
78 continue;
79 }
80 if old_sym.is_undefined || new_sym.is_undefined {
81 continue;
82 }
83 if old_sym.size == 0 && new_sym.size == 0 && cfg!(target_os = "linux") {
90 continue;
91 }
92
93 if old_sym.is_weak || new_sym.is_weak {
94 report.weak.push(name.clone());
95 }
96
97 map.insert(old_sym.address, new_sym.address);
98 }
99
100 for name in old.by_name.keys() {
102 if !new.by_name.contains_key(name) {
103 report.removed.push(name.clone());
104 }
105 }
106 report.removed.sort();
107 report.added.sort();
108 report.weak.sort();
109
110 PatchPlan {
111 table: JumpTable {
112 lib: new_lib,
113 map,
114 aslr_reference,
115 new_base_address,
116 ifunc_count: 0, },
118 report,
119 }
120}
121
122fn is_patchable(kind: SymbolKind) -> bool {
123 matches!(kind, SymbolKind::Text)
124}
125
126#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::hotpatch::symbol_table::SymbolInfo;
134 use std::collections::HashMap;
135
136 fn text(addr: u64, size: u64) -> SymbolInfo {
137 SymbolInfo {
138 address: addr,
139 kind: SymbolKind::Text,
140 size,
141 is_undefined: false,
142 is_weak: false,
143 }
144 }
145 fn data(addr: u64, size: u64) -> SymbolInfo {
146 SymbolInfo {
147 address: addr,
148 kind: SymbolKind::Data,
149 size,
150 is_undefined: false,
151 is_weak: false,
152 }
153 }
154 fn weak(addr: u64, size: u64) -> SymbolInfo {
155 SymbolInfo {
156 address: addr,
157 kind: SymbolKind::Text,
158 size,
159 is_undefined: false,
160 is_weak: true,
161 }
162 }
163 fn undef() -> SymbolInfo {
164 SymbolInfo {
165 address: 0,
166 kind: SymbolKind::Text,
167 size: 0,
168 is_undefined: true,
169 is_weak: false,
170 }
171 }
172
173 fn t(entries: Vec<(&str, SymbolInfo)>) -> SymbolTable {
174 let mut by_name = HashMap::new();
175 for (n, s) in entries {
176 by_name.insert(n.to_string(), s);
177 }
178 SymbolTable { by_name }
179 }
180
181 fn lib() -> PathBuf {
182 PathBuf::from("/tmp/patch.dylib")
183 }
184
185 #[test]
188 fn identical_tables_produce_an_identity_like_map() {
189 let same = t(vec![("foo", text(0x1000, 32)), ("bar", text(0x2000, 16))]);
190 let plan = build_jump_table(&same, &same, lib(), 0, 0);
191 assert_eq!(plan.table.map.len(), 2);
192 assert_eq!(plan.table.map.get(&0x1000), Some(&0x1000));
193 assert_eq!(plan.table.map.get(&0x2000), Some(&0x2000));
194 assert!(plan.report.removed.is_empty());
195 assert!(plan.report.added.is_empty());
196 }
197
198 #[test]
199 fn moved_function_records_old_to_new_address() {
200 let old = t(vec![("app", text(0x1000, 100))]);
201 let new = t(vec![("app", text(0x1500, 120))]);
202 let plan = build_jump_table(&old, &new, lib(), 0, 0);
203 assert_eq!(plan.table.map.len(), 1);
204 assert_eq!(plan.table.map.get(&0x1000), Some(&0x1500));
205 }
206
207 #[test]
208 fn aslr_and_base_address_are_propagated_into_the_table() {
209 let same = t(vec![("foo", text(0x1000, 32))]);
210 let plan = build_jump_table(&same, &same, lib(), 0xCAFE_BABE, 0xDEAD_BEEF);
211 assert_eq!(plan.table.aslr_reference, 0xCAFE_BABE);
212 assert_eq!(plan.table.new_base_address, 0xDEAD_BEEF);
213 assert_eq!(plan.table.lib, PathBuf::from("/tmp/patch.dylib"));
214 assert_eq!(plan.table.ifunc_count, 0);
215 }
216
217 #[test]
220 fn data_symbols_are_skipped() {
221 let old = t(vec![("g", data(0x4000, 8))]);
222 let new = t(vec![("g", data(0x4100, 8))]);
223 assert!(build_jump_table(&old, &new, lib(), 0, 0)
224 .table
225 .map
226 .is_empty());
227 }
228
229 #[test]
230 fn undefined_symbols_are_skipped_either_side() {
231 let old = t(vec![("undef", undef()), ("def", text(0x1000, 16))]);
232 let new = t(vec![("undef", text(0x9000, 16)), ("def", undef())]);
233 let plan = build_jump_table(&old, &new, lib(), 0, 0);
234 assert!(plan.table.map.is_empty());
236 }
237
238 #[test]
239 #[cfg(target_os = "linux")]
240 fn zero_sized_symbols_are_skipped_on_elf() {
241 let old = t(vec![("plt_stub", text(0x1000, 0))]);
244 let new = t(vec![("plt_stub", text(0x1100, 0))]);
245 assert!(build_jump_table(&old, &new, lib(), 0, 0)
246 .table
247 .map
248 .is_empty());
249 }
250
251 #[test]
252 #[cfg(any(target_os = "macos", target_os = "ios"))]
253 fn zero_sized_symbols_are_kept_on_mach_o() {
254 let old = t(vec![("foo", text(0x1000, 0))]);
258 let new = t(vec![("foo", text(0x1100, 0))]);
259 let plan = build_jump_table(&old, &new, lib(), 0, 0);
260 assert_eq!(plan.table.map.len(), 1);
261 assert_eq!(plan.table.map.get(&0x1000), Some(&0x1100));
262 }
263
264 #[test]
267 fn added_and_removed_show_up_in_the_report() {
268 let old = t(vec![("kept", text(0x1000, 16)), ("gone", text(0x2000, 16))]);
269 let new = t(vec![
270 ("kept", text(0x1100, 16)),
271 ("brand_new", text(0x3000, 16)),
272 ]);
273 let plan = build_jump_table(&old, &new, lib(), 0, 0);
274 assert_eq!(plan.report.removed, vec!["gone".to_string()]);
275 assert_eq!(plan.report.added, vec!["brand_new".to_string()]);
276 assert_eq!(plan.table.map.len(), 1);
277 assert_eq!(plan.table.map.get(&0x1000), Some(&0x1100));
278 }
279
280 #[test]
281 fn weak_symbol_is_emitted_but_listed_in_report() {
282 let old = t(vec![("maybe", weak(0x1000, 16))]);
283 let new = t(vec![("maybe", weak(0x1100, 16))]);
284 let plan = build_jump_table(&old, &new, lib(), 0, 0);
285 assert_eq!(plan.table.map.len(), 1);
286 assert_eq!(plan.report.weak, vec!["maybe".to_string()]);
287 }
288
289 #[test]
290 fn report_lists_are_sorted_for_stable_diagnostics() {
291 let old = t(vec![
292 ("c", text(0x1, 1)),
293 ("a", text(0x2, 1)),
294 ("b", text(0x3, 1)),
295 ]);
296 let new = t(vec![
297 ("z", text(0x4, 1)),
298 ("y", text(0x5, 1)),
299 ("x", text(0x6, 1)),
300 ]);
301 let plan = build_jump_table(&old, &new, lib(), 0, 0);
302 assert_eq!(plan.report.removed, vec!["a", "b", "c"]);
303 assert_eq!(plan.report.added, vec!["x", "y", "z"]);
304 }
305
306 #[test]
309 fn empty_inputs_produce_empty_outputs() {
310 let plan = build_jump_table(
311 &SymbolTable::default(),
312 &SymbolTable::default(),
313 lib(),
314 0,
315 0,
316 );
317 assert!(plan.table.map.is_empty());
318 assert!(plan.report.added.is_empty());
319 assert!(plan.report.removed.is_empty());
320 assert!(plan.report.weak.is_empty());
321 }
322}