From b7b2c2efab55cb2f05206a6465ceef4418ed84e6 Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Tue, 14 Apr 2026 13:53:25 -0700 Subject: [PATCH] fix: zero-fill NOBITS sections in ELF loader Some linkers emit PT_LOAD segments where filesz == memsz but contain .bss sections whose VMA range overlaps with file bytes from unrelated sections. The loader copies the full segment verbatim, leaving .bss with stale data instead of zeros. Collect NOBITS section ranges (excluding .tbss) during ELF parsing and zero-fill them after loading PT_LOAD segments. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Co-authored-by: danbugs --- src/hyperlight_host/src/mem/elf.rs | 37 ++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/hyperlight_host/src/mem/elf.rs b/src/hyperlight_host/src/mem/elf.rs index 16e506eac..7d3d0fb96 100644 --- a/src/hyperlight_host/src/mem/elf.rs +++ b/src/hyperlight_host/src/mem/elf.rs @@ -21,10 +21,7 @@ use std::sync::Arc; use goblin::elf::reloc::{R_AARCH64_NONE, R_AARCH64_RELATIVE}; #[cfg(target_arch = "x86_64")] use goblin::elf::reloc::{R_X86_64_NONE, R_X86_64_RELATIVE}; -use goblin::elf::{Elf, ProgramHeaders, Reloc}; -#[cfg(feature = "nanvix-unstable")] -use goblin::elf32::program_header::PT_LOAD; -#[cfg(not(feature = "nanvix-unstable"))] +use goblin::elf::{Elf, ProgramHeaders, Reloc, section_header}; use goblin::elf64::program_header::PT_LOAD; use super::exe::LoadInfo; @@ -45,6 +42,8 @@ pub(crate) struct ElfInfo { shdrs: Vec, entry: u64, relocs: Vec, + /// (addr, size) of NOBITS sections that need zero-filling (excludes .tbss). + nobits_ranges: Vec<(u64, u64)>, /// The hyperlight version string embedded by `hyperlight-guest-bin`, if /// present. Used to detect version/ABI mismatches between guest and host. guest_bin_version: Option, @@ -128,6 +127,20 @@ impl ElfInfo { // hyperlight-guest-bin. let guest_bin_version = Self::read_version_note(&elf, bytes); + // Collect NOBITS sections (e.g. .bss) that need zero-filling. + // Skip .tbss (SHF_TLS) since thread-local BSS is allocated per-thread. + let nobits_ranges: Vec<(u64, u64)> = { + elf.section_headers + .iter() + .filter(|sh| { + sh.sh_type == section_header::SHT_NOBITS + && sh.sh_size > 0 + && (sh.sh_flags & u64::from(section_header::SHF_TLS)) == 0 + }) + .map(|sh| (sh.sh_addr, sh.sh_size)) + .collect() + }; + Ok(ElfInfo { payload: bytes.to_vec(), phdrs: elf.program_headers, @@ -146,6 +159,7 @@ impl ElfInfo { .collect(), entry: elf.entry, relocs, + nobits_ranges, guest_bin_version, }) } @@ -206,6 +220,21 @@ impl ElfInfo { .copy_from_slice(&self.payload[payload_offset..payload_offset + payload_len]); target[start_va + payload_len..start_va + phdr.p_memsz as usize].fill(0); } + // Zero-fill NOBITS sections (e.g. .bss) that were not already + // covered by the filesz < memsz zeroing above. + for &(addr, size) in &self.nobits_ranges { + let sh_start = (addr - base_va) as usize; + let sh_end = sh_start + size as usize; + if sh_end <= target.len() { + target[sh_start..sh_end].fill(0); + } else { + tracing::warn!( + "NOBITS section at VA {:#x} (size {:#x}) extends past loaded image, skipping zero-fill", + addr, + size + ); + } + } let get_addend = |name, r: &Reloc| { r.r_addend .ok_or_else(|| new_error!("{} missing addend", name))