1use crate::sha256;
8use std::{
9 collections::{BTreeMap, BTreeSet},
10 fs,
11 path::{Path, PathBuf},
12 process::Command,
13};
14
15pub const QUALIFIED_GCC: std::ops::RangeInclusive<u32> = 11..=16;
18
19#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct Object {
22 pub target: String,
25 pub member: u32,
27 pub object: String,
29 pub kind: String,
32 pub directory: String,
35 pub command: String,
37}
38
39pub(crate) fn run(program: &Path, args: &[String], dir: &Path) -> Result<String, String> {
41 let mut command = Command::new(program);
42 command.args(args).current_dir(dir).env("LC_ALL", "C");
43 for key in [
44 "CPATH",
45 "C_INCLUDE_PATH",
46 "CPLUS_INCLUDE_PATH",
47 "OBJC_INCLUDE_PATH",
48 "LIBRARY_PATH",
49 "GCC_EXEC_PREFIX",
50 "COMPILER_PATH",
51 "DEPENDENCIES_OUTPUT",
52 "SUNPRO_DEPENDENCIES",
53 ] {
54 command.env_remove(key);
55 }
56 let output = command
57 .output()
58 .map_err(|e| format!("cannot run {}: {e}", program.display()))?;
59 if !output.status.success() {
60 return Err(format!(
61 "{} failed in {}: {}",
62 program.display(),
63 dir.display(),
64 String::from_utf8_lossy(&output.stderr).trim()
65 ));
66 }
67 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
68}
69
70#[derive(Clone, Debug)]
72pub struct Tools {
73 pub gcc: PathBuf,
75 pub ar: PathBuf,
77 pub readelf: PathBuf,
79 pub assembler: PathBuf,
81 pub linker: PathBuf,
83}
84
85impl Default for Tools {
86 fn default() -> Self {
87 Self {
88 gcc: "gcc".into(),
89 ar: "ar".into(),
90 readelf: "readelf".into(),
91 assembler: "as".into(),
92 linker: "ld".into(),
93 }
94 }
95}
96
97impl Tools {
98 pub fn versions(&self) -> Result<Vec<(&'static str, String)>, String> {
100 let mut versions = Vec::new();
101 for (name, path) in [
102 ("gcc", &self.gcc),
103 ("as", &self.assembler),
104 ("ld", &self.linker),
105 ("ar", &self.ar),
106 ("readelf", &self.readelf),
107 ] {
108 let text = run(path, &["--version".into()], Path::new("."))?;
109 versions.push((name, text.lines().next().unwrap_or("").trim().to_string()));
110 }
111 if versions[0].1.to_ascii_lowercase().contains("clang") {
112 return Err(format!(
113 "{} is Clang; glibc's sources need GCC (Clang ignores attributes glibc \
114 applies after a definition and would export hidden symbols)",
115 self.gcc.display()
116 ));
117 }
118 Ok(versions)
119 }
120
121 pub fn gcc_major(&self) -> Result<u32, String> {
123 let text = run(&self.gcc, &["-dumpversion".into()], Path::new("."))?;
124 text.trim()
125 .split('.')
126 .next()
127 .and_then(|m| m.parse().ok())
128 .ok_or_else(|| format!("cannot read GCC's version from {text:?}"))
129 }
130
131 pub fn gcc_startup(&self) -> Result<PathBuf, String> {
133 let text = run(
134 &self.gcc,
135 &["-print-file-name=crtbegin.o".into()],
136 Path::new("."),
137 )?;
138 let path = PathBuf::from(text.trim());
139 if !path.is_absolute() || !path.is_file() {
140 return Err(format!("{} has no crtbegin.o", self.gcc.display()));
141 }
142 let dir = path.parent().ok_or("crtbegin.o has no directory")?;
143 if !dir.join("crtend.o").is_file() {
144 return Err(format!("{} lacks crtend.o", dir.display()));
145 }
146 fs::canonicalize(dir).map_err(|e| e.to_string())
147 }
148}
149
150fn accepts(tools: &Tools, flag: &str, scratch: &Path) -> bool {
152 let (source, object) = (scratch.join("probe.s"), scratch.join("probe.o"));
153 if fs::write(&source, "").is_err() {
154 return false;
155 }
156 let args = [
157 "-c".to_string(),
158 source.to_string_lossy().into_owned(),
159 "-o".into(),
160 object.to_string_lossy().into_owned(),
161 flag.into(),
162 ];
163 let accepted = run(&tools.gcc, &args, scratch).is_ok();
164 let _ = fs::remove_file(&source);
165 let _ = fs::remove_file(&object);
166 accepted
167}
168
169pub fn defaults_flags(command: &str, builder: &BTreeMap<String, String>) -> Vec<String> {
174 let tokens: Vec<&str> = command.split_whitespace().collect();
175 let has = |prefixes: &[&str]| {
176 tokens
177 .iter()
178 .any(|t| prefixes.iter().any(|p| t.starts_with(p)))
179 };
180 let on = |key: &str| {
181 builder
182 .get(&format!("builder.{key}"))
183 .is_some_and(|v| v == "yes")
184 };
185 let mut flags = Vec::new();
186 if !has(&[
187 "-fPIC", "-fpic", "-fPIE", "-fpie", "-fno-pic", "-fno-PIC", "-fno-pie", "-fno-PIE",
188 ]) {
189 flags.push(if on("pie") { "-fPIE" } else { "-fno-pie" }.to_string());
190 }
191 if !has(&["-fstack-protector", "-fno-stack-protector"]) && !on("stack_protector") {
192 flags.push("-fno-stack-protector".into());
193 }
194 if !has(&["-fcf-protection"]) && !on("cet") {
195 flags.push("-fcf-protection=none".into());
196 }
197 if !on("fortify") {
198 flags.push("-U_FORTIFY_SOURCE".into());
199 }
200 if !has(&["-fstack-clash-protection", "-fno-stack-clash-protection"]) {
201 flags.push("-fno-stack-clash-protection".into());
202 }
203 flags
204}
205
206pub fn translate(
209 command: &str,
210 gcc: &str,
211 src: &str,
212 build: &str,
213 extra: &[String],
214) -> Result<Vec<String>, String> {
215 let mut tokens: Vec<String> = command
216 .split_whitespace()
217 .map(|t| t.replace("@SRC@", src).replace("@BUILD@", build))
218 .collect();
219 if tokens.first().map(String::as_str) != Some("gcc") {
220 return Err(format!("recorded command does not run gcc: {command:?}"));
221 }
222 tokens[0] = gcc.into();
223 let mut out = Vec::new();
224 let mut skip = false;
225 for token in tokens {
226 if skip {
227 skip = false;
228 continue;
229 }
230 match token.as_str() {
231 "-MD" | "-MP" => {}
232 "-MF" | "-MT" => skip = true,
233 _ => out.push(token),
234 }
235 }
236 out.extend(extra.iter().cloned());
237 Ok(out)
238}
239
240#[derive(Clone, Debug, Default, PartialEq, Eq)]
242pub struct Interface {
243 pub defined: BTreeSet<String>,
245 pub undefined: BTreeSet<String>,
247 pub sections: BTreeSet<String>,
249 pub references: BTreeSet<(String, String, String)>,
251}
252
253fn relocation_form(kind: &str) -> String {
258 match kind {
259 "R_X86_64_PC32" | "R_X86_64_PLT32" => "pc-relative".into(),
260 "R_X86_64_GOTPCREL" | "R_X86_64_GOTPCRELX" | "R_X86_64_REX_GOTPCRELX" => "got".into(),
261 other => other.into(),
262 }
263}
264
265pub fn interface(readelf: &Path, object: &Path) -> Result<(Interface, String), String> {
268 let path = object.to_string_lossy().to_string();
269 let here = Path::new(".");
270 let read = |flags: &[&str]| {
271 let mut args: Vec<String> = flags.iter().map(|f| f.to_string()).collect();
272 args.push(path.clone());
273 run(readelf, &args, here)
274 };
275 let mut names: BTreeMap<String, String> = BTreeMap::new();
276 let mut result = Interface::default();
277 let mut allocated = Vec::new();
278 for line in read(&["-W", "-S"])?.lines() {
279 let Some(rest) = line.trim_start().strip_prefix('[') else {
280 continue;
281 };
282 let Some((index, rest)) = rest.split_once(']') else {
283 continue;
284 };
285 let fields: Vec<&str> = rest.split_whitespace().collect();
286 if fields.len() < 7 || fields[0] == "Name" {
287 continue;
288 }
289 names.insert(index.trim().to_string(), fields[0].to_string());
290 let flags = if fields.len() >= 10 { fields[6] } else { "" };
292 let empty = u64::from_str_radix(fields[4], 16).is_ok_and(|size| size == 0);
293 if flags.contains('A') && fields[0] != ".note.GNU-stack" && !empty {
294 result.sections.insert(format!("{} {}", fields[0], flags));
295 allocated.push(fields[0].to_string());
296 }
297 }
298 for line in read(&["-W", "-s"])?.lines() {
299 let f: Vec<&str> = line.split_whitespace().collect();
300 if f.len() < 8 || !f[0].ends_with(':') || f[0] == "Num:" {
301 continue;
302 }
303 let (kind, bind, visibility, index, name) = (f[3], f[4], f[5], f[6], f[7]);
304 if bind == "LOCAL" {
305 continue;
306 }
307 if index == "UND" {
308 if name != "_GLOBAL_OFFSET_TABLE_" {
310 result.undefined.insert(format!("{name} {bind}"));
311 }
312 } else {
313 let section = names.get(index).map_or(index, String::as_str);
314 result
315 .defined
316 .insert(format!("{name} {kind} {bind} {visibility} {section}"));
317 }
318 }
319 let mut section = String::new();
320 for line in read(&["-W", "-r"])?.lines() {
321 if let Some(rest) = line.strip_prefix("Relocation section '") {
322 section = rest.split('\'').next().unwrap_or("").to_string();
323 continue;
324 }
325 if section.contains(".debug") || section.is_empty() {
326 continue;
327 }
328 let f: Vec<&str> = line.split_whitespace().collect();
329 if f.len() < 3 || !f[2].starts_with("R_X86_64") {
330 continue;
331 }
332 let symbol = f.get(4).map_or("", |s| s.split('+').next().unwrap_or(s));
333 result
334 .references
335 .insert((section.clone(), relocation_form(f[2]), symbol.to_string()));
336 }
337 let mut bytes = Vec::new();
338 for name in allocated {
339 bytes.extend(name.as_bytes());
340 bytes.push(0);
341 bytes.extend(hex_dump_bytes(&read(&["-W", "-x", &name])?));
342 }
343 Ok((result, sha256::hex(&sha256::sha256(&bytes))))
344}
345
346fn hex_dump_bytes(dump: &str) -> Vec<u8> {
350 let mut bytes = Vec::new();
351 for line in dump.lines() {
352 let Some(rest) = line.trim_start().strip_prefix("0x") else {
353 continue;
354 };
355 let Some((_, data)) = rest.split_once(' ') else {
356 continue;
357 };
358 let field: String = data
359 .chars()
360 .take(35)
361 .filter(|c| !c.is_whitespace())
362 .collect();
363 bytes.extend(
364 (0..field.len() / 2)
365 .filter_map(|i| u8::from_str_radix(field.get(2 * i..2 * i + 2)?, 16).ok()),
366 );
367 }
368 bytes
369}
370
371pub fn read_interfaces(text: &str) -> Result<BTreeMap<String, (Interface, String)>, String> {
373 let mut lines = text.lines();
374 if lines.next() != Some("object\tkind\tvalue") {
375 return Err("interfaces.tsv: unexpected header".into());
376 }
377 let mut map: BTreeMap<String, (Interface, String)> = BTreeMap::new();
378 for line in lines {
379 let [object, kind, value] = line.split('\t').collect::<Vec<_>>()[..] else {
380 return Err(format!("interfaces.tsv: malformed row {line:?}"));
381 };
382 let (interface, digest) = map.entry(object.to_string()).or_default();
383 let value = value.to_string();
384 match kind {
385 "defined" => drop(interface.defined.insert(value)),
386 "undefined" => drop(interface.undefined.insert(value)),
387 "section" => drop(interface.sections.insert(value)),
388 "reference" => {
389 let mut parts = value.splitn(3, ' ').map(String::from);
390 let (Some(s), Some(f), Some(n)) = (parts.next(), parts.next(), parts.next()) else {
391 return Err(format!("interfaces.tsv: malformed reference {value}"));
392 };
393 interface.references.insert((s, f, n));
394 }
395 "bytes" => *digest = value,
396 _ => return Err(format!("interfaces.tsv: unknown kind {kind}")),
397 }
398 }
399 Ok(map)
400}
401
402pub fn describe(recorded: &Interface, built: &Interface) -> String {
404 let diff = |name: &str, x: &BTreeSet<String>, y: &BTreeSet<String>| -> Option<String> {
405 let only_x: Vec<_> = x.difference(y).cloned().collect();
406 let only_y: Vec<_> = y.difference(x).cloned().collect();
407 (!only_x.is_empty() || !only_y.is_empty())
408 .then(|| format!("{name}: container only {only_x:?}, replay only {only_y:?}"))
409 };
410 let only_a: BTreeSet<_> = recorded
413 .references
414 .difference(&built.references)
415 .cloned()
416 .collect();
417 let only_b: BTreeSet<_> = built
418 .references
419 .difference(&recorded.references)
420 .cloned()
421 .collect();
422 let container: BTreeSet<String> = only_a
423 .iter()
424 .filter(|r| {
425 !(r.1 == "pc-relative" && only_b.contains(&(r.0.clone(), "got".into(), r.2.clone())))
426 })
427 .map(|r| format!("{} {} {}", r.0, r.1, r.2))
428 .collect();
429 let replay: BTreeSet<String> = only_b
430 .iter()
431 .filter(|r| {
432 !(r.1 == "got" && only_a.contains(&(r.0.clone(), "pc-relative".into(), r.2.clone())))
433 })
434 .map(|r| format!("{} {} {}", r.0, r.1, r.2))
435 .collect();
436 [
437 diff("defined", &recorded.defined, &built.defined),
438 diff("undefined", &recorded.undefined, &built.undefined),
439 diff("sections", &recorded.sections, &built.sections),
440 diff("relocations", &container, &replay),
441 ]
442 .into_iter()
443 .flatten()
444 .collect::<Vec<_>>()
445 .join("; ")
446}
447
448pub struct Recorded<'a> {
450 pub release: &'a str,
452 pub values: BTreeMap<String, String>,
454 pub objects: Vec<Object>,
456 pub interfaces: &'a str,
458 pub accepted: &'a str,
460}
461
462#[derive(Clone, Debug, PartialEq, Eq)]
464pub struct Verdict {
465 pub object: String,
467 pub status: &'static str,
469 pub detail: String,
472}
473
474pub struct Built {
476 pub targets: BTreeMap<String, PathBuf>,
478 pub verdicts: Vec<Verdict>,
480}
481
482impl Built {
483 pub fn verified(&self) -> bool {
485 self.verdicts.iter().all(|v| v.status != "differs")
486 }
487
488 pub fn summary(&self) -> String {
490 let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
491 for verdict in &self.verdicts {
492 *counts.entry(verdict.status).or_default() += 1;
493 }
494 counts
495 .iter()
496 .map(|(status, count)| format!("{status}: {count}"))
497 .collect::<Vec<_>>()
498 .join(", ")
499 }
500}
501
502pub fn build(recorded: &Recorded<'_>, inputs: &Path, tools: &Tools) -> Result<Built, String> {
506 let (src, build, uapi) = (
507 inputs.join("src"),
508 inputs.join("build"),
509 inputs.join("uapi"),
510 );
511 let interfaces = read_interfaces(recorded.interfaces)?;
512 if !recorded.values.contains_key("builder.pie") {
513 return Err("release.tsv lacks the builder defaults".into());
514 }
515 let mut extra = vec!["-nostdinc".to_string()];
517 for dir in ["include", "include-fixed"] {
518 let path = run(
519 &tools.gcc,
520 &[format!("-print-file-name={dir}")],
521 Path::new("."),
522 )?;
523 extra.extend(["-isystem".to_string(), path.trim().to_string()]);
524 }
525 let used_note = "-Wa,-mx86-used-note=no".to_string();
528 if accepts(tools, &used_note, inputs) {
529 extra.push(used_note);
530 }
531 extra.extend(["-isystem".to_string(), uapi.to_string_lossy().into_owned()]);
532 let link_extra = vec!["-no-pie".to_string(), "-Wl,--build-id=none".into()];
534 let (s, b) = (
535 src.to_string_lossy().into_owned(),
536 build.to_string_lossy().into_owned(),
537 );
538 let gcc = tools.gcc.to_string_lossy().into_owned();
539 let mut built = BTreeSet::new();
540 for pass in ["compile", "relocatable"] {
542 for row in recorded.objects.iter().filter(|r| r.kind == pass) {
543 if !built.insert(row.object.clone()) {
544 continue;
545 }
546 let cwd = if row.directory == "." {
547 src.clone()
548 } else {
549 src.join(&row.directory)
550 };
551 fs::create_dir_all(&cwd).map_err(|e| e.to_string())?;
552 if let Some(parent) = build.join(&row.object).parent() {
553 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
554 }
555 let mut flags = if pass == "compile" {
556 extra.clone()
557 } else {
558 link_extra.clone()
559 };
560 if pass == "compile" {
561 flags.extend(defaults_flags(&row.command, &recorded.values));
562 }
563 let args = translate(&row.command, &gcc, &s, &b, &flags)?;
564 run(&tools.gcc, &args[1..], &cwd)
565 .map_err(|e| format!("glibc {}: {}: {e}", recorded.release, row.object))?;
566 }
567 }
568 let mut targets = BTreeMap::new();
569 let mut archives: BTreeMap<&str, Vec<(u32, &str)>> = BTreeMap::new();
570 for row in &recorded.objects {
571 if row.target.ends_with(".a") {
572 archives
573 .entry(&row.target)
574 .or_default()
575 .push((row.member, &row.object));
576 } else if Path::new(&row.object).file_name() == Some(row.target.as_ref()) {
577 targets.insert(row.target.clone(), build.join(&row.object));
578 }
579 }
580 for (archive, mut members) in archives {
581 members.sort();
582 members.dedup();
583 let path = build.join(archive);
584 let mut args = vec!["rcD".to_string(), path.to_string_lossy().into_owned()];
585 args.extend(
586 members
587 .iter()
588 .map(|(_, m)| build.join(m).to_string_lossy().into_owned()),
589 );
590 run(&tools.ar, &args, Path::new("."))?;
591 targets.insert(archive.to_string(), path);
592 }
593 for name in recorded
594 .objects
595 .iter()
596 .map(|o| o.target.as_str())
597 .collect::<BTreeSet<_>>()
598 {
599 if !targets.contains_key(name) {
600 return Err(format!(
601 "glibc {}: no object builds {name}",
602 recorded.release
603 ));
604 }
605 }
606 let mut verdicts = Vec::new();
607 for object in &built {
608 let (reference, reference_bytes) = interfaces
609 .get(object)
610 .ok_or_else(|| format!("glibc {}: interfaces.tsv lacks {object}", recorded.release))?;
611 let (actual, actual_bytes) = interface(&tools.readelf, &build.join(object))?;
612 let difference = describe(reference, &actual);
613 let reviewed = recorded.accepted.lines().skip(1).any(|line| {
614 let row: Vec<&str> = line.split('\t').collect();
615 row.len() >= 5
616 && row[0] == recorded.release
617 && row[1] == object
618 && matches!(row[2], "gcc" | "any")
619 && row[3] == difference
620 });
621 let status = if difference.is_empty() {
622 if *reference_bytes == actual_bytes {
623 "identical-bytes"
624 } else {
625 "equivalent"
626 }
627 } else if reviewed {
628 "accepted"
629 } else {
630 "differs"
631 };
632 verdicts.push(Verdict {
633 object: object.clone(),
634 status,
635 detail: difference,
636 });
637 }
638 Ok(Built { targets, verdicts })
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 #[test]
646 fn compares_references_not_encodings() {
647 let refs = |items: &[(&str, &str)]| Interface {
648 references: items
649 .iter()
650 .map(|(kind, sym)| (".rela.text".into(), relocation_form(kind), sym.to_string()))
651 .collect(),
652 ..Interface::default()
653 };
654 let old = refs(&[("R_X86_64_GOTPCREL", "g"), ("R_X86_64_PC32", "f")]);
655 let new = refs(&[("R_X86_64_REX_GOTPCRELX", "g"), ("R_X86_64_PLT32", "f")]);
656 assert_eq!(describe(&old, &new), "");
657 let direct = refs(&[("R_X86_64_PC32", "h")]);
659 let through_got = refs(&[("R_X86_64_GOTPCRELX", "h")]);
660 assert_eq!(describe(&direct, &through_got), "");
661 assert_ne!(describe(&through_got, &direct), "");
663 assert_ne!(describe(&direct, &refs(&[])), "");
664 }
665
666 #[test]
667 fn reads_hex_dumps_without_the_ascii_column() {
668 let dump = "\nHex dump of section '.rodata.cst4':\n 0x00000000 01000200 deadbeef 31ed4989 ab ........1.I..\n 0x00000010 cafe ..\n";
669 assert_eq!(
670 hex_dump_bytes(dump),
671 [1, 0, 2, 0, 0xde, 0xad, 0xbe, 0xef, 0x31, 0xed, 0x49, 0x89, 0xab, 0xca, 0xfe]
672 );
673 }
674
675 #[test]
676 fn reads_recorded_interfaces() {
677 let text = "object\tkind\tvalue\ncsu/start.o\tdefined\t_start FUNC GLOBAL DEFAULT .text\ncsu/start.o\treference\t.rela.text got __libc_start_main\ncsu/start.o\tbytes\tab\n";
678 let read = read_interfaces(text).unwrap();
679 let (interface, digest) = &read["csu/start.o"];
680 assert_eq!(digest, "ab");
681 assert!(interface.references.contains(&(
682 ".rela.text".into(),
683 "got".into(),
684 "__libc_start_main".into()
685 )));
686 assert!(read_interfaces("object\tkind\tvalue\nx\tother\ty\n").is_err());
687 assert!(read_interfaces("bad header\n").is_err());
688 }
689
690 #[test]
691 fn makes_builder_defaults_explicit_only_where_unset() {
692 let pie: BTreeMap<String, String> = [("builder.pie".to_string(), "yes".to_string())].into();
693 let none = BTreeMap::new();
694 assert!(defaults_flags("gcc x.c -c", &pie).contains(&"-fPIE".to_string()));
695 assert!(defaults_flags("gcc x.c -c", &none).contains(&"-fno-pie".to_string()));
696 let pic = defaults_flags("gcc x.c -c -fPIC -fno-stack-protector", &none);
697 assert!(!pic
698 .iter()
699 .any(|f| f.contains("pie") || f.contains("stack-protector")));
700 assert!(pic.contains(&"-U_FORTIFY_SOURCE".to_string()));
701 }
702
703 #[test]
704 fn translates_recorded_commands() {
705 let args = translate(
706 "gcc ../x.S -c -I@BUILD@/csu -o @BUILD@/csu/x.o -MD -MP -MF @BUILD@/csu/x.o.dt -MT @BUILD@/csu/x.o",
707 "/usr/bin/gcc-14",
708 "/o/src",
709 "/o/build",
710 &["-nostdinc".into()],
711 )
712 .unwrap();
713 assert_eq!(
714 args,
715 [
716 "/usr/bin/gcc-14",
717 "../x.S",
718 "-c",
719 "-I/o/build/csu",
720 "-o",
721 "/o/build/csu/x.o",
722 "-nostdinc"
723 ]
724 );
725 assert!(translate("cc x.c", "gcc", "/s", "/b", &[]).is_err());
726 }
727}