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
19 changes: 19 additions & 0 deletions issues/build/libc-dl-iterate-phdr-allocates-on-every-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
status: open
kind: defect
opened: 2026-09-29
---

# libc's `dl_iterate_phdr` allocates on every walk

`userland/libc/src/link.rs` sizes and reads a whole `SYS_QUERY_MODULES`
answer into a heap `Vec`, and builds each module's NUL-terminated name in a
second one: every walk allocates twice and makes at least two syscalls.
libunwind calls `dl_iterate_phdr` to find a frame's unwind tables, so a C++
`throw std::bad_alloc` with the heap exhausted dies in libc's allocator
instead of reaching its handler. The answer's size grows with every library
and path, so no fixed buffer holds it: an allocation-free walk needs a query whose
answer a bounded buffer holds, which is an ABI change.

**Exit**: `dl_iterate_phdr` makes no heap allocation, and a guest C case walks
every module with `malloc` exhausted and visits each.
20 changes: 20 additions & 0 deletions issues/build/libc-dlsym-rtld-default-panics-as-unimplemented.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
status: open
kind: defect
opened: 2026-09-29
---

# libc's `dlsym(RTLD_DEFAULT, …)` panics as unimplemented

`dlfcn.h` defines `RTLD_DEFAULT` as the null handle, and libc's `dlsym`
(`userland/libc/src/misc.rs`) panics on it: a handle is a module index plus
one, and `SYS_DLSYM` answers from one loaded library by index. Nothing searches
the global scope — the executable's exports, then every library in load order —
which is what glibc's `dlsym(RTLD_DEFAULT, …)` answers from. A C program that
probes the process for an optional symbol dies. The kernel-side sibling is
`issues/kernel/a-dlopened-library-never-binds-to-the-executables-exports.md`:
both want the executable inside the global scope.

**Exit**: `dlsym(RTLD_DEFAULT, name)` answers the first definition of `name` in
the executable and then every loaded library in load order, and NULL when there
is none, and a guest C case asserts both.
19 changes: 0 additions & 19 deletions issues/build/libc-has-no-dl-iterate-phdr.md

This file was deleted.

3 changes: 1 addition & 2 deletions issues/build/toyos-builds-itself.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ and the network stack under it (`issues/hardware/the-lan-is-not-yet-production-g
`issues/design-debt/the-internet-clients-work-unchanged.md`), room for about a
gigabyte of toolchain, and threads and `mmap` mature enough for LLVM
(`issues/kernel/std-and-libc-drop-the-answer-thread-join-gives.md`). M3 needs
locale support or libc++'s no-localization build, and
`dl_iterate_phdr` in libc (`issues/build/libc-has-no-dl-iterate-phdr.md`). M4 needs git in the guest, storage durable and fast
locale support or libc++'s no-localization build. M4 needs git in the guest, storage durable and fast
enough for an LLVM build tree
(`issues/filesystem/storage-is-layers-and-a-role-is-a-filesystem.md`), and
memory beyond what 2 MiB process pages allow
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
status: open
kind: defect
opened: 2026-09-29
---

# std reads a `SYS_QUERY_MODULES` byte count as a module count

Two readers in the std fork take `query_modules`'s `Ok(n)` as a record count
and read `size_of::<ModuleInfo>()`-byte records from offset 0 until one would
pass the end of their 4096-byte buffer:

- the unwinder, `rust/library/std/src/sys/pal/toyos/mod.rs`,
`eh_frame::load_modules`, reads each as `base`/`text_end`/`eh_frame_hdr`;
- the backtrace crate, `rust/library/backtrace/src/symbolize/gimli/libs_toyos.rs`,
`native_libraries`, makes a `Library` of each whose one segment is
`text_end - base` bytes long, a subtraction that can underflow on them.

`n` counts **bytes** (`toyos_abi::syscall::query_modules`'s doc), so every
"record" after the real ones is the packed path strings and then the zeroed
tail of the buffer. Both also take `n > buf.len()`, which means nothing was
written, as an answer and read the zeroes: the call answers a short buffer
with `Ok`, so neither ever reaches the `Err` arm that grows its buffer.

No wrong frame has been seen: a path's ASCII read as a `u64` lies far above the
user half, so no user PC falls in the range it makes, and a zeroed record's
range is empty. That is luck in the bytes, not a check.

**Exit**: `load_modules` and `native_libraries` each decode the answer with
`toyos_abi::syscall::modules` over the `n` bytes the call reported, and grow
the buffer to `n` when `n` exceeds it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
status: open
kind: defect
opened: 2026-09-29
---

# A dlopen'ed library never binds to the executable's exports

`resolve_dlopen_relocs` (`kernel/src/elf/reloc.rs`) resolves a `dlopen`ed
module's `GLOB_DAT`/`JUMP_SLOT` slots against the other loaded libraries only.
Its startup sibling, `resolve_lib_bind_relocs`, tries the executable's
dynamic exports first (`kernel/src/loader/mod.rs` builds `exe_sym_map` for
it). glibc's `dlopen` searches the global scope, which starts with the
executable, so on ToyOS a plugin that calls back into a symbol its host
exports is left with an unresolved slot and faults when it uses it.

**Exit**: `resolve_dlopen_relocs` binds a slot against the executable's dynamic
exports before the other loaded libraries, as `resolve_lib_bind_relocs` does,
and a guest case `dlopen`s a library that calls a function its executable
exports, and the call lands.
5 changes: 4 additions & 1 deletion kernel/src/elf/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::vfs::BackingId;
use crate::UserAddr;
use toyos_elf::dynamic::InitArray;
use toyos_elf::rela::Rules;
use toyos_elf::{ImageRange, Op, RelaCounts, RelocKind, SymIndex, TlsRef};
use toyos_elf::{ImageRange, Op, ProgramHeaderTable, RelaCounts, RelocKind, SymIndex, TlsRef};

/// A module's non-`RELATIVE` relocations, parsed and extracted once at cache
/// time, each as `(r_offset, what it names)`.
Expand Down Expand Up @@ -82,6 +82,7 @@ struct Snapshot {
gnu_hash: Option<KernelSlice>,
rules: Rules,
eh_frame_hdr: Option<ImageRange>,
phdrs: ProgramHeaderTable,
init_array: Option<InitArray>,
span: u64,
rw_lo: u64,
Expand All @@ -100,6 +101,7 @@ impl Snapshot {
gnu_hash: lib.gnu_hash,
rules: lib.rules,
eh_frame_hdr: lib.eh_frame_hdr,
phdrs: lib.phdrs,
init_array: lib.init_array,
span: lib.span,
rw_lo: lib.rw_lo,
Expand Down Expand Up @@ -127,6 +129,7 @@ impl Snapshot {
cached_relocs,
rules: self.rules,
eh_frame_hdr: self.eh_frame_hdr,
phdrs: self.phdrs,
init_array: self.init_array,
span: self.span,
rw_lo: self.rw_lo,
Expand Down
10 changes: 9 additions & 1 deletion kernel/src/elf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ use crate::UserAddr;
use toyos_elf::dynamic::{Dynamic, InitArray};
use toyos_elf::section::{SectionTable, SHT_DYNSYM};
use toyos_elf::sym::{Sym, SymTab};
use toyos_elf::{rela, Extent, GnuHash, ImageRange, Layout, Rela, RelaTable, Reloc, RelocError, SymIndex, TlsSegment};
use toyos_elf::{
rela, Extent, GnuHash, ImageRange, Layout, ProgramHeaderTable, Rela, RelaTable, Reloc, RelocError, SymIndex,
TlsSegment,
};

/// `toyos_elf::MAX_TLS_ALIGN` must equal the kernel's largest page.
const _: () = assert!(toyos_elf::MAX_TLS_ALIGN == PAGE_2M);
Expand Down Expand Up @@ -101,6 +104,8 @@ pub struct LoadedLib {
rules: rela::Rules,
/// `PT_GNU_EH_FRAME`, for DWARF unwinding.
pub eh_frame_hdr: Option<ImageRange>,
/// The program header table, which `load_shared_lib` refuses a module without.
pub phdrs: ProgramHeaderTable,
/// `DT_INIT_ARRAY`.
pub init_array: Option<InitArray>,
/// Bytes between the image's lowest and highest virtual address.
Expand Down Expand Up @@ -355,6 +360,8 @@ pub fn load_shared_lib(
if extent.min() != 0 {
return Err("ELF: a shared object must begin at vaddr 0");
}
// `SYS_QUERY_MODULES` answers with the mapped table.
let phdrs = layout.program_headers().ok_or("ELF: no PT_LOAD maps the program header table")?;
// No writable segment yields an empty window; no relocation can target it.
let (rw_lo, rw_hi) = layout.writable_window().unwrap_or((layout.span(), layout.span()));

Expand Down Expand Up @@ -513,6 +520,7 @@ pub fn load_shared_lib(
cached_relocs: None,
rules,
eh_frame_hdr: layout.eh_frame_hdr(),
phdrs,
init_array,
span: layout.span(),
rw_lo,
Expand Down
7 changes: 7 additions & 0 deletions kernel/src/loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,12 @@ pub fn spawn(
// Where the image's first byte lands: every `ImageOffset` the file's
// numbers were parsed into is added to it, and its span fits above it.
let image_start = UserAddr::new(USER_VM_BASE);
// `SYS_QUERY_MODULES` answers with the mapped table, so an image without
// one has no true answer to give.
let Some(phdrs) = layout.program_headers() else {
log!("spawn: {}: no PT_LOAD maps the program header table", path);
return Err(SyscallError::InvalidArgument.into());
};

let exe = read_exe_tables(backing.as_ref(), &layout, path)?;
let t1 = crate::clock::nanos_since_boot();
Expand Down Expand Up @@ -597,6 +603,7 @@ pub fn spawn(
.eh_frame_hdr()
.map_or((0, 0), |r| ((image_start + r.start().get()).raw(), r.len())),
exe_vaddr_max: image_end,
exe_phdrs: ((image_start + phdrs.image().start().get()).raw(), phdrs.count()),
lib_paths,
},
mmap_regions: Vec::new(),
Expand Down
3 changes: 3 additions & 0 deletions kernel/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,8 @@ pub struct ElfInfo {
pub exe_eh_frame_hdr: (u64, u64),
/// One past the executable's last byte.
pub exe_vaddr_max: u64,
/// The executable's program header table as (address, count), `(0, 0)` with no executable.
pub exe_phdrs: (u64, u16),
/// Paths of dlopen'd libraries (parallel to loaded_libs).
pub lib_paths: Vec<String>,
}
Expand All @@ -510,6 +512,7 @@ impl ElfInfo {
elf_base: UserAddr::new(0),
exe_eh_frame_hdr: (0, 0),
exe_vaddr_max: 0,
exe_phdrs: (0, 0),
lib_paths: Vec::new(),
}
}
Expand Down
5 changes: 5 additions & 0 deletions kernel/src/syscall/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,11 +484,14 @@ pub(super) fn sys_query_modules(out: &mut UserBytesMut) -> u64 {
let mut path_offset = (module_count * info_size) as u32;

let (eh_addr, eh_size) = data.elf.exe_eh_frame_hdr;
let (phdr, phnum) = data.elf.exe_phdrs;
let exe_info = ModuleInfo {
base: data.elf.elf_base.raw(),
text_end: data.elf.exe_vaddr_max,
eh_frame_hdr: eh_addr,
eh_frame_hdr_size: eh_size,
phdr,
phnum: phnum.into(),
path_offset,
path_len: exe_path_bytes.len() as u32,
};
Expand All @@ -507,6 +510,8 @@ pub(super) fn sys_query_modules(out: &mut UserBytesMut) -> u64 {
text_end: lib.user_end(),
eh_frame_hdr: lib.eh_frame_hdr.map_or(0, |r| (lib.user_base + r.start().get()).raw()),
eh_frame_hdr_size: lib.eh_frame_hdr.map_or(0, |r| r.len()),
phdr: (lib.user_base + lib.phdrs.image().start().get()).raw(),
phnum: lib.phdrs.count().into(),
path_offset,
path_len: lib_path_bytes.len() as u32,
};
Expand Down
8 changes: 8 additions & 0 deletions tests/common/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ pub fn compile_c(name: &str) -> Vec<PathBuf> {
.collect()
}

/// Link flags a case needs beyond the corpus's, by case name.
///
/// An image whose lowest address is 0 hides a table reported from the load bias
/// instead of the image's first byte; the case prints its lowest `PT_LOAD`, so
/// an entry that stops reaching its case reds it.
const LINK_FLAGS: &[(&str, &[&str])] = &[("205_dl_iterate_phdr_image_base", &["-Wl,--image-base=0x200000"])];

/// Link `objects` into a ToyOS executable through the clang driver — which
/// names `ld.lld` and the sysroot's `libtoyos_c.a`, and makes a PIE — and
/// return its bytes.
Expand All @@ -97,6 +104,7 @@ pub fn link_toyos(objects: &[PathBuf], name: &str) -> Vec<u8> {
let output = Command::new(&c.clang)
.args(c.args())
.args(objects)
.args(LINK_FLAGS.iter().filter(|(case, _)| *case == name).flat_map(|(_, flags)| flags.iter()))
.arg("-o")
.arg(&out)
.output()
Expand Down
5 changes: 2 additions & 3 deletions tests/testcases/LICENSE
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
Licences and provenance of the C test corpus
============================================

`tinycc/` is what the toolchain's clang and ToyOS's C library are tested
against, compiled on the host and run as ToyOS processes. Most of it is not
Expand All @@ -22,9 +21,9 @@ against tinycc upstream at 64552b3faa39ee7948a9ea21bfcc11045b90c70d
(repo.or.cz/tinycc.git, 2026-08-05), allowing for the `-` → `_` renames this
project made to some filenames.

tinycc/ 318 files: 239 byte-identical to tinycc's `tests/tests2`,
tinycc/ 322 files: 239 byte-identical to tinycc's `tests/tests2`,
17 tinycc files this project modified,
62 not upstream at all — 61 cases written here, plus
66 not upstream at all — 65 cases written here, plus
`fred.txt`, which is output `40_stdio.c` writes when it
runs and which was committed by accident.

Expand Down
Loading
Loading