From cfd540c7de3b41169441b77585b39b83c1d4fe56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Thu, 31 Oct 2024 13:37:16 -0700 Subject: [PATCH] Add end-to-end test for #875 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an end-to-end test for the fix provided by pull request #875. The test basically normalizes an address in a specially crafted binary and symbolizes the resulting file offset. It fails without commit 1a4e10740652 ("Use file size in file offset -> virtual offset translation"), because then the file offset to virtual offset translation produces a virtual offset that can't be symbolized to the expected _start function. For the record, the binary looks roughly as follows: $ readelf --segments --wide test-block.bin > Program Headers: > Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align > LOAD 0x000000 0x00000000000ff000 0x00000000000ff000 0x0001c8 0x301000 RW 0x1000 > LOAD 0x0001c8 0x00000000004001c8 0x00000000004001c8 0x000030 0x000030 R 0x1000 > LOAD 0x001000 0x0000000000401000 0x0000000000401000 0x00005b 0x00005b R E 0x1000 > LOAD 0x002000 0x0000000000402000 0x0000000000402000 0x000038 0x000038 R 0x1000 > [...] Signed-off-by: Daniel Müller --- data/test-block-augmented.ld | 10 ++++++ data/test-block.c | 46 ++++++++++++++++++++++++++ dev/build.rs | 12 +++++++ tests/blazesym.rs | 64 ++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 data/test-block-augmented.ld create mode 100644 data/test-block.c diff --git a/data/test-block-augmented.ld b/data/test-block-augmented.ld new file mode 100644 index 00000000..6e708be8 --- /dev/null +++ b/data/test-block-augmented.ld @@ -0,0 +1,10 @@ +/* Linker script meant to augment the default one and insert some + * fill bytes at a relatively low address (hopefully before any of the + * regular relevant code. */ + +SECTIONS { + .whatevs (0x100000): { + FILL(0xdead) + . = ABSOLUTE(. + 0x300000); + } +} diff --git a/data/test-block.c b/data/test-block.c new file mode 100644 index 00000000..843d9f09 --- /dev/null +++ b/data/test-block.c @@ -0,0 +1,46 @@ +/* A binary basically just blocking waiting for input and exiting. It also write + * the address of its `_start` function to stdout (unformatted; just a byte + * dump). + * + * It uses raw system calls to avoid dependency on libc, which pulls in + * start up code and various other artifacts that perturb ELF layout in + * semi-unforeseeable ways, in an attempt to provide us with maximum + * control over the final binary. + * + * Likely only works on x86_64. + */ + +#include +#include + + +void _start(void) { + char buf[2]; + int rc; + void* addr = (void*)&_start; + /* Write the address of `_start` to stderr. We use stderr because it's + unbuffered, so we spare ourselves from the pains of writing a + newline as well... */ + asm volatile ( + "syscall" + : "=a"(rc) + : "a"(SYS_write), "D"(STDERR_FILENO), "S"(&addr), "d"(sizeof(addr)) + : "rcx", "r11", "memory" + ); + asm volatile ( + "syscall" + : "=a"(rc) + : "a"(SYS_read), "D"(STDIN_FILENO), "S"(buf), "d"(sizeof(buf)) + : "rcx", "r11", "memory" + ); + if (rc > 0) { + /* No error, so we can exit successfully. */ + rc = 0; + } + asm volatile ( + "syscall" + : "=a"(rc) + : "a"(SYS_exit), "D"(rc) + : "rcx", "r11", "memory" + ); +} diff --git a/dev/build.rs b/dev/build.rs index 5df12c6a..24cf129f 100644 --- a/dev/build.rs +++ b/dev/build.rs @@ -477,6 +477,18 @@ fn prepare_test_files() { let src = data_dir.join("test-mnt-ns.c"); cc(&src, "test-mnt-ns.bin", &[]); + let src = data_dir.join("test-block.c"); + let ld_script = data_dir.join("test-block-augmented.ld"); + let args = &[ + "-static", + "-Wl,--build-id=none", + "-nostdlib", + // Just passing the linker script as "regular" input file causes + // it to "augment" the default linker script. + ld_script.to_str().unwrap(), + ]; + cc(&src, "test-block.bin", args); + cc_stable_addrs( "test-stable-addrs.bin", &["-gdwarf-4", "-Wl,--build-id=none", "-O0"], diff --git a/tests/blazesym.rs b/tests/blazesym.rs index 9d030a1b..9c14466b 100644 --- a/tests/blazesym.rs +++ b/tests/blazesym.rs @@ -805,6 +805,70 @@ fn symbolize_process_with_custom_dispatch() { test(process_no_dispatch); } +/// Symbolize a normalized address from a binary with an artificially +/// inflated ELF segment. +/// +/// This is a regression test for the case that a program header +/// with a memory size greater than file size is located before a +/// program header that would otherwise match the file offset. Refer +/// to commit 1a4e10740652 ("Use file size in file offset -> virtual +/// offset translation"). +#[cfg(linux)] +#[test] +fn symbolize_normalized_large_memsize() { + let test_block = Path::new(&env!("CARGO_MANIFEST_DIR")) + .join("data") + .join("test-block.bin"); + let mut child = Command::new(&test_block) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let pid = child.id(); + defer!({ + // Best effort only. The child may end up terminating gracefully + // if everything goes as planned. + // TODO: Ideally this kill would be pid FD based to eliminate + // any possibility of killing the wrong entity. + let _rc = unsafe { libc::kill(pid as _, libc::SIGKILL) }; + }); + + let mut buf = [0u8; size_of::()]; + let count = child + .stderr + .as_mut() + .unwrap() + .read(&mut buf) + .expect("failed to read child output"); + assert_eq!(count, buf.len()); + let addr = Addr::from_ne_bytes(buf); + let pid = Pid::from(child.id()); + + let normalizer = Normalizer::new(); + let normalized = normalizer + .normalize_user_addrs(pid, [addr].as_slice()) + .unwrap(); + + assert_eq!(normalized.outputs.len(), 1); + assert_eq!(normalized.meta.len(), 1); + let file_offset = normalized.outputs[0].0; + + let elf = symbolize::Elf::new(test_block); + let src = symbolize::Source::Elf(elf); + let symbolizer = Symbolizer::new(); + let sym = symbolizer + .symbolize_single(&src, symbolize::Input::FileOffset(file_offset)) + .unwrap() + .into_sym() + .unwrap(); + assert_eq!(sym.name, "_start"); + + // "Signal" the child to terminate gracefully. + let () = child.stdin.as_ref().unwrap().write_all(&[0x04]).unwrap(); + let _status = child.wait().unwrap(); +} + /// Check that we can normalize addresses in an ELF shared object. #[cfg(linux)] #[test]