From d6ab68043c25d06b9e2ca3e84616c0abc9e51b0a Mon Sep 17 00:00:00 2001 From: Kyle Huey Date: Fri, 24 May 2024 14:50:35 -0700 Subject: [PATCH 1/5] Support zstd-compressed ELF sections. zstd has been introduced as an alternative to zlib for the compression of debug sections.[0] Toolchain support is widely present at this time but lack of support in backtrace is a severe limitation on using this feature in Rust programs. This uses a Rust reimplementation of zstd (the ruzstd crate). This has the benefit of simplifying the build process, but this crate is less used and admittedly slower than the zstd crate that binds to the C libzstd. [0] https://maskray.me/blog/2022-09-09-zstd-compressed-debug-sections --- .github/workflows/main.yml | 16 ++++++++++++++++ Cargo.lock | 8 ++++++++ Cargo.toml | 1 + crates/as-if-std/Cargo.toml | 3 ++- src/symbolize/gimli/elf.rs | 35 ++++++++++++++++++++++++++--------- 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0103a099..b3201166 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,12 @@ jobs: rust: beta - os: ubuntu-20.04 rust: nightly + - os: ubuntu-24.04 + rust: stable + - os: ubuntu-24.04 + rust: beta + - os: ubuntu-24.04 + rust: nightly - os: macos-latest rust: stable - os: macos-latest @@ -55,6 +61,12 @@ jobs: shell: bash if: contains(matrix.rust, 'i686') + # Starting with Ubuntu 22.04 libc6-dbg is needed. + - name: Install libc debug info + run: sudo apt-get install -y libc6-dbg + shell: bash + if: contains(matrix.os, 'ubuntu-24.04') + - name: Enable collapse_debuginfo based on version run: echo RUSTFLAGS="--cfg dbginfo=\"collapsible\" $RUSTFLAGS" >> $GITHUB_ENV shell: bash @@ -80,6 +92,10 @@ jobs: if: contains(matrix.os, 'ubuntu') env: RUSTFLAGS: "-C link-arg=-Wl,--compress-debug-sections=zlib" + - run: cargo test + if: contains(matrix.os, 'ubuntu-24.04') || contains(matrix.rust, 'nightly') + env: + RUSTFLAGS: "-C link-arg=-Wl,--compress-debug-sections=zstd" # Test that, on macOS, packed/unpacked debuginfo both work - run: cargo clean && cargo test diff --git a/Cargo.lock b/Cargo.lock index 8f4388b0..ea6b625f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", + "ruzstd", "windows-targets", ] @@ -43,6 +44,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", + "ruzstd", "serde", "windows-targets", ] @@ -150,6 +152,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "ruzstd" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c3938e133aac070997ddc684d4b393777d293ba170f2988c8fd5ea2ad4ce21" + [[package]] name = "serde" version = "1.0.203" diff --git a/Cargo.toml b/Cargo.toml index 00e9b246..46ee959a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ windows-targets = "0.52.6" [target.'cfg(not(all(windows, target_env = "msvc", not(target_vendor = "uwp"))))'.dependencies] miniz_oxide = { version = "0.8", default-features = false } +ruzstd = { version = "0.7.2", default-features = false } addr2line = { version = "0.24.0", default-features = false } libc = { version = "0.2.156", default-features = false } diff --git a/crates/as-if-std/Cargo.toml b/crates/as-if-std/Cargo.toml index a9c29cec..092905fe 100644 --- a/crates/as-if-std/Cargo.toml +++ b/crates/as-if-std/Cargo.toml @@ -18,6 +18,7 @@ libc = { version = "0.2.156", default-features = false } [target.'cfg(not(all(windows, target_env = "msvc", not(target_vendor = "uwp"))))'.dependencies] miniz_oxide = { version = "0.8", optional = true, default-features = false } +ruzstd = { version = "0.7.2", optional = true, default-features = false } addr2line = { version = "0.24.0", optional = true, default-features = false } [target.'cfg(not(all(windows, target_env = "msvc", not(target_vendor = "uwp"))))'.dependencies.object] @@ -31,7 +32,7 @@ windows-targets = "0.52.6" [features] default = ['backtrace'] -backtrace = ['addr2line', 'miniz_oxide', 'object'] +backtrace = ['addr2line', 'miniz_oxide', 'object', 'ruzstd'] std = [] [lints.rust] diff --git a/src/symbolize/gimli/elf.rs b/src/symbolize/gimli/elf.rs index 82c53df0..f920a6fe 100644 --- a/src/symbolize/gimli/elf.rs +++ b/src/symbolize/gimli/elf.rs @@ -9,7 +9,9 @@ use super::{gimli, Context, Endian, EndianSlice, Mapping, Stash, Vec}; use alloc::sync::Arc; use core::convert::{TryFrom, TryInto}; use core::str; -use object::elf::{ELFCOMPRESS_ZLIB, ELF_NOTE_GNU, NT_GNU_BUILD_ID, SHF_COMPRESSED}; +use object::elf::{ + ELFCOMPRESS_ZLIB, ELFCOMPRESS_ZSTD, ELF_NOTE_GNU, NT_GNU_BUILD_ID, SHF_COMPRESSED, +}; use object::read::elf::{CompressionHeader, FileHeader, SectionHeader, SectionTable, Sym}; use object::read::StringTable; use object::{BigEndian, Bytes, NativeEndian}; @@ -213,7 +215,8 @@ impl<'a> Object<'a> { let mut data = Bytes(section.data(self.endian, self.data).ok()?); // Check for DWARF-standard (gABI) compression, i.e., as generated - // by ld's `--compress-debug-sections=zlib-gabi` flag. + // by ld's `--compress-debug-sections=zlib-gabi` and + // `--compress-debug-sections=zstd` flags. let flags: u64 = section.sh_flags(self.endian).into(); if (flags & u64::from(SHF_COMPRESSED)) == 0 { // Not compressed. @@ -221,14 +224,21 @@ impl<'a> Object<'a> { } let header = data.read::<::CompressionHeader>().ok()?; - if header.ch_type(self.endian) != ELFCOMPRESS_ZLIB { - // Zlib compression is the only known type. - return None; + match header.ch_type(self.endian) { + ELFCOMPRESS_ZLIB => { + let size = usize::try_from(header.ch_size(self.endian)).ok()?; + let buf = stash.allocate(size); + decompress_zlib(data.0, buf)?; + return Some(buf); + } + ELFCOMPRESS_ZSTD => { + let size = usize::try_from(header.ch_size(self.endian)).ok()?; + let buf = stash.allocate(size); + decompress_zstd(data.0, buf)?; + return Some(buf); + } + _ => return None, // Unknown compression type. } - let size = usize::try_from(header.ch_size(self.endian)).ok()?; - let buf = stash.allocate(size); - decompress_zlib(data.0, buf)?; - return Some(buf); } // Check for the nonstandard GNU compression format, i.e., as generated @@ -347,6 +357,13 @@ fn decompress_zlib(input: &[u8], output: &mut [u8]) -> Option<()> { } } +fn decompress_zstd(input: &[u8], output: &mut [u8]) -> Option<()> { + use ruzstd::io::Read; + + let mut decoder = ruzstd::StreamingDecoder::new(input).ok()?; + decoder.read_exact(output).ok() +} + const DEBUG_PATH: &[u8] = b"/usr/lib/debug"; fn debug_path_exists() -> bool { From b0ae948a3352fc62a87e8d24b665cad1fb7be7bb Mon Sep 17 00:00:00 2001 From: Kyle Huey Date: Fri, 13 Sep 2024 09:04:08 -0700 Subject: [PATCH 2/5] Work around ruzstd only decoding a single frame of the zstd data. lld chunks the data into 1MB frames for parallel compression so almost everything will have multiple frames. --- src/symbolize/gimli/elf.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/symbolize/gimli/elf.rs b/src/symbolize/gimli/elf.rs index f920a6fe..bf088f8d 100644 --- a/src/symbolize/gimli/elf.rs +++ b/src/symbolize/gimli/elf.rs @@ -357,11 +357,26 @@ fn decompress_zlib(input: &[u8], output: &mut [u8]) -> Option<()> { } } -fn decompress_zstd(input: &[u8], output: &mut [u8]) -> Option<()> { +fn decompress_zstd(mut input: &[u8], mut output: &mut [u8]) -> Option<()> { use ruzstd::io::Read; - let mut decoder = ruzstd::StreamingDecoder::new(input).ok()?; - decoder.read_exact(output).ok() + while !input.is_empty() { + let mut decoder = ruzstd::StreamingDecoder::new(&mut input).ok()?; + loop { + let bytes_written = decoder.read(output).ok()?; + if bytes_written == 0 { + break; + } + output = &mut output[bytes_written..]; + } + } + + if !output.is_empty() { + // Lengths didn't match, something is wrong. + return None; + } + + Some(()) } const DEBUG_PATH: &[u8] = b"/usr/lib/debug"; From 8d94e0d8f0b4abf6d40890eeefb590f55ab7d93d Mon Sep 17 00:00:00 2001 From: Kyle Huey Date: Fri, 13 Sep 2024 09:12:24 -0700 Subject: [PATCH 3/5] Run zstd tests on Linux nightlies but not on other platforms. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b3201166..b5dfef87 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,7 +93,8 @@ jobs: env: RUSTFLAGS: "-C link-arg=-Wl,--compress-debug-sections=zlib" - run: cargo test - if: contains(matrix.os, 'ubuntu-24.04') || contains(matrix.rust, 'nightly') + if: contains(matrix.os, 'ubuntu-24.04') || + (contains(matrix.os, 'ubuntu') && contains(matrix.rust, 'nightly')) env: RUSTFLAGS: "-C link-arg=-Wl,--compress-debug-sections=zstd" From e5473c3981852e751c5e123052221d710ef61714 Mon Sep 17 00:00:00 2001 From: Kyle Huey Date: Fri, 13 Sep 2024 09:33:01 -0700 Subject: [PATCH 4/5] MSRV bump for next_multiple_of in ruzstd. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b5dfef87..0ece8890 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -265,7 +265,7 @@ jobs: with: submodules: true - name: Install Rust - run: rustup update 1.65.0 --no-self-update && rustup default 1.65.0 + run: rustup update 1.73.0 --no-self-update && rustup default 1.73.0 - run: cargo build miri: From fbfaf5aeec279cf2c567bfebb4ae64bc1140a32e Mon Sep 17 00:00:00 2001 From: Kyle Huey Date: Fri, 13 Sep 2024 10:28:06 -0700 Subject: [PATCH 5/5] Handle skip frame errors. --- src/symbolize/gimli/elf.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/symbolize/gimli/elf.rs b/src/symbolize/gimli/elf.rs index bf088f8d..b73f6aac 100644 --- a/src/symbolize/gimli/elf.rs +++ b/src/symbolize/gimli/elf.rs @@ -358,10 +358,22 @@ fn decompress_zlib(input: &[u8], output: &mut [u8]) -> Option<()> { } fn decompress_zstd(mut input: &[u8], mut output: &mut [u8]) -> Option<()> { + use ruzstd::frame::ReadFrameHeaderError; + use ruzstd::frame_decoder::FrameDecoderError; use ruzstd::io::Read; while !input.is_empty() { - let mut decoder = ruzstd::StreamingDecoder::new(&mut input).ok()?; + let mut decoder = match ruzstd::StreamingDecoder::new(&mut input) { + Ok(decoder) => decoder, + Err(FrameDecoderError::ReadFrameHeaderError(ReadFrameHeaderError::SkipFrame { + length, + .. + })) => { + input = &input.get(length as usize..)?; + continue; + } + Err(_) => return None, + }; loop { let bytes_written = decoder.read(output).ok()?; if bytes_written == 0 {