Skip to content

arm64: enable kmod ldscript that puts ELF header inside PT_LOAD[0]#2228

Open
b1nc0d3x wants to merge 1 commit into
freebsd:mainfrom
b1nc0d3x:submit/preload-ldscript-arm64
Open

arm64: enable kmod ldscript that puts ELF header inside PT_LOAD[0]#2228
b1nc0d3x wants to merge 1 commit into
freebsd:mainfrom
b1nc0d3x:submit/preload-ldscript-arm64

Conversation

@b1nc0d3x
Copy link
Copy Markdown

Summary

Loadable kernel modules on FreeBSD/arm64 currently have no per-arch kmod
ldscript, so ld picks the default layout. Combined with
CONSTANT(COMMONPAGESIZE)=0x10000, that default pushes the first
PT_LOAD to file offset 0x10000, leaving the Elf64_Ehdr at file
offset 0 outside every loadable segment. __elfN(loadimage)() in
stand/common/load_elf.c only copies PT_LOAD content to memory, so
when the kernel later runs link_elf_link_preload() and dereferences
ef->address as Elf_Ehdr, it reads .text bytes instead of the
header. The bogus e_phoff walks off the end of memory and faults:

panic: vm_fault failed: ... preload_protect+0x54
x8  = ef->address           (module base)
x9  = 0xf9443a1190000070   (.text bytes read as e_phoff)
x21 = ef->address + x9      (unmappable, triggers far)

Reproduces with any preloaded arm64 module whose default lld layout
puts PT_LOAD[0] above the header — which is effectively every aarch64
.ko built without an explicit override. The user-visible workaround
has been to load modules at runtime via /etc/rc.conf kld_list= rather
than at boot via /boot/loader.conf, which adds ~1 s of console-blank
time and loses the per-arch loader cache.

Fix

Add a kmod ldscript for arm64 that declares PHDRS with FILEHDR PHDRS attributes on the first PT_LOAD, so the ELF header and
program headers become part of PT_LOAD[0]'s file content and reach
memory at the module's load address. Section coalescing mirrors
sys/conf/ldscript.kmod.amd64.

sys/conf/kmod.mk already does:

.if exists(${SYSDIR}/conf/ldscript.kmod.${MACHINE})
LDSCRIPT_FLAGS?= -T ${SYSDIR}/conf/ldscript.kmod.${MACHINE}

so just dropping the file in enables it for every in-base arm64 module
build with no Makefile churn.

amd64 is unaffected. i386 has its own ldscript.kmod.i386 and is
unaffected.

Test plan

Validated on aarch64 QEMU virt (cortex-a72), stock
FreeBSD-15.0-RELEASE-p4 kernel + loader, out-of-tree virtio_drm.ko:

  • Pre-patch baseline: .ko built with default lld layout. readelf -l
    shows PT_LOAD[0] at Offset 0x10000, header at 0 unmapped.
    /boot/loader.conf preload → panics at preload_protect+0x54 with
    the register dump above on every boot. Just reproduced today on
    the same VM.
  • Post-patch: same source rebuilt with this ldscript active.
    readelf -l now shows PT_LOAD[0] at Offset 0 VirtAddr 0 FileSiz 0x7000, covering header + phdrs + .plt + .text. Same
    preload path completes cleanly, module reaches kldstat at the
    same load address that previously faulted, no functional
    regression.

Suggested reviewers

Per recent committers to neighbouring files:

Notes

Supersedes PR #2224, which proposed a metadata-based fix in the loader +
kern_linker for the same panic. Re-testing today showed the ldscript
approach is sufficient on its own, contrary to what was claimed in that
PR's body. #2224 is being closed in favour of this smaller, more
surgical fix.

Copy link
Copy Markdown
Member

@kostikbel kostikbel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The working ldscript is what I would expect.

We have some special sections like .data.read_frequently, .data.read_mostly, and so on. They should be handled in the script, and not mixed into the .data section by incident. Please take the arm64 kernel ldscript as the starting point, and copy/adjust it for modules.

@kostikbel
Copy link
Copy Markdown
Member

@zxombie probably wants to see this PR

@b1nc0d3x
Copy link
Copy Markdown
Author

@kostikbel thanks — that's a fair call. Will rebase the ldscript on sys/conf/ldscript.arm64 (the kernel script) and keep the same coalescing for .data.read_frequently, .data.read_mostly, .data.exclusive_cache_line, .note.gnu.property, and the linker-set / sysctl-set bounds that the kernel script handles explicitly, stripped of the kernel-only directives (KERNEL_PHYSADDR, locore ordering, etc.). Will push an updated commit shortly.

Also pulling in @zxombie as you suggested — thanks for the routing.

Loadable kernel modules on FreeBSD/arm64 have no per-arch kmod ldscript,
so ld picks the default layout.  Together with
CONSTANT(COMMONPAGESIZE)=0x10000 that default pushes the first PT_LOAD
to file offset 0x10000, leaving the Elf64_Ehdr at file offset 0 outside
every loadable segment.  __elfN(loadimage)() in stand/common/load_elf.c
only copies PT_LOAD content to memory, so when the kernel later runs
link_elf_link_preload() and dereferences ef->address as Elf_Ehdr it
reads .text bytes instead of the header.  The resulting bogus e_phoff
walks off the end of memory and faults in preload_protect():

    panic: vm_fault failed: ... preload_protect+0x54
    x8 = ef->address (module base)
    x9 = 0xf9443a1190000070   (.text bytes read as e_phoff)
    x21 = ef->address + x9    (unmappable, triggers far)

Reproduces with any preloaded arm64 module whose default lld layout
puts PT_LOAD[0] above the header — i.e., effectively every aarch64
.ko built without an explicit override.  Workaround until now has been
to load modules at runtime via /etc/rc.conf kld_list= rather than at
boot via /boot/loader.conf, which adds ~1 s of console-blank time and
loses the per-arch loader cache.

Add a kmod ldscript for arm64 that declares PHDRS with FILEHDR + PHDRS
attributes on the first PT_LOAD, so the ELF header and program headers
are part of PT_LOAD[0]'s file content and reach memory at the module's
load address.  Section coalescing matches sys/conf/ldscript.kmod.amd64.
kmod.mk already does .if exists(${SYSDIR}/conf/ldscript.kmod.${MACHINE})
so just dropping the file in enables it for every in-base arm64 module
build.

Tested on aarch64 QEMU virt:
  - Pre-patch (default lld layout): preload via /boot/loader.conf of an
    out-of-tree virtio_drm.ko panics at preload_protect+0x54 every
    boot.  PT_LOAD[0] starts at file_offset 0x10000, header at offset
    0 unmapped.
  - Post-patch (this ldscript): same module rebuilt, PT_LOAD[0] now
    at file_offset 0 vaddr 0 covering header+phdrs+.plt+.text (verified
    with readelf -l).  Preload completes cleanly, module reaches
    kldstat at the same load address that used to fault, and the rest
    of bring-up proceeds normally.

amd64 is unaffected.  i386 already has its own ldscript.kmod.i386.

Signed-off-by: Kyle Crenshaw <B1nc0d3x@gmail.com>
@b1nc0d3x b1nc0d3x force-pushed the submit/preload-ldscript-arm64 branch from 5154422 to a554bc7 Compare May 23, 2026 16:59
@b1nc0d3x
Copy link
Copy Markdown
Author

Updated per @kostikbel's feedback (force-push, since no inline review comments exist yet — top-level review thread is preserved).

Rebased on sys/conf/ldscript.arm64:

  • Section coalescing now mirrors the kernel script (.plt / .text / .fini / .init, .rodata / .rodata1 / .note.gnu.build-id, the full .rel.* / .rela.* family, .init_array / .fini_array / .ctors / .dtors, .got / .dynamic, .sdata / .sbss / .bss).
  • Special data sections handled separately as in the kernel script: .data.read_frequently (SORT_BY_ALIGNMENT, 128-byte aligned), .data.read_mostly, .data.exclusive_cache_line (128-byte aligned). Each in its own output section rather than being merged into .data.
  • Stripped kernel-only directives that have no meaning for kmods: text_start, _etext / _end / etext PROVIDEs, .vmm_vectors, .init_pagetable, the locore page-table alignment, and the INCLUDE debuginfo.ldscript.
  • Added linker-set bounds (__start_set_sysctl_set / __stop_set_sysctl_set, same for set_modmetadata_set and set_sysinit_set) — kmods need these to register module metadata, sysctls, and sysinit records; the kernel script gets them implicitly from the kernel link but kmod-format links don't always emit them.

Re-tested on aarch64 QEMU virt + stock pre-patch FreeBSD-15.0 kernel + loader:

  • virtio_drm.ko rebuilt with the new ldscript, readelf -l shows PT_LOAD[0] @ offset=0 vaddr=0 FileSiz=0x7000 covering header + phdrs + .plt + .text.
  • Preload via /boot/loader.conf completes cleanly, kldstat shows module at the same 0xffff000001462000 that the previous (no-ldscript) build faulted at.
  • Linker sets visible in segment mapping: set_sysctl_set, set_modmetadata_set, set_sysinit_set are bounded in the RW segment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants