Segment Loading and W^X
July 16, 2026 ยท View on GitHub
Each PT_LOAD segment becomes a run of mapped pages in the target address space. This is where
the loader enforces the invariant that no page is both writable and executable, derives page
permissions from the segment flags, and zero-fills backing memory so a segment never exposes
stale bytes or uninitialized .bss. This page documents load_segment. The code is under
src/elf/loader/core/load_segment/.
The W^X gate
Before a segment is planned, input_fields (src/elf/loader/core/load_segment/validate.rs:20)
validates it, and the load-bearing check is the write-xor-execute gate:
input_fields(elf_len, header):
if p_filesz > p_memsz: SegmentDataOutOfBounds
if header.is_writable() and header.is_executable(): WXViolation
if p_align > 1 and not power_of_two(p_align): AlignmentError
if p_align > 1 and p_vaddr % p_align != p_offset % p_align: AlignmentError
if file_offset + file_size > elf_len: SegmentDataOutOfBounds
A segment carrying both the writable and executable flags is rejected outright with
WXViolation. There is no code path that maps a W|X segment, so the classic attack surface of
a writable-executable region does not exist for a loaded capsule. The other checks reject a
segment whose file size exceeds its memory size, whose alignment is not a power of two, whose
virtual and file offsets are not congruent modulo the alignment (the ELF alignment invariant),
or whose file contents run past the end of the image.
Permission derivation
For a segment that passes the gate, its page permissions are derived directly from its flags
(load_segment/pte_flags.rs:20):
pte_perms_from_phdr(ph):
perms = READ | USER // always readable, always user
if ph.is_writable(): perms |= WRITE
if ph.is_executable(): perms |= EXECUTE
perms
The base is READ | USER: a loaded capsule segment is always user-accessible and readable. Write
is added only for a writable segment and execute only for an executable one, so a read-only data
segment is mapped non-writable and non-executable, a text segment is read-execute, and a data
segment is read-write with no execute. Because the W^X gate already ran, WRITE and EXECUTE
are never both present. These permissions feed straight into the paging manager,
which is the layer that actually enforces them in the page tables.
Populating pages
load_segment (load_segment/run.rs:28) builds a page plan, derives the permissions once, and
populates each page (load_segment/populate_page.rs):
populate_page(asid, page_va, perms, dst_off, src):
frame = allocate_frame() else MemoryAllocationFailed
if map_page_in_asid(asid, page_va, frame, perms) fails:
free frame; MemoryAllocationFailed // no leak on the error path
dst = directmap(frame)
write_bytes(dst, 0, PAGE) // zero the whole page first
if src not empty: copy src into dst at dst_off (clamped to the page)
Each page is allocated, mapped into the target address space with the segment's permissions, and
then zeroed in full through the direct map before the file bytes are copied in. Zeroing the whole
page first is what gives .bss for free: a segment whose p_memsz exceeds its p_filesz has its
trailing pages (and the tail of its last file page) already zero, with no separate .bss
handling. If the mapping fails, the just-allocated frame is freed before returning, so an error
midway through a segment does not leak physical memory. The pages are written through the kernel
direct map rather than through the user mapping, so the loader never dereferences a user virtual
address it just created.
Security analysis
Segment loading is where untrusted file contents become executable memory, so its invariants are the ones that decide whether a capsule can turn a data buffer into code or read a previous tenant's bytes. Three properties hold.
W^X by construction, not by later check. input_fields
(src/elf/loader/core/load_segment/validate.rs:20) rejects a segment carrying both the writable and
executable flags with WXViolation before the segment is ever planned, and pte_perms_from_phdr
(load_segment/pte_flags.rs:20) derives permissions from the flags such that WRITE and EXECUTE are
never both set, since the gate already ran. There is no code path that maps a W|X segment, so the
classic writable-executable region does not exist for a loaded capsule. This is the same invariant the
paging manager enforces at the PTE level, checked here at load time so a
hostile segment header is refused rather than mapped and then caught.
File contents cannot escape the image, and memory cannot exceed the declared size. The gate refuses a
segment whose p_filesz exceeds its p_memsz (SegmentDataOutOfBounds), whose alignment is not a power
of two (AlignmentError), whose virtual and file offsets are not congruent modulo the alignment, or
whose file_offset + file_size runs past the end of the image, again with checked_add. So the slice
&elf_data[plan.file_offset..plan.file_end] that load_segment (load_segment/run.rs:28) copies from is
always inside the buffer, and a malicious header cannot point the copy source at bytes the image does not
contain.
Every page is zeroed before file bytes land, so .bss and page tails never leak. populate_page
(load_segment/populate_page.rs:28) allocates a frame, maps it into the target ASID with the segment's
permissions, then does write_bytes(dst, 0, PAGE) across the whole page through the direct map before
copying any file bytes, and it clamps the copy length to the space remaining in the page. A segment whose
p_memsz exceeds its p_filesz therefore gets zeroed .bss for free, with no separate handling, and the
tail of the last file page is zero rather than whatever the frame held before. If the mapping fails, the
just-allocated frame is freed before returning MemoryAllocationFailed, so an error midway through a
segment leaks no physical memory. The write goes through the kernel direct map, not the user mapping, so
the loader never dereferences a user virtual address it just created.
Debugging segment loading
The segment path raises the same ElfError variants (src/elf/errors/types/state.rs:17) the rest of the
loader uses, and on a spawn they are printed after the capsule's debug tag
(.../install/load_elf_into_pid.rs:27), so a segment failure names its own cause:
Segment requested both writable and executable permissions WXViolation a W|X program header
Segment data out of bounds SegmentDataOutOfBounds filesz > memsz, or file bytes past the image
Alignment requirements not met AlignmentError p_align not a power of two, or vaddr/offset not congruent
Memory allocation failed MemoryAllocationFailed no frame, or the per-page map into the ASID failed
The useful split is between a rejected segment and an exhausted one. WXViolation,
SegmentDataOutOfBounds, and AlignmentError all come out of input_fields before any frame is touched,
so they mean the program header itself is malformed or hostile and the load never started allocating.
MemoryAllocationFailed is different: it comes from populate_page after validation passed, so it means
the image was fine but either the frame allocator was empty or the map into the target ASID failed
partway through a segment. A capsule that loads on one boot and fails with MemoryAllocationFailed on
another is a memory-pressure signal, not a bad image, whereas a WXViolation is a property of the binary
that will fail every time until the binary is rebuilt without a writable-executable segment. On the
top-level spawn path all of these surface as reason=elf_load (.../from_vfs/load.rs:99); the ElfError
string that precedes that line is what tells the two apart.
Source map
src/elf/loader/core/load_segment/validate.rs the W^X gate and segment validation
src/elf/loader/core/load_segment/pte_flags.rs permission derivation from p_flags
src/elf/loader/core/load_segment/run.rs the per-page load loop
src/elf/loader/core/load_segment/populate_page.rs frame alloc, map, zero-fill, copy
src/elf/errors/types/state.rs the ElfError variants raised here
Every reference above is verified against those trees. The header checks that run before any segment is
loaded are on the validation page, the paging layer that enforces these permissions in
the page tables is the paging manager, and the load orchestration that
calls load_segment per program header is on the layout page.