Rust · 10660 bytes Raw Blame History
1 use std::fs;
2 use std::path::{Path, PathBuf};
3 use std::process::Command;
4
5 use afs_ld::macho::constants::{LC_LOAD_DYLIB, LC_SOURCE_VERSION, LC_UUID, MH_DYLIB, MH_EXECUTE};
6 use afs_ld::macho::reader::{parse_commands, parse_header, LoadCommand, Section64Header};
7
8 fn have_xcrun() -> bool {
9 Command::new("xcrun")
10 .arg("-f")
11 .arg("as")
12 .output()
13 .map(|o| o.status.success())
14 .unwrap_or(false)
15 }
16
17 fn have_tool(name: &str) -> bool {
18 Command::new(name)
19 .arg("-h")
20 .output()
21 .map(|_| true)
22 .unwrap_or(false)
23 }
24
25 fn have_clang() -> bool {
26 Command::new("xcrun")
27 .arg("-f")
28 .arg("clang")
29 .output()
30 .map(|o| o.status.success())
31 .unwrap_or(false)
32 }
33
34 fn assemble(src_text: &str, out: &Path) -> Result<(), String> {
35 let tmp = std::env::temp_dir().join(format!(
36 "afs-ld-link-write-{}-{}.s",
37 std::process::id(),
38 out.file_stem().and_then(|s| s.to_str()).unwrap_or("t")
39 ));
40 fs::write(&tmp, src_text).map_err(|e| format!("write .s: {e}"))?;
41 let status = Command::new("xcrun")
42 .args(["--sdk", "macosx", "as", "-arch", "arm64"])
43 .arg(&tmp)
44 .arg("-o")
45 .arg(out)
46 .output()
47 .map_err(|e| format!("spawn xcrun as: {e}"))?;
48 let _ = fs::remove_file(&tmp);
49 if !status.status.success() {
50 return Err(format!(
51 "xcrun as failed: {}",
52 String::from_utf8_lossy(&status.stderr)
53 ));
54 }
55 Ok(())
56 }
57
58 fn build_test_dylib(src: &str, out: &Path, install_name: &str) -> Result<(), String> {
59 let mut child = Command::new("xcrun")
60 .args([
61 "--sdk", "macosx", "clang", "-x", "c", "-arch", "arm64", "-shared", "-o",
62 ])
63 .arg(out)
64 .arg("-install_name")
65 .arg(install_name)
66 .arg("-")
67 .stdin(std::process::Stdio::piped())
68 .stdout(std::process::Stdio::piped())
69 .stderr(std::process::Stdio::piped())
70 .spawn()
71 .map_err(|e| format!("spawn clang: {e}"))?;
72 use std::io::Write;
73 child
74 .stdin
75 .as_mut()
76 .unwrap()
77 .write_all(src.as_bytes())
78 .map_err(|e| format!("write clang stdin: {e}"))?;
79 let out = child.wait_with_output().map_err(|e| format!("wait: {e}"))?;
80 if !out.status.success() {
81 return Err(format!(
82 "clang failed: {}",
83 String::from_utf8_lossy(&out.stderr)
84 ));
85 }
86 Ok(())
87 }
88
89 fn scratch(name: &str) -> PathBuf {
90 std::env::temp_dir().join(format!("afs-ld-link-write-{}-{name}", std::process::id()))
91 }
92
93 fn link_with_afs_ld(args: &[&str]) -> Result<(), String> {
94 let exe = env!("CARGO_BIN_EXE_afs-ld");
95 let out = Command::new(exe)
96 .args(args)
97 .output()
98 .map_err(|e| format!("spawn afs-ld: {e}"))?;
99 if !out.status.success() {
100 return Err(format!(
101 "afs-ld failed: {}\n{}",
102 out.status,
103 String::from_utf8_lossy(&out.stderr)
104 ));
105 }
106 Ok(())
107 }
108
109 fn section<'a>(cmds: &'a [LoadCommand], seg: &str, sect: &str) -> &'a Section64Header {
110 cmds.iter()
111 .find_map(|cmd| match cmd {
112 LoadCommand::Segment64(segment) => segment
113 .sections
114 .iter()
115 .find(|s| s.segname_str() == seg && s.sectname_str() == sect),
116 _ => None,
117 })
118 .unwrap_or_else(|| panic!("missing section {seg},{sect}"))
119 }
120
121 fn run_otool_lv(path: &Path) -> Result<String, String> {
122 let out = Command::new("otool")
123 .arg("-lV")
124 .arg(path)
125 .output()
126 .map_err(|e| format!("spawn otool -lV: {e}"))?;
127 if !out.status.success() {
128 return Err(format!(
129 "otool -lV failed: {}",
130 String::from_utf8_lossy(&out.stderr)
131 ));
132 }
133 Ok(String::from_utf8_lossy(&out.stdout).into_owned())
134 }
135
136 fn fixture_source() -> &'static str {
137 r#"
138 .section __TEXT,__text,regular,pure_instructions
139 .globl _main
140 .p2align 2
141 _main:
142 ret
143
144 .section __TEXT,__cstring,cstring_literals
145 _lit:
146 .asciz "hi"
147
148 .section __DATA,__data
149 .globl _num
150 .p2align 3
151 _num:
152 .quad 0x1122334455667788
153 "#
154 }
155
156 #[test]
157 fn linker_writes_executable_with_real_section_bytes() {
158 if !have_xcrun() {
159 eprintln!("skipping: xcrun as unavailable");
160 return;
161 }
162
163 let obj = scratch("fixture.o");
164 let out = scratch("linked-exec");
165 if let Err(e) = assemble(fixture_source(), &obj) {
166 eprintln!("skipping: assemble failed: {e}");
167 return;
168 }
169 link_with_afs_ld(&[obj.to_str().unwrap(), "-o", out.to_str().unwrap()])
170 .expect("link executable");
171
172 let bytes = fs::read(&out).expect("read executable");
173 let hdr = parse_header(&bytes).expect("parse header");
174 assert_eq!(hdr.filetype, MH_EXECUTE);
175 let cmds = parse_commands(&hdr, &bytes).expect("parse commands");
176
177 let text = section(&cmds, "__TEXT", "__text");
178 assert_eq!(
179 &bytes[text.offset as usize..text.offset as usize + text.size as usize],
180 &[0xc0, 0x03, 0x5f, 0xd6]
181 );
182
183 let cstring = section(&cmds, "__TEXT", "__cstring");
184 assert_eq!(
185 &bytes[cstring.offset as usize..cstring.offset as usize + cstring.size as usize],
186 b"hi\0"
187 );
188
189 let data = section(&cmds, "__DATA", "__data");
190 assert_eq!(
191 &bytes[data.offset as usize..data.offset as usize + data.size as usize],
192 &0x1122_3344_5566_7788u64.to_le_bytes()
193 );
194
195 if have_tool("otool") {
196 let dump = run_otool_lv(&out).expect("otool -lV");
197 assert!(dump.contains("segname __TEXT"));
198 assert!(dump.contains("sectname __cstring"));
199 assert!(dump.contains("segname __DATA"));
200 }
201
202 let _ = fs::remove_file(&obj);
203 let _ = fs::remove_file(&out);
204 }
205
206 #[test]
207 fn linker_writes_dylib_with_real_text_section() {
208 if !have_xcrun() {
209 eprintln!("skipping: xcrun as unavailable");
210 return;
211 }
212
213 let obj = scratch("fixture-dylib.o");
214 let out = scratch("libfixture.dylib");
215 if let Err(e) = assemble(fixture_source(), &obj) {
216 eprintln!("skipping: assemble failed: {e}");
217 return;
218 }
219 link_with_afs_ld(&["-dylib", obj.to_str().unwrap(), "-o", out.to_str().unwrap()])
220 .expect("link dylib");
221
222 let bytes = fs::read(&out).expect("read dylib");
223 let hdr = parse_header(&bytes).expect("parse header");
224 assert_eq!(hdr.filetype, MH_DYLIB);
225 let cmds = parse_commands(&hdr, &bytes).expect("parse commands");
226
227 let text = section(&cmds, "__TEXT", "__text");
228 assert_eq!(
229 &bytes[text.offset as usize..text.offset as usize + text.size as usize],
230 &[0xc0, 0x03, 0x5f, 0xd6]
231 );
232
233 let _ = fs::remove_file(&obj);
234 let _ = fs::remove_file(&out);
235 }
236
237 #[test]
238 fn linker_emits_load_dylib_for_direct_dependency_input() {
239 if !have_xcrun() || !have_clang() {
240 eprintln!("skipping: xcrun as / clang unavailable");
241 return;
242 }
243
244 let obj = scratch("dep-main.o");
245 let dep = scratch("libdep.dylib");
246 let out = scratch("dep-linked");
247 if let Err(e) = assemble(
248 r#"
249 .section __TEXT,__text,regular,pure_instructions
250 .globl _main
251 _main:
252 ret
253 "#,
254 &obj,
255 ) {
256 eprintln!("skipping: assemble failed: {e}");
257 return;
258 }
259 if let Err(e) = build_test_dylib(
260 "int afsld_dep(void) { return 7; }\n",
261 &dep,
262 "@rpath/libafslddep.dylib",
263 ) {
264 eprintln!("skipping: clang failed: {e}");
265 return;
266 }
267
268 link_with_afs_ld(&[
269 obj.to_str().unwrap(),
270 dep.to_str().unwrap(),
271 "-o",
272 out.to_str().unwrap(),
273 ])
274 .expect("link executable with dylib");
275
276 let bytes = fs::read(&out).expect("read executable");
277 let hdr = parse_header(&bytes).expect("parse header");
278 let cmds = parse_commands(&hdr, &bytes).expect("parse commands");
279 assert!(cmds.iter().any(|cmd| {
280 matches!(
281 cmd,
282 LoadCommand::Dylib(d)
283 if d.cmd == LC_LOAD_DYLIB && d.name == "@rpath/libafslddep.dylib"
284 )
285 }));
286
287 let _ = fs::remove_file(&obj);
288 let _ = fs::remove_file(&dep);
289 let _ = fs::remove_file(&out);
290 }
291
292 #[test]
293 fn linker_emits_rpath_load_command() {
294 if !have_xcrun() {
295 eprintln!("skipping: xcrun as unavailable");
296 return;
297 }
298
299 let obj = scratch("rpath-main.o");
300 let out = scratch("rpath-linked");
301 if let Err(e) = assemble(
302 r#"
303 .section __TEXT,__text,regular,pure_instructions
304 .globl _main
305 _main:
306 ret
307 "#,
308 &obj,
309 ) {
310 eprintln!("skipping: assemble failed: {e}");
311 return;
312 }
313
314 link_with_afs_ld(&[
315 obj.to_str().unwrap(),
316 "-rpath",
317 "@executable_path/../lib",
318 "-o",
319 out.to_str().unwrap(),
320 ])
321 .expect("link executable with rpath");
322
323 let bytes = fs::read(&out).expect("read executable");
324 let hdr = parse_header(&bytes).expect("parse header");
325 let cmds = parse_commands(&hdr, &bytes).expect("parse commands");
326 assert!(cmds.iter().any(|cmd| {
327 matches!(
328 cmd,
329 LoadCommand::Rpath(r) if r.path == "@executable_path/../lib"
330 )
331 }));
332
333 let _ = fs::remove_file(&obj);
334 let _ = fs::remove_file(&out);
335 }
336
337 #[test]
338 fn linker_emits_uuid_and_source_version_commands() {
339 if !have_xcrun() {
340 eprintln!("skipping: xcrun as unavailable");
341 return;
342 }
343
344 let obj = scratch("uuid-main.o");
345 let out = scratch("uuid-linked");
346 if let Err(e) = assemble(
347 r#"
348 .section __TEXT,__text,regular,pure_instructions
349 .globl _main
350 _main:
351 ret
352 "#,
353 &obj,
354 ) {
355 eprintln!("skipping: assemble failed: {e}");
356 return;
357 }
358
359 link_with_afs_ld(&[obj.to_str().unwrap(), "-o", out.to_str().unwrap()])
360 .expect("link executable with metadata");
361
362 let bytes = fs::read(&out).expect("read executable");
363 let hdr = parse_header(&bytes).expect("parse header");
364 let ids: Vec<u32> = parse_commands(&hdr, &bytes)
365 .expect("parse commands")
366 .into_iter()
367 .map(|cmd| match cmd {
368 LoadCommand::Raw { cmd, .. } => cmd,
369 other => other.cmd(),
370 })
371 .collect();
372 assert!(ids.contains(&LC_UUID));
373 assert!(ids.contains(&LC_SOURCE_VERSION));
374
375 let _ = fs::remove_file(&obj);
376 let _ = fs::remove_file(&out);
377 }
378