Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "libsui"
version = "0.16.0"
version = "0.16.1"
authors = ["the Deno authors"]
edition = "2021"
license = "MIT"
Expand Down
39 changes: 34 additions & 5 deletions lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1331,6 +1331,7 @@ impl<'a> Elf<'a> {
// binary has no section table to extend, and keeps the note via
// PT_NOTE alone.
const SHENTSIZE64: usize = 64;
const SHT_PROGBITS: u32 = 1;
const SHT_NOTE: u32 = 7;
const SHF_ALLOC: u64 = 2;
const SHN_XINDEX: usize = 0xffff;
Expand Down Expand Up @@ -1364,10 +1365,12 @@ impl<'a> Elf<'a> {
});

if let Some((shstr_off, shstr_size)) = shstr_range {
// Relocate and grow .shstrtab so it carries the new section name.
// Relocate and grow .shstrtab so it carries the new section names.
let mut new_shstr = data[shstr_off..shstr_off + shstr_size].to_vec();
let name_index = new_shstr.len() as u32;
let note_name_index = new_shstr.len() as u32;
new_shstr.extend_from_slice(b".note.sui\0");
let phdr_name_index = new_shstr.len() as u32;
new_shstr.extend_from_slice(b".sui.phdrs\0");

// .shstrtab and the section header table are non-allocated
// metadata: place them past the note, outside the new PT_LOAD.
Expand All @@ -1377,15 +1380,41 @@ impl<'a> Elf<'a> {
out.resize(sht_new_off, 0);

// Copy the original section headers, repoint the relocated
// .shstrtab entry, then append the .note.sui section header.
// .shstrtab entry, then append the new section headers.
let mut sht = data[e_shoff..e_shoff + e_shnum * e_shentsize].to_vec();
{
let e = &mut sht[e_shstrndx * e_shentsize..e_shstrndx * e_shentsize + SHENTSIZE64];
w64(&mut e[24..32], shstr_new_off as u64);
w64(&mut e[32..40], new_shstr.len() as u64);
}

// The relocated program header table lives past the original EOF,
// in the file gap before the note. Cover it with an allocated
// section so section-based strip tools that keep the file's byte
// layout preserve it. `eu-strip` in particular lays the output out
// with ELF_F_LAYOUT and zero-fills every file gap that lies between
// two allocated sections; without a section spanning the program
// header table it treats those bytes as dead space and clobbers
// them, leaving a binary with all-zero program headers that
// segfaults on exec. This must be a plain PROGBITS section, not part
// of the note: BFD `strip` parses SHT_NOTE contents, so folding the
// (non-note) program header bytes into `.note.sui` corrupts the note
// on strip and loses it.
let mut phdr_sh = vec![0u8; e_shentsize];
w32(&mut phdr_sh[0..4], phdr_name_index); // sh_name
w32(&mut phdr_sh[4..8], SHT_PROGBITS); // sh_type
w64(&mut phdr_sh[8..16], SHF_ALLOC); // sh_flags
w64(&mut phdr_sh[16..24], load_vaddr); // sh_addr
w64(&mut phdr_sh[24..32], new_phoff as u64); // sh_offset
w64(&mut phdr_sh[32..40], (note_file_off - new_phoff) as u64); // sh_size
w64(&mut phdr_sh[48..56], PAGE as u64); // sh_addralign
sht.extend_from_slice(&phdr_sh);

// The allocated SHT_NOTE section keeps the note reachable through the
// section table (what BFD-based tools rebuild from); the runtime
// reads it through its PT_NOTE program header regardless.
let mut note_sh = vec![0u8; e_shentsize];
w32(&mut note_sh[0..4], name_index); // sh_name
w32(&mut note_sh[0..4], note_name_index); // sh_name
w32(&mut note_sh[4..8], SHT_NOTE); // sh_type
w64(&mut note_sh[8..16], SHF_ALLOC); // sh_flags
w64(&mut note_sh[16..24], note_vaddr); // sh_addr
Expand All @@ -1396,7 +1425,7 @@ impl<'a> Elf<'a> {
out.extend_from_slice(&sht);

new_shoff = sht_new_off as u64;
new_shnum = (e_shnum + 1) as u16;
new_shnum = (e_shnum + 2) as u16;
}

// Point the ELF header at the relocated, enlarged program header table
Expand Down
71 changes: 67 additions & 4 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,68 @@ fn test_elf_note_survives_strip() {
assert_eq!(section, payload.as_slice());
}

// Regression for a segfault of appended binaries after `eu-strip`
// (as run by e.g. flatpak-builder): elfutils lays the stripped output out
// itself and zero-fills every file gap between two allocated sections. The
// relocated program header table sits in the gap before the note, so unless an
// allocated section covers it (`.sui.phdrs`) eu-strip clobbers it into all-zero
// program headers and the binary segfaults on exec.
#[cfg(all(unix, not(target_vendor = "apple"), target_arch = "x86_64"))]
#[test]
fn test_elf_note_survives_eu_strip() {
let _lock = PROCESS_LOCK.lock().unwrap();

let input = std::fs::read("tests/exec_elf64").unwrap();
let elf = Elf::new(&input);
let path = std::env::temp_dir().join("exec_elf64_eu_strip_out");

let payload = b"hello-eu-strip-note".to_vec();
let mut out = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o755)
.open(&path)
.unwrap();
elf.append(RESOURCE_NAME, &payload, &mut out).unwrap();
drop(out);

let output = std::process::Command::new("eu-strip").arg(&path).output();
match output {
Ok(output) if output.status.success() => {}
// eu-strip not installed / refused the file: nothing to assert.
Ok(_) => return,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return,
Err(err) => panic!("failed to run eu-strip: {}", err),
}

// The note still round-trips out of the stripped binary...
let stripped = std::fs::read(&path).unwrap();
let section = find_section_in_bytes(&stripped, RESOURCE_NAME).unwrap();
assert_eq!(section, payload.as_slice());

// ...the program header table was not zero-filled...
let r64 = |b: &[u8]| u64::from_le_bytes(b[..8].try_into().unwrap());
let r16 = |b: &[u8]| u16::from_le_bytes(b[..2].try_into().unwrap());
let e_phoff = r64(&stripped[0x20..0x28]) as usize;
let e_phentsize = r16(&stripped[0x36..0x38]) as usize;
let e_phnum = r16(&stripped[0x38..0x3a]) as usize;
assert!(e_phnum > 0, "no program headers");
let all_zero = (0..e_phnum).all(|i| {
let p = &stripped[e_phoff + i * e_phentsize..e_phoff + (i + 1) * e_phentsize];
p.iter().all(|&b| b == 0)
});
assert!(!all_zero, "eu-strip zero-filled the program header table");

// ...and the stripped binary still runs.
let status = std::process::Command::new(&path).status().unwrap();
assert!(
status.success(),
"stripped binary failed to run: {}",
status
);
}

#[cfg(all(unix, not(target_vendor = "apple")))]
#[test]
fn test_elf_note_mapped_and_preserves() {
Expand Down Expand Up @@ -643,16 +705,17 @@ fn test_elf_append_preserves_original_bytes() {
let r64 = |b: &[u8]| u64::from_le_bytes(b[..8].try_into().unwrap());
let r16 = |b: &[u8]| u16::from_le_bytes(b[..2].try_into().unwrap());

// A real allocated .note.sui section was added, so the section header
// table was relocated (past EOF) and grew by exactly one entry.
// Two allocated sections were added (.sui.phdrs covering the relocated
// program header table and .note.sui covering the note), so the section
// header table was relocated (past EOF) and grew by exactly two entries.
assert!(
r64(&out[0x28..0x30]) >= input.len() as u64,
"section header table not relocated past the original image"
);
assert_eq!(
r16(&out[0x3c..0x3e]),
r16(&input[0x3c..0x3e]) + 1,
"e_shnum did not grow by 1"
r16(&input[0x3c..0x3e]) + 2,
"e_shnum did not grow by 2"
);

// Program header table grew by exactly two entries (PT_LOAD + PT_NOTE).
Expand Down
Loading