summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:24:56 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:24:56 +0800
commit1767d390fbf3d0c65389de46caf0b6d43ef3b6ad (patch)
tree0f5e01cd7c7a05abb6b1c7472d7f00f49caf02cc
parent63dc025705f6533b91fb1a7d26e6ed864efd9900 (diff)
downloadlume-1767d390fbf3d0c65389de46caf0b6d43ef3b6ad.tar.gz
lume-1767d390fbf3d0c65389de46caf0b6d43ef3b6ad.tar.xz
feat: establish bootable new architecture checkpoint
Replace the compressed boot path with a relocatable higher-half RISC-V entry, expand memory and SMP support, and add the driver, TTY, VFS, exec, signal, syscall, initramfs, and self-test infrastructure needed for a bootable kernel checkpoint.
-rw-r--r--na/Makefile292
-rw-r--r--na/arch/riscv/boot/compressed.cc471
-rw-r--r--na/arch/riscv/boot/entry.S309
-rw-r--r--na/arch/riscv/include/arch/config.h5
-rw-r--r--na/arch/riscv/include/arch/cpu.h16
-rw-r--r--na/arch/riscv/include/arch/fpstate.h94
-rw-r--r--na/arch/riscv/include/arch/mmu.h15
-rw-r--r--na/arch/riscv/include/arch/pmap.h1
-rw-r--r--na/arch/riscv/include/arch/sbi.h80
-rw-r--r--na/arch/riscv/include/arch/tlb.h19
-rw-r--r--na/arch/riscv/include/arch/trap.h12
-rw-r--r--na/arch/riscv/include/arch/trapframe.h48
-rw-r--r--na/arch/riscv/include/asm/csr.h37
-rw-r--r--na/arch/riscv/include/asm/page.h62
-rw-r--r--na/arch/riscv/include/asm/pgtable.h55
-rw-r--r--na/arch/riscv/include/asm/sections.h50
-rw-r--r--na/arch/riscv/kernel/asm-offsets.c29
-rw-r--r--na/arch/riscv/kernel/head.S317
-rw-r--r--na/arch/riscv/kernel/lumeos.lds.S42
-rw-r--r--na/arch/riscv/kernel/signal.cc163
-rw-r--r--na/arch/riscv/mm/init.cc241
-rw-r--r--na/arch/riscv/mm/pmap.cc71
-rw-r--r--na/arch/riscv/mm/tlb_md.cc17
-rw-r--r--na/arch/riscv/module.mk8
-rw-r--r--na/arch/riscv/relocate.cc100
-rw-r--r--na/arch/riscv/syscall/syscall_md.cc14
-rw-r--r--na/arch/riscv/trap/fpu.S103
-rw-r--r--na/arch/riscv/trap/trap_md.cc38
-rw-r--r--na/arch/riscv/trap/trap_vector.S10
-rw-r--r--na/drivers/base/bus.cc32
-rw-r--r--na/drivers/base/core.cc156
-rw-r--r--na/drivers/base/driver.cc72
-rw-r--r--na/drivers/base/module.mk9
-rw-r--r--na/drivers/base/platform.cc231
-rw-r--r--na/drivers/console.cc119
-rw-r--r--na/drivers/driver.cc27
-rw-r--r--na/drivers/module.mk2
-rw-r--r--na/drivers/plic.cc28
-rw-r--r--na/drivers/tty/module.mk5
-rw-r--r--na/drivers/tty/n_tty.cc539
-rw-r--r--na/drivers/tty/tty_core.cc536
-rw-r--r--na/drivers/tty/tty_uart.cc251
-rw-r--r--na/drivers/uart.cc113
-rw-r--r--na/fs/chardev.cc43
-rw-r--r--na/fs/dcache.cc287
-rw-r--r--na/fs/devfs.cc137
-rw-r--r--na/fs/fd.cc216
-rw-r--r--na/fs/file_table.cc164
-rw-r--r--na/fs/fs_struct.cc90
-rw-r--r--na/fs/initramfs.cc285
-rw-r--r--na/fs/inode.cc166
-rw-r--r--na/fs/module.mk17
-rw-r--r--na/fs/namei.cc250
-rw-r--r--na/fs/pipe.cc278
-rw-r--r--na/fs/ramfs/file.cc216
-rw-r--r--na/fs/ramfs/inode.cc164
-rw-r--r--na/fs/ramfs/super.cc154
-rw-r--r--na/fs/super.cc95
-rw-r--r--na/fs/vfs_init.cc78
-rw-r--r--na/include/lume/args.h28
-rw-r--r--na/include/lume/atomic.h10
-rw-r--r--na/include/lume/boot_params.h85
-rw-r--r--na/include/lume/chardev.h45
-rw-r--r--na/include/lume/clone.h34
-rw-r--r--na/include/lume/compiler.h61
-rw-r--r--na/include/lume/compressed.h37
-rw-r--r--na/include/lume/config.h9
-rw-r--r--na/include/lume/console.h54
-rw-r--r--na/include/lume/container_of.h17
-rw-r--r--na/include/lume/dcache.h143
-rw-r--r--na/include/lume/decompress.h11
-rw-r--r--na/include/lume/device.h75
-rw-r--r--na/include/lume/driver.h35
-rw-r--r--na/include/lume/elf.h150
-rw-r--r--na/include/lume/errno.h319
-rw-r--r--na/include/lume/fdtable.h49
-rw-r--r--na/include/lume/file.h132
-rw-r--r--na/include/lume/fs.h310
-rw-r--r--na/include/lume/fs_struct.h39
-rw-r--r--na/include/lume/fs_types.h197
-rw-r--r--na/include/lume/init.h33
-rw-r--r--na/include/lume/kbuild.h40
-rw-r--r--na/include/lume/kernel_map.h55
-rw-r--r--na/include/lume/klog.h15
-rw-r--r--na/include/lume/kobject.h57
-rw-r--r--na/include/lume/kref.h46
-rw-r--r--na/include/lume/libfdt.h8
-rw-r--r--na/include/lume/libfdt_env.h78
-rw-r--r--na/include/lume/limits.h48
-rw-r--r--na/include/lume/list.h56
-rw-r--r--na/include/lume/lumeos.lds.h129
-rw-r--r--na/include/lume/mm_layout.h73
-rw-r--r--na/include/lume/mount.h52
-rw-r--r--na/include/lume/mutex.h63
-rw-r--r--na/include/lume/namei.h32
-rw-r--r--na/include/lume/page.h132
-rw-r--r--na/include/lume/page_cache.h82
-rw-r--r--na/include/lume/pgrp.h125
-rw-r--r--na/include/lume/pgtable_types.h69
-rw-r--r--na/include/lume/pipe.h52
-rw-r--r--na/include/lume/platform_device.h64
-rw-r--r--na/include/lume/pmm.h28
-rw-r--r--na/include/lume/poll.h68
-rw-r--r--na/include/lume/ptrace.h76
-rw-r--r--na/include/lume/rbtree.h11
-rw-r--r--na/include/lume/relocate.h53
-rw-r--r--na/include/lume/sched.h29
-rw-r--r--na/include/lume/setup.h42
-rw-r--r--na/include/lume/signal.h54
-rw-r--r--na/include/lume/signal_types.h263
-rw-r--r--na/include/lume/smp.h74
-rw-r--r--na/include/lume/stdarg.h11
-rw-r--r--na/include/lume/stringify.h14
-rw-r--r--na/include/lume/syscall_nr.h21
-rw-r--r--na/include/lume/task.h39
-rw-r--r--na/include/lume/timer.h21
-rw-r--r--na/include/lume/tlb.h48
-rw-r--r--na/include/lume/tty.h165
-rw-r--r--na/include/lume/tty_ioctl.h202
-rw-r--r--na/include/lume/types.h83
-rw-r--r--na/include/lume/uuid.h207
-rw-r--r--na/include/lume/vmm.h16
-rw-r--r--na/include/lume/wait.h19
-rw-r--r--na/include/lume/waitqueue.h45
-rw-r--r--na/kernel/boot_params.cc45
-rw-r--r--na/kernel/exec.cc659
-rw-r--r--na/kernel/main.cc395
-rw-r--r--na/kernel/module.mk11
-rw-r--r--na/kernel/pgrp.cc148
-rw-r--r--na/kernel/pid.cc6
-rw-r--r--na/kernel/poll.cc250
-rw-r--r--na/kernel/ptrace.cc92
-rw-r--r--na/kernel/sched_class_o1.cc7
-rw-r--r--na/kernel/sched_core.cc306
-rw-r--r--na/kernel/signal.cc442
-rw-r--r--na/kernel/smp.cc112
-rw-r--r--na/kernel/sync/mutex.cc156
-rw-r--r--na/kernel/sync/spinlock.cc11
-rw-r--r--na/kernel/sys_fs.cc679
-rw-r--r--na/kernel/sys_misc.cc360
-rw-r--r--na/kernel/sys_mm.cc8
-rw-r--r--na/kernel/sys_process.cc86
-rw-r--r--na/kernel/sys_signal.cc351
-rw-r--r--na/kernel/syscall.cc115
-rw-r--r--na/kernel/task.cc481
-rw-r--r--na/kernel/timer.cc76
-rw-r--r--na/kernel/trap.cc118
-rw-r--r--na/kernel/uaccess.cc22
-rw-r--r--na/kernel/waitqueue.cc87
-rw-r--r--na/lib/decompress_lz4.cc77
-rw-r--r--na/lib/fdt.c2
-rw-r--r--na/lib/fdt_addresses.c2
-rw-r--r--na/lib/fdt_empty_tree.c2
-rw-r--r--na/lib/fdt_ro.c2
-rw-r--r--na/lib/fdt_rw.c2
-rw-r--r--na/lib/fdt_strerror.c2
-rw-r--r--na/lib/fdt_sw.c2
-rw-r--r--na/lib/fdt_wip.c2
-rw-r--r--na/lib/klog.cc20
-rw-r--r--na/lib/kobject.cc82
-rw-r--r--na/lib/kprintf.cc41
-rw-r--r--na/lib/module.mk27
-rw-r--r--na/mm/memblock.cc70
-rw-r--r--na/mm/module.mk2
-rw-r--r--na/mm/page_cache.cc252
-rw-r--r--na/mm/pmm.cc180
-rw-r--r--na/mm/slab.cc8
-rw-r--r--na/mm/tlb.cc90
-rw-r--r--na/mm/vma.cc81
-rw-r--r--na/mm/vmm.cc65
-rw-r--r--na/scripts/Kbuild.include110
-rw-r--r--na/scripts/Kconfig.mk12
-rw-r--r--na/scripts/dtc/libfdt/fdt.c347
-rw-r--r--na/scripts/dtc/libfdt/fdt.h66
-rw-r--r--na/scripts/dtc/libfdt/fdt_addresses.c101
-rw-r--r--na/scripts/dtc/libfdt/fdt_empty_tree.c38
-rw-r--r--na/scripts/dtc/libfdt/fdt_overlay.c1101
-rw-r--r--na/scripts/dtc/libfdt/fdt_ro.c888
-rw-r--r--na/scripts/dtc/libfdt/fdt_rw.c512
-rw-r--r--na/scripts/dtc/libfdt/fdt_strerror.c60
-rw-r--r--na/scripts/dtc/libfdt/fdt_sw.c384
-rw-r--r--na/scripts/dtc/libfdt/fdt_wip.c94
-rw-r--r--na/scripts/dtc/libfdt/libfdt.h2540
-rw-r--r--na/scripts/dtc/libfdt/libfdt_env.h69
-rw-r--r--na/scripts/dtc/libfdt/libfdt_internal.h202
-rw-r--r--na/scripts/extract-asm-offsets.sed18
-rwxr-xr-xna/scripts/gen_initramfs.sh250
-rw-r--r--na/scripts/gen_reloc.py224
-rw-r--r--na/scripts/pack_image.py227
-rw-r--r--na/test/module.mk14
-rw-r--r--na/test/selftest.cc30
-rw-r--r--na/test/test_dcache.cc232
-rw-r--r--na/test/test_driver_model.cc209
-rw-r--r--na/test/test_exec.cc447
-rw-r--r--na/test/test_mutex.cc183
-rw-r--r--na/test/test_namei.cc173
-rw-r--r--na/test/test_page_cache.cc180
-rw-r--r--na/test/test_pid.cc106
-rw-r--r--na/test/test_pipe_devfs.cc420
-rw-r--r--na/test/test_pmm.cc6
-rw-r--r--na/test/test_reloc.cc46
-rw-r--r--na/test/test_signal.cc193
-rw-r--r--na/test/test_slab.cc4
-rw-r--r--na/test/test_smp.cc136
-rw-r--r--na/test/test_sys_fs_extra.cc238
-rw-r--r--na/test/test_sys_process.cc93
-rw-r--r--na/test/test_uaccess.cc18
-rw-r--r--na/test/test_vfs_io.cc345
-rw-r--r--na/test/test_vmm.cc15
-rw-r--r--na/test/test_waitqueue.cc2
-rw-r--r--na/usr/default_cpio_list24
-rw-r--r--na/usr/gen_init_cpio.c781
212 files changed, 26859 insertions, 2530 deletions
diff --git a/na/Makefile b/na/Makefile
index e950b35..88689f5 100644
--- a/na/Makefile
+++ b/na/Makefile
@@ -18,16 +18,37 @@
# ============================================================
BUILD := build
+# Make sure `make` with no arguments builds the kernel image, not the
+# first rule encountered during makefile parsing. scripts/Kconfig.mk
+# (included next) defines `defconfig:` at the top of its rule list,
+# which would otherwise become the default goal and silently overwrite
+# .config from arch/$(ARCH)/defconfig.
+.DEFAULT_GOAL := all
+
+# Load Kbuild helpers (Q, $(call cmd,...), quiet_cmd_* / cmd_* templates,
+# kbuild-objs translator). Must come before Kconfig.mk so subsequent
+# rules can use $(call cmd,...) immediately.
+include scripts/Kbuild.include
+
# Load Kconfig infrastructure (reads .config, sets KCPPFLAGS)
include scripts/Kconfig.mk
ARCH ?= $(CONFIG_ARCH)
-VMLINUX_PRE := $(BUILD)/kernel.pre.elf
-VMLINUX := $(BUILD)/kernel.elf
-VMLINUX_RAW := $(BUILD)/vmlume.raw.bin
-IMAGE := $(BUILD)/vmlume.bin
-RELOC_BIN := $(BUILD)/reloc.bin
-RELOC_OBJ := $(BUILD)/reloc.o
+
+# Two boot products — same pair as Linux RISC-V's arch/riscv/boot/Makefile
+# (vmlinux + Image), but named LumeOS-style:
+# VMLUME -> build/vmlume.elf : ELF with .symtab/PT_DYNAMIC/.rela.dyn
+# IMAGE -> build/vmlume.bin : raw binary (objcopy -O binary)
+VMLUME := $(BUILD)/vmlume.elf
+IMAGE := $(BUILD)/vmlume.bin
+
+# Initramfs paths
+INITRAMFS_DIR := $(BUILD)/initramfs
+INITRAMFS_CPIO := $(BUILD)/initramfs.cpio
+INITRAMFS_OBJ := $(BUILD)/initramfs.o
+USR_INIT_ELF := $(BUILD)/usr/init
+# Relocations: linker emits .rela.dyn under -shared -Bsymbolic; boot
+# code in arch/riscv/mm/init.cc::relocate_kernel walks it at startup.
LDS_SRC := arch/$(ARCH)/kernel/lumeos.lds.S
LINKER_SCRIPT := $(BUILD)/lumeos.lds
PYTHON ?= python3
@@ -37,15 +58,18 @@ PYTHON ?= python3
# ============================================================
ifndef TOOLCHAIN_PREFIX
ifeq ($(ARCH),riscv)
+ # Prefer the linux-gnu toolchain — it supports -shared / -pie which
+ # are required to emit .rela.dyn (Linux RISC-V kernel uses the same).
+ # The bare-metal `riscv64-unknown-elf-` is kept only as a last-resort
+ # fallback; with that one, `-shared` fails ("not supported") and the
+ # kernel can't relocate at runtime.
TOOLCHAIN_PREFIX := $(shell \
- if command -v riscv64-linux-gnu-gcc >/dev/null 2>&1; then \
- echo riscv64-linux-gnu-; \
- elif command -v riscv64-unknown-linux-gnu-gcc >/dev/null 2>&1; then \
+ if command -v riscv64-unknown-linux-gnu-g++ >/dev/null 2>&1; then \
echo riscv64-unknown-linux-gnu-; \
- elif command -v riscv64-unknown-elf-gcc >/dev/null 2>&1; then \
- echo riscv64-unknown-elf-; \
- else \
+ elif command -v riscv64-linux-gnu-g++ >/dev/null 2>&1; then \
echo riscv64-linux-gnu-; \
+ else \
+ echo riscv64-unknown-linux-gnu-; \
fi)
else ifeq ($(ARCH),aarch64)
TOOLCHAIN_PREFIX := aarch64-linux-gnu-
@@ -86,8 +110,10 @@ SRCS_s :=
-include arch/$(ARCH)/module.mk
include kernel/module.mk
include mm/module.mk
-# include fs/module.mk # Phase 3
+include fs/module.mk
+include drivers/base/module.mk
include drivers/module.mk
+include drivers/tty/module.mk
include fdt/module.mk
include lib/module.mk
include test/module.mk
@@ -96,11 +122,17 @@ include test/module.mk
CFLAGS := $(WARNINGS) -O2 -g \
-ffreestanding -nostdlib \
-mcmodel=medany \
+ -fPIE \
-MMD -MP \
$(KCPPFLAGS) \
$(INCLUDES) \
-I$(BUILD)/generated
+# -fPIE makes the compiler emit position-independent code (PC-relative
+# addressing + GOT for globals), which is what `-shared` linking expects.
+# Mirrors Linux's arch/riscv/Makefile under CONFIG_RELOCATABLE=y:
+# KBUILD_CFLAGS += -fPIE
+
# C flags for musl libc sources: add libc-shim headers first
CFLAGS_LIBC := $(CFLAGS) -Iinclude/libc-shim \
-Wno-parentheses -Wno-sign-compare
@@ -110,9 +142,23 @@ CXXFLAGS := $(CFLAGS) \
-fno-exceptions -fno-rtti -fno-use-cxa-atexit \
-fno-threadsafe-statics
-ASFLAGS := $(CFLAGS)
-
-LDFLAGS := -nostdlib -static --emit-relocs
+# Assembler flags MUST NOT include -fPIE.
+# Linux's arch/riscv/Makefile adds -fPIE only to KBUILD_CFLAGS, NOT to
+# KBUILD_AFLAGS: with -fPIE the gcc driver puts the assembler in PIC
+# mode, where the `la` macro silently expands to a GOT-indirect load
+# (`auipc + ld` reading a slot that holds the link-time VA). In our
+# pre-MMU boot path that VA is unmapped, so any `la sym, sym` faults
+# and reboots into a `_start` loop. Stripping -fPIE makes `la` emit
+# direct PC-relative addressing (`auipc + addi`) which works at PA.
+ASFLAGS := $(filter-out -fPIE,$(CFLAGS))
+
+# Link as a position-independent shared object so the linker emits
+# `.rela.dyn` with R_RISCV_RELATIVE entries that the boot stub applies
+# at load time. Mirrors Linux's arch/riscv/Makefile §10:
+# LDFLAGS_vmlinux += -shared -Bsymbolic -z notext
+# `-Bsymbolic` resolves all global refs at link time (no dynamic
+# resolution); `-z notext` lets us keep relocations in code pages.
+LDFLAGS := -nostdlib -shared -Bsymbolic -z notext -z norelro --no-dynamic-linker
# ============================================================
# Object File Computation
@@ -129,14 +175,94 @@ DEPS := $(OBJS:.o=.d)
# ============================================================
.PHONY: all clean run debug fmt disasm defconfig savedefconfig showconfig
-all: $(IMAGE)
+all: $(VMLUME) $(IMAGE)
# Generate autoconf.h before any compilation
$(AUTOCONF_H): scripts/Kconfig.mk $(wildcard .config)
$(generate-autoconf)
-# All objects depend on autoconf.h
-$(OBJS): $(AUTOCONF_H)
+# ============================================================
+# Generated header: asm-offsets.h
+#
+# Linux-style trick (mirrors include/linux/kbuild.h + arch/*/kernel/
+# asm-offsets.c + scripts/Makefile.build): compile asm-offsets.c to
+# assembly, scrape the magic `->SYM val` markers produced by the
+# DEFINE/OFFSET inline-asm macros, rewrite them into `#define`
+# directives via sed. head.S and friends then `#include
+# <generated/asm-offsets.h>` to access struct field offsets without
+# having to hand-maintain them — re-order a struct field, the
+# generated constant follows.
+# ============================================================
+ASM_OFFSETS_C := arch/$(ARCH)/kernel/asm-offsets.c
+ASM_OFFSETS_S := $(BUILD)/arch/$(ARCH)/kernel/asm-offsets.s
+ASM_OFFSETS_H := $(BUILD)/generated/asm-offsets.h
+ASM_OFFSETS_SED := scripts/extract-asm-offsets.sed
+
+# Compile asm-offsets.c to *.s only — we never want the .o linked in.
+$(ASM_OFFSETS_S): $(ASM_OFFSETS_C) $(AUTOCONF_H)
+ @echo " CC -S $<"
+ @mkdir -p $(dir $@)
+ @$(CC) $(CFLAGS_LIBC) -S $< -o $@
+
+# Rewrite ->NAME markers into #define lines. Header guard hand-rolled
+# at the top, sed body in the middle, closing #endif at the bottom.
+$(ASM_OFFSETS_H): $(ASM_OFFSETS_S) $(ASM_OFFSETS_SED)
+ @echo " GEN $@"
+ @mkdir -p $(dir $@)
+ @(echo "/* AUTO-GENERATED — DO NOT EDIT */"; \
+ echo "/* Generated from $(ASM_OFFSETS_C) by scripts/extract-asm-offsets.sed */"; \
+ echo "#ifndef _LUME_GENERATED_ASM_OFFSETS_H"; \
+ echo "#define _LUME_GENERATED_ASM_OFFSETS_H"; \
+ echo ""; \
+ sed -n -f $(ASM_OFFSETS_SED) $<; \
+ echo ""; \
+ echo "#endif /* _LUME_GENERATED_ASM_OFFSETS_H */") > $@
+
+# All objects depend on autoconf.h and asm-offsets.h
+$(OBJS): $(AUTOCONF_H) $(ASM_OFFSETS_H)
+
+# ============================================================
+# Generic %.pi.o post-processing rule
+#
+# Mirrors arch/riscv/kernel/pi/Makefile §26-30 in Linux:
+#
+# $(obj)/%.pi.o: OBJCOPYFLAGS := --prefix-symbols=__pi_
+# --remove-section=.note.gnu.property
+# --prefix-alloc-sections=.init.pi
+# $(obj)/%.pi.o: $(obj)/%.o FORCE
+# $(call if_changed,objcopy)
+#
+# Purpose: take a regular .o produced by the normal compile rule and
+# rewrite it so the early position-independent code can coexist with
+# the rest of the kernel without symbol clashes:
+#
+# --prefix-symbols=__pi_ Every global symbol becomes __pi_<name>.
+# head.S calls them as __pi_fdt_path_offset
+# etc., keeping the pre-MMU namespace
+# cleanly isolated.
+#
+# --prefix-alloc-sections=.init.pi
+# All SHF_ALLOC sections get re-prefixed
+# so they land in .init.pi in vmlinux.lds.S,
+# which positions them where the boot
+# loader has already mapped memory.
+#
+# --remove-section=.note.gnu.property
+# Discarded because post-rename it can
+# confuse the linker about ELF features.
+#
+# Generic pattern (`$(BUILD)/%.pi.o`) so any subdir's source can opt in
+# by listing a `.pi.o` target in its SRCS. The companion compile-side
+# CFLAGS overrides (replacing -fPIE with -fpie, adding -Os, etc.) are
+# expected to come from the subdir's own .mk fragment via target-
+# specific CFLAGS_LIBC / CXXFLAGS overrides.
+# ============================================================
+$(BUILD)/%.pi.o: OBJCOPYFLAGS := --prefix-symbols=__pi_ \
+ --remove-section=.note.gnu.property \
+ --prefix-alloc-sections=.init.pi
+
+$(BUILD)/%.pi.o: $(BUILD)/%.o
+ $(call cmd,objcopy)
# Preprocess linker script template (.lds.S → .lds)
$(LINKER_SCRIPT): $(LDS_SRC) include/lume/lumeos.lds.h
@@ -144,68 +270,96 @@ $(LINKER_SCRIPT): $(LDS_SRC) include/lume/lumeos.lds.h
@mkdir -p $(dir $@)
@$(CC) -E -P -x c $(INCLUDES) $< -o $@
-# Link kernel ELF
-$(VMLINUX_PRE): $(OBJS) $(LINKER_SCRIPT)
- @echo " LD $@"
- @mkdir -p $(dir $@)
- @$(LD) $(LDFLAGS) -T $(LINKER_SCRIPT) -o $@ $(OBJS)
- @$(TOOLCHAIN_PREFIX)size $@
+# ============================================================
+# Initramfs: pack a root filesystem into a newc cpio.
+#
+# Pipeline mirrors Linux's Kbuild:
+# usr/gen_init_cpio — host C tool, produces cpio bytes
+# scripts/gen_initramfs.sh — wrapper that prepares the listing,
+# handles directories vs. listing files,
+# applies uid/gid remap, then invokes the
+# host tool.
+# Sourced from $(CONFIG_INITRAMFS_SOURCE) — see scripts/Kconfig.mk.
+# ============================================================
+
+# Host compiler — distinct from $(CC), which may be the cross compiler.
+HOSTCC ?= cc
+HOSTCFLAGS ?= -O2 -Wall
+
+# Output path matches Linux convention (usr/gen_init_cpio) so the
+# verbatim copy of scripts/gen_initramfs.sh from Linux finds it via its
+# hardcoded relative path when invoked from the kernel source root.
+GEN_INIT_CPIO := usr/gen_init_cpio
+
+$(GEN_INIT_CPIO): usr/gen_init_cpio.c
+ @echo " HOSTCC $@"
+ @$(HOSTCC) $(HOSTCFLAGS) -o $@ $<
+
+INITRAMFS_SOURCE := $(CONFIG_INITRAMFS_SOURCE)
+
+# Record the chosen source so changing CONFIG_INITRAMFS_SOURCE on the
+# command line forces a rebuild — without this, `make` only compares
+# file mtimes and would happily reuse a cpio packed from a different
+# source as long as that other source isn't newer. Evaluated at
+# Makefile parse time so the dep edge is correct from the first rule
+# check. Mirrors the idea behind Linux's $(call if_changed,...).
+INITRAMFS_SRC_STAMP := $(BUILD)/.initramfs_src.stamp
+$(shell mkdir -p $(BUILD) 2>/dev/null; \
+ prev=$$([ -f "$(INITRAMFS_SRC_STAMP)" ] && cat "$(INITRAMFS_SRC_STAMP)" || echo); \
+ if [ "$$prev" != "$(INITRAMFS_SOURCE)" ]; then \
+ printf '%s\n' "$(INITRAMFS_SOURCE)" > "$(INITRAMFS_SRC_STAMP)"; \
+ fi)
-$(RELOC_BIN): $(VMLINUX_PRE) scripts/gen_reloc.py
- @echo " RELOC $@"
+$(INITRAMFS_CPIO): $(INITRAMFS_SOURCE) $(INITRAMFS_SRC_STAMP) \
+ $(GEN_INIT_CPIO) scripts/gen_initramfs.sh
+ @echo " INITRAMFS $(INITRAMFS_SOURCE)"
@mkdir -p $(dir $@)
- @$(PYTHON) scripts/gen_reloc.py --elf $< --out $@
+ @./scripts/gen_initramfs.sh -o $@ \
+ -u $(CONFIG_INITRAMFS_ROOT_UID) \
+ -g $(CONFIG_INITRAMFS_ROOT_GID) \
+ $(INITRAMFS_SOURCE)
-$(RELOC_OBJ): $(RELOC_BIN)
- @echo " OBJCOPY $@"
+$(INITRAMFS_OBJ): $(INITRAMFS_CPIO)
+ @echo " OBJCOPY $@ (initramfs)"
@$(OBJCOPY) -I binary -O $(OBJCOPY_ELF) -B $(OBJCOPY_BINARCH) \
- --rename-section .data=.lume.reloc,alloc,load,readonly,data,contents \
+ --rename-section .data=.init.ramfs,alloc,load,readonly,data,contents \
$< $@
-$(VMLINUX): $(OBJS) $(RELOC_OBJ) $(LINKER_SCRIPT)
+# Single-pass kernel link. With `-shared -Bsymbolic`, the linker
+# emits `.rela.dyn` containing R_RISCV_RELATIVE entries directly into
+# the output ELF — no separate gen_reloc pass.
+$(VMLUME): $(OBJS) $(INITRAMFS_OBJ) $(LINKER_SCRIPT)
@echo " LD $@"
@mkdir -p $(dir $@)
- @$(LD) $(LDFLAGS) -T $(LINKER_SCRIPT) -o $@ $(OBJS) $(RELOC_OBJ)
+ @$(LD) $(LDFLAGS) -T $(LINKER_SCRIPT) -o $@ $(OBJS) $(INITRAMFS_OBJ)
@$(TOOLCHAIN_PREFIX)size $@
-# Generate flat binary for QEMU loading
-$(VMLINUX_RAW): $(VMLINUX)
+# Image = raw binary, mirrors Linux's `arch/riscv/boot/Image` (objcopy
+# vmlinux to raw bytes, no ELF header — what U-Boot or QEMU's `-bios`
+# bare-loading would consume). Linux always builds both vmlinux and
+# Image; LumeOS follows the same convention.
+$(IMAGE): $(VMLUME)
@echo " OBJCOPY $@"
@$(OBJCOPY) -O binary $< $@
-ifeq ($(CONFIG_KERNEL_COMPRESSED),y)
-$(IMAGE): $(VMLINUX_RAW) $(VMLINUX) scripts/pack_image.py
- @echo " PACK $@"
- @$(PYTHON) scripts/pack_image.py --elf $(VMLINUX) --input $< --output $@ --compress lz4
-else
-$(IMAGE): $(VMLINUX_RAW) $(VMLINUX) scripts/pack_image.py
- @echo " PACK $@"
- @$(PYTHON) scripts/pack_image.py --elf $(VMLINUX) --input $< --output $@ --compress none
-endif
-
-# Compile C++ sources
+# Compile C++ sources. Recipe goes through $(call cmd,...) so that
+# `make V=1` echoes the full command line. $(Q) prefix on mkdir keeps
+# the directory-creation step silent in quiet mode but visible at V=1.
$(BUILD)/%.o: %.cc
- @echo " CXX $<"
- @mkdir -p $(dir $@)
- @$(CXX) $(CXXFLAGS) -c $< -o $@
+ $(Q)mkdir -p $(dir $@)
+ $(call cmd,cxx_o_cc)
-# Compile C sources (musl libc)
$(BUILD)/%.o: %.c
- @echo " CC $<"
- @mkdir -p $(dir $@)
- @$(CC) $(CFLAGS_LIBC) -c $< -o $@
+ $(Q)mkdir -p $(dir $@)
+ $(call cmd,cc_o_c)
-# Assemble (preprocessed .S)
$(BUILD)/%.o: %.S
- @echo " AS $<"
- @mkdir -p $(dir $@)
- @$(CC) $(ASFLAGS) -c $< -o $@
+ $(Q)mkdir -p $(dir $@)
+ $(call cmd,as_o_S)
-# Assemble (raw .s, no preprocessing)
$(BUILD)/%.o: %.s
- @echo " AS $<"
- @mkdir -p $(dir $@)
- @$(CC) $(ASFLAGS) -c $< -o $@
+ $(Q)mkdir -p $(dir $@)
+ $(call cmd,as_o_S)
# Auto-dependency tracking
-include $(DEPS)
@@ -215,7 +369,7 @@ $(BUILD)/%.o: %.s
# ============================================================
ifeq ($(ARCH),riscv)
QEMU := qemu-system-riscv64
-QEMU_MACHINE := -machine virt -m 128M -nographic
+QEMU_MACHINE := -machine virt -smp 4 -m 128M -nographic
QEMU_BIOS := -bios default
else ifeq ($(ARCH),aarch64)
QEMU := qemu-system-aarch64
@@ -227,13 +381,13 @@ QEMU_MACHINE := -m 128M -nographic
QEMU_BIOS :=
endif
-run: $(IMAGE)
+run: $(VMLUME) $(IMAGE)
@echo " QEMU Running..."
- $(QEMU) $(QEMU_MACHINE) $(QEMU_BIOS) -kernel $(IMAGE)
+ $(QEMU) $(QEMU_MACHINE) $(QEMU_BIOS) -kernel $(VMLUME)
-debug: $(IMAGE)
+debug: $(VMLUME) $(IMAGE)
@echo " QEMU Waiting for GDB on :1234..."
- $(QEMU) $(QEMU_MACHINE) $(QEMU_BIOS) -kernel $(IMAGE) -s -S
+ $(QEMU) $(QEMU_MACHINE) $(QEMU_BIOS) -kernel $(VMLUME) -s -S
# ============================================================
# Utilities
@@ -241,10 +395,10 @@ debug: $(IMAGE)
clean:
rm -rf $(BUILD)
-disasm: $(VMLINUX)
+disasm: $(VMLUME)
@echo " OBJDUMP $<"
- @$(OBJDUMP) -d -S $(VMLINUX) > $(VMLINUX).asm
- @echo " Written to $(VMLINUX).asm"
+ @$(OBJDUMP) -d -S $(VMLUME) > $(VMLUME).asm
+ @echo " Written to $(VMLUME).asm"
fmt:
@echo " FMT Formatting source files..."
diff --git a/na/arch/riscv/boot/compressed.cc b/na/arch/riscv/boot/compressed.cc
deleted file mode 100644
index 27513bf..0000000
--- a/na/arch/riscv/boot/compressed.cc
+++ /dev/null
@@ -1,471 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#include <arch/config.h>
-#include <lume/boot_params.h>
-#include <lume/compressed.h>
-#include <lume/console.h>
-#include <lume/config.h>
-#include <lume/decompress.h>
-#include <lume/fdt.h>
-#include <lume/klog.h>
-#include <lume/relocate.h>
-#include <lume/types.h>
-
-extern "C" const CompressedKernelHeader __lume_image_header;
-extern "C" char _start[];
-extern "C" char __lume_image_end[];
-extern "C" char __lume_reloc_start[];
-extern "C" char __lume_reloc_end[];
-
-struct FdtHeader {
- uint32 magic;
- uint32 totalsize;
- uint32 off_dt_struct;
- uint32 off_dt_strings;
- uint32 off_mem_rsvmap;
- uint32 version;
- uint32 last_comp_version;
- uint32 boot_cpuid_phys;
- uint32 size_dt_strings;
- uint32 size_dt_struct;
-};
-
-struct ReservedRange {
- uint64 start;
- uint64 end;
-};
-
-static inline uint32 bswap32(uint32 x)
-{
- return ((x & 0xFF000000u) >> 24) |
- ((x & 0x00FF0000u) >> 8) |
- ((x & 0x0000FF00u) << 8) |
- ((x & 0x000000FFu) << 24);
-}
-
-static inline uint64 read_be64(const uint8* p)
-{
- uint64 v = 0;
- for (int i = 0; i < 8; ++i) {
- v = (v << 8) | p[i];
- }
- return v;
-}
-
-static inline uint64 addr_to_phys_early(const void* ptr)
-{
- uint64 addr = reinterpret_cast<uint64>(ptr);
- if (addr >= arch::kDirectMapBaseDefault) {
- return addr - arch::kDirectMapBaseDefault;
- }
- return addr;
-}
-
-static inline uint64 align_down_page(uint64 v)
-{
- return v & ~(kPageSize - 1);
-}
-
-static inline bool ranges_overlap(uint64 base, uint64 size,
- const ReservedRange& r)
-{
- if (size == 0) {
- return false;
- }
- if (base > ~0ULL - size) {
- return true;
- }
- uint64 end = base + size;
- return (base < r.end) && (end > r.start);
-}
-
-static uint32 crc32_init()
-{
- return 0xFFFFFFFFu;
-}
-
-static uint32 crc32_update(uint32 crc, const uint8* data, uint64 len)
-{
- for (uint64 i = 0; i < len; ++i) {
- crc ^= data[i];
- for (int b = 0; b < 8; ++b) {
- uint32 mask = static_cast<uint32>(-(crc & 1u));
- crc = (crc >> 1) ^ (0xEDB88320u & mask);
- }
- }
- return crc;
-}
-
-static uint32 crc32_final(uint32 crc)
-{
- return crc ^ 0xFFFFFFFFu;
-}
-
-static uint32 crc32_bytes(const uint8* data, uint64 len)
-{
- return crc32_final(crc32_update(crc32_init(), data, len));
-}
-
-static void early_put_hex_u64(const char* label, uint64 value)
-{
- static const char kHex[] = "0123456789abcdef";
- early_puts(label);
- early_puts("0x");
- for (int i = 60; i >= 0; i -= 4) {
- early_putc(kHex[(value >> i) & 0xF]);
- }
- early_putc('\n');
-}
-
-/*
- * boot_stage3_trace — lightweight early-boot diagnostic hook.
- *
- * Called from entry.S at three points during the decompress/relocate
- * flow. Arguments: a0=fdt_paddr, a1=link_base, a2=load_base, a3=phase.
- *
- * Phase 0: before decompress (load_base not yet known → 0)
- * Phase 1: after decompress, before jump
- * Phase 2: after flag-set, before re-entry jump
- */
-extern "C" void boot_stage3_trace(uint64 fdt_paddr, uint64 link_base,
- uint64 load_base, uint64 phase)
-{
- /* Phase 0 may fire before console is initialised — keep it cheap. */
- if (phase == 0) {
- console_early_init(fdt_paddr);
- early_puts("[boot] stage3 trace: phase 0 (pre-decompress)\n");
- return;
- }
-
- static const char* labels[] = {
- "[boot] stage3 trace: phase 0\n",
- "[boot] stage3 trace: phase 1 (post-decompress)\n",
- "[boot] stage3 trace: phase 2 (pre-reentry)\n",
- };
- if (phase < 3) {
- early_puts(labels[phase]);
- }
- early_put_hex_u64(" link_base=", link_base);
- early_put_hex_u64(" load_base=", load_base);
-}
-
-extern "C" void boot_stage3_loop_panic(uint64 fdt_paddr, uint64 link_base)
-{
- console_early_init(fdt_paddr);
- early_puts("[boot] stage3 loop detected\n");
-
- auto* hdr = reinterpret_cast<const CompressedKernelHeader*>(link_base + 64);
- early_put_hex_u64("link_base=", link_base);
- early_put_hex_u64("hdr.flags=", hdr->flags);
- early_put_hex_u64("hdr.comp=", hdr->compression);
- early_put_hex_u64("hdr.uncomp=", hdr->uncompressed_size);
- early_put_hex_u64("hdr.comp_size=", hdr->compressed_size);
- early_put_hex_u64("hdr.payload_off=", hdr->payload_offset);
- early_put_hex_u64("hdr.load_base=", hdr->load_phys_base);
- early_put_hex_u64("hdr.reloc_off=", hdr->reloc_offset);
- early_put_hex_u64("hdr.reloc_size=", hdr->reloc_size);
-
- kernel_panic("boot: stage3 loop");
-}
-
-static int collect_fdt_reserved(uint64 fdt_paddr, ReservedRange* out, int max)
-{
- if (!out || max <= 0 || fdt_paddr == 0) {
- return 0;
- }
-
- const FdtHeader* hdr = reinterpret_cast<const FdtHeader*>(fdt_paddr);
- if (bswap32(hdr->magic) != 0xd00dfeedu) {
- return 0;
- }
-
- int count = 0;
- uint64 totalsize = bswap32(hdr->totalsize);
- if (totalsize != 0 && count < max) {
- out[count++] = ReservedRange{fdt_paddr, fdt_paddr + totalsize};
- }
-
- uint64 rsv_off = bswap32(hdr->off_mem_rsvmap);
- const uint8* p = reinterpret_cast<const uint8*>(fdt_paddr + rsv_off);
- while (count < max) {
- uint64 addr = read_be64(p);
- uint64 size = read_be64(p + 8);
- p += 16;
- if (addr == 0 && size == 0) {
- break;
- }
- if (size == 0) {
- continue;
- }
- out[count++] = ReservedRange{addr, addr + size};
- }
-
- return count;
-}
-
-static uint64 xorshift64(uint64* state)
-{
- uint64 x = *state;
- x ^= x << 13;
- x ^= x >> 7;
- x ^= x << 17;
- *state = x;
- return x;
-}
-
-static uint64 find_gap_from(uint64 start, uint64 min_base,
- const ReservedRange* ranges, int count,
- uint64 image_size)
-{
- uint64 candidate = align_down_page(start);
-
- for (int iter = 0; iter < 64; ++iter) {
- if (candidate < min_base) {
- return 0;
- }
-
- bool hit = false;
- uint64 next = candidate;
- for (int i = 0; i < count; ++i) {
- if (!ranges_overlap(candidate, image_size, ranges[i])) {
- continue;
- }
- hit = true;
- if (ranges[i].start < image_size) {
- return 0;
- }
- uint64 below = ranges[i].start - image_size;
- below = align_down_page(below);
- if (below < next) {
- next = below;
- }
- }
-
- if (!hit) {
- return candidate;
- }
- if (next >= candidate) {
- return 0;
- }
- candidate = next;
- }
-
- return 0;
-}
-
-static uint64 choose_load_base(uint64 fdt_paddr, uint64 image_size,
- uint64 link_base, uint64 payload_base,
- uint64 payload_size)
-{
- uint64 mem_base = 0;
- uint64 mem_size = 0;
- FdtManager::early_scan_mem(fdt_paddr, &mem_base, &mem_size);
- if (mem_size == 0) {
- return link_base;
- }
-
- uint64 mem_top = mem_base + mem_size;
- if (mem_top < mem_base || mem_top < image_size) {
- return link_base;
- }
-
- ReservedRange reserved[32] = {};
- int count = 0;
-
- if (count < static_cast<int>(sizeof(reserved) / sizeof(reserved[0]))) {
- reserved[count++] = ReservedRange{link_base, link_base + image_size};
- }
- if (payload_size != 0 &&
- count < static_cast<int>(sizeof(reserved) / sizeof(reserved[0]))) {
- reserved[count++] = ReservedRange{payload_base, payload_base + payload_size};
- }
- count += collect_fdt_reserved(fdt_paddr, reserved + count,
- static_cast<int>(sizeof(reserved) /
- sizeof(reserved[0])) - count);
-
- uint64 min_base = align_down_page(mem_base);
- uint64 max_base = align_down_page(mem_top - image_size);
- if (max_base < min_base) {
- return link_base;
- }
-
- uint64 seed = 0;
- FdtManager::early_scan_kaslr(fdt_paddr, &seed);
- uint64 candidate = max_base;
- if (seed != 0) {
- seed ^= link_base;
- uint64 slots = (max_base - min_base) / kPageSize;
- uint64 pick = xorshift64(&seed) % (slots + 1);
- candidate = min_base + pick * kPageSize;
- }
-
- uint64 base = find_gap_from(candidate, min_base, reserved, count, image_size);
- if (base != 0) {
- return base;
- }
- if (candidate != max_base) {
- base = find_gap_from(max_base, min_base, reserved, count, image_size);
- if (base != 0) {
- return base;
- }
- }
-
- return link_base;
-}
-
-extern "C" uint64 boot_decompress_and_relocate(uint64 fdt_paddr)
-{
- console_early_init(fdt_paddr);
- early_puts("[boot] stage3 start\n");
-
- const uint64 image_va_base = reinterpret_cast<uint64>(_start);
- uint64 image_size = reinterpret_cast<uint64>(__lume_image_end) - image_va_base;
- uint64 reloc_offset = reinterpret_cast<uint64>(__lume_reloc_start) - image_va_base;
- uint64 reloc_size = reinterpret_cast<uint64>(__lume_reloc_end) -
- reinterpret_cast<uint64>(__lume_reloc_start);
-
- const uint64 link_base = addr_to_phys_early(_start);
- uint64 load_base = link_base;
-
- uint32 comp = KERNEL_COMP_NONE;
- uint32 flags = 0;
- uint64 compressed_size = 0;
- uint64 payload_offset = 0;
-
- const CompressedKernelHeader* hdr = &__lume_image_header;
- const uint64 header_offset = reinterpret_cast<uint64>(&__lume_image_header) -
- image_va_base;
- if (hdr->magic == kCompressedKernelMagic &&
- hdr->version == kCompressedKernelVersion &&
- hdr->header_size >= sizeof(CompressedKernelHeader)) {
- if (hdr->header_crc32 != 0) {
- uint32 crc = crc32_init();
- const uint8* hbytes = reinterpret_cast<const uint8*>(hdr);
- constexpr uint64 crc_off = offsetof(CompressedKernelHeader, header_crc32);
- uint64 header_size = hdr->header_size;
- if (header_size > sizeof(CompressedKernelHeader)) {
- header_size = sizeof(CompressedKernelHeader);
- }
- crc = crc32_update(crc, hbytes, crc_off);
- uint32 zero = 0;
- crc = crc32_update(crc, reinterpret_cast<const uint8*>(&zero), sizeof(zero));
- uint64 tail_off = crc_off + sizeof(uint32);
- if (header_size > tail_off) {
- crc = crc32_update(crc, hbytes + tail_off, header_size - tail_off);
- }
- uint32 calc = crc32_final(crc);
- if (calc != hdr->header_crc32) {
- early_puts("[boot] header crc mismatch\n");
- kernel_panic("boot: header crc mismatch");
- }
- }
- comp = hdr->compression;
- compressed_size = hdr->compressed_size;
- if (hdr->uncompressed_size != 0) {
- image_size = hdr->uncompressed_size;
- }
- if (hdr->load_phys_base != 0) {
- load_base = hdr->load_phys_base;
- } else if (comp != KERNEL_COMP_NONE) {
- uint64 payload_base = link_base + payload_offset;
- load_base = choose_load_base(fdt_paddr, image_size, link_base,
- payload_base, compressed_size);
- }
- if (hdr->reloc_size != 0) {
- reloc_offset = hdr->reloc_offset;
- reloc_size = hdr->reloc_size;
- }
- payload_offset = (hdr->payload_offset != 0)
- ? hdr->payload_offset
- : hdr->header_size;
- }
-
- if (comp != KERNEL_COMP_NONE && load_base == link_base) {
- early_puts("[boot] no safe relocation base\n");
- flags |= BOOT_IMAGE_RELOC_FAILED;
- boot_params_set_image_info(load_base, image_size, reloc_offset,
- reloc_size, comp, flags);
- kernel_panic("boot: no safe relocation base");
- }
-
- if (comp != KERNEL_COMP_NONE) {
- flags |= BOOT_IMAGE_COMPRESSED;
-
- if (compressed_size == 0 || payload_offset == 0) {
- early_puts("[boot] compressed header invalid\n");
- flags |= BOOT_IMAGE_DECOMP_FAILED;
- boot_params_set_image_info(load_base, image_size, reloc_offset,
- reloc_size, comp, flags);
- kernel_panic("boot: compressed header invalid");
- }
-
- if (hdr->payload_crc32 != 0) {
- const uint8* payload = reinterpret_cast<const uint8*>(
- image_va_base + payload_offset);
- uint32 calc = crc32_bytes(payload, compressed_size);
- if (calc != hdr->payload_crc32) {
- early_puts("[boot] payload crc mismatch\n");
- flags |= BOOT_IMAGE_DECOMP_FAILED;
- boot_params_set_image_info(load_base, image_size, reloc_offset,
- reloc_size, comp, flags);
- kernel_panic("boot: payload crc mismatch");
- }
- }
-
- if (comp == KERNEL_COMP_LZ4) {
- const uint8* payload = reinterpret_cast<const uint8*>(
- image_va_base + payload_offset);
- uint8* out = reinterpret_cast<uint8*>(load_base);
- int written = lz4_decompress(payload,
- static_cast<uint32>(compressed_size),
- out, static_cast<uint32>(image_size));
- if (written < 0 || static_cast<uint64>(written) != image_size) {
- early_puts("[boot] lz4 decompress failed\n");
- flags |= BOOT_IMAGE_DECOMP_FAILED;
- boot_params_set_image_info(load_base, image_size, reloc_offset,
- reloc_size, comp, flags);
- kernel_panic("boot: lz4 decompress failed");
- }
- } else {
- early_puts("[boot] unsupported compression\n");
- flags |= BOOT_IMAGE_DECOMP_FAILED;
- boot_params_set_image_info(load_base, image_size, reloc_offset,
- reloc_size, comp, flags);
- kernel_panic("boot: unsupported compression");
- }
- }
-
- if (reloc_size != 0 && load_base != link_base) {
- if (reloc_offset + reloc_size > image_size) {
- early_puts("[boot] reloc table out of range\n");
- flags |= BOOT_IMAGE_RELOC_FAILED;
- boot_params_set_image_info(load_base, image_size, reloc_offset,
- reloc_size, comp, flags);
- kernel_panic("boot: reloc table out of range");
- }
- const void* reloc_blob = reinterpret_cast<const void*>(
- image_va_base + reloc_offset);
- RelocStatus st = relocate_apply(load_base, image_size, link_base,
- reloc_blob, reloc_size);
- if (st != RELOC_OK) {
- early_puts("[boot] relocation failed\n");
- flags |= BOOT_IMAGE_RELOC_FAILED;
- boot_params_set_image_info(load_base, image_size, reloc_offset,
- reloc_size, comp, flags);
- kernel_panic("boot: relocation failed", reloc_status_str(st));
- }
- flags |= BOOT_IMAGE_RELOCATED;
- }
-
- if (load_base != link_base) {
- auto* new_hdr = reinterpret_cast<CompressedKernelHeader*>(
- load_base + header_offset);
- new_hdr->flags |= LUME_HDR_FLAG_STAGE3_DONE;
- }
-
- early_puts("[boot] stage3 done\n");
- boot_params_set_image_info(load_base, image_size, reloc_offset,
- reloc_size, comp, flags);
- return load_base;
-}
diff --git a/na/arch/riscv/boot/entry.S b/na/arch/riscv/boot/entry.S
deleted file mode 100644
index 9154b7f..0000000
--- a/na/arch/riscv/boot/entry.S
+++ /dev/null
@@ -1,309 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-/*
- * LumeOS Kernel Entry Point (RISC-V 64)
- *
- * This file implements the earliest boot sequence:
- * 1. RISC-V Linux Boot Protocol Image Header (64 bytes)
- * 2. BSP/AP synchronization
- * 3. BSS zeroing (BSP only)
- * 4. Early page table setup (1GB superpages for identity + higher-half)
- * 5. MMU enable and higher-half trampoline
- * 6. C++ global constructor invocation
- * 7. Jump to kernel_main()
- *
- * Register contract on entry (from OpenSBI / GRUB / U-Boot):
- * a0 = CPU ID (Hart ID)
- * a1 = FDT physical base address
- *
- * Reference: docs/specs/linker_and_entry.md
- */
-
-#include <lume/config.h>
-
-#define PAGE_OFFSET 0xFFFFFFC000000000
-#define BOOT_STACK_SIZE 8192
-
-/* SV39 PTE flags */
-#define PTE_V (1 << 0) /* Valid */
-#define PTE_R (1 << 1) /* Readable */
-#define PTE_W (1 << 2) /* Writable */
-#define PTE_X (1 << 3) /* Executable */
-#define PTE_A (1 << 6) /* Accessed */
-#define PTE_D (1 << 7) /* Dirty */
-
-/* SATP mode field for SV39 */
-#define SATP_SV39 (8UL << 60)
-
-/* LumeOS compressed image header constants */
-#define LUME_COMP_NONE 0
-#define LUME_HDR_SIZE 80
-#define LUME_HDR_MAGIC 0x4C554D45434F4D50 /* "LUMECOMP" */
-#define LUME_HDR_OFF 64
-#define LUME_HDR_FLAGS_OFF 14
-#define LUME_HDR_FLAG_STAGE3_DONE 0x1
-
-/* ========================================================================
- * Section: .head.text — RISC-V Linux Boot Protocol Image Header
- * Must be at byte 0 of the kernel binary.
- * ======================================================================== */
- .section .head.text, "ax"
- .globl _start
-_start:
- /* code0 (u32, offset 0): executable jump for OpenSBI bare boot */
- j real_start
- /* code1 (u32, offset 4): reserved */
- .word 0
- /* text_offset (u64, offset 8): load offset from RAM base */
- .dword 0x200000
- /* image_size (u64, offset 16): will be filled post-link, 0 = unknown */
- .dword 0
- /* flags (u64, offset 24): bit 0: 0=LE, 1=BE */
- .dword 0
- /* version (u32, offset 32): header version 0.2 */
- .word 0x0002
- /* res1 (u32, offset 36) */
- .word 0
- /* res2 (u64, offset 40) */
- .dword 0
- /* magic (u64, offset 48): RISCV_IMAGE_MAGIC "RISCV\0\0\0" LE */
- .dword 0x5643534952
- /* magic2 (u32, offset 56): RISCV_IMAGE_MAGIC2 "RSC\x05" LE */
- .word 0x05435352
- /* res3 (u32, offset 60): reserved for PE/COFF */
- .word 0
-
- /* LumeOS compressed kernel header (80 bytes) */
- .align 3
- .globl __lume_image_header
-__lume_image_header:
- .dword LUME_HDR_MAGIC /* magic */
- .word 1 /* version */
- .half LUME_HDR_SIZE /* header_size */
- .half 0 /* flags */
- .word LUME_COMP_NONE /* compression */
- .word 0 /* reserved0 */
- .dword __lume_image_end - _start /* uncompressed_size */
- .dword 0 /* compressed_size */
- .dword 0 /* load_phys_base (0 = auto) */
- .dword __lume_reloc_start - _start /* reloc_offset */
- .dword __lume_reloc_end - __lume_reloc_start /* reloc_size */
- .dword _ebss - _start /* payload_offset (when compressed) */
- .word 0 /* header_crc32 */
- .word 0 /* payload_crc32 */
-
-/* ========================================================================
- * real_start: Actual boot logic begins here (all CPUs land here)
- * ======================================================================== */
- .text
- .globl real_start
-real_start:
- /* ---- Step 1: Disable interrupts ---- */
- csrw sie, zero
- csrw sip, zero
-
- /* ---- Step 2: Set up gp (global pointer) ---- */
- /* Critical for medany model: all global variable access goes through gp */
- .option push
- .option norelax
- la gp, __global_pointer$
- .option pop
-
- /* ---- Step 3: Set up per-CPU boot stack ---- */
- /* sp = boot_stack + (cpu_id + 1) * BOOT_STACK_SIZE */
- la t0, boot_stack
- li t1, BOOT_STACK_SIZE
- addi t2, a0, 1 /* t2 = cpu_id + 1 */
- mul t1, t1, t2 /* t1 = (cpu_id + 1) * BOOT_STACK_SIZE */
- add sp, t0, t1
-
- /* ---- Step 4: Store boot args (cpu_id and fdt_paddr) ---- */
- mv tp, a0 /* tp = cpu_id */
- mv s0, a1 /* s0 = fdt_paddr (preserved across all C++ calls) */
- /* ---- Step 5: BSP / AP divergence ---- */
- /* CPU 0 is the BSP; all others spin-wait */
- bnez a0, .Lap_wait
-
-/* ===================================================================
- * BSP-only path: Clear BSS, set up early page table, release APs
- * =================================================================== */
-.Lbsp_init:
- /* ---- 5.1: Clear BSS ---- */
- la t0, _sbss
- la t1, _ebss
-.Lbss_loop:
- bgeu t0, t1, .Lbss_done
- sd zero, 0(t0)
- addi t0, t0, 8
- j .Lbss_loop
-.Lbss_done:
-
-
- /* ---- 5.2: Build early page table (1GB superpages) ---- */
- /*
- * SV39 root page table (512 entries x 8 bytes = 4KB)
- * We need 4 entries:
- * [0] = Identity map: 0x00000000 -> 0x00000000 (MMIO: UART, PLIC, CLINT)
- * [2] = Identity map: 0x80000000 -> 0x80000000 (RAM)
- * [256] = Higher-half: 0xFFFFFFC000000000 -> 0x00000000 (MMIO via VA)
- * [258] = Higher-half: 0xFFFFFFC080000000 -> 0x80000000 (RAM via VA)
- *
- * VPN[2] index = (VA >> 30) & 0x1FF
- * 0x00000000 -> 0 0xFFFFFFC000000000 -> 256
- * 0x80000000 -> 2 0xFFFFFFC080000000 -> 258
- */
- la t0, early_pgdir
-
- /* PTE flags: V|R|W|X|A|D (1GB leaf superpage) */
- li t2, (PTE_V | PTE_R | PTE_W | PTE_X | PTE_A | PTE_D)
-
- /* --- MMIO region: physical 0x00000000 --- */
- /* PPN = 0x00000000 >> 12 = 0, PTE = flags only */
- mv t1, t2 /* PTE = 0 | flags */
-
- /* Entry [0]: Identity map MMIO */
- sd t1, (0 * 8)(t0)
-
- /* Entry [256]: Higher-half MMIO (0xFFFFFFC000000000) */
- li t3, (256 * 8)
- add t3, t0, t3
- sd t1, 0(t3)
-
- /* --- RAM region: physical 0x80000000 --- */
- li t1, 0x80000 /* PPN = 0x80000000 >> 12 */
- slli t1, t1, 10 /* PPN field in PTE */
- or t1, t1, t2 /* t1 = complete PTE */
-
- /* Entry [2]: Identity map RAM */
- sd t1, (2 * 8)(t0)
-
- /* Entry [258]: Higher-half RAM (0xFFFFFFC080000000) */
- li t3, (258 * 8)
- add t3, t0, t3
- sd t1, 0(t3)
-
-
- /* ---- 5.3: Memory fence ---- */
- fence rw, rw
-
- /* ---- 5.4: Release APs ---- */
- la t0, boot_sync_flag
- li t1, 1
- amoswap.w.rl zero, t1, (t0)
- j .Lenable_mmu
-
-/* ===================================================================
- * AP path: spin-wait until BSP signals readiness
- * =================================================================== */
-.Lap_wait:
- la t0, boot_sync_flag
-.Lap_spin:
- lw t1, 0(t0)
- beqz t1, .Lap_spin
- fence r, rw /* Acquire: ensure we see BSP's writes */
-
-/* ===================================================================
- * All CPUs: Enable MMU and jump to higher-half virtual addresses
- * =================================================================== */
-.Lenable_mmu:
-
-
- /* ---- 6.1: Write satp = SV39 mode | (early_pgdir >> 12) ---- */
- /* NOTE: `la` at this point is PC-relative. Since we are still executing
- * at physical addresses, it yields the PHYSICAL address directly.
- * No VA-to-PA conversion needed. */
- la t0, early_pgdir
- srli t0, t0, 12 /* PPN = PA >> 12 */
- li t1, SATP_SV39
- or t0, t0, t1 /* satp = mode | PPN */
- csrw satp, t0
-
- /* ---- 6.2: Flush TLB ---- */
- sfence.vma zero, zero
-
- /* ---- 6.3: Jump to higher-half virtual address ---- */
- /* Now we are executing via the identity map (PA == VA for 0x8...).
- * `la` is PC-relative (medany), so it yields a PHYSICAL address.
- * We must add PAGE_OFFSET to convert to the higher-half VA. */
- la t0, .Lhigher_half
- li t1, PAGE_OFFSET
- add t0, t0, t1
- jr t0
-
-.Lhigher_half:
- /* We are now running at virtual addresses (0xFFFFFFC0...) !
- * The identity map is still active but will be removed by vmm_init(). */
-
- /* ---- 6.4: Fix sp and gp to virtual addresses ---- */
- /* sp was set up before MMU using PC-relative `la`, so it holds a
- * physical address. Add PAGE_OFFSET to convert to higher-half VA.
- * gp is similarly reloaded — now that PC is at VA, `la` resolves
- * correctly via PC-relative addressing. */
- li t0, PAGE_OFFSET
- add sp, sp, t0
- .option push
- .option norelax
- la gp, __global_pointer$
- .option pop
-
- /* Mark MMU enabled for early console mapping logic */
- call console_set_mmu_enabled
-
- /* ---- Step 7: BSP calls early_cpp_init ---- */
- bnez tp, .Lskip_cpp_init
-
- /* BSP only: invoke C++ global constructors */
- call early_cpp_init
-
-.Lskip_cpp_init:
-
- /* All CPUs: enter kernel_main(cpu_id, fdt_paddr) */
- mv a0, tp /* a0 = cpu_id (from tp) */
- mv a1, s0 /* a1 = fdt_paddr (from s0) */
- call kernel_main
-
- /* kernel_main should never return; if it does, halt */
-.Lhalt:
- csrc sstatus, 2 /* Clear SIE */
- wfi
- j .Lhalt
-
-
-/* ========================================================================
- * early_cpp_init: Traverse .init_array and call global constructors
- * Called by BSP only, before kernel_main.
- * ======================================================================== */
- .globl early_cpp_init
-early_cpp_init:
- /* Prologue: save ra and callee-saved regs we use as loop state */
- addi sp, sp, -32
- sd ra, 0(sp)
- sd s1, 8(sp)
- sd s2, 16(sp)
-
- la s1, __init_array_start
- la s2, __init_array_end
-.Linit_loop:
- bgeu s1, s2, .Linit_done
- ld t0, 0(s1) /* Load function pointer */
- jalr ra, t0 /* Call constructor (sets ra = return addr) */
- addi s1, s1, 8 /* Next entry */
- j .Linit_loop
-.Linit_done:
- /* Epilogue: restore and return */
- ld ra, 0(sp)
- ld s1, 8(sp)
- ld s2, 16(sp)
- addi sp, sp, 32
- ret
-
-
-/* ========================================================================
- * BSS Data: boot_sync_flag
- * ======================================================================== */
- .section .bss
- .align 2
- .globl boot_sync_flag
-boot_sync_flag:
- .word 0
diff --git a/na/arch/riscv/include/arch/config.h b/na/arch/riscv/include/arch/config.h
index 0949c41..8ae0502 100644
--- a/na/arch/riscv/include/arch/config.h
+++ b/na/arch/riscv/include/arch/config.h
@@ -35,6 +35,11 @@ inline constexpr uint64 kVmemmapBaseDefault = 0xFFFFFFD000000000ULL;
/* Physical memory base (where RAM starts on RISC-V virt platform) */
inline constexpr uint64 kPhysBase = 0x80000000ULL;
+/* Kernel physical load address (= PHYS_BASE in linker script).
+ * Everything in [kPhysBase, kPhysStart) is firmware territory (PMP-protected).
+ * Analogous to Linux's CONFIG_PHYSICAL_START. */
+inline constexpr uint64 kPhysStart = 0x80200000ULL;
+
/* ================================================================
* Runtime (post-KASLR) virtual addresses
*
diff --git a/na/arch/riscv/include/arch/cpu.h b/na/arch/riscv/include/arch/cpu.h
index d16cf2d..8c573ec 100644
--- a/na/arch/riscv/include/arch/cpu.h
+++ b/na/arch/riscv/include/arch/cpu.h
@@ -76,4 +76,20 @@ static inline void timer_intr_off() {
__asm__ volatile("csrc sie, %0" :: "r"(SIE_STIE));
}
+// Enable software interrupt in sie (sie.SSIE = 1) — needed for IPI
+static inline void software_intr_on() {
+ __asm__ volatile("csrs sie, %0" :: "r"(SIE_SSIE));
+}
+
+// Enable external interrupt in sie (sie.SEIE = 1) — needed for PLIC (UART, etc.)
+static inline void external_intr_on() {
+ __asm__ volatile("csrs sie, %0" :: "r"(SIE_SEIE));
+}
+
+// Clear S-mode software interrupt pending bit (sip.SSIP = 0)
+// Must be called in the IPI handler to acknowledge the interrupt.
+static inline void clear_sip_ssip() {
+ __asm__ volatile("csrc sip, %0" :: "r"(1UL << 1));
+}
+
} // namespace arch::cpu
diff --git a/na/arch/riscv/include/arch/fpstate.h b/na/arch/riscv/include/arch/fpstate.h
new file mode 100644
index 0000000..4b42a22
--- /dev/null
+++ b/na/arch/riscv/include/arch/fpstate.h
@@ -0,0 +1,94 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * FPU State — Floating-Point Register Context
+ *
+ * Pre-reserved structure for RISC-V F/D extension floating-point state.
+ * MUST be allocated in every TaskControlBlock so that future FPU
+ * save/restore logic has a stable destination without restructuring TCB.
+ *
+ * RISC-V F/D extension state:
+ * - 32 FP registers (f0-f31), each 64 bits for D extension
+ * - fcsr (Floating-point Control and Status Register)
+ *
+ * sstatus.FS field management:
+ * - FS=0 (Off): FPU disabled, any FP instruction traps
+ * - FS=1 (Initial): FPU enabled, registers are initial state
+ * - FS=2 (Clean): FPU enabled, registers not modified since last save
+ * - FS=3 (Dirty): FPU enabled, registers modified (need save)
+ *
+ * Current status: Structure defined, save/restore NOT implemented.
+ * When implemented, trap_dispatch and swtch.S will use lazy
+ * save/restore based on sstatus.FS dirty tracking.
+ *
+ * Reference: RISC-V ISA Manual §3.1.6.5 (FS field)
+ */
+
+#include <lume/types.h>
+
+namespace arch {
+
+struct FpState {
+ uint64 f[32]; // f0-f31 (64-bit each for D extension)
+ uint32 fcsr; // Floating-point CSR (frm + fflags)
+ uint32 _pad; // Alignment padding
+};
+
+static_assert(sizeof(FpState) == 32 * 8 + 8, "FpState must be 264 bytes");
+
+/*
+ * sstatus.FS field constants
+ */
+inline constexpr uint64 kSstatusFS_Off = 0ULL << 13;
+inline constexpr uint64 kSstatusFS_Initial = 1ULL << 13;
+inline constexpr uint64 kSstatusFS_Clean = 2ULL << 13;
+inline constexpr uint64 kSstatusFS_Dirty = 3ULL << 13;
+inline constexpr uint64 kSstatusFS_Mask = 3ULL << 13;
+
+/*
+ * Check if sstatus indicates FPU state is dirty (needs save).
+ */
+inline bool fpu_state_dirty(uint64 sstatus) {
+ return (sstatus & kSstatusFS_Mask) == kSstatusFS_Dirty;
+}
+
+/*
+ * Check if FPU is enabled (FS != Off).
+ */
+inline bool fpu_enabled(uint64 sstatus) {
+ return (sstatus & kSstatusFS_Mask) != kSstatusFS_Off;
+}
+
+} // namespace arch
+
+/* Assembly routines for FPU register save/restore (fpu.S) */
+extern "C" void fpu_save(arch::FpState* state);
+extern "C" void fpu_restore(arch::FpState* state);
+
+namespace arch {
+
+/*
+ * Read sstatus.FS field (current FPU state)
+ */
+inline uint64 fpu_get_fs() {
+ uint64 sstatus;
+ __asm__ volatile("csrr %0, sstatus" : "=r"(sstatus));
+ return sstatus & kSstatusFS_Mask;
+}
+
+/*
+ * Set sstatus.FS field.
+ * @fs: one of kSstatusFS_Off, kSstatusFS_Initial, kSstatusFS_Clean, kSstatusFS_Dirty
+ */
+inline void fpu_set_fs(uint64 fs) {
+ __asm__ volatile(
+ "csrc sstatus, %0\n"
+ "csrs sstatus, %1\n"
+ :: "r"(kSstatusFS_Mask), "r"(fs)
+ );
+}
+
+} // namespace arch
diff --git a/na/arch/riscv/include/arch/mmu.h b/na/arch/riscv/include/arch/mmu.h
index a0ee1ed..f4c9b26 100644
--- a/na/arch/riscv/include/arch/mmu.h
+++ b/na/arch/riscv/include/arch/mmu.h
@@ -50,6 +50,14 @@ static inline uint64 get_page_table() {
return (satp_val & SATP_PPN_MASK) << 12;
}
+// Read the raw satp CSR value (mode + PPN).
+// Used when the caller needs the exact register value (e.g. for TrapFrame metadata).
+static inline uint64 read_satp() {
+ uint64 val;
+ __asm__ volatile("csrr %0, satp" : "=r"(val));
+ return val;
+}
+
// Flush the entire TLB (all ASIDs, all addresses)
static inline void flush_tlb_all() {
__asm__ volatile("sfence.vma zero, zero" ::: "memory");
@@ -60,4 +68,11 @@ static inline void flush_tlb_page(uint64 vaddr) {
__asm__ volatile("sfence.vma %0, zero" :: "r"(vaddr) : "memory");
}
+// Flush the instruction cache.
+// Required after loading executable code into memory (e.g. ELF loading).
+// sfence.vma only flushes the TLB; fence.i is needed for the I-cache.
+static inline void flush_icache() {
+ __asm__ volatile("fence.i" ::: "memory");
+}
+
} // namespace arch::mmu
diff --git a/na/arch/riscv/include/arch/pmap.h b/na/arch/riscv/include/arch/pmap.h
index a99825a..0b43b9e 100644
--- a/na/arch/riscv/include/arch/pmap.h
+++ b/na/arch/riscv/include/arch/pmap.h
@@ -35,6 +35,7 @@ void unmap(uint64 root_pa, uint64 va);
int protect(uint64 root_pa, uint64 va, uint64 perm);
int protect_range(uint64 root_pa, uint64 start, uint64 end, uint64 perm);
bool lookup(uint64 root_pa, uint64 va, uint64* pa_out);
+uint64 lookup_pte(uint64 root_pa, uint64 va);
bool is_cow(uint64 root_pa, uint64 va);
int make_cow(uint64 root_pa, uint64 va);
void activate(uint64 root_pa);
diff --git a/na/arch/riscv/include/arch/sbi.h b/na/arch/riscv/include/arch/sbi.h
index b65c37e..80a6d7c 100644
--- a/na/arch/riscv/include/arch/sbi.h
+++ b/na/arch/riscv/include/arch/sbi.h
@@ -3,6 +3,15 @@
#pragma once
+/*
+ * sbi.h — RISC-V Supervisor Binary Interface (SBI) wrappers
+ *
+ * Provides inline ecall wrappers for SBI calls from S-mode.
+ * Uses the SBI v1.0+ extension-based calling convention.
+ *
+ * Reference: RISC-V SBI Specification v2.0
+ */
+
#include <lume/types.h>
namespace arch::sbi {
@@ -12,7 +21,20 @@ struct sbiret {
long value;
};
-static inline sbiret sbi_ecall(int ext, int fid, uint64 arg0, uint64 arg1, uint64 arg2, uint64 arg3, uint64 arg4, uint64 arg5) {
+/* SBI error codes */
+inline constexpr long SBI_SUCCESS = 0;
+inline constexpr long SBI_ERR_NOT_SUPPORTED = -2;
+
+/* Extension IDs */
+inline constexpr int SBI_EXT_TIME = 0x54494D45; /* "TIME" */
+inline constexpr int SBI_EXT_IPI = 0x735049; /* "sPI" */
+inline constexpr int SBI_EXT_RFENCE = 0x52464E43; /* "RFNC" */
+inline constexpr int SBI_EXT_HSM = 0x48534D; /* "HSM" */
+
+static inline sbiret sbi_ecall(int ext, int fid, uint64 arg0 = 0,
+ uint64 arg1 = 0, uint64 arg2 = 0,
+ uint64 arg3 = 0, uint64 arg4 = 0,
+ uint64 arg5 = 0) {
sbiret ret;
register uint64 a0 asm("a0") = arg0;
register uint64 a1 asm("a1") = arg1;
@@ -23,7 +45,7 @@ static inline sbiret sbi_ecall(int ext, int fid, uint64 arg0, uint64 arg1, uint6
register uint64 a6 asm("a6") = fid;
register uint64 a7 asm("a7") = ext;
__asm__ volatile("ecall"
- : "+r"(a0), "=r"(a1)
+ : "+r"(a0), "+r"(a1)
: "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(a6), "r"(a7)
: "memory");
ret.error = static_cast<long>(a0);
@@ -31,9 +53,59 @@ static inline sbiret sbi_ecall(int ext, int fid, uint64 arg0, uint64 arg1, uint6
return ret;
}
+/* ---- Timer Extension (EID 0x54494D45) ---- */
+
static inline void set_timer(uint64 stime_value) {
- // Standard SBI TIME Extension (EID 0x54494D45)
- sbi_ecall(0x54494D45, 0, stime_value, 0, 0, 0, 0, 0);
+ sbi_ecall(SBI_EXT_TIME, 0, stime_value);
+}
+
+/* ---- IPI Extension (EID 0x735049) ---- */
+
+/*
+ * sbi_send_ipi — Send S-mode software interrupt to target harts
+ *
+ * @hart_mask: bitmask of target harts (relative to hart_mask_base)
+ * @hart_mask_base: base hart ID for the mask (0 = harts 0-63)
+ */
+static inline sbiret sbi_send_ipi(uint64 hart_mask,
+ uint64 hart_mask_base = 0) {
+ return sbi_ecall(SBI_EXT_IPI, 0, hart_mask, hart_mask_base);
+}
+
+/* ---- Remote Fence Extension (EID 0x52464E43) ---- */
+
+/*
+ * sbi_remote_sfence_vma — Execute SFENCE.VMA on target harts
+ *
+ * @hart_mask/base: target harts
+ * @start_addr: start of VA range to invalidate
+ * @size: size of range (use ~0UL for full flush)
+ */
+static inline sbiret sbi_remote_sfence_vma(uint64 hart_mask,
+ uint64 hart_mask_base,
+ uint64 start_addr,
+ uint64 size) {
+ return sbi_ecall(SBI_EXT_RFENCE, 1,
+ hart_mask, hart_mask_base,
+ start_addr, size);
+}
+
+/* ---- HSM Extension (EID 0x48534D) ---- */
+
+/*
+ * sbi_hart_start — Start (wake) a hart that is in STOPPED state
+ *
+ * @hartid: target hart ID to start
+ * @start_addr: PHYSICAL address where the hart should begin execution
+ * @opaque: passed in a1 register to the hart (e.g. FDT pointer)
+ *
+ * OpenSBI v1.0+ parks non-boot harts; they must be explicitly started.
+ */
+static inline sbiret sbi_hart_start(uint64 hartid,
+ uint64 start_addr,
+ uint64 opaque) {
+ return sbi_ecall(SBI_EXT_HSM, 0, hartid, start_addr, opaque);
}
} // namespace arch::sbi
+
diff --git a/na/arch/riscv/include/arch/tlb.h b/na/arch/riscv/include/arch/tlb.h
new file mode 100644
index 0000000..46c6b97
--- /dev/null
+++ b/na/arch/riscv/include/arch/tlb.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+#include <lume/types.h>
+
+namespace arch::tlb {
+
+/*
+ * Perform a cross-CPU TLB flush (shootdown).
+ *
+ * @param cpu_mask: Bitmask of CPUs to target.
+ * @param start: Start virtual address, or 0 for full address space.
+ * @param size: Size in bytes, or ~0ULL for full address space.
+ */
+void flush_remote(uint64 cpu_mask, uint64 start, uint64 size);
+
+} // namespace arch::tlb
diff --git a/na/arch/riscv/include/arch/trap.h b/na/arch/riscv/include/arch/trap.h
index c99d8cb..8e55250 100644
--- a/na/arch/riscv/include/arch/trap.h
+++ b/na/arch/riscv/include/arch/trap.h
@@ -79,4 +79,16 @@ static inline uint64 cause_code(uint64 cause) {
return cause & ~SCAUSE_INTERRUPT;
}
+// Enable Supervisor User Memory access (set SUM bit in sstatus).
+// When SUM=1, S-mode can read/write pages marked PTE_U.
+// This is needed for syscall handlers to access user-space buffers.
+static inline void enable_sum() {
+ __asm__ volatile("csrs sstatus, %0" :: "r"(1UL << 18));
+}
+
+// Disable Supervisor User Memory access (clear SUM bit in sstatus).
+static inline void disable_sum() {
+ __asm__ volatile("csrc sstatus, %0" :: "r"(1UL << 18));
+}
+
} // namespace arch::trap
diff --git a/na/arch/riscv/include/arch/trapframe.h b/na/arch/riscv/include/arch/trapframe.h
index de49e9c..b198646 100644
--- a/na/arch/riscv/include/arch/trapframe.h
+++ b/na/arch/riscv/include/arch/trapframe.h
@@ -61,6 +61,7 @@
#ifndef __ASSEMBLER__
#include <lume/types.h>
+#include <arch/cpu.h>
namespace arch {
@@ -89,4 +90,51 @@ static_assert(sizeof(TrapFrame) == 296, "TrapFrame size mismatch with assembly o
} // namespace arch
+/* Forward declaration — defined in trap_md.cc.
+ * Must be after TrapFrame definition. */
+extern "C" [[noreturn]] void trap_dispatch(arch::TrapFrame*);
+
+namespace arch {
+
+/*
+ * trapframe_init_user — prepare a TrapFrame for returning to user mode.
+ *
+ * Sets arch-specific sstatus:
+ * SPP = 0 (return to U-mode on sret)
+ * SPIE = 1 (enable interrupts in U-mode)
+ * FS = Initial (allow FP instructions)
+ *
+ * Called from MI exec.cc instead of setting sstatus directly.
+ */
+static inline void trapframe_init_user(TrapFrame* tf, uint64 entry, uint64 sp)
+{
+ /* Clear all registers */
+ for (auto& t : tf->t) { t = 0; }
+ for (auto& s : tf->s) { s = 0; }
+ for (auto& a : tf->a) { a = 0; }
+ tf->ra = 0;
+ tf->gp = 0;
+ tf->tp = 0;
+
+ tf->epc = entry;
+ tf->sp = sp;
+
+ /* sstatus: SPP=0 (U-mode), SPIE=1 (interrupts), FS=Initial (FP ok) */
+ constexpr uint64 kSPIE = (1ULL << 5);
+ constexpr uint64 kFS_Initial = (1ULL << 13);
+ tf->sstatus = kSPIE | kFS_Initial;
+
+ /* Kernel metadata used by trap_vector.S on every trap from U-mode.
+ * These are RISC-V specific — trap_vector reads them from the
+ * TrapFrame to switch to kernel context. */
+ uint64 satp;
+ asm volatile("csrr %0, satp" : "=r"(satp));
+ tf->kernel_satp = satp;
+ tf->kernel_sp = reinterpret_cast<uint64>(tf);
+ tf->kernel_trap = reinterpret_cast<uint64>(&trap_dispatch);
+ tf->kernel_cpu_id = cpu::id();
+}
+
+} // namespace arch
+
#endif /* __ASSEMBLER__ */
diff --git a/na/arch/riscv/include/asm/csr.h b/na/arch/riscv/include/asm/csr.h
new file mode 100644
index 0000000..42f1eb4
--- /dev/null
+++ b/na/arch/riscv/include/asm/csr.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _ASM_RISCV_CSR_H
+#define _ASM_RISCV_CSR_H
+
+/*
+ * RISC-V CSR (Control & Status Register) definitions used by the early
+ * boot / MMU-switch path.
+ *
+ * LumeOS-native subset of Linux arch/riscv/include/asm/csr.h. Only the
+ * symbols head.S and mm/init.cc actually reference are defined — adding
+ * the rest is straightforward when new code needs them, and skipping the
+ * Linux ALTERNATIVE / vendor-extension machinery keeps the cascade flat.
+ */
+
+/* SATP (Supervisor Address Translation and Protection) layout — RV64 */
+#define SATP_PPN_MASK 0x00000FFFFFFFFFFFUL /* bits [43:0] = root PPN */
+#define SATP_ASID_SHIFT 44
+#define SATP_MODE_SHIFT 60
+
+/* SATP MODE encodings (RV64) */
+#define SATP_MODE_BARE (0UL << SATP_MODE_SHIFT)
+#define SATP_MODE_SV39 (8UL << SATP_MODE_SHIFT)
+#define SATP_MODE_SV48 (9UL << SATP_MODE_SHIFT)
+
+/* Compose a SATP value from a page-table physical address.
+ * Result: MODE | (root_ppn) where root_ppn = pgd_pa >> PAGE_SHIFT. */
+#define MAKE_SATP(mode, pgd_pa) ((mode) | (((pgd_pa) >> 12) & SATP_PPN_MASK))
+
+/* SSTATUS bits we touch at boot — disable FP / vector at entry so that an
+ * accidental FP / V instruction in early code traps cleanly rather than
+ * silently using dirty state from before reset. */
+#define SR_FS_OFF (0UL << 13)
+#define SR_VS_OFF (0UL << 9)
+
+#endif /* _ASM_RISCV_CSR_H */
diff --git a/na/arch/riscv/include/asm/page.h b/na/arch/riscv/include/asm/page.h
new file mode 100644
index 0000000..7b9f155
--- /dev/null
+++ b/na/arch/riscv/include/asm/page.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _ASM_RISCV_PAGE_H
+#define _ASM_RISCV_PAGE_H
+
+/*
+ * RISC-V page / virtual-memory layout constants.
+ *
+ * Mirrors the subset of Linux arch/riscv/include/asm/page.h that LumeOS's
+ * early boot path needs:
+ * - PAGE_SIZE / PAGE_SHIFT / PAGE_MASK
+ * - PAGE_OFFSET (start of the higher-half direct map)
+ * - KERNEL_LINK_ADDR (link-time VA of _start)
+ * - SV39 PTE flag bits
+ * - PTRS_PER_PGD (512 for SV39)
+ *
+ * Values match Linux RISC-V SV39 defaults so that struct kernel_mapping
+ * (filled by setup_vm in arch/riscv/mm/init.cc) carries the same numeric
+ * relations Linux's mm/init.c maintains.
+ */
+
+#define PAGE_SHIFT 12
+#define PAGE_SIZE (1UL << PAGE_SHIFT)
+#define PAGE_MASK (~(PAGE_SIZE - 1))
+
+/* Higher-half base of the kernel direct map. Bottom of the SV39 supervisor
+ * address space minus the unmapped guard region. Identical to Linux's
+ * CONFIG_PAGE_OFFSET default on RV64. */
+#define PAGE_OFFSET 0xFFFFFFC000000000UL
+
+/* Where _start was linked. OpenSBI hands control to PHYS_BASE; the kernel
+ * runs at KERNEL_LINK_ADDR after relocate_enable_mmu jumps to higher-half. */
+#define PHYS_BASE 0x80200000UL
+#define KERNEL_LINK_ADDR 0xFFFFFFC080200000UL
+
+/* SATP mode field — SV39 = 8, shifted to bits [63:60]. */
+#define SATP_MODE_SV39 (8UL << 60)
+
+#define PTRS_PER_PGD 512
+
+/* 1 GB superpage size — entry.S used to build the early page table with
+ * these; setup_vm in arch/riscv/mm/init.cc now does the same. */
+#define PGDIR_SHIFT 30
+#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
+#define PGDIR_MASK (~(PGDIR_SIZE - 1))
+
+/* SV39 PTE flag bits (one byte). */
+#define _PAGE_PRESENT (1UL << 0) /* aka VALID — historic Linux name */
+#define _PAGE_READ (1UL << 1)
+#define _PAGE_WRITE (1UL << 2)
+#define _PAGE_EXEC (1UL << 3)
+#define _PAGE_USER (1UL << 4)
+#define _PAGE_GLOBAL (1UL << 5)
+#define _PAGE_ACCESSED (1UL << 6)
+#define _PAGE_DIRTY (1UL << 7)
+
+#define PAGE_KERNEL_EXEC \
+ (_PAGE_PRESENT | _PAGE_READ | _PAGE_WRITE | _PAGE_EXEC | \
+ _PAGE_ACCESSED | _PAGE_DIRTY)
+
+#endif /* _ASM_RISCV_PAGE_H */
diff --git a/na/arch/riscv/include/asm/pgtable.h b/na/arch/riscv/include/asm/pgtable.h
new file mode 100644
index 0000000..28fc4c5
--- /dev/null
+++ b/na/arch/riscv/include/asm/pgtable.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _ASM_RISCV_PGTABLE_H
+#define _ASM_RISCV_PGTABLE_H
+
+/*
+ * Early page-table construction API used by arch/riscv/mm/init.cc::setup_vm.
+ *
+ * Mirrors the create_pgd_mapping interface from Linux's
+ * arch/riscv/mm/init.c — same name, same semantics:
+ *
+ * create_pgd_mapping(pgdir, va, pa, sz, prot)
+ *
+ * pgdir : root of the page table to populate
+ * va : virtual address to map
+ * pa : physical address to map to
+ * sz : mapping size — must be a supported leaf size (PAGE_SIZE,
+ * 2 MiB megapage, or 1 GiB gigapage)
+ * prot : PTE flag bits (PAGE_KERNEL_EXEC etc., from <asm/page.h>)
+ *
+ * The boot-time variant in this file only supports gigapage (1 GiB)
+ * mappings — that is all setup_vm needs for the early_pg_dir / trampoline_pg_dir
+ * built before the slab allocator exists. Finer-grained mappings come
+ * later via vmm_init().
+ */
+
+#include <lume/types.h>
+#include <asm/page.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct { unsigned long pgd; } pgd_t;
+
+/* Linker-provided early page-table storage (defined in lumeos.lds.S
+ * ARCH_BSS_EXTRA, lives in .bss so it's zero-initialized). Allocating
+ * them in the linker script — not in C — mirrors Linux and keeps the
+ * symbols visible from head.S without a forward C decl mismatch. */
+extern pgd_t early_pg_dir[PTRS_PER_PGD];
+extern pgd_t trampoline_pg_dir[PTRS_PER_PGD];
+extern pgd_t fixmap_pg_dir[PTRS_PER_PGD];
+
+/* Install a single PGD entry mapping `va` → `pa` with size `sz` and PTE
+ * flags `prot`. Only sz == PGDIR_SIZE (1 GiB) is supported in this early
+ * variant — vmm_init() takes over with create_pgd_mapping_pmd later. */
+void create_pgd_mapping(pgd_t *pgdir, unsigned long va, unsigned long pa,
+ unsigned long sz, unsigned long prot);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _ASM_RISCV_PGTABLE_H */
diff --git a/na/arch/riscv/include/asm/sections.h b/na/arch/riscv/include/asm/sections.h
new file mode 100644
index 0000000..454d9de
--- /dev/null
+++ b/na/arch/riscv/include/asm/sections.h
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _ASM_RISCV_SECTIONS_H
+#define _ASM_RISCV_SECTIONS_H
+
+/*
+ * Linker-emitted symbols that bracket each section of the kernel image.
+ *
+ * The lumeos.lds.S linker script defines these as the start / end
+ * addresses of contiguous regions. Code that walks a section
+ * (relocate_kernel walks .rela.dyn, the C++ ctor loop walks
+ * .init_array, the VMM mapping code walks .text vs .rodata, ...)
+ * imports the pair via this header.
+ *
+ * Mirrors Linux arch/riscv/include/asm/sections.h / asm-generic/sections.h.
+ * Each name and pair is identical to Linux so that direct ports of mm/init.c
+ * relocate_kernel logic compile unchanged against LumeOS.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern char _start[];
+extern char _end[];
+
+extern char _stext[];
+extern char _etext[];
+extern char _srodata[];
+extern char _erodata[];
+extern char _sdata[];
+extern char _edata[];
+extern char _sbss[];
+extern char _ebss[];
+
+extern char __init_array_start[];
+extern char __init_array_end[];
+
+/* .rela.dyn — array of Elf64_Rela entries emitted by the linker under
+ * `-shared -Bsymbolic`. relocate_kernel walks this range and patches
+ * each R_RISCV_RELATIVE slot. */
+extern char __rela_dyn_start[];
+extern char __rela_dyn_end[];
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _ASM_RISCV_SECTIONS_H */
diff --git a/na/arch/riscv/kernel/asm-offsets.c b/na/arch/riscv/kernel/asm-offsets.c
new file mode 100644
index 0000000..edecab7
--- /dev/null
+++ b/na/arch/riscv/kernel/asm-offsets.c
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// Copyright (C) 2026 Richard Qin
+//
+// arch/riscv/kernel/asm-offsets.c — emit struct field offsets and
+// scalar constants for consumption by assembly sources (head.S etc.).
+//
+// 1:1 with Linux arch/riscv/kernel/asm-offsets.c. The Makefile
+// compiles this to an .s file (not .o) and runs
+// scripts/extract-asm-offsets.sed over the output to produce
+// include/generated/asm-offsets.h.
+//
+// Adding a new offset: include the struct's header, add an OFFSET()
+// or DEFINE() line below, rebuild. head.S can then reference the
+// generated #define directly.
+
+#include <lume/kbuild.h>
+#include <lume/kernel_map.h>
+
+void asm_offsets(void)
+{
+ COMMENT("LumeOS asm-offsets — struct kernel_mapping field offsets");
+ OFFSET(KERNEL_MAP_VIRT_ADDR, kernel_mapping, virt_addr);
+ OFFSET(KERNEL_MAP_VIRT_OFFSET, kernel_mapping, virt_offset);
+ OFFSET(KERNEL_MAP_PHYS_ADDR, kernel_mapping, phys_addr);
+ OFFSET(KERNEL_MAP_SIZE, kernel_mapping, size);
+ OFFSET(KERNEL_MAP_VA_PA_OFFSET, kernel_mapping, va_pa_offset);
+ OFFSET(KERNEL_MAP_PAGE_OFFSET, kernel_mapping, page_offset);
+ OFFSET(KERNEL_MAP_VA_KERNEL_PA_OFFSET, kernel_mapping, va_kernel_pa_offset);
+}
diff --git a/na/arch/riscv/kernel/head.S b/na/arch/riscv/kernel/head.S
new file mode 100644
index 0000000..e04c44c
--- /dev/null
+++ b/na/arch/riscv/kernel/head.S
@@ -0,0 +1,317 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * RISC-V kernel entry — Linux-aligned layout.
+ *
+ * Flow (mirrors arch/riscv/kernel/head.S in Linux):
+ *
+ * _start [.head.text, byte 0 of image]
+ * · 64-byte Boot Protocol Image Header
+ * · j real_start
+ *
+ * real_start [.text]
+ * · save a0 (hartid) / a1 (dtb_pa)
+ * · setup gp / per-hart sp
+ * · BSP/AP election via amoswap
+ * · BSP: clear BSS, call setup_vm, then relocate_enable_mmu
+ * · AP: spin-wait, then relocate_enable_mmu
+ *
+ * relocate_enable_mmu [C function, arch/riscv/mm/init.cc]
+ * · csrw satp <- trampoline_pg_dir
+ * · jump to higher-half VA
+ * · csrw satp <- early_pg_dir
+ * · slide sp/ra/gp to higher-half
+ * · call relocate_kernel (walks .rela.dyn, patches R_RISCV_RELATIVE)
+ *
+ * real_start (continued in higher-half)
+ * · BSP only: call early_cpp_init (walks .init_array)
+ * · call kernel_main(hartid, dtb_pa)
+ *
+ * Register contract from OpenSBI / U-Boot:
+ * a0 = hart ID
+ * a1 = FDT physical base
+ */
+
+#include <lume/config.h>
+
+#define PAGE_OFFSET 0xFFFFFFC000000000
+#define BOOT_STACK_SIZE 8192
+
+/* ========================================================================
+ * .head.text — RISC-V Boot Protocol Image Header
+ *
+ * Layout per Documentation/arch/riscv/boot-image-header.rst:
+ * code0/code1 (8 B) : executable code, lands at PC after OpenSBI hand-off
+ * text_offset (8 B) : load offset from RAM base
+ * image_size (8 B) : effective image size (0 = unknown)
+ * flags (8 B) : bit 0: endian
+ * version (4 B) : major.minor
+ * res1/res2 (4+8) : reserved
+ * magic (8 B) : "RISCV\0\0\0"
+ * magic2 (4 B) : "RSC\x05"
+ * res3 (4 B) : PE/COFF reserved
+ * ======================================================================== */
+ .section .head.text, "ax"
+ .globl _start
+_start:
+ j real_start /* code0 */
+ .word 0 /* code1 */
+ .dword 0x200000 /* text_offset */
+ .dword 0 /* image_size (post-link) */
+ .dword 0 /* flags */
+ .word 0x0002 /* version 0.2 */
+ .word 0 /* res1 */
+ .dword 0 /* res2 */
+ .dword 0x5643534952 /* magic "RISCV\0\0\0" */
+ .word 0x05435352 /* magic2 "RSC\x05" */
+ .word 0 /* res3 */
+
+/* ========================================================================
+ * real_start — all harts land here at physical PC
+ * ======================================================================== */
+ .text
+ .globl real_start
+real_start:
+ /* DEBUG: 'R<hartid>\n' — UART direct at 0x10000000, MMU off. */
+ li t0, 0x10000000
+ li t1, 'R'
+ sb t1, 0(t0)
+ addi t1, a0, '0'
+ sb t1, 0(t0)
+ li t1, '\n'
+ sb t1, 0(t0)
+
+ /* Mask interrupts and supervisor pending bits. */
+ csrw sie, zero
+ csrw sip, zero
+
+ /* gp for medany — relax must be disabled across the `la`. */
+ .option push
+ .option norelax
+ la gp, __global_pointer$
+ .option pop
+
+ /* per-hart boot stack: sp = boot_stack + (hartid + 1) * STACK_SIZE */
+ la t0, boot_stack
+ li t1, BOOT_STACK_SIZE
+ addi t2, a0, 1
+ mul t1, t1, t2
+ add sp, t0, t1
+
+ /* Save hartid / dtb_pa in callee-saved regs preserved across calls. */
+ mv tp, a0 /* tp = hartid */
+ mv s0, a1 /* s0 = dtb_pa */
+
+ /* BSP/AP election: first hart to swap bsp_elected 0→1 is BSP. */
+ la t0, bsp_elected
+ li t1, 1
+ amoswap.w.aq t2, t1, (t0)
+ bnez t2, .Lap_wait /* prev != 0 → we're an AP */
+
+ /* === BSP path === */
+ la t0, bsp_hart_id
+ sw a0, 0(t0)
+
+ li t3, 0x10000000
+ li t4, 'B'
+ sb t4, 0(t3)
+ li t4, '\n'
+ sb t4, 0(t3)
+
+ /* Clear BSS — must precede setup_vm, which writes kernel_map. */
+ la t0, _sbss
+ la t1, _ebss
+.Lbss_loop:
+ bgeu t0, t1, .Lbss_done
+ sd zero, 0(t0)
+ addi t0, t0, 8
+ j .Lbss_loop
+.Lbss_done:
+
+ li t3, 0x10000000
+ li t4, 'V'
+ sb t4, 0(t3)
+ li t4, '\n'
+ sb t4, 0(t3)
+
+ /* ---- BSP-only: pre-MMU PIC fixup of .rela.dyn ----
+ *
+ * The kernel is built with -fPIE -shared. All global-data accesses
+ * compile to GOT-indirect loads (auipc + ld), where the linker
+ * emits each .got slot's initial value as the link-time VA and a
+ * companion R_RISCV_RELATIVE entry in .rela.dyn. At this point
+ * we're running at the physical load address — those link-time VAs
+ * are not yet mapped, so any global access would fault.
+ *
+ * Walk .rela.dyn in pure assembly (PC-relative only — no globals)
+ * and patch every R_RISCV_RELATIVE slot to addend + (PA - LINK_VA).
+ * After this, setup_vm can dereference kernel_map / early_pg_dir
+ * / etc. via the GOT and get correct PA-side pointers.
+ *
+ * Mirrors Linux arch/riscv/kernel/head.S — same idea, same loop.
+ *
+ * reloc:
+ * t0 = pa_offset = PA(_start) - KERNEL_LINK_ADDR (negative)
+ * t1 = &__rela_dyn_start
+ * t2 = &__rela_dyn_end
+ * for each 24-byte rela in [t1, t2):
+ * t3 = r_offset; t4 = r_info; t5 = r_addend
+ * if (t4 & 0xFFFFFFFF) != R_RISCV_RELATIVE: skip
+ * slot_pa = r_offset + pa_offset
+ * *slot_pa = r_addend + pa_offset
+ */
+#define R_RISCV_RELATIVE 3
+
+ la t0, _start /* PA of _start */
+ li t1, 0xFFFFFFC080200000 /* KERNEL_LINK_ADDR (asm/page.h) */
+ sub t0, t0, t1 /* t0 = pa_offset (PA - LINK_VA) */
+
+ la t1, __rela_dyn_start
+ la t2, __rela_dyn_end
+.Lrela_loop:
+ bgeu t1, t2, .Lrela_done
+ ld t3, 0(t1) /* r_offset */
+ ld t4, 8(t1) /* r_info */
+ ld t5, 16(t1) /* r_addend */
+ /* r_info low 32 bits = type */
+ li t6, 0xFFFFFFFF
+ and t4, t4, t6
+ li t6, R_RISCV_RELATIVE
+ bne t4, t6, .Lrela_next
+ /* slot_pa = r_offset + pa_offset */
+ add t3, t3, t0
+ /* value = r_addend + pa_offset */
+ add t5, t5, t0
+ sd t5, 0(t3)
+.Lrela_next:
+ addi t1, t1, 24 /* sizeof(Elf64_Rela) */
+ j .Lrela_loop
+.Lrela_done:
+
+ /* Memory fence so subsequent loads see the patched slots. */
+ fence w, r
+
+ li t3, 0x10000000
+ li t4, 'P'
+ sb t4, 0(t3)
+ li t4, '\n'
+ sb t4, 0(t3)
+
+ /* Call setup_vm(load_pa = PA of _start, dtb_pa = s0).
+ * Both args physical at this point — MMU still off. */
+ la a0, _start
+ mv a1, s0
+ call setup_vm
+
+ li t3, 0x10000000
+ li t4, 'v'
+ sb t4, 0(t3)
+ li t4, '\n'
+ sb t4, 0(t3)
+
+ /* Release APs once page tables are built. */
+ la t0, boot_sync_flag
+ li t1, 1
+ amoswap.w.rl zero, t1, (t0)
+ j .Lenable_mmu
+
+/* ========================================================================
+ * AP path — spin until BSP signals readiness.
+ * ======================================================================== */
+.Lap_wait:
+ la t0, boot_sync_flag
+.Lap_spin:
+ lw t1, 0(t0)
+ beqz t1, .Lap_spin
+ fence r, rw
+
+ /* fall through */
+
+/* ========================================================================
+ * .Lenable_mmu — all harts: enable MMU, jump to higher-half, run reloc.
+ *
+ * relocate_enable_mmu does its own SATP write + higher-half jump in
+ * C (with inline asm), then calls relocate_kernel. After it returns,
+ * we are at higher-half VA with sp / ra / gp slid up, and .rela.dyn
+ * is applied (so .init_array pointers are valid).
+ * ======================================================================== */
+.Lenable_mmu:
+ la a0, early_pg_dir
+ call relocate_enable_mmu
+ /* On return: PC, sp, ra, gp are all at higher-half VA. */
+
+ li t3, 0x10000000
+ li t4, 'M'
+ sb t4, 0(t3)
+ li t4, '\n'
+ sb t4, 0(t3)
+
+ /* Mark MMU enabled for console / pmap layers that check this. */
+ call console_set_mmu_enabled
+
+ /* BSP only: walk __init_array and call each C++ ctor. */
+ la t0, bsp_hart_id
+ lw t0, 0(t0)
+ bne tp, t0, .Lskip_cpp_init
+ call early_cpp_init
+.Lskip_cpp_init:
+
+ /* Hand off to kernel_main(hartid, dtb_pa). */
+ mv a0, tp
+ mv a1, s0
+ call kernel_main
+
+ /* kernel_main should not return. */
+.Lhalt:
+ csrc sstatus, 2
+ wfi
+ j .Lhalt
+
+/* ========================================================================
+ * early_cpp_init — walk .init_array and call each constructor.
+ *
+ * Called only by BSP, after relocate_kernel has patched the function
+ * pointers stored in __init_array. Before relocate_kernel runs those
+ * pointers are zero-filled by the linker.
+ * ======================================================================== */
+ .globl early_cpp_init
+early_cpp_init:
+ addi sp, sp, -32
+ sd ra, 0(sp)
+ sd s1, 8(sp)
+ sd s2, 16(sp)
+
+ la s1, __init_array_start
+ la s2, __init_array_end
+.Linit_loop:
+ bgeu s1, s2, .Linit_done
+ ld t0, 0(s1)
+ jalr ra, t0
+ addi s1, s1, 8
+ j .Linit_loop
+.Linit_done:
+ ld ra, 0(sp)
+ ld s1, 8(sp)
+ ld s2, 16(sp)
+ addi sp, sp, 32
+ ret
+
+/* ========================================================================
+ * Election state — in .data so explicit zero survives the BSP BSS clear
+ * (these are read before BSS is cleared on the BSP, and concurrently by
+ * APs while BSP is in the clear loop).
+ * ======================================================================== */
+ .section .data
+ .align 2
+ .globl boot_sync_flag
+boot_sync_flag:
+ .word 0
+
+ .globl bsp_elected
+bsp_elected:
+ .word 0
+
+ .globl bsp_hart_id
+bsp_hart_id:
+ .word 0
diff --git a/na/arch/riscv/kernel/lumeos.lds.S b/na/arch/riscv/kernel/lumeos.lds.S
index ca5218f..9bd083b 100644
--- a/na/arch/riscv/kernel/lumeos.lds.S
+++ b/na/arch/riscv/kernel/lumeos.lds.S
@@ -38,12 +38,33 @@ ENTRY(_start)
PROVIDE(__global_pointer$ = . + 0x800); \
*(.sdata .sdata.*)
-/* ===== RISC-V: early page table + per-CPU boot stacks in .bss ===== */
+/* ===== RISC-V: page-table storage + per-CPU boot stacks in .bss =====
+ *
+ * Linux RISC-V layout (arch/riscv/kernel/vmlinux.lds.S):
+ * early_pg_dir — first early page table built by setup_vm,
+ * used for the rest of boot after MMU enable
+ * trampoline_pg_dir — single-page table used only during the SATP
+ * switch from physical to higher-half VA; its
+ * identity + virt mappings of _start must both
+ * be valid in the same instruction window
+ * fixmap_pg_dir — covers the FIXADDR_START range used by the
+ * early console (UART MMIO before kernel page
+ * table proper is built)
+ *
+ * All three are allocated here so head.S can take their addresses without
+ * a C definition; their PA == VA mapping covers them during setup_vm.
+ */
#define ARCH_BSS_EXTRA \
- /* Early page table: 4KB aligned, used by entry.S before VMM */ \
. = ALIGN(4096); \
- early_pgdir = .; \
+ early_pg_dir = .; \
+ . += 4096; \
+ trampoline_pg_dir = .; \
+ . += 4096; \
+ fixmap_pg_dir = .; \
. += 4096; \
+ /* Legacy name retained for any caller still referencing it; */ \
+ /* points at the same first 4 KiB as early_pg_dir. */ \
+ early_pgdir = early_pg_dir; \
/* Per-CPU boot stacks: 8KB per CPU, max 8 CPUs */ \
. = ALIGN(4096); \
boot_stack = .; \
@@ -65,16 +86,27 @@ SECTIONS {
. = ALIGN(4096);
- RODATA_SECTION
+ /* .rela.dyn lives between .text and .rodata so it ends up in the
+ * read-only PT_LOAD segment. Mirrors Linux vmlinux.lds.S where
+ * .rela.dyn sits at the tail of init data, just before RO_DATA. */
+ RELA_DYN_SECTION
. = ALIGN(4096);
- RELOC_SECTION
+ RODATA_SECTION
. = ALIGN(4096);
DATA_SECTION
+ . = ALIGN(8);
+
+ /* .got lives right after .data in the RW PT_LOAD. All other
+ * dynamic-linker artefacts (.plt, .dynamic, .dynsym, .dynstr,
+ * .hash, .gnu.hash, .interp, .dynbss) are discarded by the
+ * DISCARDS macro — mirrors Linux arch/riscv/kernel/vmlinux.lds.S. */
+ GOT_SECTION
+
. = ALIGN(4096);
/* End of file-backed image (BSS follows as NOBITS) */
diff --git a/na/arch/riscv/kernel/signal.cc b/na/arch/riscv/kernel/signal.cc
new file mode 100644
index 0000000..39fe34e
--- /dev/null
+++ b/na/arch/riscv/kernel/signal.cc
@@ -0,0 +1,163 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * arch/riscv/signal.cc — Signal frame construction and sigreturn
+ *
+ * Constructs a signal frame on the user stack before returning to
+ * the user signal handler. On sigreturn, restores the original
+ * registers from the saved frame.
+ *
+ * Frame layout on user stack (grows downward):
+ *
+ * +-----------------------+ <- original sp
+ * | sigset_t saved_mask | 8 bytes
+ * | TrapFrame saved_regs | 296 bytes
+ * | kernel_siginfo info | 128 bytes
+ * +-----------------------+ <- new sp (16-byte aligned)
+ *
+ * Reference: Linux arch/riscv/kernel/signal.c
+ */
+
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/uaccess.h>
+#include <lume/errno.h>
+#include <lume/klog.h>
+#include <arch/trapframe.h>
+
+/* ================================================================== */
+/* Signal frame layout */
+/* ================================================================== */
+
+struct rt_sigframe {
+ kernel_siginfo info; /* siginfo for SA_SIGINFO handlers */
+ arch::TrapFrame saved_regs; /* saved register state */
+ sigset_t saved_mask; /* blocked mask to restore on sigreturn */
+};
+
+/* ================================================================== */
+/* Signal trampoline (written to user stack) */
+/* ================================================================== */
+
+/*
+ * When the user's signal handler returns (via `ret`), ra points here.
+ * This tiny code sequence invokes sys_rt_sigreturn to restore the
+ * original execution context.
+ *
+ * RISC-V instructions (8 bytes):
+ * li a7, 139 → addi a7, zero, 139 → 0x08b00893
+ * ecall → 0x00000073
+ */
+static constexpr uint32 sigreturn_trampoline[2] = {
+ 0x08b00893, /* li a7, 139 (SYS_rt_sigreturn) */
+ 0x00000073, /* ecall */
+};
+
+/* ================================================================== */
+/* setup_signal_frame */
+/* ================================================================== */
+
+int setup_signal_frame(arch::TrapFrame* tf, int signo,
+ kernel_sigaction* ka, kernel_siginfo* info)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return -EINVAL;
+
+ /* Calculate frame location on user stack */
+ uint64 sp = tf->sp;
+
+ /* Reserve space for the trampoline below the frame */
+ sp -= sizeof(sigreturn_trampoline);
+ sp &= ~15ULL; /* 16-byte align */
+ uint64 trampoline_addr = sp;
+
+ /* Reserve space for the signal frame */
+ sp -= sizeof(rt_sigframe);
+ sp &= ~15ULL; /* 16-byte align */
+ uint64 frame_addr = sp;
+
+ /* Build the frame in kernel memory, then copy to user stack */
+ rt_sigframe frame{};
+ if (info) {
+ frame.info = *info;
+ }
+ frame.saved_regs = *tf;
+ frame.saved_mask = curr->blocked;
+
+ /* Copy frame to user stack */
+ int err = copy_to_user(frame_addr, &frame, sizeof(frame));
+ if (err < 0) return -EFAULT;
+
+ /* Copy trampoline to user stack */
+ err = copy_to_user(trampoline_addr, sigreturn_trampoline,
+ sizeof(sigreturn_trampoline));
+ if (err < 0) return -EFAULT;
+
+ /* Set up the trap frame to execute the signal handler */
+ tf->epc = ka->sa_handler;
+ tf->sp = frame_addr;
+
+ /* Arguments to the handler: handler(int signo, siginfo_t*, ucontext_t*) */
+ tf->a[0] = static_cast<uint64>(signo);
+
+ if (ka->sa_flags & SA_SIGINFO) {
+ /* SA_SIGINFO: pass siginfo and ucontext pointers */
+ tf->a[1] = frame_addr; /* &info */
+ tf->a[2] = frame_addr + sizeof(kernel_siginfo); /* &saved_regs (as ucontext) */
+ } else {
+ tf->a[1] = 0;
+ tf->a[2] = 0;
+ }
+
+ /* Return address: when the handler does `ret`, jump to trampoline */
+ if (ka->sa_flags & SA_RESTORER) {
+ tf->ra = ka->sa_restorer;
+ } else {
+ tf->ra = trampoline_addr;
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* arch_sigreturn — restore state from signal frame */
+/* ================================================================== */
+
+int64 arch_sigreturn(arch::TrapFrame* tf)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return -EINVAL;
+
+ /*
+ * The signal frame is located at the current sp.
+ * We need to read it back and restore the original state.
+ */
+ uint64 frame_addr = tf->sp;
+
+ rt_sigframe frame{};
+ int err = copy_from_user(&frame, frame_addr, sizeof(frame));
+ if (err < 0) {
+ printk(KERN_ERR "sigreturn: failed to read signal frame at %llx\n",
+ frame_addr);
+ task_kill_current(SIGSEGV);
+ return -EFAULT;
+ }
+
+ /* Restore the saved register state */
+ *tf = frame.saved_regs;
+
+ /* Restore the blocked mask */
+ curr->blocked = frame.saved_mask;
+ curr->blocked.del(SIGKILL);
+ curr->blocked.del(SIGSTOP);
+
+ /*
+ * Return value: we return the saved a0 from the original context.
+ * The syscall dispatch will NOT overwrite a0 because sigreturn
+ * is special — it returns via the restored trap frame directly.
+ */
+ return tf->a[0];
+}
diff --git a/na/arch/riscv/mm/init.cc b/na/arch/riscv/mm/init.cc
new file mode 100644
index 0000000..3cebcd9
--- /dev/null
+++ b/na/arch/riscv/mm/init.cc
@@ -0,0 +1,241 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * RISC-V early memory-management bootstrap.
+ *
+ * Mirrors Linux arch/riscv/mm/init.c — same file name, same C function
+ * names (setup_vm / relocate_enable_mmu / relocate_kernel), same global
+ * state (kernel_map, early_pg_dir, trampoline_pg_dir). head.S calls
+ * setup_vm immediately after BSS-clear, then later calls
+ * relocate_enable_mmu, which switches SATP, jumps to higher-half VA,
+ * and finally calls relocate_kernel to apply .rela.dyn.
+ *
+ * Differences from Linux:
+ * - KASLR offset is hard-wired to 0 for now (no pi/ subsystem) — the
+ * code path that would consume kernel_map.virt_offset is in place
+ * so adding pi/-based seed reading later only requires populating
+ * that one field.
+ * - Only 1 GiB superpages supported in create_pgd_mapping; sufficient
+ * for the early page tables. Finer-grained mappings come from
+ * vmm_init() once slab is up.
+ */
+
+#include <lume/init.h>
+#include <lume/setup.h>
+#include <lume/elf.h>
+#include <lume/kernel_map.h>
+#include <lume/types.h>
+#include <asm/page.h>
+#include <asm/csr.h>
+#include <asm/pgtable.h>
+#include <asm/sections.h>
+
+/*
+ * Global kernel mapping descriptor — single source of truth for the
+ * runtime-vs-link address relationship. Lives in .data so its initial
+ * zero contents survive the BSS-clear that runs before setup_vm fills
+ * the fields. Read by VMM, page-fault handler, ptdump, etc.
+ *
+ * Linux equivalent: arch/riscv/mm/init.c::kernel_map
+ */
+struct kernel_mapping kernel_map __attribute__((section(".data")));
+
+/*
+ * create_pgd_mapping — install one PGD entry covering a 1 GiB region.
+ *
+ * Only 1 GiB superpages are supported in this early variant; setup_vm
+ * needs nothing finer. Identical interface to Linux's same-named
+ * function in arch/riscv/mm/init.c, restricted in arity.
+ */
+extern "C" void create_pgd_mapping(pgd_t *pgdir, unsigned long va,
+ unsigned long pa, unsigned long sz,
+ unsigned long prot)
+{
+ /* This early variant only supports 1 GiB superpages. */
+ if (sz != PGDIR_SIZE) {
+ return;
+ }
+
+ /* SV39 PGD index = (va >> 30) & 0x1FF — see Volume II, §4.4.1.
+ * Each PGD entry is 8 bytes; pgdir[i] is the slot for that VPN[2]. */
+ unsigned long idx = (va >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1);
+
+ /* PTE PPN field: physical page number shifted up by 10 (SV39 PTE
+ * layout: bits [53:10] = PPN, bits [9:0] = flags). */
+ unsigned long ppn = pa >> PAGE_SHIFT;
+ pgdir[idx].pgd = (ppn << 10) | prot;
+}
+
+/*
+ * setup_vm — fill kernel_map and build early_pg_dir + trampoline_pg_dir.
+ *
+ * Called from head.S after BSS is cleared, with:
+ * load_pa = runtime physical address of _start
+ * dtb_pa = physical address of the device tree blob (from a1)
+ *
+ * Mirrors Linux arch/riscv/mm/init.c::setup_vm. The actual SATP write
+ * happens later in relocate_enable_mmu; here we only populate page
+ * tables and the kernel_map descriptor.
+ */
+extern "C" void __init setup_vm(uintptr_t load_pa, uintptr_t dtb_pa)
+{
+ (void)dtb_pa; /* KASLR pi/ subsystem will consume this later. */
+
+ /* --- 1. Fill kernel_map ----------------------------------------- */
+
+ /* KASLR offset = 0 in this configuration. */
+ kernel_map.virt_offset = 0;
+ kernel_map.virt_addr = KERNEL_LINK_ADDR + kernel_map.virt_offset;
+ kernel_map.phys_addr = load_pa;
+ kernel_map.size = (uintptr_t)_end - (uintptr_t)_start;
+ kernel_map.page_offset = PAGE_OFFSET;
+
+ /* Mirrors Linux: va_pa_offset is the constant added to a physical
+ * address to obtain its direct-map VA. */
+ kernel_map.va_pa_offset = PAGE_OFFSET - load_pa;
+
+ /* va_kernel_pa_offset is the constant added to a load PA to obtain
+ * the kernel's link VA (used by relocate_enable_mmu to fix sp/ra). */
+ kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - load_pa;
+
+ /* --- 2. Build early_pg_dir ------------------------------------- *
+ *
+ * SV39 root page table — 4 entries cover identity + higher-half for
+ * both MMIO (PA 0x0000_0000, 1 GiB) and RAM (PA 0x8000_0000, 1 GiB):
+ *
+ * [0] identity PA 0x0000_0000 → VA 0x0000_0000
+ * [2] identity PA 0x8000_0000 → VA 0x8000_0000
+ * [256] high-half PA 0x0000_0000 → VA 0xFFFFFFC000000000
+ * [258] high-half PA 0x8000_0000 → VA 0xFFFFFFC080000000
+ *
+ * Identity mappings keep PC valid right after csrw satp. High-half
+ * mappings let the post-MMU `jr` to .Lhigher_half resolve. */
+ create_pgd_mapping(early_pg_dir, 0x00000000UL, 0x00000000UL,
+ PGDIR_SIZE, PAGE_KERNEL_EXEC);
+ create_pgd_mapping(early_pg_dir, 0x80000000UL, 0x80000000UL,
+ PGDIR_SIZE, PAGE_KERNEL_EXEC);
+ create_pgd_mapping(early_pg_dir, PAGE_OFFSET + 0x00000000UL,
+ 0x00000000UL, PGDIR_SIZE, PAGE_KERNEL_EXEC);
+ create_pgd_mapping(early_pg_dir, PAGE_OFFSET + 0x80000000UL,
+ 0x80000000UL, PGDIR_SIZE, PAGE_KERNEL_EXEC);
+
+ /* --- 3. Build trampoline_pg_dir -------------------------------- *
+ *
+ * The trampoline maps just the gigapage containing _start, both at
+ * its identity PA and at its higher-half VA. This is the page
+ * table SATP holds during the single instruction window between
+ * "MMU off" and "MMU on with early_pg_dir" — the PC must resolve
+ * under both, so we re-use the same two entries above. */
+ create_pgd_mapping(trampoline_pg_dir, load_pa & PGDIR_MASK,
+ load_pa & PGDIR_MASK, PGDIR_SIZE, PAGE_KERNEL_EXEC);
+ create_pgd_mapping(trampoline_pg_dir,
+ (kernel_map.virt_addr) & PGDIR_MASK,
+ load_pa & PGDIR_MASK, PGDIR_SIZE, PAGE_KERNEL_EXEC);
+}
+
+/*
+ * relocate_kernel — walk .rela.dyn and apply R_RISCV_RELATIVE entries.
+ *
+ * Called from relocate_enable_mmu *after* the SATP switch and the jump
+ * to higher-half VA — meaning __rela_dyn_start / __rela_dyn_end resolve
+ * to valid higher-half pointers we can dereference directly.
+ *
+ * Mirrors Linux arch/riscv/mm/init.c::relocate_kernel. Key invariant:
+ * even when kaslr_offset == 0, the slot store MUST still happen — the
+ * linker leaves R_RISCV_RELATIVE slots zero-filled at link time, and
+ * only relocate_kernel writes the link-time addend into them. Without
+ * this pass, .init_array entries (and every other relocated symbol)
+ * are NULL pointers.
+ */
+void __init relocate_kernel(void)
+{
+ auto *rela = (Elf64_Rela *)__rela_dyn_start;
+ auto *rela_end = (Elf64_Rela *)__rela_dyn_end;
+
+ const uint64 kaslr_offset = kernel_map.virt_offset;
+
+ for (; rela < rela_end; ++rela) {
+ if (ELF64_R_TYPE(rela->r_info) != R_RISCV_RELATIVE) {
+ continue;
+ }
+ auto *slot = (Elf64_Addr *)(rela->r_offset + kaslr_offset);
+ *slot = (Elf64_Addr)(rela->r_addend) + kaslr_offset;
+ }
+}
+
+/*
+ * relocate_enable_mmu — switch SATP, jump to higher-half, apply relocations.
+ *
+ * Called from head.S after setup_vm has filled kernel_map and built the
+ * page tables. Equivalent to Linux's same-named asmlinkage routine but
+ * written in C++ so it can share kernel_map / pgtable definitions with
+ * setup_vm directly.
+ *
+ * Three logical steps:
+ *
+ * 1. csrw satp ← trampoline_pg_dir. PC continues running at the
+ * identity PA, which the trampoline still maps; the high-half PA
+ * mapping is now also live.
+ *
+ * 2. la t0, .Lvirt; add t0, t0, va_kernel_pa_offset; jr t0.
+ * A PC-relative `la` under medany yields the runtime PA of the
+ * label; adding va_kernel_pa_offset (= link_va - load_pa) converts
+ * it to the link-time higher-half VA, which the trampoline maps.
+ * The `jr` then atomically pushes PC into higher-half space.
+ *
+ * 3. csrw satp ← early_pg_dir. We're now executing at higher-half
+ * VA, so the trampoline can be retired in favour of the full
+ * early page table. sp / ra / gp must be slid to higher-half too,
+ * otherwise the next ret-or-call uses physical-low addresses
+ * whose mapping is about to disappear when vmm_init drops the
+ * identity entries.
+ *
+ * 4. Call relocate_kernel — now safe, because all link-time VA
+ * pointers in __rela_dyn_start[..] dereference correctly.
+ */
+extern "C" void __init relocate_enable_mmu(pgd_t *pgdir)
+{
+ (void)pgdir; /* head.S passes early_pg_dir for documentation; we
+ * read the global symbol directly inside the asm. */
+
+ const unsigned long va_kernel_pa_offset = kernel_map.va_kernel_pa_offset;
+ const unsigned long satp_mode = SATP_MODE_SV39;
+
+ asm volatile (
+ /* Step 1: csrw satp <- trampoline */
+ "la t0, trampoline_pg_dir\n"
+ "srli t0, t0, 12\n"
+ "or t0, t0, %[mode]\n"
+ "csrw satp, t0\n"
+ "sfence.vma\n"
+
+ /* Step 2: jump to higher-half VA */
+ "la t0, 1f\n"
+ "add t0, t0, %[off]\n"
+ "jr t0\n"
+ "1:\n"
+
+ /* Step 3a: csrw satp <- early_pg_dir (now in higher-half) */
+ "la t0, early_pg_dir\n"
+ "srli t0, t0, 12\n"
+ "or t0, t0, %[mode]\n"
+ "csrw satp, t0\n"
+ "sfence.vma\n"
+
+ /* Step 3b: slide sp / ra / gp to higher-half */
+ "add sp, sp, %[off]\n"
+ "add ra, ra, %[off]\n"
+ ".option push\n"
+ ".option norelax\n"
+ "la gp, __global_pointer$\n"
+ ".option pop\n"
+ :
+ : [off] "r"(va_kernel_pa_offset),
+ [mode] "r"(satp_mode)
+ : "t0", "memory"
+ );
+
+ /* Step 4: apply .rela.dyn now that VA dereferences work. */
+ relocate_kernel();
+}
diff --git a/na/arch/riscv/mm/pmap.cc b/na/arch/riscv/mm/pmap.cc
index ae21d0e..f0a6848 100644
--- a/na/arch/riscv/mm/pmap.cc
+++ b/na/arch/riscv/mm/pmap.cc
@@ -21,6 +21,7 @@
#include <lume/memblock.h>
#include <arch/mmu.h>
#include <lume/klog.h>
+#include <lume/errno.h>
namespace pmap {
@@ -45,6 +46,13 @@ inline constexpr uint64 PTE_COW = 1ULL << 8;
inline constexpr int kLevels = 3;
inline constexpr int kPtePerPage = 512;
+/* SV39: upper half of VA space (kernel) starts at L2 index 256 */
+inline constexpr int kKernelPgdStart = kPtePerPage / 2;
+
+/* SATP PPN mask (44 bits) and page shift */
+inline constexpr uint64 kSatpPpnMask = 0xFFFFFFFFFFFULL;
+inline constexpr int kPageShift = 12;
+
/* Extract VPN[level] from a virtual address */
static inline uint64 vpn(uint64 va, int level)
{
@@ -101,13 +109,43 @@ static uint64 alloc_pt_page()
uint64 create()
{
- return alloc_pt_page();
+ uint64 new_root = alloc_pt_page();
+ if (!new_root) return 0;
+
+ /* Copy kernel mappings from the current kernel page table.
+ * In SV39, entries 256..511 (VPN[2] >= 256) cover the upper
+ * half of the 64-bit VA space where the kernel lives.
+ * Without these, switching to the new page table would
+ * immediately fault on any kernel VA access.
+ *
+ * Only do this after the runtime page table is active —
+ * during early boot the bootstrap PT doesn't cover all of
+ * RAM so pa_to_ptr on the kernel root would fault. */
+ if (g_runtime_mapping_ready) {
+ uint64 kernel_satp;
+ asm volatile("csrr %0, satp" : "=r"(kernel_satp));
+ uint64 kernel_root_pa = (kernel_satp & kSatpPpnMask) << kPageShift;
+
+ uint64 *new_l2 = pa_to_ptr(new_root);
+ uint64 *kern_l2 = pa_to_ptr(kernel_root_pa);
+
+ for (int i = kKernelPgdStart; i < kPtePerPage; i++) {
+ new_l2[i] = kern_l2[i];
+ }
+ }
+
+ return new_root;
}
void destroy(uint64 root_pa)
{
uint64 *l2 = pa_to_ptr(root_pa);
- for (int i = 0; i < kPtePerPage; i++) {
+
+ /* Only free the user half (entries 0..255).
+ * Entries 256..511 are shared kernel PGD pointers copied
+ * from the kernel page table — freeing them would corrupt
+ * the kernel's own mappings. */
+ for (int i = 0; i < kKernelPgdStart; i++) {
if (!(l2[i] & PTE_V))
continue;
/* Non-leaf L2 entry? (no R/W/X bits = pointer to L1 table) */
@@ -139,7 +177,7 @@ int map(uint64 root_pa, uint64 va, uint64 pa, uint64 perm)
if (!(table[idx] & PTE_V)) {
uint64 child_pa = alloc_pt_page();
if (!child_pa)
- return -12; /* -ENOMEM */
+ return -ENOMEM; /* -ENOMEM */
table[idx] = pa_to_pte(child_pa, PTE_V);
}
table = pa_to_ptr(pte_to_pa(table[idx]));
@@ -156,7 +194,7 @@ int map_2mb(uint64 root_pa, uint64 va, uint64 pa, uint64 perm)
/* VA and PA must be 2MB-aligned */
constexpr uint64 k2MB = 2ULL * 1024 * 1024;
if ((va & (k2MB - 1)) != 0 || (pa & (k2MB - 1)) != 0) {
- return -22; /* -EINVAL */
+ return -EINVAL; /* -EINVAL */
}
uint64 *table = pa_to_ptr(root_pa);
@@ -166,7 +204,7 @@ int map_2mb(uint64 root_pa, uint64 va, uint64 pa, uint64 perm)
if (!(table[idx2] & PTE_V)) {
uint64 child_pa = alloc_pt_page();
if (!child_pa) {
- return -12; /* -ENOMEM */
+ return -ENOMEM; /* -ENOMEM */
}
table[idx2] = pa_to_pte(child_pa, PTE_V);
}
@@ -197,7 +235,7 @@ int protect(uint64 root_pa, uint64 va, uint64 perm)
uint64 idx = vpn(va, level);
uint64 pte = table[idx];
if (!(pte & PTE_V))
- return -14; /* -EFAULT */
+ return -EFAULT; /* -EFAULT */
if (pte & (PTE_R | PTE_W | PTE_X)) {
uint64 pa = pte_to_pa(pte);
table[idx] = pa_to_pte(pa, perm | PTE_V);
@@ -205,7 +243,7 @@ int protect(uint64 root_pa, uint64 va, uint64 perm)
}
table = pa_to_ptr(pte_to_pa(pte));
}
- return -14;
+ return -EFAULT;
}
int protect_range(uint64 root_pa, uint64 start, uint64 end, uint64 perm)
@@ -250,6 +288,21 @@ bool lookup(uint64 root_pa, uint64 va, uint64 *pa_out)
return false;
}
+uint64 lookup_pte(uint64 root_pa, uint64 va)
+{
+ uint64 *table = pa_to_ptr(root_pa);
+ for (int level = 2; level >= 0; level--) {
+ uint64 idx = vpn(va, level);
+ uint64 pte = table[idx];
+ if (!(pte & PTE_V))
+ return 0;
+ if (pte & (PTE_R | PTE_W | PTE_X))
+ return pte; // leaf PTE raw value
+ table = pa_to_ptr(pte_to_pa(pte));
+ }
+ return 0;
+}
+
bool is_cow(uint64 root_pa, uint64 va)
{
uint64 *table = pa_to_ptr(root_pa);
@@ -273,7 +326,7 @@ int make_cow(uint64 root_pa, uint64 va)
uint64 idx = vpn(va, level);
uint64 pte = table[idx];
if (!(pte & PTE_V))
- return -14; /* -EFAULT */
+ return -EFAULT; /* -EFAULT */
if (pte & (PTE_R | PTE_W | PTE_X)) {
/* Clear write bit, set COW bit */
pte &= ~PTE_W;
@@ -283,7 +336,7 @@ int make_cow(uint64 root_pa, uint64 va)
}
table = pa_to_ptr(pte_to_pa(pte));
}
- return -14;
+ return -EFAULT;
}
void activate(uint64 root_pa)
diff --git a/na/arch/riscv/mm/tlb_md.cc b/na/arch/riscv/mm/tlb_md.cc
new file mode 100644
index 0000000..ed9aa29
--- /dev/null
+++ b/na/arch/riscv/mm/tlb_md.cc
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#include <arch/tlb.h>
+#include <arch/sbi.h>
+
+namespace arch::tlb {
+
+void flush_remote(uint64 cpu_mask, uint64 start, uint64 size) {
+ if (size == ~0ULL) {
+ arch::sbi::sbi_remote_sfence_vma(cpu_mask, 0, 0, ~0UL);
+ } else {
+ arch::sbi::sbi_remote_sfence_vma(cpu_mask, 0, start, size);
+ }
+}
+
+} // namespace arch::tlb
diff --git a/na/arch/riscv/module.mk b/na/arch/riscv/module.mk
index af1cc26..97d22de 100644
--- a/na/arch/riscv/module.mk
+++ b/na/arch/riscv/module.mk
@@ -1,16 +1,18 @@
# arch/riscv/module.mk
SRCS_CC += arch/riscv/mm/pmap.cc
+SRCS_CC += arch/riscv/mm/init.cc
SRCS_CC += arch/riscv/config.cc
SRCS_CC += arch/riscv/trap/trap_md.cc
SRCS_CC += arch/riscv/syscall/syscall_md.cc
SRCS_CC += arch/riscv/lib/random.cc
-SRCS_CC += arch/riscv/boot/compressed.cc
-SRCS_CC += arch/riscv/relocate.cc
-SRCS_S += arch/riscv/boot/entry.S
+SRCS_CC += arch/riscv/mm/tlb_md.cc
+SRCS_S += arch/riscv/kernel/head.S
SRCS_S += arch/riscv/trap/swtch.S
+SRCS_S += arch/riscv/trap/fpu.S
SRCS_S += arch/riscv/trap/trap_vector.S
SRCS_S += arch/riscv/uaccess/uaccess_memcpy.S
SRCS_CC += arch/riscv/trap/extable.cc
+SRCS_CC += arch/riscv/kernel/signal.cc
# Arch-specific headers (added to include path)
INCLUDES += -Iarch/riscv/include
diff --git a/na/arch/riscv/relocate.cc b/na/arch/riscv/relocate.cc
deleted file mode 100644
index 0216827..0000000
--- a/na/arch/riscv/relocate.cc
+++ /dev/null
@@ -1,100 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#include <lume/relocate.h>
-
-static inline bool range_ok(uint64 offset, uint64 size, uint64 image_size)
-{
- if (offset > image_size) {
- return false;
- }
- if (size > image_size - offset) {
- return false;
- }
- return true;
-}
-
-RelocStatus relocate_apply(uint64 load_base, uint64 image_size,
- uint64 link_base, const void* reloc_blob,
- uint64 reloc_size)
-{
- if (!reloc_blob || reloc_size == 0) {
- return RELOC_OK;
- }
- if (reloc_size < sizeof(RelocTableHeader)) {
- return RELOC_ERR_SIZE;
- }
-
- const auto* hdr = reinterpret_cast<const RelocTableHeader*>(reloc_blob);
- if (hdr->magic != kRelocTableMagic) {
- return RELOC_ERR_MAGIC;
- }
- if (hdr->version != kRelocTableVersion) {
- return RELOC_ERR_VERSION;
- }
- if (hdr->arch != RELOC_ARCH_RISCV) {
- return RELOC_ERR_ARCH;
- }
- if (hdr->entry_size < sizeof(RelocEntry) || (hdr->entry_size % 8) != 0) {
- return RELOC_ERR_SIZE;
- }
-
- uint64 max_entries = (reloc_size - sizeof(RelocTableHeader)) /
- static_cast<uint64>(hdr->entry_size);
- if (hdr->entry_count > max_entries) {
- return RELOC_ERR_SIZE;
- }
-
- if (image_size < sizeof(uint64)) {
- return RELOC_ERR_RANGE;
- }
-
- const uint64 delta = load_base - link_base;
- const uint8* p = reinterpret_cast<const uint8*>(reloc_blob) +
- sizeof(RelocTableHeader);
-
- for (uint32 i = 0; i < hdr->entry_count; ++i) {
- const auto* ent = reinterpret_cast<const RelocEntry*>(p);
- if ((ent->offset & 0x7) != 0 ||
- !range_ok(ent->offset, sizeof(uint64), image_size)) {
- return RELOC_ERR_RANGE;
- }
-
- uint64* target = reinterpret_cast<uint64*>(load_base + ent->offset);
- switch (ent->type) {
- case RELOC_RISCV_RELATIVE:
- *target = load_base + ent->addend;
- break;
- case RELOC_RISCV_64:
- *target = ent->addend + delta;
- break;
- default:
- return RELOC_ERR_UNSUPPORTED;
- }
- p += hdr->entry_size;
- }
-
- return RELOC_OK;
-}
-
-const char* reloc_status_str(RelocStatus st)
-{
- switch (st) {
- case RELOC_OK:
- return "ok";
- case RELOC_ERR_MAGIC:
- return "bad magic";
- case RELOC_ERR_VERSION:
- return "bad version";
- case RELOC_ERR_ARCH:
- return "arch mismatch";
- case RELOC_ERR_SIZE:
- return "size mismatch";
- case RELOC_ERR_RANGE:
- return "range";
- case RELOC_ERR_UNSUPPORTED:
- return "unsupported";
- default:
- return "unknown";
- }
-}
diff --git a/na/arch/riscv/syscall/syscall_md.cc b/na/arch/riscv/syscall/syscall_md.cc
index a52eb0e..26320c8 100644
--- a/na/arch/riscv/syscall/syscall_md.cc
+++ b/na/arch/riscv/syscall/syscall_md.cc
@@ -11,7 +11,9 @@
*/
#include <lume/syscall.h>
+#include <lume/syscall_nr.h>
#include <arch/trapframe.h>
+#include <arch/cpu.h>
void syscall_dispatch_md(arch::TrapFrame* tf)
{
@@ -22,7 +24,19 @@ void syscall_dispatch_md(arch::TrapFrame* tf)
tf->a[3], tf->a[4], tf->a[5],
};
+ /* Enable interrupts so timer and preemption work during syscalls */
+ arch::cpu::intr_on();
+
int64 ret = syscall_dispatch(nr, args);
+ arch::cpu::intr_off();
+
+ /* sys_rt_sigreturn restores the entire TrapFrame from the signal
+ * frame on the user stack. Writing ret into a[0] would clobber
+ * the restored value, so we must skip the writeback. */
+ if (nr == static_cast<uint64>(SYS_rt_sigreturn)){
+ return;
+ }
tf->a[0] = static_cast<uint64>(ret); /* Return value in a0 */
}
+
diff --git a/na/arch/riscv/trap/fpu.S b/na/arch/riscv/trap/fpu.S
new file mode 100644
index 0000000..90d96d9
--- /dev/null
+++ b/na/arch/riscv/trap/fpu.S
@@ -0,0 +1,103 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * FPU Save / Restore (RISC-V D extension)
+ *
+ * void fpu_save(FpState* state);
+ * Saves f0-f31 and fcsr to the FpState structure.
+ *
+ * void fpu_restore(FpState* state);
+ * Restores f0-f31 and fcsr from the FpState structure.
+ *
+ * FpState layout: f[0..31] at offset 0 (each 8 bytes), fcsr at offset 256.
+ *
+ * Called from schedule() when switching between tasks that have
+ * used the FPU (sstatus.FS == Dirty on save path).
+ */
+
+ .section .text
+
+/* ======================================================================
+ * void fpu_save(FpState* state); // a0 = state pointer
+ * ====================================================================== */
+ .globl fpu_save
+ .align 2
+fpu_save:
+ fsd f0, 0(a0)
+ fsd f1, 8(a0)
+ fsd f2, 16(a0)
+ fsd f3, 24(a0)
+ fsd f4, 32(a0)
+ fsd f5, 40(a0)
+ fsd f6, 48(a0)
+ fsd f7, 56(a0)
+ fsd f8, 64(a0)
+ fsd f9, 72(a0)
+ fsd f10, 80(a0)
+ fsd f11, 88(a0)
+ fsd f12, 96(a0)
+ fsd f13, 104(a0)
+ fsd f14, 112(a0)
+ fsd f15, 120(a0)
+ fsd f16, 128(a0)
+ fsd f17, 136(a0)
+ fsd f18, 144(a0)
+ fsd f19, 152(a0)
+ fsd f20, 160(a0)
+ fsd f21, 168(a0)
+ fsd f22, 176(a0)
+ fsd f23, 184(a0)
+ fsd f24, 192(a0)
+ fsd f25, 200(a0)
+ fsd f26, 208(a0)
+ fsd f27, 216(a0)
+ fsd f28, 224(a0)
+ fsd f29, 232(a0)
+ fsd f30, 240(a0)
+ fsd f31, 248(a0)
+ frcsr t0
+ sw t0, 256(a0)
+ ret
+
+/* ======================================================================
+ * void fpu_restore(FpState* state); // a0 = state pointer
+ * ====================================================================== */
+ .globl fpu_restore
+ .align 2
+fpu_restore:
+ fld f0, 0(a0)
+ fld f1, 8(a0)
+ fld f2, 16(a0)
+ fld f3, 24(a0)
+ fld f4, 32(a0)
+ fld f5, 40(a0)
+ fld f6, 48(a0)
+ fld f7, 56(a0)
+ fld f8, 64(a0)
+ fld f9, 72(a0)
+ fld f10, 80(a0)
+ fld f11, 88(a0)
+ fld f12, 96(a0)
+ fld f13, 104(a0)
+ fld f14, 112(a0)
+ fld f15, 120(a0)
+ fld f16, 128(a0)
+ fld f17, 136(a0)
+ fld f18, 144(a0)
+ fld f19, 152(a0)
+ fld f20, 160(a0)
+ fld f21, 168(a0)
+ fld f22, 176(a0)
+ fld f23, 184(a0)
+ fld f24, 192(a0)
+ fld f25, 200(a0)
+ fld f26, 208(a0)
+ fld f27, 216(a0)
+ fld f28, 224(a0)
+ fld f29, 232(a0)
+ fld f30, 240(a0)
+ fld f31, 248(a0)
+ lw t0, 256(a0)
+ fscsr t0
+ ret
diff --git a/na/arch/riscv/trap/trap_md.cc b/na/arch/riscv/trap/trap_md.cc
index f910b26..e30aea6 100644
--- a/na/arch/riscv/trap/trap_md.cc
+++ b/na/arch/riscv/trap/trap_md.cc
@@ -120,16 +120,45 @@ static TrapInfo build_trap_info(arch::TrapFrame* tf,
*
* Reads RISC-V CSRs, translates to MI TrapCause, applies arch-specific
* fixups, then calls MI trap_handle().
+ *
+ * After trap_handle returns, we must return to user mode via userret().
+ * The assembly jalr t3 that called us has NO return-to-user code after it,
+ * so trap_dispatch must never return normally.
* ======================================================================== */
-extern "C" void trap_dispatch(arch::TrapFrame* tf)
+extern "C" void userret(arch::TrapFrame* tf);
+
+extern "C" [[noreturn]] void trap_dispatch(arch::TrapFrame* tf)
{
+ /* Switch stvec to kernelvec immediately so that any traps during
+ * kernel execution (timer IRQs, page faults from copy_from_user)
+ * go through the kernel trap entry, not the user trap entry.
+ * This matches Linux which sets stvec to handle_exception (kernel)
+ * at the top of the user trap path. */
+ extern void kernelvec();
+ arch::trap::set_vector(reinterpret_cast<uint64>(&kernelvec));
+
+ /* Enable SUM so S-mode can access user pages (PTE_U).
+ * Without this, any dereference of user-space pointers in syscall
+ * handlers (sys_write buf, sys_read buf, etc.) would page fault. */
+ arch::trap::enable_sum();
+
uint64 scause = arch::trap::get_cause();
uint64 stval = arch::trap::get_value();
TrapOrigin origin = origin_from_sstatus(tf->sstatus);
TrapInfo info = build_trap_info(tf, scause, stval, origin);
+ /* Per-trap trace from user mode. Stored at KLOG_DEBUG / KLOG_SUBSYS_TRAP
+ * so it lives in the ring buffer for postmortem but stays off the console
+ * by default. No more in-band 20-trap window — flip console_level or
+ * dump the ring on demand instead. */
+ if (origin == TrapOrigin::FromUser) {
+ pr_debug_s(KLOG_SUBSYS_TRAP,
+ "trap: cause=%u epc=%llx val=%llx\n",
+ static_cast<unsigned>(info.cause), info.epc, info.val);
+ }
+
/* Save trapframe in TCB for syscall handlers (like fork) */
TaskControlBlock* curr = current_task();
if (curr) {
@@ -152,6 +181,13 @@ extern "C" void trap_dispatch(arch::TrapFrame* tf)
/* Call MI-layer dispatcher (zero inline assembly from here on) */
trap_handle(tf, info);
+
+ /* Return to user mode.
+ * Set stvec back to trap_vector for the next user-mode trap,
+ * then jump to userret which restores registers and does sret. */
+ arch::trap::set_vector(reinterpret_cast<uint64>(&trap_vector));
+ userret(tf);
+ __builtin_unreachable();
}
/* ========================================================================
diff --git a/na/arch/riscv/trap/trap_vector.S b/na/arch/riscv/trap/trap_vector.S
index 160b122..8e45cae 100644
--- a/na/arch/riscv/trap/trap_vector.S
+++ b/na/arch/riscv/trap/trap_vector.S
@@ -90,6 +90,15 @@ trap_vector:
/* Step 6: flush TLB after page table switch */
sfence.vma zero, zero
+ /* Step 6.5: restore kernel gp (global pointer).
+ * The compiler uses gp-relative addressing for global variable access
+ * (e.g. ld reg, offset(gp)). User mode may have set gp to anything,
+ * so we MUST reload it before entering any C++ code. */
+.option push
+.option norelax
+ la gp, __global_pointer$
+.option pop
+
/* Pass TrapFrame* to C++ handler (a0 = tf) */
mv a0, tp
@@ -97,7 +106,6 @@ trap_vector:
mv tp, t2
/* Step 7: jump to C++ trap_handler */
- /* Step 7: jump to C++ trap_handler */
jalr t3
diff --git a/na/drivers/base/bus.cc b/na/drivers/base/bus.cc
new file mode 100644
index 0000000..975f676
--- /dev/null
+++ b/na/drivers/base/bus.cc
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * drivers/base/bus.cc — bus_type registration.
+ *
+ * Mirrors Linux drivers/base/bus.c. Trivial in this iteration:
+ * initialize the device/driver lists and the per-bus spinlock.
+ *
+ * Reference: docs/specs/storage_stack.md §4.3
+ */
+
+#include <lume/device.h>
+#include <lume/klog.h>
+#include <lume/kprintf.h>
+#include <lume/errno.h>
+
+int bus_register(bus_type* bus)
+{
+ if (bus == nullptr || bus->name == nullptr) {
+ return -EINVAL;
+ }
+ if (bus->registered) {
+ return 0;
+ }
+ bus->devices.init();
+ bus->drivers.init();
+ bus->lock.init(bus->name);
+ bus->registered = true;
+ kprintf("[bus] '%s' registered\n", bus->name);
+ return 0;
+}
diff --git a/na/drivers/base/core.cc b/na/drivers/base/core.cc
new file mode 100644
index 0000000..f92b2a4
--- /dev/null
+++ b/na/drivers/base/core.cc
@@ -0,0 +1,156 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * drivers/base/core.cc — device core.
+ *
+ * Mirrors Linux drivers/base/core.c (greatly simplified). Provides
+ * device_initialize, device_register, device_unregister, and the
+ * try_bind helper shared with driver registration.
+ *
+ * Reference: docs/specs/storage_stack.md §4.3
+ */
+
+#include <lume/device.h>
+#include <lume/container_of.h>
+#include <lume/klog.h>
+#include <lume/kprintf.h>
+#include <lume/errno.h>
+#include <lume/list.h>
+
+/* ------------------------------------------------------------------ */
+/* device ktype — generic release (no-op; subclasses override) */
+/* ------------------------------------------------------------------ */
+
+static void device_release(kobject* kobj)
+{
+ /* Concrete device flavors (platform_device, virtio_device, ...) are
+ * expected to allocate via kmalloc and free in their own release
+ * paths after device_unregister + final kobject_put. The generic
+ * device kobj_type only fires for stack-allocated devices used in
+ * tests — nothing to do. */
+ (void)kobj;
+}
+
+static const kobj_type device_ktype = {
+ .release = device_release,
+};
+
+/* ------------------------------------------------------------------ */
+/* device_initialize / device_register */
+/* ------------------------------------------------------------------ */
+
+void device_initialize(device* dev)
+{
+ if (dev == nullptr) {
+ return;
+ }
+ kobject_init(&dev->kobj, &device_ktype);
+ dev->parent = nullptr;
+ dev->driver = nullptr;
+ dev->driver_data = nullptr;
+ dev->platform_data = nullptr;
+ dev->devt = 0;
+ dev->bus_list.init();
+ dev->driver_list.init();
+}
+
+const char* dev_name(const device* dev)
+{
+ if (dev == nullptr) {
+ return "(null)";
+ }
+ if (dev->kobj.name != nullptr) {
+ return dev->kobj.name;
+ }
+ if (dev->init_name != nullptr) {
+ return dev->init_name;
+ }
+ return "(unnamed)";
+}
+
+/* try_bind: attempt to match `dev` against `drv` and probe.
+ * Returns 1 on successful bind, 0 otherwise.
+ * Caller must hold bus->lock. */
+int device_try_bind(device* dev, device_driver* drv);
+
+int device_try_bind(device* dev, device_driver* drv)
+{
+ bus_type* bus = dev->bus;
+
+ if (bus->match != nullptr && bus->match(dev, drv) == 0) {
+ return 0;
+ }
+
+ dev->driver = drv;
+
+ int rc = 0;
+ if (bus->probe != nullptr) {
+ rc = bus->probe(dev);
+ } else if (drv->probe != nullptr) {
+ rc = drv->probe(dev);
+ }
+
+ if (rc == 0) {
+ list_add_tail(&dev->driver_list, &drv->devices);
+ return 1;
+ }
+ dev->driver = nullptr;
+ return 0;
+}
+
+int device_register(device* dev)
+{
+ if (dev == nullptr || dev->bus == nullptr) {
+ return -EINVAL;
+ }
+ if (!dev->bus->registered) {
+ kprintf("[device] register '%s' fail: bus '%s' not registered\n",
+ dev_name(dev),
+ dev->bus->name != nullptr ? dev->bus->name : "?");
+ return -ENODEV;
+ }
+
+ /* device_initialize is idempotent — safe to call again, but most
+ * paths will already have initialized. Detect via ktype set. */
+ if (dev->kobj.ktype == nullptr) {
+ device_initialize(dev);
+ }
+ if (dev->kobj.name == nullptr) {
+ kobject_add(&dev->kobj, nullptr, dev->init_name);
+ }
+ dev->bus_list.init();
+ dev->driver_list.init();
+
+ LockGuard g(dev->bus->lock);
+ list_add_tail(&dev->bus_list, &dev->bus->devices);
+
+ list_node* cur = dev->bus->drivers.next;
+ while (cur != &dev->bus->drivers) {
+ device_driver* drv =
+ list_entry<device_driver, &device_driver::bus_node>(cur);
+ cur = cur->next;
+ if (device_try_bind(dev, drv) != 0) {
+ break;
+ }
+ }
+ return 0;
+}
+
+void device_unregister(device* dev)
+{
+ if (dev == nullptr || dev->bus == nullptr) {
+ return;
+ }
+
+ LockGuard g(dev->bus->lock);
+
+ if (dev->driver != nullptr) {
+ if (dev->driver->remove != nullptr) {
+ dev->driver->remove(dev);
+ }
+ list_del_init(&dev->driver_list);
+ dev->driver = nullptr;
+ }
+ list_del_init(&dev->bus_list);
+}
diff --git a/na/drivers/base/driver.cc b/na/drivers/base/driver.cc
new file mode 100644
index 0000000..ce15b6d
--- /dev/null
+++ b/na/drivers/base/driver.cc
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * drivers/base/driver.cc — driver_register / driver_unregister.
+ *
+ * Mirrors Linux drivers/base/driver.c. Registers a device_driver on
+ * its bus and tries to bind any unbound matching devices.
+ *
+ * Reference: docs/specs/storage_stack.md §4.3
+ */
+
+#include <lume/device.h>
+#include <lume/container_of.h>
+#include <lume/klog.h>
+#include <lume/kprintf.h>
+#include <lume/errno.h>
+
+/* Implemented in core.cc — bind a device to a driver on its bus.
+ * Returns 1 on success, 0 on no-match-or-probe-fail. */
+int device_try_bind(device* dev, device_driver* drv);
+
+int driver_register(device_driver* drv)
+{
+ if (drv == nullptr || drv->bus == nullptr) {
+ return -EINVAL;
+ }
+ if (!drv->bus->registered) {
+ kprintf("[driver] register '%s' fail: bus '%s' not registered\n",
+ drv->name != nullptr ? drv->name : "?",
+ drv->bus->name != nullptr ? drv->bus->name : "?");
+ return -ENODEV;
+ }
+
+ drv->bus_node.init();
+ drv->devices.init();
+
+ LockGuard g(drv->bus->lock);
+ list_add_tail(&drv->bus_node, &drv->bus->drivers);
+
+ /* Try to bind any unbound devices already on the bus. */
+ list_node* cur = drv->bus->devices.next;
+ while (cur != &drv->bus->devices) {
+ device* dev = list_entry<device, &device::bus_list>(cur);
+ cur = cur->next;
+ if (dev->driver == nullptr) {
+ device_try_bind(dev, drv);
+ }
+ }
+ return 0;
+}
+
+void driver_unregister(device_driver* drv)
+{
+ if (drv == nullptr || drv->bus == nullptr) {
+ return;
+ }
+
+ LockGuard g(drv->bus->lock);
+
+ /* Unbind every device bound to this driver. */
+ while (!drv->devices.is_empty()) {
+ list_node* first = drv->devices.next;
+ device* dev = list_entry<device, &device::driver_list>(first);
+ if (drv->remove != nullptr) {
+ drv->remove(dev);
+ }
+ list_del_init(&dev->driver_list);
+ dev->driver = nullptr;
+ }
+ list_del_init(&drv->bus_node);
+}
diff --git a/na/drivers/base/module.mk b/na/drivers/base/module.mk
new file mode 100644
index 0000000..1d8fb05
--- /dev/null
+++ b/na/drivers/base/module.mk
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2026 Richard Qin
+#
+# Driver model core (mirrors Linux drivers/base/)
+
+SRCS_CC += drivers/base/bus.cc
+SRCS_CC += drivers/base/core.cc
+SRCS_CC += drivers/base/driver.cc
+SRCS_CC += drivers/base/platform.cc
diff --git a/na/drivers/base/platform.cc b/na/drivers/base/platform.cc
new file mode 100644
index 0000000..65ec21e
--- /dev/null
+++ b/na/drivers/base/platform.cc
@@ -0,0 +1,231 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * drivers/base/platform.cc — platform bus + FDT-driven device creation.
+ *
+ * Mirrors Linux drivers/base/platform.c. Provides:
+ * - platform_bus — the bus_type instance
+ * - platform_match — compatible-string matching via of_match_table
+ * - platform_dispatch — bus->probe forwarder that down-casts and calls
+ * platform_driver::probe(platform_device*)
+ * - driver_probe_all() — boot entry: register bus, register linker-set
+ * drivers, walk FDT, register platform_devices
+ *
+ * Reference: docs/specs/storage_stack.md §4.3
+ */
+
+#include <lume/platform_device.h>
+#include <lume/driver.h>
+#include <lume/device_node.h>
+#include <lume/fdt.h>
+#include <string.h>
+
+/* kmalloc is declared extern "C" in <lume/slab.h>, but mm/slab.cc defines
+ * it WITHOUT extern "C" (i.e. C++-mangled). Declare locally with C++
+ * linkage so the symbol matches what mm/slab.o exports. */
+void* kmalloc(uint32 size);
+#include <lume/klog.h>
+#include <lume/kprintf.h>
+#include <lume/errno.h>
+#include <lume/container_of.h>
+
+/* ------------------------------------------------------------------ */
+/* Linker-section registry */
+/* ------------------------------------------------------------------ */
+
+extern "C" {
+ extern platform_driver* __drivers_start[];
+ extern platform_driver* __drivers_end[];
+}
+
+/* ------------------------------------------------------------------ */
+/* platform_bus_type */
+/* ------------------------------------------------------------------ */
+
+static int platform_match(device* dev, device_driver* drv)
+{
+ platform_device* pdev = to_platform_device(dev);
+ platform_driver* pdrv = to_platform_driver(drv);
+
+ if (pdrv->of_match_table == nullptr || pdev->of_node == nullptr) {
+ return 0;
+ }
+ const char* node_compat = pdev->of_node->get_prop_string("compatible");
+ if (node_compat == nullptr) {
+ return 0;
+ }
+ for (const of_device_id* id = pdrv->of_match_table;
+ id->compatible != nullptr; ++id) {
+ if (strcmp(id->compatible, node_compat) == 0) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int platform_dispatch(device* dev)
+{
+ platform_device* pdev = to_platform_device(dev);
+ platform_driver* pdrv = to_platform_driver(dev->driver);
+ if (pdrv->probe == nullptr) {
+ return 0;
+ }
+ return pdrv->probe(pdev);
+}
+
+bus_type platform_bus = {
+ .name = "platform",
+ .match = platform_match,
+ .probe = platform_dispatch,
+ .devices = {},
+ .drivers = {},
+ .lock = {},
+ .registered = false,
+};
+
+void platform_bus_init()
+{
+ bus_register(&platform_bus);
+}
+
+/* ------------------------------------------------------------------ */
+/* platform_driver_register / platform_device_register */
+/* ------------------------------------------------------------------ */
+
+int platform_driver_register(platform_driver* pdrv)
+{
+ if (pdrv == nullptr) {
+ return -EINVAL;
+ }
+ pdrv->driver.bus = &platform_bus;
+ /* driver.probe is unused — we route through bus->probe. Set name
+ * from the first of_match if not already provided. */
+ if (pdrv->driver.name == nullptr && pdrv->of_match_table != nullptr) {
+ pdrv->driver.name = pdrv->of_match_table[0].compatible;
+ }
+ return driver_register(&pdrv->driver);
+}
+
+int platform_device_register(platform_device* pdev)
+{
+ if (pdev == nullptr) {
+ return -EINVAL;
+ }
+ pdev->dev.bus = &platform_bus;
+ if (pdev->dev.init_name == nullptr && pdev->of_node != nullptr) {
+ pdev->dev.init_name = pdev->of_node->get_prop_string("compatible");
+ }
+ device_initialize(&pdev->dev);
+ return device_register(&pdev->dev);
+}
+
+/* ------------------------------------------------------------------ */
+/* FDT-driven device instantiation */
+/* ------------------------------------------------------------------ */
+
+static platform_device* alloc_platform_device_from_node(DeviceNode* node,
+ const char* compat)
+{
+ auto* pdev = static_cast<platform_device*>(
+ kmalloc(static_cast<uint32>(sizeof(platform_device))));
+ if (pdev == nullptr) {
+ return nullptr;
+ }
+ memset(pdev, 0, sizeof(*pdev));
+ pdev->of_node = node;
+ pdev->mmio_base = node->get_prop_u64("reg", 0);
+ /* "reg" second cell is size — for now we don't bother parsing it
+ * separately; drivers that need it pull from of_node directly. */
+ pdev->mmio_size = 0;
+ pdev->irq = -1;
+ pdev->dev.init_name = compat;
+ return pdev;
+}
+
+/* For each registered platform_driver, walk its of_match_table and
+ * spawn a platform_device for each FDT compatible match found.
+ *
+ * NOTE: This is a simplified FDT walker — it only finds the FIRST FDT
+ * node matching each compatible string (delegating to
+ * fdt_find_compatible). Devices with multiple FDT instances of the
+ * same compatible will only see one bound here. A full FDT tree walk
+ * is left for when virtio-mmio or multi-instance UART arrives.
+ */
+static void instantiate_devices_for_driver(platform_driver* pdrv)
+{
+ if (pdrv == nullptr || pdrv->of_match_table == nullptr) {
+ return;
+ }
+ for (const of_device_id* id = pdrv->of_match_table;
+ id->compatible != nullptr; ++id) {
+ DeviceNode* node = fdt_find_compatible(id->compatible);
+ if (node == nullptr) {
+ continue;
+ }
+ platform_device* pdev =
+ alloc_platform_device_from_node(node, id->compatible);
+ if (pdev == nullptr) {
+ kprintf("[platform] OOM allocating pdev for %s\n", id->compatible);
+ continue;
+ }
+ kprintf("[platform] instantiating '%s' -> pdev mmio=0x%llx\n",
+ id->compatible, pdev->mmio_base);
+ platform_device_register(pdev);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Boot entry — replaces the old driver_probe_all() */
+/* ------------------------------------------------------------------ */
+
+void driver_probe_all()
+{
+ kprintf("[dpa] step 1: &platform_bus=%p (link 0xffffffc08033a0a8)\n",
+ &platform_bus);
+ kprintf("[dpa] step 1: pre-touch platform_bus.name slot...\n");
+ const char** name_slot = &platform_bus.name;
+ kprintf("[dpa] step 1: name_slot=%p\n", name_slot);
+ kprintf("[dpa] step 1: about to deref *name_slot\n");
+ const char* nm = *name_slot;
+ kprintf("[dpa] step 1: *name_slot=%p\n", nm);
+ kprintf("[dpa] step 1: about to read first byte of nm\n");
+ char first = nm != nullptr ? nm[0] : '!';
+ kprintf("[dpa] step 1: nm[0]='%c'\n", first);
+
+ kprintf("[dpa] step 1: platform_bus_init enter\n");
+ platform_bus_init();
+ kprintf("[dpa] step 1: platform_bus_init done\n");
+
+ platform_driver** start = __drivers_start;
+ platform_driver** end = __drivers_end;
+ kprintf("[dpa] step 2: __drivers_start=%p __drivers_end=%p count=%lld\n",
+ start, end, (long long)(end - start));
+
+ for (platform_driver** slot = start; slot < end; ++slot) {
+ kprintf("[dpa] step 2: slot=%p *slot=%p\n", slot, *slot);
+ if (*slot != nullptr) {
+ platform_driver* pdrv = *slot;
+ kprintf("[dpa] step 2: pdrv->of_match_table=%p\n",
+ pdrv->of_match_table);
+ if (pdrv->of_match_table != nullptr) {
+ kprintf("[dpa] step 2: pdrv->of_match_table[0].compatible=%p\n",
+ pdrv->of_match_table[0].compatible);
+ if (pdrv->of_match_table[0].compatible != nullptr) {
+ kprintf("[dpa] step 2: compatible='%s'\n",
+ pdrv->of_match_table[0].compatible);
+ }
+ }
+ platform_driver_register(pdrv);
+ kprintf("[dpa] step 2: registered\n");
+ }
+ }
+
+ kprintf("[dpa] step 3: instantiate devices\n");
+ for (platform_driver** slot = start; slot < end; ++slot) {
+ if (*slot != nullptr) {
+ instantiate_devices_for_driver(*slot);
+ }
+ }
+ kprintf("[dpa] done\n");
+}
diff --git a/na/drivers/console.cc b/na/drivers/console.cc
new file mode 100644
index 0000000..f741dd2
--- /dev/null
+++ b/na/drivers/console.cc
@@ -0,0 +1,119 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * console.cc — Console character device (/dev/console)
+ *
+ * Wraps the existing early_putc() UART driver as a file_operations
+ * chardev. read() is a stub for now (returns 0 = EOF).
+ * write() sends each byte through early_putc().
+ */
+
+#include <lume/file.h>
+#include <lume/chardev.h>
+#include <lume/types.h>
+
+extern "C" void early_putc(char c);
+
+/* ================================================================== */
+/* console_write */
+/* ================================================================== */
+
+static int64 console_write(file* fp, const char* buf, uint64 count, int64* pos)
+{
+ (void)fp;
+ (void)pos;
+
+ for (uint64 i = 0; i < count; i++) {
+ early_putc(buf[i]);
+ }
+
+ return static_cast<int64>(count);
+}
+
+/* ================================================================== */
+/* console_read — stub (returns 0 = EOF for now) */
+/* ================================================================== */
+
+static int64 console_read(file* fp, char* buf, uint64 count, int64* pos)
+{
+ (void)fp;
+ (void)buf;
+ (void)count;
+ (void)pos;
+ /* TODO: implement UART RX polling/IRQ-based blocking read */
+ return 0;
+}
+
+/* ================================================================== */
+/* null device — /dev/null */
+/* ================================================================== */
+
+static int64 null_read(file* /*fp*/, char* /*buf*/, uint64 /*count*/, int64* /*pos*/)
+{
+ return 0; /* EOF */
+}
+
+static int64 null_write(file* /*fp*/, const char* /*buf*/, uint64 count, int64* /*pos*/)
+{
+ return static_cast<int64>(count); /* discard */
+}
+
+/* ================================================================== */
+/* zero device — /dev/zero */
+/* ================================================================== */
+
+static int64 zero_read(file* /*fp*/, char* buf, uint64 count, int64* /*pos*/)
+{
+ for (uint64 i = 0; i < count; i++) {
+ buf[i] = 0;
+ }
+ return static_cast<int64>(count);
+}
+
+static int64 zero_write(file* /*fp*/, const char* /*buf*/, uint64 count, int64* /*pos*/)
+{
+ return static_cast<int64>(count); /* discard */
+}
+
+/* ================================================================== */
+/* Registration — runtime init (no static initializers!) */
+/* ================================================================== */
+
+static file_operations console_fops;
+static file_operations null_fops;
+static file_operations zero_fops;
+
+static cdev console_cdev;
+static cdev null_cdev;
+static cdev zero_cdev;
+
+void console_cdev_init()
+{
+ /* Console: major=5, minor=1 */
+ console_fops.read = console_read;
+ console_fops.write = console_write;
+
+ console_cdev.name = "console";
+ console_cdev.dev = MKDEV(5, 1);
+ console_cdev.fops = &console_fops;
+ cdev_register(&console_cdev);
+
+ /* /dev/null: major=1, minor=3 */
+ null_fops.read = null_read;
+ null_fops.write = null_write;
+
+ null_cdev.name = "null";
+ null_cdev.dev = MKDEV(1, 3);
+ null_cdev.fops = &null_fops;
+ cdev_register(&null_cdev);
+
+ /* /dev/zero: major=1, minor=5 */
+ zero_fops.read = zero_read;
+ zero_fops.write = zero_write;
+
+ zero_cdev.name = "zero";
+ zero_cdev.dev = MKDEV(1, 5);
+ zero_cdev.fops = &zero_fops;
+ cdev_register(&zero_cdev);
+}
diff --git a/na/drivers/driver.cc b/na/drivers/driver.cc
deleted file mode 100644
index 5810645..0000000
--- a/na/drivers/driver.cc
+++ /dev/null
@@ -1,27 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#include <lume/driver.h>
-#include <lume/fdt.h>
-#include <lume/kprintf.h>
-
-extern "C" {
- extern DriverEntry __drivers_start[];
- extern DriverEntry __drivers_end[];
-}
-
-void driver_probe_all() {
- DeviceNode* root = fdt_get_node_by_path("/");
- if (!root) {
- kprintf("[driver] WARN: No FDT root found\n");
- return;
- }
-
- for (DriverEntry* drv = __drivers_start; drv < __drivers_end; ++drv) {
- DeviceNode* node = fdt_find_compatible(drv->compatible);
- if (node) {
- kprintf("[driver] Probing %s\n", drv->compatible);
- drv->probe(node);
- }
- }
-}
diff --git a/na/drivers/module.mk b/na/drivers/module.mk
index 6e1396c..9326f30 100644
--- a/na/drivers/module.mk
+++ b/na/drivers/module.mk
@@ -4,7 +4,7 @@
# Drivers module
SRCS_CC += drivers/uart.cc
-SRCS_CC += drivers/driver.cc
+SRCS_CC += drivers/console.cc
ifeq ($(ARCH),riscv)
SRCS_CC += drivers/plic.cc
diff --git a/na/drivers/plic.cc b/na/drivers/plic.cc
index ba6ac44..2818700 100644
--- a/na/drivers/plic.cc
+++ b/na/drivers/plic.cc
@@ -25,7 +25,7 @@
#include <lume/addr.h>
#include <lume/config.h>
#include <lume/fdt.h>
-#include <lume/driver.h>
+#include <lume/platform_device.h>
#include <lume/irq.h>
#include <lume/types.h>
#include <lume/vmm.h>
@@ -33,6 +33,8 @@
/* PLIC virtual base (after MMIO mapping) */
static uint64 plic_va;
+extern "C" uint32 bsp_hart_id;
+
/* PLIC register offsets */
static constexpr uint64 PLIC_PRIORITY_BASE = 0x000000;
static constexpr uint64 PLIC_PENDING_BASE = 0x001000;
@@ -159,7 +161,8 @@ static void plic_probe(DeviceNode* node) {
vmm_map_kernel_mmio(plic_pa, 0x4000000);
plic_va = pa_to_va(phys_addr(plic_pa)).raw;
- uint64 ctx = s_context(0); /* BSP = hart 0 */
+ /* Use the actual BSP hart ID (it may not be hart 0) */
+ uint64 ctx = s_context(bsp_hart_id);
/* Set priority for all sources to 1 (minimum non-zero = enabled) */
for (uint32 irq = 1; irq < IrqManager::kMaxIrqs; irq++) {
@@ -233,9 +236,24 @@ static const IrqChip plic_chip = {
plic_set_priority
};
-static void plic_probe_wrapper(DeviceNode* node) {
- plic_probe(node);
+static int plic_platform_probe(platform_device* pdev) {
+ plic_probe(pdev->of_node);
g_irq_manager.set_chip(&plic_chip);
+ return 0;
}
-REGISTER_DRIVER("riscv,plic0", plic_probe_wrapper);
+static const of_device_id plic_of_match[] = {
+ { .compatible = "riscv,plic0", .data = nullptr },
+ { .compatible = nullptr, .data = nullptr }, /* sentinel */
+};
+
+platform_driver plic_driver = {
+ .driver = { .name = "plic", .bus = nullptr,
+ .probe = nullptr, .remove = nullptr,
+ .bus_node = {}, .devices = {} },
+ .probe = plic_platform_probe,
+ .remove = nullptr,
+ .of_match_table = plic_of_match,
+};
+
+PLATFORM_DRIVER(plic_driver);
diff --git a/na/drivers/tty/module.mk b/na/drivers/tty/module.mk
new file mode 100644
index 0000000..49190d4
--- /dev/null
+++ b/na/drivers/tty/module.mk
@@ -0,0 +1,5 @@
+# drivers/tty/module.mk — TTY subsystem build rules
+
+SRCS_CC += drivers/tty/tty_core.cc
+SRCS_CC += drivers/tty/n_tty.cc
+SRCS_CC += drivers/tty/tty_uart.cc
diff --git a/na/drivers/tty/n_tty.cc b/na/drivers/tty/n_tty.cc
new file mode 100644
index 0000000..0e312b0
--- /dev/null
+++ b/na/drivers/tty/n_tty.cc
@@ -0,0 +1,539 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * n_tty.cc — N_TTY line discipline
+ *
+ * Implements the standard terminal line discipline:
+ *
+ * Canonical mode (ICANON):
+ * Input is buffered until newline/EOF. Backspace (VERASE) and
+ * kill-line (VKILL) editing are supported.
+ *
+ * Raw mode (!ICANON):
+ * Characters are available immediately (subject to VMIN/VTIME).
+ *
+ * Signal generation (ISIG):
+ * VINTR → SIGINT, VQUIT → SIGQUIT, VSUSP → SIGTSTP
+ *
+ * Echo (ECHO):
+ * Typed characters are reflected to output.
+ *
+ * Input processing (c_iflag):
+ * ICRNL: CR → NL mapping
+ * INLCR: NL → CR mapping
+ * IGNCR: ignore CR
+ * ISTRIP: strip high bit
+ *
+ * Output processing (c_oflag):
+ * OPOST + ONLCR: NL → CR+NL on output
+ *
+ * Reference: Linux drivers/tty/n_tty.c
+ */
+
+#include <lume/tty.h>
+#include <lume/tty_ioctl.h>
+#include <lume/signal.h>
+#include <lume/signal_nr.h>
+#include <lume/slab.h>
+#include <lume/new.h>
+#include <lume/errno.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+
+#ifndef ENOIOCTLCMD
+#define ENOIOCTLCMD 515
+#endif
+
+extern void schedule();
+
+/* ================================================================== */
+/* N_TTY internal data */
+/* ================================================================== */
+
+inline constexpr int N_TTY_BUF_SIZE = 4096;
+
+struct n_tty_data {
+ char read_buf[N_TTY_BUF_SIZE];
+ int read_head; /* producer writes here (from receive_buf) */
+ int read_tail; /* consumer reads from here (from read()) */
+ int canon_head; /* in canonical mode: end of last complete line */
+ bool eof_pending; /* Ctrl+D was pressed */
+ Spinlock lock;
+};
+
+static inline int buf_count(const n_tty_data* d)
+{
+ return (d->read_head - d->read_tail + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+}
+
+static inline int canon_count(const n_tty_data* d)
+{
+ return (d->canon_head - d->read_tail + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+}
+
+static inline bool buf_full(const n_tty_data* d)
+{
+ return buf_count(d) >= N_TTY_BUF_SIZE - 1;
+}
+
+static inline void buf_put(n_tty_data* d, char c)
+{
+ d->read_buf[d->read_head] = c;
+ d->read_head = (d->read_head + 1) % N_TTY_BUF_SIZE;
+}
+
+static inline char buf_get(n_tty_data* d)
+{
+ char c = d->read_buf[d->read_tail];
+ d->read_tail = (d->read_tail + 1) % N_TTY_BUF_SIZE;
+ return c;
+}
+
+/* ================================================================== */
+/* Output helpers */
+/* ================================================================== */
+
+static void tty_put_char(tty_struct* tty, char c)
+{
+ if (tty->driver && tty->driver->ops && tty->driver->ops->put_char) {
+ tty->driver->ops->put_char(tty, c);
+ }
+}
+
+/*
+ * Process and output a single character, applying c_oflag transformations.
+ */
+static void tty_output_char(tty_struct* tty, char c)
+{
+ if (tty->termios_data.c_oflag & OPOST) {
+ if (c == '\n' && (tty->termios_data.c_oflag & ONLCR)) {
+ tty_put_char(tty, '\r');
+ tty_put_char(tty, '\n');
+ return;
+ }
+ if (c == '\r' && (tty->termios_data.c_oflag & OCRNL)) {
+ tty_put_char(tty, '\n');
+ return;
+ }
+ }
+ tty_put_char(tty, c);
+}
+
+/*
+ * Echo a character back to the terminal.
+ * Control characters are displayed as ^X.
+ */
+static void echo_char(tty_struct* tty, char c)
+{
+ if (!(tty->termios_data.c_lflag & ECHO))
+ return;
+
+ if (c == '\n') {
+ tty_output_char(tty, '\n');
+ return;
+ }
+
+ if (c == '\t') {
+ tty_put_char(tty, '\t');
+ return;
+ }
+
+ /* Control characters: echo as ^X */
+ if ((tty->termios_data.c_lflag & ECHOCTL) && c < 0x20 && c != '\t' && c != '\n') {
+ tty_put_char(tty, '^');
+ tty_put_char(tty, c + '@');
+ return;
+ }
+
+ if (c == 0x7f) { /* DEL */
+ tty_put_char(tty, '^');
+ tty_put_char(tty, '?');
+ return;
+ }
+
+ tty_output_char(tty, c);
+}
+
+/*
+ * Erase one character visually (backspace + space + backspace).
+ */
+static void erase_char(tty_struct* tty, char erased)
+{
+ if (!(tty->termios_data.c_lflag & ECHO))
+ return;
+
+ if ((tty->termios_data.c_lflag & ECHOE)) {
+ /* If the erased char was a control char displayed as ^X, erase 2 cols */
+ if ((erased < 0x20 && erased != '\t' && erased != '\n') || erased == 0x7f) {
+ tty_put_char(tty, '\b');
+ tty_put_char(tty, ' ');
+ tty_put_char(tty, '\b');
+ tty_put_char(tty, '\b');
+ tty_put_char(tty, ' ');
+ tty_put_char(tty, '\b');
+ } else {
+ tty_put_char(tty, '\b');
+ tty_put_char(tty, ' ');
+ tty_put_char(tty, '\b');
+ }
+ }
+}
+
+/* ================================================================== */
+/* Input processing — receive_buf (called from IRQ or driver) */
+/* ================================================================== */
+
+static void n_tty_receive_buf(tty_struct* tty, const char* cp, int count)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return;
+
+ const termios& t = tty->termios_data;
+
+ LockGuard guard(d->lock);
+
+ for (int i = 0; i < count; i++) {
+ char c = cp[i];
+
+ /* Strip high bit if requested */
+ if (t.c_iflag & ISTRIP)
+ c &= 0x7f;
+
+ /* Input CR/NL processing */
+ if (c == '\r') {
+ if (t.c_iflag & IGNCR)
+ continue;
+ if (t.c_iflag & ICRNL)
+ c = '\n';
+ } else if (c == '\n') {
+ if (t.c_iflag & INLCR)
+ c = '\r';
+ }
+
+ /* Signal generation */
+ if (t.c_lflag & ISIG) {
+ if (c == t.c_cc[VINTR]) {
+ tty_signal_pgrp(tty, SIGINT);
+ echo_char(tty, c);
+ if (!(t.c_lflag & NOFLSH)) {
+ /* Flush input buffer */
+ d->read_head = d->read_tail;
+ d->canon_head = d->read_tail;
+ }
+ continue;
+ }
+ if (c == t.c_cc[VQUIT]) {
+ tty_signal_pgrp(tty, SIGQUIT);
+ echo_char(tty, c);
+ if (!(t.c_lflag & NOFLSH)) {
+ d->read_head = d->read_tail;
+ d->canon_head = d->read_tail;
+ }
+ continue;
+ }
+ if (c == t.c_cc[VSUSP]) {
+ tty_signal_pgrp(tty, SIGTSTP);
+ echo_char(tty, c);
+ continue;
+ }
+ }
+
+ /* Canonical mode processing */
+ if (t.c_lflag & ICANON) {
+ /* EOF (Ctrl+D) */
+ if (c == t.c_cc[VEOF]) {
+ d->eof_pending = true;
+ d->canon_head = d->read_head;
+ tty->read_wait.wake_all();
+ continue;
+ }
+
+ /* ERASE (backspace/DEL) */
+ if (c == t.c_cc[VERASE]) {
+ if (d->read_head != d->canon_head &&
+ d->read_head != d->read_tail) {
+ int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+ char erased = d->read_buf[prev];
+ d->read_head = prev;
+ erase_char(tty, erased);
+ }
+ continue;
+ }
+
+ /* KILL (Ctrl+U — erase entire line) */
+ if (c == t.c_cc[VKILL]) {
+ while (d->read_head != d->canon_head &&
+ d->read_head != d->read_tail) {
+ int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+ char erased = d->read_buf[prev];
+ d->read_head = prev;
+ erase_char(tty, erased);
+ }
+ if (t.c_lflag & ECHOK) {
+ tty_output_char(tty, '\n');
+ }
+ continue;
+ }
+
+ /* WERASE (Ctrl+W — erase word) */
+ if ((t.c_lflag & IEXTEN) && c == t.c_cc[VWERASE]) {
+ /* Skip trailing spaces */
+ while (d->read_head != d->canon_head) {
+ int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+ if (d->read_buf[prev] != ' ' && d->read_buf[prev] != '\t')
+ break;
+ erase_char(tty, d->read_buf[prev]);
+ d->read_head = prev;
+ }
+ /* Erase word */
+ while (d->read_head != d->canon_head) {
+ int prev = (d->read_head - 1 + N_TTY_BUF_SIZE) % N_TTY_BUF_SIZE;
+ if (d->read_buf[prev] == ' ' || d->read_buf[prev] == '\t')
+ break;
+ erase_char(tty, d->read_buf[prev]);
+ d->read_head = prev;
+ }
+ continue;
+ }
+
+ /* REPRINT (Ctrl+R) */
+ if ((t.c_lflag & IEXTEN) && c == t.c_cc[VREPRINT]) {
+ tty_output_char(tty, '\n');
+ int pos = d->canon_head;
+ while (pos != d->read_head) {
+ echo_char(tty, d->read_buf[pos]);
+ pos = (pos + 1) % N_TTY_BUF_SIZE;
+ }
+ continue;
+ }
+
+ /* Regular character — buffer it */
+ if (!buf_full(d)) {
+ buf_put(d, c);
+ echo_char(tty, c);
+ }
+
+ /* Line complete on newline or EOL */
+ if (c == '\n' || c == t.c_cc[VEOL] ||
+ (t.c_cc[VEOL2] && c == t.c_cc[VEOL2])) {
+ d->canon_head = d->read_head;
+ tty->read_wait.wake_all();
+ }
+ } else {
+ /* Raw / non-canonical mode */
+ if (!buf_full(d)) {
+ buf_put(d, c);
+ echo_char(tty, c);
+ tty->read_wait.wake_all();
+ }
+ }
+ }
+}
+
+/* ================================================================== */
+/* N_TTY read — blocking read from the input buffer */
+/* ================================================================== */
+
+static int n_tty_read(tty_struct* tty, file* fp, char* buf, uint64 count)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return -EIO;
+
+ const termios& t = tty->termios_data;
+ TaskControlBlock* curr = current_task();
+ int copied = 0;
+
+ if (t.c_lflag & ICANON) {
+ /* Canonical mode: block until a complete line is available */
+ while (canon_count(d) == 0 && !d->eof_pending) {
+ if (tty->flags & TTY_IO_ERROR)
+ return -EIO;
+
+ /* Check for pending signals — return -EINTR so the
+ * trap return path can deliver them via do_signal() */
+ if (signal_pending(curr))
+ return -EINTR;
+
+ tty->read_wait.prepare_to_wait(curr, TaskState::Sleeping);
+ if (canon_count(d) == 0 && !d->eof_pending)
+ schedule();
+ tty->read_wait.finish_wait(curr);
+ }
+
+ if (d->eof_pending && canon_count(d) == 0) {
+ d->eof_pending = false;
+ return 0; /* EOF */
+ }
+
+ /* Copy up to one line */
+ LockGuard guard(d->lock);
+ while (copied < static_cast<int>(count) && d->read_tail != d->canon_head) {
+ buf[copied++] = buf_get(d);
+ }
+ } else {
+ /* Non-canonical mode */
+ int vmin = t.c_cc[VMIN];
+ /* VTIME not implemented (would need timers) */
+
+ if (vmin == 0) vmin = 1;
+
+ while (buf_count(d) < vmin) {
+ if (tty->flags & TTY_IO_ERROR)
+ return -EIO;
+
+ if (signal_pending(curr))
+ return -EINTR;
+
+ tty->read_wait.prepare_to_wait(curr, TaskState::Sleeping);
+ if (buf_count(d) < vmin)
+ schedule();
+ tty->read_wait.finish_wait(curr);
+ }
+
+ LockGuard guard(d->lock);
+ while (copied < static_cast<int>(count) && buf_count(d) > 0) {
+ buf[copied++] = buf_get(d);
+ }
+ }
+
+ return copied;
+}
+
+/* ================================================================== */
+/* N_TTY write — output through the driver with post-processing */
+/* ================================================================== */
+
+static int n_tty_write(tty_struct* tty, file* fp, const char* buf, uint64 count)
+{
+ if (!tty->driver || !tty->driver->ops || !tty->driver->ops->write)
+ return -EIO;
+
+ /* Apply output processing (OPOST) */
+ if (tty->termios_data.c_oflag & OPOST) {
+ for (uint64 i = 0; i < count; i++) {
+ tty_output_char(tty, buf[i]);
+ }
+ return static_cast<int>(count);
+ }
+
+ /* Raw output: pass directly to driver */
+ return tty->driver->ops->write(tty, buf, static_cast<int>(count));
+}
+
+/* ================================================================== */
+/* N_TTY ioctl */
+/* ================================================================== */
+
+static int n_tty_ioctl(tty_struct* tty, unsigned int cmd, uint64 arg)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+
+ switch (cmd) {
+ case FIONREAD: {
+ int count = buf_count(d);
+ auto* p = reinterpret_cast<int*>(arg);
+ *p = count;
+ return 0;
+ }
+ default:
+ return -ENOIOCTLCMD;
+ }
+}
+
+/* ================================================================== */
+/* N_TTY open / close */
+/* ================================================================== */
+
+static int n_tty_open(tty_struct* tty)
+{
+ auto* d = new n_tty_data{};
+ if (!d) return -ENOMEM;
+
+ d->read_head = 0;
+ d->read_tail = 0;
+ d->canon_head = 0;
+ d->eof_pending = false;
+ d->lock.init("n_tty");
+
+ tty->ldisc.data = d;
+ return 0;
+}
+
+static void n_tty_close(tty_struct* tty)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (d) {
+ delete d;
+ tty->ldisc.data = nullptr;
+ }
+}
+
+static void n_tty_flush_buffer(tty_struct* tty)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return;
+ LockGuard guard(d->lock);
+ d->read_head = 0;
+ d->read_tail = 0;
+ d->canon_head = 0;
+ d->eof_pending = false;
+}
+
+static int n_tty_chars_in_buffer(tty_struct* tty)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return 0;
+ return buf_count(d);
+}
+
+/*
+ * Probe input readiness for poll(). In canonical mode the read syscall
+ * blocks until a whole line is queued, so we mirror that: POLLIN only
+ * when canon_count() shows a finalized line (or pending EOF). In raw
+ * mode any character satisfies VMIN==1 immediately. Without this
+ * mirror, ppoll() would falsely signal POLLIN on partial canonical
+ * input, then read() would block — exactly the busy-loop the shell's
+ * ppoll-then-read pattern is trying to avoid.
+ */
+bool n_tty_input_ready(tty_struct* tty)
+{
+ auto* d = static_cast<n_tty_data*>(tty->ldisc.data);
+ if (!d) return false;
+ const termios& t = tty->termios_data;
+ if (t.c_lflag & ICANON)
+ return canon_count(d) != 0 || d->eof_pending;
+ int vmin = t.c_cc[VMIN];
+ if (vmin == 0) vmin = 1;
+ return buf_count(d) >= vmin;
+}
+
+static void n_tty_set_termios(tty_struct* /*tty*/, const termios* /*old*/)
+{
+ /* Nothing special needed for now */
+}
+
+static void n_tty_hangup(tty_struct* tty)
+{
+ n_tty_flush_buffer(tty);
+ tty->read_wait.wake_all();
+}
+
+/* ================================================================== */
+/* N_TTY ops table */
+/* ================================================================== */
+
+const tty_ldisc_ops n_tty_ops = {
+ .name = "n_tty",
+ .num = 0, /* N_TTY */
+ .open = n_tty_open,
+ .close = n_tty_close,
+ .read = n_tty_read,
+ .write = n_tty_write,
+ .receive_buf = n_tty_receive_buf,
+ .ioctl = n_tty_ioctl,
+ .flush_buffer = n_tty_flush_buffer,
+ .chars_in_buffer = n_tty_chars_in_buffer,
+ .set_termios = n_tty_set_termios,
+ .hangup = n_tty_hangup,
+};
diff --git a/na/drivers/tty/tty_core.cc b/na/drivers/tty/tty_core.cc
new file mode 100644
index 0000000..5985bd9
--- /dev/null
+++ b/na/drivers/tty/tty_core.cc
@@ -0,0 +1,536 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * tty_core.cc — TTY subsystem core
+ *
+ * Provides the tty_struct lifecycle, file_operations dispatch,
+ * and ioctl routing between the line discipline and driver.
+ *
+ * Call chain:
+ * sys_read → tty_fops.read → tty_read → ldisc.read
+ * sys_write → tty_fops.write → tty_write → ldisc.write
+ * sys_ioctl → tty_fops.ioctl → tty_ioctl → {core / ldisc / driver}
+ */
+
+#include <lume/tty.h>
+#include <lume/tty_ioctl.h>
+#include <lume/file.h>
+#include <lume/fs_types.h>
+#include <lume/chardev.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/pgrp.h>
+#include <lume/poll.h>
+#include <lume/new.h>
+#include <lume/errno.h>
+#include <lume/klog.h>
+#include <lume/slab.h>
+
+#ifndef ENOIOCTLCMD
+#define ENOIOCTLCMD 515
+#endif
+
+/* ================================================================== */
+/* Global console TTY */
+/* ================================================================== */
+
+tty_struct* g_console_tty = nullptr;
+
+/* ================================================================== */
+/* tty_struct allocation */
+/* ================================================================== */
+
+tty_struct* tty_alloc(const tty_driver* driver, int index)
+{
+ auto* tty = new tty_struct{};
+ if (!tty) return nullptr;
+
+ tty->index = index;
+ tty->driver = driver;
+ tty->link = nullptr;
+ tty->pgrp = 0;
+ tty->session = 0;
+ tty->write_cnt = 0;
+ tty->flags = 0;
+ tty->open_count = 0;
+ tty->driver_data = nullptr;
+
+ tty_default_termios(&tty->termios_data);
+
+ tty->ws.ws_row = 24;
+ tty->ws.ws_col = 80;
+ tty->ws.ws_xpixel = 0;
+ tty->ws.ws_ypixel = 0;
+
+ tty->lock.init("tty");
+ tty->read_wait.init("tty_rd");
+ tty->write_wait.init("tty_wr");
+
+ /* Attach default line discipline (N_TTY) */
+ tty->ldisc.ops = &n_tty_ops;
+ tty->ldisc.data = nullptr;
+
+ return tty;
+}
+
+/* ================================================================== */
+/* tty_open / tty_close */
+/* ================================================================== */
+
+int tty_open(tty_struct* tty)
+{
+ if (!tty) return -EINVAL;
+
+ if (tty->open_count == 0) {
+ /* Initialize line discipline */
+ if (tty->ldisc.ops && tty->ldisc.ops->open) {
+ int ret = tty->ldisc.ops->open(tty);
+ if (ret < 0) return ret;
+ }
+
+ /* Initialize driver */
+ if (tty->driver && tty->driver->ops && tty->driver->ops->open) {
+ int ret = tty->driver->ops->open(tty);
+ if (ret < 0) {
+ if (tty->ldisc.ops && tty->ldisc.ops->close)
+ tty->ldisc.ops->close(tty);
+ return ret;
+ }
+ }
+ }
+
+ tty->open_count++;
+ return 0;
+}
+
+void tty_close(tty_struct* tty)
+{
+ if (!tty) return;
+
+ tty->open_count--;
+ if (tty->open_count <= 0) {
+ tty->open_count = 0;
+
+ if (tty->driver && tty->driver->ops && tty->driver->ops->close)
+ tty->driver->ops->close(tty);
+
+ if (tty->ldisc.ops && tty->ldisc.ops->close)
+ tty->ldisc.ops->close(tty);
+ }
+}
+
+/* ================================================================== */
+/* Core I/O */
+/* ================================================================== */
+
+int64 tty_read(tty_struct* tty, file* fp, char* buf, uint64 count)
+{
+ if (!tty || !tty->ldisc.ops || !tty->ldisc.ops->read)
+ return -EIO;
+
+ return tty->ldisc.ops->read(tty, fp, buf, count);
+}
+
+int64 tty_write(tty_struct* tty, file* fp, const char* buf, uint64 count)
+{
+ if (!tty || !tty->ldisc.ops || !tty->ldisc.ops->write)
+ return -EIO;
+
+ return tty->ldisc.ops->write(tty, fp, buf, count);
+}
+
+/* ================================================================== */
+/* tty_receive_buf — called by driver on RX */
+/* ================================================================== */
+
+void tty_receive_buf(tty_struct* tty, const char* buf, int count)
+{
+ if (!tty || !tty->ldisc.ops || !tty->ldisc.ops->receive_buf)
+ return;
+
+ tty->ldisc.ops->receive_buf(tty, buf, count);
+}
+
+/* ================================================================== */
+/* tty_signal_pgrp — send signal to foreground process group */
+/* ================================================================== */
+
+extern int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task);
+
+void tty_signal_pgrp(tty_struct* tty, int sig)
+{
+ if (!tty) return;
+
+ int32 pgid;
+ {
+ LockGuard guard(tty->lock);
+ pgid = tty->pgrp;
+ }
+ if (pgid <= 0) return;
+
+ /* Deliver to the leader of every thread group in the foreground
+ * process group. ldisc paths call this for SIGINT/SIGQUIT/SIGWINCH
+ * etc.; previously we only signalled the current task, which was
+ * incorrect when the fg pgrp had multiple processes. */
+ kernel_siginfo info{};
+ info.si_signo = sig;
+ info.si_code = 0; /* SI_KERNEL would be 0x80; user rarely checks */
+ for_each_pgrp_member(pgid, [&](signal_struct* s) {
+ if (s->leader)
+ send_signal(sig, &info, s->leader);
+ });
+}
+
+/* ================================================================== */
+/* tty_ioctl — ioctl dispatch */
+/* ================================================================== */
+
+long tty_ioctl(tty_struct* tty, file* /*fp*/, unsigned int cmd, uint64 arg)
+{
+ if (!tty) return -EINVAL;
+
+ switch (cmd) {
+ case TCGETS: {
+ auto* p = reinterpret_cast<termios*>(arg);
+ if (!p) return -EINVAL;
+ *p = tty->termios_data;
+ return 0;
+ }
+
+ case TCSETS:
+ case TCSETSW:
+ case TCSETSF: {
+ auto* p = reinterpret_cast<const termios*>(arg);
+ if (!p) return -EINVAL;
+ termios old = tty->termios_data;
+ tty->termios_data = *p;
+
+ /* Notify line discipline of termios change */
+ if (tty->ldisc.ops && tty->ldisc.ops->set_termios)
+ tty->ldisc.ops->set_termios(tty, &old);
+
+ /* If TCSETSF, flush input buffer */
+ if (cmd == TCSETSF) {
+ if (tty->ldisc.ops && tty->ldisc.ops->flush_buffer)
+ tty->ldisc.ops->flush_buffer(tty);
+ }
+ return 0;
+ }
+
+ case TIOCGWINSZ: {
+ auto* p = reinterpret_cast<winsize*>(arg);
+ if (!p) return -EINVAL;
+ *p = tty->ws;
+ return 0;
+ }
+
+ case TIOCSWINSZ: {
+ auto* p = reinterpret_cast<const winsize*>(arg);
+ if (!p) return -EINVAL;
+ winsize old = tty->ws;
+ tty->ws = *p;
+ if (old.ws_row != tty->ws.ws_row || old.ws_col != tty->ws.ws_col) {
+ tty_signal_pgrp(tty, SIGWINCH);
+ }
+ return 0;
+ }
+
+ case TIOCGPGRP: {
+ auto* p = reinterpret_cast<int32*>(arg);
+ if (!p) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ENOTTY;
+ if (curr->signal->tty != tty) return -ENOTTY;
+
+ int32 pgid;
+ {
+ LockGuard guard(tty->lock);
+ pgid = tty->pgrp != 0 ? tty->pgrp : curr->signal->pgid;
+ }
+ *p = pgid;
+ return 0;
+ }
+
+ case TIOCSPGRP: {
+ auto* p = reinterpret_cast<const int32*>(arg);
+ if (!p) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ENOTTY;
+ if (curr->signal->tty != tty) return -ENOTTY;
+
+ int32 new_pgid = *p;
+ if (new_pgid <= 0) return -EINVAL;
+
+ /* new_pgid must be in the caller's session. */
+ bool ok = false;
+ for_each_pgrp_member(new_pgid, [&](signal_struct* s) {
+ if (s->sid == curr->signal->sid) ok = true;
+ });
+ if (!ok) return -EPERM;
+
+ LockGuard guard(tty->lock);
+ tty->pgrp = new_pgid;
+ return 0;
+ }
+
+ case TIOCSCTTY: {
+ /* Become the controlling terminal of the caller's session.
+ * Linux: only session leader without ctty may do this; with
+ * arg==1, CAP_SYS_ADMIN may steal a ctty from another session.
+ * We support the basic path; "steal" is left unimplemented. */
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -EPERM;
+ signal_struct* sig = curr->signal;
+
+ if (sig->sid != curr->pid) return -EPERM; /* not leader */
+
+ LockGuard sg(sig->lock);
+ if (sig->tty != nullptr) return -EPERM; /* already have one */
+
+ LockGuard tg(tty->lock);
+ if (tty->session != 0 && tty->session != sig->sid) {
+ return -EPERM; /* owned elsewhere */
+ }
+ tty->session = sig->sid;
+ tty->pgrp = sig->pgid;
+ sig->tty = tty;
+ return 0;
+ }
+
+ case TIOCNOTTY: {
+ /* Detach from controlling terminal. */
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ENOTTY;
+ signal_struct* sig = curr->signal;
+ if (sig->tty != tty) return -ENOTTY;
+
+ /* Session leader: HUP everyone left in the old fg pgrp.
+ * Job-control conformance — without this, daemons spawned by
+ * a relinquishing leader keep blocking on a dead terminal. */
+ bool was_leader = (sig->sid == curr->pid);
+ int32 hup_pgrp = 0;
+ {
+ LockGuard tg(tty->lock);
+ hup_pgrp = tty->pgrp;
+ if (was_leader) {
+ tty->session = 0;
+ tty->pgrp = 0;
+ }
+ }
+ {
+ LockGuard sg(sig->lock);
+ sig->tty = nullptr;
+ }
+ if (was_leader && hup_pgrp > 0) {
+ tty_signal_pgrp(tty, SIGHUP);
+ tty_signal_pgrp(tty, SIGCONT);
+ (void)hup_pgrp;
+ }
+ return 0;
+ }
+
+ case FIONREAD: {
+ /* Delegate to line discipline */
+ if (tty->ldisc.ops && tty->ldisc.ops->ioctl) {
+ int ret = tty->ldisc.ops->ioctl(tty, cmd, arg);
+ if (ret != -ENOIOCTLCMD)
+ return ret;
+ }
+ return 0;
+ }
+
+ case TCFLSH: {
+ int queue = static_cast<int>(arg);
+ if (queue == 0 || queue == 2) {
+ /* Flush input */
+ if (tty->ldisc.ops && tty->ldisc.ops->flush_buffer)
+ tty->ldisc.ops->flush_buffer(tty);
+ }
+ if (queue == 1 || queue == 2) {
+ /* Flush output */
+ if (tty->driver && tty->driver->ops && tty->driver->ops->flush_buffer)
+ tty->driver->ops->flush_buffer(tty);
+ }
+ return 0;
+ }
+
+ default:
+ break;
+ }
+
+ /* Try line discipline ioctl */
+ if (tty->ldisc.ops && tty->ldisc.ops->ioctl) {
+ int ret = tty->ldisc.ops->ioctl(tty, cmd, arg);
+ if (ret != -ENOIOCTLCMD)
+ return ret;
+ }
+
+ /* Try driver ioctl */
+ if (tty->driver && tty->driver->ops && tty->driver->ops->ioctl) {
+ int ret = tty->driver->ops->ioctl(tty, cmd, arg);
+ if (ret != -ENOIOCTLCMD)
+ return ret;
+ }
+
+ return -ENOTTY;
+}
+
+/* ================================================================== */
+/* file_operations for TTY devices */
+/* ================================================================== */
+
+static int64 tty_fops_read(file* fp, char* buf, uint64 count, int64* /*pos*/)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ return tty_read(tty, fp, buf, count);
+}
+
+static int64 tty_fops_write(file* fp, const char* buf, uint64 count, int64* /*pos*/)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ return tty_write(tty, fp, buf, count);
+}
+
+static long tty_fops_ioctl(file* fp, unsigned int cmd, uint64 arg)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ return tty_ioctl(tty, fp, cmd, arg);
+}
+
+/*
+ * tty poll callback — Linux drivers/tty/tty_io.c:tty_poll() does the
+ * same thing: register tty->read_wait so the poller is woken on input,
+ * then ask the ldisc whether data is currently available. Write side
+ * is always considered ready since our writes don't block.
+ */
+extern bool n_tty_input_ready(tty_struct* tty);
+extern void poll_wait(file*, WaitQueue*, poll_table*);
+
+static uint32 tty_fops_poll(file* fp, void* pt)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ if (!tty) return POLLNVAL;
+
+ poll_wait(fp, &tty->read_wait, static_cast<poll_table*>(pt));
+
+ uint32 mask = POLLOUT | POLLWRNORM; /* always writable */
+ if (n_tty_input_ready(tty))
+ mask |= POLLIN | POLLRDNORM;
+ return mask;
+}
+
+/*
+ * Auto-acquire a controlling terminal on open.
+ *
+ * POSIX rule: when a session leader without a controlling tty opens a
+ * terminal that itself has no controlling session, and the caller did
+ * not pass O_NOCTTY, that terminal becomes the controlling terminal of
+ * the caller's session. tty->session and tty->pgrp are seeded from the
+ * caller's sid/pgid so that the very first tcgetpgrp() call returns a
+ * value matching the caller's pgid — which is precisely what BusyBox
+ * ash:setjobctl() needs to exit its setup loop.
+ *
+ * Reference: Linux drivers/tty/tty_jobctrl.c:tty_open_proc_set_tty()
+ */
+static void tty_acquire_ctty(tty_struct* tty, file* fp)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return;
+ if (fp->f_flags & O_NOCTTY) return;
+
+ signal_struct* sig = curr->signal;
+
+ /* Must be session leader (sid == pid). */
+ if (sig->sid != curr->pid) return;
+
+ LockGuard sig_guard(sig->lock);
+ if (sig->tty != nullptr) return; /* already have one */
+
+ LockGuard tty_guard(tty->lock);
+ if (tty->session != 0) return; /* tty owned by another sess */
+
+ tty->session = sig->sid;
+ tty->pgrp = sig->pgid;
+ sig->tty = tty;
+}
+
+static int tty_fops_open(inode* /*ip*/, file* fp)
+{
+ /* The console TTY is pre-allocated; just attach it */
+ if (!g_console_tty) return -ENXIO;
+ fp->private_data = g_console_tty;
+ int err = tty_open(g_console_tty);
+ if (err == 0) {
+ tty_acquire_ctty(g_console_tty, fp);
+ }
+ return err;
+}
+
+static int tty_fops_release(inode* /*ip*/, file* fp)
+{
+ auto* tty = static_cast<tty_struct*>(fp->private_data);
+ tty_close(tty);
+ return 0;
+}
+
+file_operations tty_fops;
+
+static void init_tty_fops()
+{
+ tty_fops.read = tty_fops_read;
+ tty_fops.write = tty_fops_write;
+ tty_fops.unlocked_ioctl = tty_fops_ioctl;
+ tty_fops.poll = tty_fops_poll;
+ tty_fops.open = tty_fops_open;
+ tty_fops.release = tty_fops_release;
+}
+
+/* ================================================================== */
+/* TTY chardev registration */
+/* ================================================================== */
+
+static cdev tty_cdev;
+
+void tty_init()
+{
+ init_tty_fops();
+
+ /* Allocate the console TTY */
+ g_console_tty = tty_alloc(&uart_tty_driver, 0);
+ if (!g_console_tty) {
+ return;
+ }
+
+ /* Open the TTY (initializes ldisc + driver) */
+ tty_open(g_console_tty);
+
+ /* Leave tty->session and tty->pgrp at 0. The first session-leader
+ * to open /dev/console without O_NOCTTY claims it as its controlling
+ * terminal via tty_acquire_ctty(), which fills both fields. Pre-
+ * seeding them here (as we used to with the hardcoded pid==1) would
+ * make every subsequent acquire path see "owned by another session"
+ * and bail out, leaving sh's tcgetpgrp() returning -ENOTTY and
+ * disabling job control. */
+
+ /* Initialize UART RX interrupts */
+ uart_tty_init(g_console_tty);
+
+ /* Register UART as a struct console driver.
+ * After this, printk/klog output goes through console_write_all()
+ * → uart_console_write() instead of raw early_putc(). */
+ extern void uart_register_console();
+ uart_register_console();
+
+ /* Register as /dev/console chardev — replace the old console_cdev.
+ * We use the same major/minor (5,1) so that existing opens
+ * to /dev/console will get the TTY fops. */
+ tty_cdev.name = "console";
+ tty_cdev.dev = MKDEV(5, 1);
+ tty_cdev.fops = &tty_fops;
+ cdev_register(&tty_cdev);
+}
diff --git a/na/drivers/tty/tty_uart.cc b/na/drivers/tty/tty_uart.cc
new file mode 100644
index 0000000..973a6e9
--- /dev/null
+++ b/na/drivers/tty/tty_uart.cc
@@ -0,0 +1,251 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * tty_uart.cc — UART → tty_driver adapter
+ *
+ * Bridges the 16550A UART hardware to the TTY subsystem.
+ *
+ * TX: Calls early_putc() for each character (same as before).
+ * RX: Registers a PLIC IRQ handler. On UART RX interrupt,
+ * reads available bytes and feeds them to the line discipline
+ * via tty_receive_buf().
+ *
+ * QEMU virt UART:
+ * - IRQ 10 on the PLIC
+ * - 16550A register layout at the base address discovered from FDT
+ */
+
+#include <lume/tty.h>
+#include <lume/irq.h>
+#include <lume/addr.h>
+#include <lume/fdt.h>
+#include <lume/vmm.h>
+#include <lume/errno.h>
+#include <lume/klog.h>
+
+#ifndef ENOIOCTLCMD
+#define ENOIOCTLCMD 515
+#endif
+
+extern "C" void early_putc(char c);
+
+/* 16550A register offsets */
+static constexpr int UART_RBR = 0; /* Receive Buffer Register */
+static constexpr int UART_THR = 0; /* Transmit Holding Register */
+static constexpr int UART_IER = 1; /* Interrupt Enable Register */
+static constexpr int UART_IIR = 2; /* Interrupt Identification Register */
+static constexpr int UART_FCR = 2; /* FIFO Control Register */
+static constexpr int UART_LCR = 3; /* Line Control Register */
+static constexpr int UART_MCR = 4; /* Modem Control Register */
+static constexpr int UART_LSR = 5; /* Line Status Register */
+
+/* LSR bits */
+static constexpr uint8 UART_LSR_DR = 0x01; /* Data Ready */
+static constexpr uint8 UART_LSR_THRE = 0x20; /* TX Holding Register Empty */
+
+/* IER bits */
+static constexpr uint8 UART_IER_RDI = 0x01; /* Received Data Available */
+
+/* MCR bits */
+static constexpr uint8 UART_MCR_DTR = 0x01; /* Data Terminal Ready */
+static constexpr uint8 UART_MCR_RTS = 0x02; /* Request To Send */
+static constexpr uint8 UART_MCR_OUT2 = 0x08; /* Auxiliary output 2 — gates the
+ IRQ pin on PC-style 16550A.
+ Without this the UART
+ internally asserts but does
+ NOT propagate to the PLIC. */
+
+/* QEMU virt default UART IRQ */
+static constexpr uint32 UART_IRQ = 10;
+
+/* UART MMIO base (virtual address) */
+static volatile uint8* g_uart_base;
+static tty_struct* g_uart_tty;
+
+/* ================================================================== */
+/* UART MMIO helpers */
+/* ================================================================== */
+
+static inline uint8 uart_read(int reg)
+{
+ return g_uart_base[reg];
+}
+
+static inline void uart_write(int reg, uint8 val)
+{
+ g_uart_base[reg] = val;
+}
+
+static inline bool uart_rx_ready()
+{
+ return (uart_read(UART_LSR) & UART_LSR_DR) != 0;
+}
+
+static inline char uart_rx_char()
+{
+ return static_cast<char>(uart_read(UART_RBR));
+}
+
+/* ================================================================== */
+/* UART RX interrupt handler */
+/* ================================================================== */
+
+static void uart_irq_handler(uint32 /*irq*/, void* arg)
+{
+ auto* tty = static_cast<tty_struct*>(arg);
+
+ /* Read all available bytes from the FIFO */
+ char buf[16];
+ int count = 0;
+
+ while (uart_rx_ready() && count < 16) {
+ buf[count++] = uart_rx_char();
+ }
+
+ if (count > 0) {
+ tty_receive_buf(tty, buf, count);
+ }
+}
+
+/* ================================================================== */
+/* tty_driver_ops for UART */
+/* ================================================================== */
+
+static int uart_tty_open_op(tty_struct* /*tty*/)
+{
+ return 0;
+}
+
+static void uart_tty_close_op(tty_struct* /*tty*/)
+{
+ /* Nothing to do */
+}
+
+static int uart_tty_write_op(tty_struct* /*tty*/, const char* buf, int count)
+{
+ for (int i = 0; i < count; i++) {
+ early_putc(buf[i]);
+ }
+ return count;
+}
+
+static int uart_tty_write_room_op(tty_struct* /*tty*/)
+{
+ return TTY_BUF_SIZE; /* Always room (blocking putc) */
+}
+
+static void uart_tty_put_char_op(tty_struct* /*tty*/, char ch)
+{
+ early_putc(ch);
+}
+
+static void uart_tty_flush_chars_op(tty_struct* /*tty*/)
+{
+ /* No buffering in early_putc, nothing to flush */
+}
+
+static int uart_tty_chars_in_buffer_op(tty_struct* /*tty*/)
+{
+ return 0; /* No TX buffer */
+}
+
+static void uart_tty_flush_buffer_op(tty_struct* /*tty*/)
+{
+ /* No TX buffer to flush */
+}
+
+static int uart_tty_ioctl_op(tty_struct* /*tty*/, unsigned int /*cmd*/, uint64 /*arg*/)
+{
+ return -ENOIOCTLCMD;
+}
+
+static void uart_tty_hangup_op(tty_struct* /*tty*/)
+{
+ /* Disable UART RX interrupts */
+ if (g_uart_base) {
+ uart_write(UART_IER, 0);
+ }
+}
+
+static const tty_driver_ops uart_ops = {
+ .open = uart_tty_open_op,
+ .close = uart_tty_close_op,
+ .write = uart_tty_write_op,
+ .write_room = uart_tty_write_room_op,
+ .put_char = uart_tty_put_char_op,
+ .flush_chars = uart_tty_flush_chars_op,
+ .chars_in_buffer = uart_tty_chars_in_buffer_op,
+ .flush_buffer = uart_tty_flush_buffer_op,
+ .ioctl = uart_tty_ioctl_op,
+ .throttle = nullptr,
+ .unthrottle = nullptr,
+ .hangup = uart_tty_hangup_op,
+};
+
+const tty_driver uart_tty_driver = {
+ .name = "ttyS",
+ .major = 4,
+ .minor_start = 64,
+ .num = 1,
+ .ops = &uart_ops,
+};
+
+/* ================================================================== */
+/* UART TTY init — discover base address, enable RX IRQ */
+/* ================================================================== */
+
+void uart_tty_init(tty_struct* tty)
+{
+ g_uart_tty = tty;
+
+ /* Discover UART base from FDT */
+ DeviceNode* node = fdt_find_compatible("ns16550a");
+ if (!node) {
+ /* Try alternative compatible string */
+ node = fdt_find_compatible("snps,dw-apb-uart");
+ }
+
+ uint64 uart_pa = 0;
+ if (node) {
+ uart_pa = node->get_prop_u64("reg", 0);
+ }
+
+ if (uart_pa == 0) {
+ /* Fallback: QEMU virt default */
+ uart_pa = 0x10000000;
+ }
+
+ g_uart_base = reinterpret_cast<volatile uint8*>(
+ pa_to_va(phys_addr(uart_pa)).raw);
+
+ /* Set FIFO control: enable FIFOs, clear RX/TX FIFOs, 1-byte trigger */
+ uart_write(UART_FCR, 0x07);
+
+ /* MCR: assert OUT2 (gates the IRQ pin on PC-style 16550A), plus
+ * DTR/RTS for completeness. Mirrors Linux drivers/tty/serial/8250/
+ * 8250_port.c: serial8250_do_startup() does the same. */
+ uart_write(UART_MCR, UART_MCR_DTR | UART_MCR_RTS | UART_MCR_OUT2);
+
+ /* Enable UART receive-data-available interrupt */
+ uart_write(UART_IER, UART_IER_RDI);
+
+ /* Register IRQ handler with PLIC */
+ uint32 irq = UART_IRQ;
+ if (node) {
+ /* Try to get IRQ number from FDT */
+ const FdtProperty* irq_prop = node->get_prop("interrupts");
+ if (irq_prop && irq_prop->len >= 4) {
+ const uint8* val = static_cast<const uint8*>(irq_prop->value);
+ irq = (uint32(val[0]) << 24) | (uint32(val[1]) << 16) |
+ (uint32(val[2]) << 8) | val[3];
+ }
+ }
+
+ g_irq_manager.register_handler(irq, uart_irq_handler, tty);
+ g_irq_manager.enable(irq);
+
+ printk(KERN_INFO "uart_tty_init: base=0x%llx irq=%u IER=0x%x LSR=0x%x\n",
+ (unsigned long long)uart_pa, irq,
+ uart_read(UART_IER), uart_read(UART_LSR));
+}
diff --git a/na/drivers/uart.cc b/na/drivers/uart.cc
index 264ec6a..467b496 100644
--- a/na/drivers/uart.cc
+++ b/na/drivers/uart.cc
@@ -2,9 +2,18 @@
/* Copyright (C) 2026 Richard Qin */
/*
- * uart.cc — 16550a UART driver (early console)
+ * uart.cc — 16550a UART driver + console registry
*
- * Implements the early UART output mapping required by panic and kernel_main.
+ * Two roles:
+ * 1. Early console: early_putc() for pre-init output (panic, kprintf)
+ * 2. Runtime console: struct console registered with register_console().
+ * klog/printk route through console_write_all() → uart_console_write().
+ *
+ * The console registry is a simple linked list of struct console.
+ * console_write_all() iterates the list and calls each console's write().
+ * If no console is registered, it falls back to early_putc().
+ *
+ * Reference: Linux kernel/printk/printk.c, drivers/tty/serial/8250/
*/
#include <lume/console.h>
@@ -13,11 +22,19 @@
#include <lume/addr.h>
#include <arch/config.h>
+/* ================================================================== */
+/* UART state */
+/* ================================================================== */
+
static uint64 g_uart_pa = 0;
static uint64 g_uart_size = 0;
static bool g_runtime_mapping_ready = false;
static bool g_mmu_enabled = false;
+/* ================================================================== */
+/* Early console (Phase 1) */
+/* ================================================================== */
+
extern "C" void console_set_mmu_enabled() {
g_mmu_enabled = true;
}
@@ -70,3 +87,95 @@ void console_init() {
void console_use_runtime_mapping() {
g_runtime_mapping_ready = true;
}
+
+/* ================================================================== */
+/* Console registry (Phase 2) — linked list of struct console */
+/* ================================================================== */
+
+static list_node g_console_list;
+static bool g_console_list_init = false;
+
+void register_console(console* con)
+{
+ if (!con) return;
+
+ if (!g_console_list_init) {
+ g_console_list.init();
+ g_console_list_init = true;
+ }
+
+ con->flags |= CON_ENABLED;
+ con->list.init();
+
+ /* Insert at tail */
+ list_add_tail(&con->list, &g_console_list);
+}
+
+void unregister_console(console* con)
+{
+ if (!con) return;
+
+ list_del_init(&con->list);
+ con->flags &= ~CON_ENABLED;
+}
+
+bool console_drivers_registered()
+{
+ if (!g_console_list_init) return false;
+ return !list_empty(&g_console_list);
+}
+
+void console_write_all(const char* s, int count)
+{
+ if (g_console_list_init && !list_empty(&g_console_list)) {
+ list_node* pos;
+ list_for_each(pos, &g_console_list) {
+ auto* con = container_of(pos, console, list);
+ if ((con->flags & CON_ENABLED) && con->write) {
+ con->write(con, s, static_cast<unsigned int>(count));
+ }
+ }
+ } else {
+ /* Fallback: direct early_putc (pre-registration) */
+ for (int i = 0; i < count; i++) {
+ early_putc(s[i]);
+ }
+ }
+}
+
+/* ================================================================== */
+/* UART console driver */
+/* ================================================================== */
+
+static void uart_console_write(console* /*con*/, const char* s, unsigned int count)
+{
+ for (unsigned int i = 0; i < count; i++) {
+ if (s[i] == '\n') {
+ early_putc('\r');
+ }
+ early_putc(s[i]);
+ }
+}
+
+/*
+ * uart_console must be initialized at RUNTIME, not with static
+ * aggregate initializers. Reason: KASLR self-relocation processes
+ * .rela.dyn entries for simple pointer variables, but may not cover
+ * function pointer fields inside static aggregate initializers.
+ * A static `{ .write = uart_console_write }` would store the
+ * pre-relocation address, causing a crash when called.
+ */
+static console uart_console;
+
+void uart_register_console()
+{
+ uart_console.name = "ttyS";
+ uart_console.write = uart_console_write;
+ uart_console.setup = nullptr;
+ uart_console.flags = CON_ENABLED | CON_BOOT;
+ uart_console.index = 0;
+ uart_console.data = nullptr;
+ uart_console.list.init();
+ register_console(&uart_console);
+}
+
diff --git a/na/fs/chardev.cc b/na/fs/chardev.cc
new file mode 100644
index 0000000..fa96276
--- /dev/null
+++ b/na/fs/chardev.cc
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * chardev.cc — Character device registry
+ *
+ * Simple linked list of registered cdevs, searched by dev number.
+ */
+
+#include <lume/chardev.h>
+#include <lume/list.h>
+#include <lume/klog.h>
+#include <kernel/sync/spinlock.h>
+
+static list_node g_cdev_list;
+static Spinlock g_cdev_lock;
+
+void cdev_init()
+{
+ list_init(&g_cdev_list);
+}
+
+void cdev_register(cdev* dev)
+{
+ LockGuard guard(g_cdev_lock);
+ list_init(&dev->list);
+ list_add_tail(&dev->list, &g_cdev_list);
+ printk("[cdev] registered '%s' (%d,%d)\n",
+ dev->name, MAJOR(dev->dev), MINOR(dev->dev));
+}
+
+cdev* cdev_find(uint32 dev_nr)
+{
+ LockGuard guard(g_cdev_lock);
+ list_node* pos;
+ for (pos = g_cdev_list.prev; pos != &g_cdev_list; pos = pos->prev) {
+ cdev* cd = container_of(pos, cdev, list);
+ if (cd->dev == dev_nr) {
+ return cd;
+ }
+ }
+ return nullptr;
+}
diff --git a/na/fs/dcache.cc b/na/fs/dcache.cc
new file mode 100644
index 0000000..94ab287
--- /dev/null
+++ b/na/fs/dcache.cc
@@ -0,0 +1,287 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * dcache.cc — Directory entry cache
+ *
+ * Provides dentry allocation, lookup, instantiation, and d_path.
+ */
+
+#include <lume/dcache.h>
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* ================================================================== */
+/* Hash function */
+/* ================================================================== */
+
+uint32 full_name_hash(const char* name, uint32 len)
+{
+ uint32 hash = 0;
+ for (uint32 i = 0; i < len; i++)
+ hash = hash * 31 + static_cast<uint8>(name[i]);
+ return hash;
+}
+
+/* ================================================================== */
+/* d_set_type — set DCACHE_*_TYPE bits from inode mode */
+/* ================================================================== */
+
+void d_set_type(dentry* de, uint32 i_mode)
+{
+ de->d_flags &= ~DCACHE_ENTRY_TYPE;
+
+ if (S_ISDIR(i_mode))
+ de->d_flags |= DCACHE_DIRECTORY_TYPE;
+ else if (S_ISLNK(i_mode))
+ de->d_flags |= DCACHE_SYMLINK_TYPE;
+ else if (S_ISREG(i_mode))
+ de->d_flags |= DCACHE_REGULAR_TYPE;
+ else
+ de->d_flags |= DCACHE_SPECIAL_TYPE;
+}
+
+/* ================================================================== */
+/* d_alloc — allocate a dentry under a parent */
+/* ================================================================== */
+
+static void dentry_init(dentry* de, dentry* parent, const qstr* name)
+{
+ de->d_flags = DCACHE_MISS_TYPE; /* negative until instantiated */
+ de->d_parent = parent ? parent : de;
+ de->d_inode = nullptr;
+ de->d_op = nullptr;
+ de->d_sb = parent ? parent->d_sb : nullptr;
+ de->d_time = 0;
+ de->d_fsdata = nullptr;
+ de->d_lockref.store(1);
+
+ list_init(&de->d_lru);
+ list_init(&de->d_child);
+ list_init(&de->d_children);
+ list_init(&de->d_alias);
+
+ /* Copy name */
+ if (name && name->name && name->len > 0) {
+ uint32 len = name->len;
+ if (len < DNAME_INLINE_LEN) {
+ for (uint32 i = 0; i < len; i++)
+ de->d_iname[i] = name->name[i];
+ de->d_iname[len] = '\0';
+ de->d_name.name = reinterpret_cast<const char*>(de->d_iname);
+ } else {
+ /* Heap-allocate for long names */
+ char* buf = new char[len + 1];
+ for (uint32 i = 0; i < len; i++)
+ buf[i] = name->name[i];
+ buf[len] = '\0';
+ de->d_name.name = buf;
+ }
+ de->d_name.len = len;
+ de->d_name.hash = name->hash ? name->hash : full_name_hash(name->name, len);
+ } else {
+ de->d_iname[0] = '\0';
+ de->d_name.name = reinterpret_cast<const char*>(de->d_iname);
+ de->d_name.len = 0;
+ de->d_name.hash = 0;
+ }
+}
+
+dentry* d_alloc(dentry* parent, const qstr* name)
+{
+ auto* de = new dentry{};
+ if (!de) return nullptr;
+
+ dentry_init(de, parent, name);
+ de->d_sb = parent->d_sb;
+
+ /* Use sb default d_op if set */
+ if (parent->d_sb->s_d_op)
+ de->d_op = parent->d_sb->s_d_op;
+
+ /* Link into parent's children list */
+ if (parent) {
+ LockGuard guard(parent->d_lock);
+ list_add_tail(&de->d_child, &parent->d_children);
+ dget(parent); /* child holds ref on parent */
+ }
+
+ return de;
+}
+
+dentry* d_alloc_anon(super_block* sb)
+{
+ auto* de = new dentry{};
+ if (!de) return nullptr;
+
+ dentry_init(de, nullptr, nullptr);
+ de->d_sb = sb;
+ de->d_parent = de; /* root: parent is self */
+
+ return de;
+}
+
+/* ================================================================== */
+/* d_instantiate — bind an inode to a dentry */
+/* ================================================================== */
+
+void d_instantiate(dentry* de, inode* ip)
+{
+ if (!ip) return;
+
+ LockGuard guard(de->d_lock);
+ de->d_inode = ip;
+ d_set_type(de, ip->i_mode);
+
+ /* Add to inode's alias list */
+ list_add_tail(&de->d_alias, &ip->i_dentry);
+}
+
+/* ================================================================== */
+/* d_make_root — create root dentry for a superblock */
+/* ================================================================== */
+
+dentry* d_make_root(inode* root_inode)
+{
+ if (!root_inode) return nullptr;
+
+ dentry* de = d_alloc_anon(root_inode->i_sb);
+ if (!de) {
+ iput(root_inode);
+ return nullptr;
+ }
+
+ d_instantiate(de, root_inode);
+ return de;
+}
+
+/* ================================================================== */
+/* d_add — combined d_instantiate + d_rehash */
+/* ================================================================== */
+
+void d_add(dentry* de, inode* ip)
+{
+ d_instantiate(de, ip);
+ /* d_rehash would add to dcache hash table; simplified for now */
+}
+
+/* ================================================================== */
+/* d_lookup — search children of parent for a name match */
+/* ================================================================== */
+
+dentry* d_lookup(const dentry* parent, const qstr* name)
+{
+ LockGuard guard(const_cast<Spinlock&>(parent->d_lock));
+
+ list_node* pos;
+ list_for_each(pos, &parent->d_children) {
+ dentry* child = list_entry(pos, dentry, d_child);
+
+ if (child->d_name.len != name->len)
+ continue;
+
+ /* Compare names */
+ const char* a = child->d_name.name;
+ const char* b = name->name;
+ uint32 len = name->len;
+ bool match = true;
+ for (uint32 i = 0; i < len; i++) {
+ if (a[i] != b[i]) { match = false; break; }
+ }
+ if (match) {
+ dget(child);
+ return child;
+ }
+ }
+
+ return nullptr;
+}
+
+/* ================================================================== */
+/* dget / dput — dentry reference counting */
+/* ================================================================== */
+
+dentry* dget(dentry* de)
+{
+ if (de)
+ de->d_lockref.fetch_add(1);
+ return de;
+}
+
+void dput(dentry* de)
+{
+ if (!de) return;
+
+ int old = de->d_lockref.fetch_sub(1);
+ if (old > 1) return;
+
+ /* Last reference — could free, but for ramfs we keep dentries alive.
+ * A real implementation would check d_op->d_delete and possibly
+ * add to LRU for later reclaim. For now, just leak (Phase 1). */
+}
+
+/* ================================================================== */
+/* d_drop / d_delete */
+/* ================================================================== */
+
+void d_drop(dentry* de)
+{
+ /* Remove from parent's children list */
+ LockGuard guard(de->d_lock);
+ if (!list_empty(&de->d_child))
+ list_del_init(&de->d_child);
+}
+
+void d_delete(dentry* de)
+{
+ d_drop(de);
+}
+
+/* ================================================================== */
+/* d_path — build absolute pathname from a path */
+/* ================================================================== */
+
+char* d_path(const path* p, char* buf, int buflen)
+{
+ if (!p || !p->de || buflen <= 0)
+ return nullptr;
+
+ /* Build path by walking up d_parent chain, writing backwards */
+ char* end = buf + buflen - 1;
+ *end = '\0';
+
+ dentry* de = p->de;
+
+ if (d_is_root(de)) {
+ if (end <= buf) return nullptr;
+ *(--end) = '/';
+ return end;
+ }
+
+ while (!d_is_root(de)) {
+ uint32 len = de->d_name.len;
+ end -= len;
+ if (end <= buf) return nullptr;
+
+ const char* name = de->d_name.name;
+ for (uint32 i = 0; i < len; i++)
+ end[i] = name[i];
+
+ *(--end) = '/';
+ de = de->d_parent;
+ }
+
+ return end;
+}
+
+/* ================================================================== */
+/* dcache_init */
+/* ================================================================== */
+
+void dcache_init()
+{
+ /* Future: init dcache hash table */
+}
diff --git a/na/fs/devfs.cc b/na/fs/devfs.cc
new file mode 100644
index 0000000..b4b2cf3
--- /dev/null
+++ b/na/fs/devfs.cc
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * devfs.cc — /dev population and chardev-aware open
+ *
+ * Creates /dev as a subdirectory under the ramfs root, then
+ * populates device nodes. On open(), if the inode is S_IFCHR,
+ * we look up the cdev by i_rdev and use its fops.
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/chardev.h>
+#include <lume/mount.h>
+#include <lume/klog.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* ================================================================== */
+/* devfs_create_node — create a char device inode under parent */
+/* ================================================================== */
+
+static int devfs_create_node(dentry* parent, const char* name,
+ uint32 major, uint32 minor)
+{
+ inode* dir_ip = parent->d_inode;
+ if (!dir_ip || !dir_ip->i_op || !dir_ip->i_op->mknod) {
+ return -EPERM;
+ }
+
+ qstr q{};
+ q.name = name;
+ q.len = 0;
+ for (const char* s = name; *s; s++) {
+ q.len++;
+ }
+
+ dentry* de = d_alloc(parent, &q);
+ if (!de) {
+ return -ENOMEM;
+ }
+
+ int err = dir_ip->i_op->mknod(dir_ip, de, S_IFCHR | 0666,
+ MKDEV(major, minor));
+ if (err) {
+ return err;
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* devfs_open — chardev-aware open hook */
+/* ================================================================== */
+
+/*
+ * Called from sys_openat when the inode is S_IFCHR.
+ * Replaces f_op with the chardev's file_operations.
+ */
+int devfs_chrdev_open(inode* ip, file* fp)
+{
+ if (!S_ISCHR(ip->i_mode)) {
+ return 0;
+ }
+
+ cdev* cd = cdev_find(ip->i_rdev);
+ if (!cd || !cd->fops) {
+ return -ENXIO; /* No such device */
+ }
+
+ fp->f_op = cd->fops;
+
+ /* Call the device's own open if it has one */
+ if (fp->f_op->open) {
+ return fp->f_op->open(ip, fp);
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* devfs_init — create /dev and populate device nodes */
+/* ================================================================== */
+
+void devfs_init()
+{
+ if (!g_root_dentry || !g_root_dentry->d_inode) {
+ printk(KERN_ERR "[devfs] no root filesystem!\n");
+ return;
+ }
+
+ /* Create /dev directory — or reuse if initramfs already created it.
+ * Linux: devtmpfs similarly handles pre-existing directories. */
+ inode* root_ip = g_root_dentry->d_inode;
+
+ qstr dev_name{};
+ dev_name.name = "dev";
+ dev_name.len = 3;
+ dev_name.hash = full_name_hash("dev", 3);
+
+ dentry* dev_de = d_lookup(g_root_dentry, &dev_name);
+ if (dev_de) {
+ /* /dev already exists (from initramfs) — reuse it */
+ printk("[devfs] reusing existing /dev directory\n");
+ } else {
+ /* Create /dev from scratch */
+ if (!root_ip->i_op || !root_ip->i_op->mkdir) {
+ printk(KERN_ERR "[devfs] root has no mkdir!\n");
+ return;
+ }
+
+ dev_de = d_alloc(g_root_dentry, &dev_name);
+ if (!dev_de) {
+ printk(KERN_ERR "[devfs] failed to alloc /dev dentry\n");
+ return;
+ }
+
+ int err = root_ip->i_op->mkdir(root_ip, dev_de, S_IFDIR | 0755);
+ if (err) {
+ printk(KERN_ERR "[devfs] mkdir /dev failed: %d\n", err);
+ return;
+ }
+ }
+
+ /* Populate: /dev/console (5,1) */
+ devfs_create_node(dev_de, "console", 5, 1);
+
+ /* Populate: /dev/null (1,3) */
+ devfs_create_node(dev_de, "null", 1, 3);
+
+ /* Populate: /dev/zero (1,5) */
+ devfs_create_node(dev_de, "zero", 1, 5);
+
+ printk("[devfs] /dev populated (console, null, zero)\n");
+}
diff --git a/na/fs/fd.cc b/na/fs/fd.cc
new file mode 100644
index 0000000..5fc8ab0
--- /dev/null
+++ b/na/fs/fd.cc
@@ -0,0 +1,216 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * fd.cc — File descriptor table management
+ */
+
+#include <lume/fdtable.h>
+#include <lume/file.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+
+/* ================================================================== */
+/* Bitmap helpers */
+/* ================================================================== */
+
+static inline bool bit_test(const uint64* bmap, int bit) {
+ return (bmap[bit / 64] >> (bit % 64)) & 1;
+}
+static inline void bit_set(uint64* bmap, int bit) {
+ bmap[bit / 64] |= (1ULL << (bit % 64));
+}
+static inline void bit_clear(uint64* bmap, int bit) {
+ bmap[bit / 64] &= ~(1ULL << (bit % 64));
+}
+
+/* ================================================================== */
+/* alloc_files_struct */
+/* ================================================================== */
+
+files_struct* alloc_files_struct()
+{
+ auto* files = new files_struct{};
+ if (!files) return nullptr;
+
+ files->count.store(1);
+ files->next_fd = 0;
+ files->max_fds = NR_OPEN_DEFAULT;
+
+ for (int i = 0; i < NR_OPEN_DEFAULT; i++)
+ files->fd_array[i] = nullptr;
+
+ int bmap_words = (NR_OPEN_DEFAULT + 63) / 64;
+ for (int i = 0; i < bmap_words; i++) {
+ files->close_on_exec_init[i] = 0;
+ files->open_fds_init[i] = 0;
+ }
+
+ return files;
+}
+
+/* ================================================================== */
+/* put_files_struct — drop reference, free on last */
+/* ================================================================== */
+
+void put_files_struct(files_struct* files)
+{
+ if (!files) return;
+
+ int old = files->count.fetch_sub(1);
+ if (old > 1) return;
+
+ /* Close all open fds */
+ for (int i = 0; i < files->max_fds; i++) {
+ if (files->fd_array[i]) {
+ fput(files->fd_array[i]);
+ files->fd_array[i] = nullptr;
+ }
+ }
+
+ delete files;
+}
+
+/* ================================================================== */
+/* dup_fd — duplicate entire fd table (for fork) */
+/* ================================================================== */
+
+files_struct* dup_fd(files_struct* old_files)
+{
+ auto* new_files = alloc_files_struct();
+ if (!new_files) return nullptr;
+
+ LockGuard guard(old_files->file_lock);
+
+ for (int i = 0; i < old_files->max_fds; i++) {
+ file* fp = old_files->fd_array[i];
+ if (fp) {
+ get_file(fp);
+ new_files->fd_array[i] = fp;
+ }
+ }
+
+ /* Copy bitmaps */
+ int bmap_words = (NR_OPEN_DEFAULT + 63) / 64;
+ for (int i = 0; i < bmap_words; i++) {
+ new_files->open_fds_init[i] = old_files->open_fds_init[i];
+ new_files->close_on_exec_init[i] = old_files->close_on_exec_init[i];
+ }
+
+ new_files->next_fd = old_files->next_fd;
+
+ return new_files;
+}
+
+/* ================================================================== */
+/* alloc_fd — find lowest free fd >= start */
+/* ================================================================== */
+
+int alloc_fd(files_struct* files, unsigned int start)
+{
+ LockGuard guard(files->file_lock);
+
+ for (int fd = static_cast<int>(start); fd < files->max_fds; fd++) {
+ if (!bit_test(files->open_fds_init, fd)) {
+ bit_set(files->open_fds_init, fd);
+ files->fd_array[fd] = nullptr; /* placeholder, fd_install sets real file */
+ if (fd >= files->next_fd)
+ files->next_fd = fd + 1;
+ return fd;
+ }
+ }
+
+ return -EMFILE;
+}
+
+/* ================================================================== */
+/* fd_install — place a file* at a given fd */
+/* ================================================================== */
+
+void fd_install(files_struct* files, int fd, file* fp)
+{
+ LockGuard guard(files->file_lock);
+ files->fd_array[fd] = fp;
+}
+
+/* ================================================================== */
+/* fcheck — look up file by fd (no refcount bump) */
+/* ================================================================== */
+
+file* fcheck(files_struct* files, int fd)
+{
+ if (fd < 0 || fd >= files->max_fds) return nullptr;
+ return files->fd_array[fd];
+}
+
+/* ================================================================== */
+/* close_fd */
+/* ================================================================== */
+
+int close_fd(files_struct* files, int fd)
+{
+ file* fp;
+ {
+ LockGuard guard(files->file_lock);
+ if (fd < 0 || fd >= files->max_fds) return -EBADF;
+
+ fp = files->fd_array[fd];
+ if (!fp) return -EBADF;
+
+ files->fd_array[fd] = nullptr;
+ bit_clear(files->open_fds_init, fd);
+ bit_clear(files->close_on_exec_init, fd);
+ }
+
+ fput(fp);
+ return 0;
+}
+
+/* ================================================================== */
+/* do_dup2 — duplicate oldfd to newfd */
+/* ================================================================== */
+
+int do_dup2(files_struct* files, int oldfd, int newfd, int flags)
+{
+ if (oldfd == newfd) return -EINVAL;
+
+ file* existing = nullptr;
+ {
+ LockGuard guard(files->file_lock);
+
+ if (oldfd < 0 || oldfd >= files->max_fds) return -EBADF;
+ if (newfd < 0 || newfd >= files->max_fds) return -EBADF;
+
+ file* old_fp = files->fd_array[oldfd];
+ if (!old_fp) return -EBADF;
+
+ existing = files->fd_array[newfd];
+
+ get_file(old_fp);
+ files->fd_array[newfd] = old_fp;
+ bit_set(files->open_fds_init, newfd);
+
+ if (flags & O_CLOEXEC)
+ bit_set(files->close_on_exec_init, newfd);
+ else
+ bit_clear(files->close_on_exec_init, newfd);
+ }
+
+ if (existing)
+ fput(existing);
+
+ return newfd;
+}
+
+/* ================================================================== */
+/* do_close_on_exec — close all fds with CLOEXEC set */
+/* ================================================================== */
+
+void do_close_on_exec(files_struct* files)
+{
+ for (int fd = 0; fd < files->max_fds; fd++) {
+ if (bit_test(files->close_on_exec_init, fd)) {
+ close_fd(files, fd);
+ }
+ }
+}
diff --git a/na/fs/file_table.cc b/na/fs/file_table.cc
new file mode 100644
index 0000000..06095c1
--- /dev/null
+++ b/na/fs/file_table.cc
@@ -0,0 +1,164 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * file_table.cc — Open file allocation and reference counting
+ */
+
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+
+/* ================================================================== */
+/* alloc_empty_file */
+/* ================================================================== */
+
+file* alloc_empty_file(int flags)
+{
+ auto* fp = new file{};
+ if (!fp) return nullptr;
+
+ fp->f_count.store(1);
+ fp->f_flags = flags;
+ fp->f_mode = 0;
+ fp->f_op = nullptr;
+ fp->f_mapping = nullptr;
+ fp->f_inode = nullptr;
+ fp->f_pos = 0;
+ fp->private_data = nullptr;
+ fp->f_path.mnt = nullptr;
+ fp->f_path.de = nullptr;
+
+ /* Derive f_mode from O_* flags */
+ int acc = flags & O_ACCMODE;
+ if (acc == O_RDONLY)
+ fp->f_mode = FMODE_READ | FMODE_CAN_READ;
+ else if (acc == O_WRONLY)
+ fp->f_mode = FMODE_WRITE | FMODE_CAN_WRITE;
+ else if (acc == O_RDWR)
+ fp->f_mode = FMODE_READ | FMODE_WRITE | FMODE_CAN_READ | FMODE_CAN_WRITE;
+
+ fp->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
+
+ return fp;
+}
+
+/* ================================================================== */
+/* get_file / fput */
+/* ================================================================== */
+
+file* get_file(file* f)
+{
+ f->f_count.fetch_add(1);
+ return f;
+}
+
+void fput(file* fp)
+{
+ if (!fp) return;
+
+ int old = fp->f_count.fetch_sub(1);
+ if (old > 1) return;
+
+ /* Last reference — release */
+ if (fp->f_op && fp->f_op->release)
+ fp->f_op->release(fp->f_inode, fp);
+
+ if (fp->f_path.de)
+ dput(fp->f_path.de);
+
+ /* Future: mntput(fp->f_path.mnt) */
+
+ delete fp;
+}
+
+/* ================================================================== */
+/* vfs_read / vfs_write / vfs_llseek / vfs_fstat */
+/* ================================================================== */
+
+int64 vfs_read(file* fp, char* buf, uint64 count, int64* pos)
+{
+ if (!(fp->f_mode & FMODE_CAN_READ))
+ return -EBADF;
+ if (!fp->f_op || !fp->f_op->read)
+ return -EINVAL;
+
+ return fp->f_op->read(fp, buf, count, pos);
+}
+
+int64 vfs_write(file* fp, const char* buf, uint64 count, int64* pos)
+{
+ if (!(fp->f_mode & FMODE_CAN_WRITE))
+ return -EBADF;
+ if (!fp->f_op || !fp->f_op->write)
+ return -EINVAL;
+
+ return fp->f_op->write(fp, buf, count, pos);
+}
+
+int64 vfs_llseek(file* fp, int64 offset, int whence)
+{
+ if (fp->f_op && fp->f_op->llseek)
+ return fp->f_op->llseek(fp, offset, whence);
+ return generic_file_llseek(fp, offset, whence);
+}
+
+int64 generic_file_llseek(file* fp, int64 offset, int whence)
+{
+ int64 new_pos;
+ switch (whence) {
+ case SEEK_SET:
+ new_pos = offset;
+ break;
+ case SEEK_CUR:
+ new_pos = fp->f_pos + offset;
+ break;
+ case SEEK_END:
+ new_pos = fp->f_inode->i_size + offset;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (new_pos < 0) return -EINVAL;
+ fp->f_pos = new_pos;
+ return new_pos;
+}
+
+int vfs_fstat(file* fp, kstat* stat)
+{
+ inode* ip = file_inode(fp);
+ if (!ip) return -EBADF;
+
+ if (ip->i_op && ip->i_op->getattr) {
+ return ip->i_op->getattr(&fp->f_path, stat, 0, 0);
+ }
+
+ generic_fillattr(ip, stat);
+ return 0;
+}
+
+/* ================================================================== */
+/* path_get / path_put (simplified) */
+/* ================================================================== */
+
+void path_get(const path* p)
+{
+ if (p->de) dget(p->de);
+ /* Future: mntget(p->mnt) */
+}
+
+void path_put(const path* p)
+{
+ if (p->de) dput(p->de);
+ /* Future: mntput(p->mnt) */
+}
+
+/* ================================================================== */
+/* mntget / mntput (stubs for Phase 1) */
+/* ================================================================== */
+
+vfsmount* mntget(vfsmount* mnt) { return mnt; }
+void mntput(vfsmount*) {}
diff --git a/na/fs/fs_struct.cc b/na/fs/fs_struct.cc
new file mode 100644
index 0000000..090819f
--- /dev/null
+++ b/na/fs/fs_struct.cc
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * fs_struct.cc — Per-task filesystem context (cwd, root)
+ */
+
+#include <lume/fs_struct.h>
+#include <lume/slab.h>
+
+/* ================================================================== */
+/* alloc_fs_struct */
+/* ================================================================== */
+
+fs_struct* alloc_fs_struct()
+{
+ auto* fs = new fs_struct{};
+ if (!fs) return nullptr;
+
+ fs->count.store(1);
+ fs->umask = 022;
+ fs->root = {nullptr, nullptr};
+ fs->pwd = {nullptr, nullptr};
+
+ return fs;
+}
+
+/* ================================================================== */
+/* put_fs_struct */
+/* ================================================================== */
+
+void put_fs_struct(fs_struct* fs)
+{
+ if (!fs) return;
+
+ int old = fs->count.fetch_sub(1);
+ if (old > 1) return;
+
+ path_put(&fs->root);
+ path_put(&fs->pwd);
+ delete fs;
+}
+
+/* ================================================================== */
+/* copy_fs_struct — for fork without CLONE_FS */
+/* ================================================================== */
+
+fs_struct* copy_fs_struct(const fs_struct* old_fs)
+{
+ auto* fs = alloc_fs_struct();
+ if (!fs) return nullptr;
+
+ fs->umask = old_fs->umask;
+
+ fs->root = old_fs->root;
+ path_get(&fs->root);
+
+ fs->pwd = old_fs->pwd;
+ path_get(&fs->pwd);
+
+ return fs;
+}
+
+/* ================================================================== */
+/* set_fs_root / set_fs_pwd */
+/* ================================================================== */
+
+void set_fs_root(fs_struct* fs, const path* p)
+{
+ path old_root;
+ {
+ LockGuard guard(fs->lock);
+ old_root = fs->root;
+ fs->root = *p;
+ path_get(p);
+ }
+ path_put(&old_root);
+}
+
+void set_fs_pwd(fs_struct* fs, const path* p)
+{
+ path old_pwd;
+ {
+ LockGuard guard(fs->lock);
+ old_pwd = fs->pwd;
+ fs->pwd = *p;
+ path_get(p);
+ }
+ path_put(&old_pwd);
+}
diff --git a/na/fs/initramfs.cc b/na/fs/initramfs.cc
new file mode 100644
index 0000000..ba10a3e
--- /dev/null
+++ b/na/fs/initramfs.cc
@@ -0,0 +1,285 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * initramfs.cc — Unpack embedded CPIO archive into ramfs
+ *
+ * The build system embeds a newc-format CPIO archive into the kernel
+ * ELF as the section .init.ramfs. The linker script exports
+ * __initramfs_start / __initramfs_end symbols.
+ *
+ * At boot, populate_rootfs() walks the CPIO entries and creates
+ * directories and files in the already-mounted ramfs root.
+ *
+ * Reference: Linux init/initramfs.c
+ *
+ * CPIO "newc" header format (110 bytes, ASCII hex fields):
+ * magic[6] "070701"
+ * ino[8] mode[8] uid[8] gid[8] nlink[8] mtime[8]
+ * filesize[8] devmajor[8] devminor[8] rdevmajor[8] rdevminor[8]
+ * namesize[8] check[8]
+ * filename (namesize bytes, NUL terminated, padded to 4-byte boundary)
+ * file data (filesize bytes, padded to 4-byte boundary)
+ */
+
+#include <lume/klog.h>
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* Linker-provided symbols for the embedded CPIO archive */
+extern "C" const char __initramfs_start[];
+extern "C" const char __initramfs_end[];
+
+/* ramfs inode creation (from fs/ramfs/inode.cc) */
+extern inode* ramfs_get_inode(super_block* sb, const inode* dir,
+ uint32 mode, uint32 rdev);
+
+/* Global root dentry (from fs/vfs_init.cc) */
+extern dentry* g_root_dentry;
+
+/* ================================================================== */
+/* CPIO newc header parsing */
+/* ================================================================== */
+
+struct cpio_header {
+ char magic[6];
+ char ino[8];
+ char mode[8];
+ char uid[8];
+ char gid[8];
+ char nlink[8];
+ char mtime[8];
+ char filesize[8];
+ char devmajor[8];
+ char devminor[8];
+ char rdevmajor[8];
+ char rdevminor[8];
+ char namesize[8];
+ char check[8];
+};
+
+static constexpr int kCpioHeaderSize = 110;
+
+static uint32 parse_hex8(const char* s)
+{
+ uint32 val = 0;
+ for (int i = 0; i < 8; i++) {
+ char c = s[i];
+ uint32 nibble;
+ if (c >= '0' && c <= '9') nibble = c - '0';
+ else if (c >= 'a' && c <= 'f') nibble = c - 'a' + 10;
+ else if (c >= 'A' && c <= 'F') nibble = c - 'A' + 10;
+ else return val;
+ val = (val << 4) | nibble;
+ }
+ return val;
+}
+
+static inline uint32 align4(uint32 v)
+{
+ return (v + 3) & ~3U;
+}
+
+/* ================================================================== */
+/* Path resolution helpers */
+/* ================================================================== */
+
+static dentry* cpio_mkdir_p(dentry* root, const char* path,
+ const char** out_name)
+{
+ dentry* cur = root;
+
+ while (*path == '/') path++;
+
+ while (true) {
+ const char* slash = path;
+ while (*slash && *slash != '/') slash++;
+
+ if (*slash == '\0') {
+ *out_name = path;
+ return cur;
+ }
+
+ uint32 len = static_cast<uint32>(slash - path);
+
+ qstr name;
+ name.name = path;
+ name.len = len;
+ name.hash = full_name_hash(path, len);
+
+ dentry* child = d_lookup(cur, &name);
+ if (!child) {
+ child = d_alloc(cur, &name);
+ if (!child) return nullptr;
+
+ inode* ip = ramfs_get_inode(cur->d_inode->i_sb, cur->d_inode,
+ S_IFDIR | 0755, 0);
+ if (!ip) return nullptr;
+
+ d_instantiate(child, ip);
+ dget(child);
+ inc_nlink(cur->d_inode);
+ }
+
+ cur = child;
+ path = slash + 1;
+ while (*path == '/') path++;
+ }
+}
+
+/* ================================================================== */
+/* populate_rootfs — the main entry point */
+/* ================================================================== */
+
+void populate_rootfs()
+{
+ const char* p = __initramfs_start;
+ const char* end = __initramfs_end;
+ uint64 archive_size = static_cast<uint64>(end - p);
+
+ if (archive_size == 0) {
+ printk("[initramfs] no embedded archive found\n");
+ return;
+ }
+
+ printk("[initramfs] unpacking %llu bytes...\n", archive_size);
+
+ int file_count = 0;
+ int dir_count = 0;
+
+ while (p + kCpioHeaderSize <= end) {
+ auto* hdr = reinterpret_cast<const cpio_header*>(p);
+
+ /* Verify magic */
+ if (hdr->magic[0] != '0' || hdr->magic[1] != '7' ||
+ hdr->magic[2] != '0' || hdr->magic[3] != '7' ||
+ hdr->magic[4] != '0' || hdr->magic[5] != '1') {
+ printk("[initramfs] bad cpio magic at offset %llu\n",
+ static_cast<uint64>(p - __initramfs_start));
+ break;
+ }
+
+ uint32 mode = parse_hex8(hdr->mode);
+ uint32 filesize = parse_hex8(hdr->filesize);
+ uint32 namesize = parse_hex8(hdr->namesize);
+
+ const char* name = p + kCpioHeaderSize;
+ uint32 name_padded = align4(kCpioHeaderSize + namesize);
+ const char* data = p + name_padded;
+ uint32 data_padded = align4(filesize);
+
+ const char* next = p + name_padded + data_padded;
+
+ /* Check for TRAILER!!! end-of-archive marker */
+ if (namesize == 11 && name[0] == 'T' && name[1] == 'R' &&
+ name[2] == 'A' && name[3] == 'I' && name[4] == 'L' &&
+ name[5] == 'E' && name[6] == 'R' && name[7] == '!' &&
+ name[8] == '!' && name[9] == '!') {
+ break;
+ }
+
+ /* Skip "." entry */
+ if (namesize == 2 && name[0] == '.' && name[1] == '\0') {
+ p = next;
+ continue;
+ }
+
+ /* Resolve parent directory, creating intermediate dirs */
+ const char* basename = nullptr;
+ dentry* parent = cpio_mkdir_p(g_root_dentry, name, &basename);
+ if (!parent || !basename || basename[0] == '\0') {
+ p = next;
+ continue;
+ }
+
+ /* Build qstr for the final component */
+ uint32 blen = 0;
+ while (basename[blen] && basename[blen] != '/') blen++;
+
+ qstr bname;
+ bname.name = basename;
+ bname.len = blen;
+ bname.hash = full_name_hash(basename, blen);
+
+ if (S_ISDIR(mode)) {
+ dentry* existing = d_lookup(parent, &bname);
+ if (!existing) {
+ dentry* de = d_alloc(parent, &bname);
+ if (de) {
+ inode* ip = ramfs_get_inode(parent->d_inode->i_sb,
+ parent->d_inode,
+ S_IFDIR | (mode & 0777), 0);
+ if (ip) {
+ d_instantiate(de, ip);
+ dget(de);
+ inc_nlink(parent->d_inode);
+ dir_count++;
+ }
+ }
+ }
+ } else if (S_ISREG(mode)) {
+ dentry* de = d_alloc(parent, &bname);
+ if (de) {
+ inode* ip = ramfs_get_inode(parent->d_inode->i_sb,
+ parent->d_inode,
+ S_IFREG | (mode & 0777), 0);
+ if (ip) {
+ d_instantiate(de, ip);
+ dget(de);
+
+ /* Write file content via ramfs write path */
+ if (filesize > 0 && ip->i_fop && ip->i_fop->write) {
+ file tmp_fp = {};
+ tmp_fp.f_inode = ip;
+ tmp_fp.f_path.de = de;
+ tmp_fp.f_pos = 0;
+ tmp_fp.f_flags = O_WRONLY;
+ tmp_fp.f_count.store(1);
+
+ int64 pos = 0;
+ ip->i_fop->write(&tmp_fp, data,
+ filesize, &pos);
+ }
+
+ file_count++;
+ printk("[initramfs] %s (%u bytes, mode %03o)\n",
+ name, filesize, mode & 0777);
+ }
+ }
+ } else if (S_ISLNK(mode)) {
+ /* Symlink: file data is the target path (not NUL-terminated).
+ * Linux: init/initramfs.c do_symlink() */
+ dentry* de = d_alloc(parent, &bname);
+ if (de) {
+ inode* ip = ramfs_get_inode(parent->d_inode->i_sb,
+ parent->d_inode,
+ S_IFLNK | 0777, 0);
+ if (ip) {
+ /* Store symlink target in inode.
+ * Allocate a kernel buffer and copy the target. */
+ char* target = new char[filesize + 1];
+ if (target) {
+ for (uint32 i = 0; i < filesize; i++)
+ target[i] = data[i];
+ target[filesize] = '\0';
+ ip->i_link = target;
+ }
+
+ d_instantiate(de, ip);
+ dget(de);
+ file_count++;
+ printk("[initramfs] %s -> %s (symlink)\n",
+ name, target ? target : "?");
+ }
+ }
+ }
+
+ p = next;
+ }
+
+ printk("[initramfs] done: %d files, %d dirs\n",
+ file_count, dir_count);
+}
diff --git a/na/fs/inode.cc b/na/fs/inode.cc
new file mode 100644
index 0000000..1d3d006
--- /dev/null
+++ b/na/fs/inode.cc
@@ -0,0 +1,166 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * inode.cc — Inode allocation, lifecycle, and generic helpers
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+
+namespace {
+lume::atomic<uint64> g_next_ino{1};
+} // namespace
+
+/* ================================================================== */
+/* new_inode — allocate a fresh inode on a superblock */
+/* ================================================================== */
+
+inode* new_inode(super_block* sb)
+{
+ inode* ip = nullptr;
+
+ if (sb->s_op && sb->s_op->alloc_inode)
+ ip = sb->s_op->alloc_inode(sb);
+ else
+ ip = new inode{};
+
+ if (!ip) return nullptr;
+
+ /* Zero-init key fields */
+ ip->i_sb = sb;
+ ip->i_ino = g_next_ino.fetch_add(1);
+ ip->i_mode = 0;
+ ip->i_flags = 0;
+ ip->i_uid = 0;
+ ip->i_gid = 0;
+ ip->i_nlink = 1;
+ ip->i_rdev = 0;
+ ip->i_size = 0;
+ ip->i_blocks = 0;
+ ip->i_bytes = 0;
+ ip->i_blkbits = sb->s_blocksize_bits;
+ ip->i_generation = 0;
+ ip->i_state = I_NEW;
+ ip->i_count.store(1);
+ ip->i_writecount.store(0);
+
+ ip->i_atime_sec = ip->i_mtime_sec = ip->i_ctime_sec = 0;
+ ip->i_atime_nsec = ip->i_mtime_nsec = ip->i_ctime_nsec = 0;
+
+ ip->i_op = nullptr;
+ ip->i_fop = nullptr;
+ ip->i_mapping = &ip->i_data;
+ ip->i_private = nullptr;
+ ip->i_pipe = nullptr;
+
+ /* Init embedded address_space */
+ ip->i_data.host = ip;
+ ip->i_data.a_ops = nullptr;
+ ip->i_data.nrpages = 0;
+ ip->i_data.flags = 0;
+
+ /* Init lists */
+ list_init(&ip->i_sb_list);
+ list_init(&ip->i_dentry);
+
+ /* Add to superblock's inode list */
+ {
+ LockGuard guard(sb->s_inode_list_lock);
+ list_add_tail(&ip->i_sb_list, &sb->s_inodes);
+ }
+
+ return ip;
+}
+
+/* ================================================================== */
+/* ihold / iput — reference counting */
+/* ================================================================== */
+
+void ihold(inode* ip)
+{
+ ip->i_count.fetch_add(1);
+}
+
+void iput(inode* ip)
+{
+ if (!ip) return;
+
+ int old = ip->i_count.fetch_sub(1);
+ if (old > 1) return;
+
+ /* Last reference dropped — evict */
+ if (ip->i_sb->s_op && ip->i_sb->s_op->evict_inode)
+ ip->i_sb->s_op->evict_inode(ip);
+
+ /* Remove from sb list */
+ {
+ LockGuard guard(ip->i_sb->s_inode_list_lock);
+ list_del(&ip->i_sb_list);
+ }
+
+ if (ip->i_sb->s_op && ip->i_sb->s_op->destroy_inode)
+ ip->i_sb->s_op->destroy_inode(ip);
+ else
+ delete ip;
+}
+
+/* ================================================================== */
+/* nlink helpers */
+/* ================================================================== */
+
+void set_nlink(inode* ip, uint32 nlink) { ip->i_nlink = nlink; }
+void inc_nlink(inode* ip) { ip->i_nlink++; }
+void drop_nlink(inode* ip) { ip->i_nlink--; }
+void clear_nlink(inode* ip) { ip->i_nlink = 0; }
+
+/* ================================================================== */
+/* inode_init_owner */
+/* ================================================================== */
+
+void inode_init_owner(inode* ip, const inode* dir, uint32 mode)
+{
+ ip->i_uid = 0; /* Phase 1: always root */
+ ip->i_gid = 0;
+ ip->i_mode = mode;
+}
+
+/* ================================================================== */
+/* generic_fillattr — fill kstat from inode (used by getattr) */
+/* ================================================================== */
+
+void generic_fillattr(inode* ip, kstat* stat)
+{
+ stat->st_dev = ip->i_sb->s_dev;
+ stat->st_ino = ip->i_ino;
+ stat->st_mode = ip->i_mode;
+ stat->st_nlink = ip->i_nlink;
+ stat->st_uid = ip->i_uid;
+ stat->st_gid = ip->i_gid;
+ stat->st_rdev = ip->i_rdev;
+ stat->__pad = 0;
+ stat->st_size = ip->i_size;
+ stat->st_blksize = 1 << ip->i_blkbits;
+ stat->__pad2 = 0;
+ stat->st_blocks = ip->i_blocks;
+ stat->st_atime_sec = ip->i_atime_sec;
+ stat->st_atime_nsec = ip->i_atime_nsec;
+ stat->st_mtime_sec = ip->i_mtime_sec;
+ stat->st_mtime_nsec = ip->i_mtime_nsec;
+ stat->st_ctime_sec = ip->i_ctime_sec;
+ stat->st_ctime_nsec = ip->i_ctime_nsec;
+ stat->__unused[0] = 0;
+ stat->__unused[1] = 0;
+}
+
+/* ================================================================== */
+/* mark_inode_dirty (stub) */
+/* ================================================================== */
+
+void mark_inode_dirty(inode* ip)
+{
+ ip->i_state |= I_DIRTY_SYNC;
+ /* Phase 1: no writeback, ramfs is memory-only */
+}
diff --git a/na/fs/module.mk b/na/fs/module.mk
new file mode 100644
index 0000000..f1be157
--- /dev/null
+++ b/na/fs/module.mk
@@ -0,0 +1,17 @@
+# fs/module.mk — VFS subsystem build rules
+
+SRCS_CC += fs/super.cc
+SRCS_CC += fs/inode.cc
+SRCS_CC += fs/dcache.cc
+SRCS_CC += fs/file_table.cc
+SRCS_CC += fs/fd.cc
+SRCS_CC += fs/fs_struct.cc
+SRCS_CC += fs/namei.cc
+SRCS_CC += fs/ramfs/inode.cc
+SRCS_CC += fs/ramfs/file.cc
+SRCS_CC += fs/ramfs/super.cc
+SRCS_CC += fs/vfs_init.cc
+SRCS_CC += fs/chardev.cc
+SRCS_CC += fs/devfs.cc
+SRCS_CC += fs/pipe.cc
+SRCS_CC += fs/initramfs.cc
diff --git a/na/fs/namei.cc b/na/fs/namei.cc
new file mode 100644
index 0000000..9124ea9
--- /dev/null
+++ b/na/fs/namei.cc
@@ -0,0 +1,250 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * namei.cc — Pathname resolution (path lookup)
+ *
+ * Simplified Phase 1: no symlink, no RCU, no mount traversal.
+ */
+
+#include <lume/namei.h>
+#include <lume/dcache.h>
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/fdtable.h>
+#include <lume/fs_struct.h>
+#include <lume/mount.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* ================================================================== */
+/* Helpers */
+/* ================================================================== */
+
+static const char* next_component(const char* path, qstr* result)
+{
+ while (*path == '/') path++;
+ if (*path == '\0') return nullptr;
+
+ const char* start = path;
+ while (*path != '\0' && *path != '/') path++;
+
+ uint32 len = static_cast<uint32>(path - start);
+ result->name = start;
+ result->len = len;
+ result->hash = full_name_hash(start, len);
+
+ return path;
+}
+
+static bool is_dot(const qstr* name) {
+ return name->len == 1 && name->name[0] == '.';
+}
+static bool is_dotdot(const qstr* name) {
+ return name->len == 2 && name->name[0] == '.' && name->name[1] == '.';
+}
+
+/* ================================================================== */
+/* walk_component */
+/* ================================================================== */
+
+static dentry* walk_component(dentry* parent, const qstr* name)
+{
+ if (!parent->d_inode || !S_ISDIR(parent->d_inode->i_mode))
+ return reinterpret_cast<dentry*>(-ENOTDIR);
+
+ if (is_dot(name))
+ return dget(parent);
+
+ if (is_dotdot(name))
+ return dget(parent->d_parent);
+
+ /* Try dcache */
+ dentry* child = d_lookup(parent, name);
+ if (child)
+ return child;
+
+ /* Cache miss — call filesystem lookup */
+ inode* dir = parent->d_inode;
+ if (!dir->i_op || !dir->i_op->lookup)
+ return reinterpret_cast<dentry*>(-ENOENT);
+
+ child = d_alloc(parent, name);
+ if (!child)
+ return reinterpret_cast<dentry*>(-ENOMEM);
+
+ dentry* result = dir->i_op->lookup(dir, child, 0);
+ if (result) {
+ dput(child);
+ return result;
+ }
+
+ /* If lookup didn't set an inode, this is a negative dentry
+ * (file doesn't exist). Remove from dcache and return error. */
+ if (!child->d_inode) {
+ d_drop(child);
+ dput(child);
+ return reinterpret_cast<dentry*>(-ENOENT);
+ }
+
+ return child;
+}
+
+/* ================================================================== */
+/* filename_lookup */
+/* ================================================================== */
+
+int filename_lookup(int dfd, const char* name, unsigned int flags, path* result)
+{
+ if (!name || !result)
+ return -EINVAL;
+
+ TaskControlBlock* task = current_task();
+ dentry* start;
+
+ /* Determine starting dentry */
+ if (name[0] == '/') {
+ start = task->fs->root.de;
+ if (!start) start = g_root_dentry;
+ name++;
+ } else if (dfd == AT_FDCWD) {
+ start = task->fs->pwd.de;
+ if (!start) start = g_root_dentry;
+ } else {
+ file* dir_fp = fcheck(task->files, dfd);
+ if (!dir_fp) return -EBADF;
+ start = dir_fp->f_path.de;
+ if (!start || !start->d_inode || !S_ISDIR(start->d_inode->i_mode))
+ return -ENOTDIR;
+ }
+
+ dentry* current_de = dget(start);
+ const char* p = name;
+
+ while (p && *p) {
+ qstr component;
+ const char* next = next_component(p, &component);
+ if (!next && component.len == 0)
+ break;
+
+ const char* after = next;
+ while (after && *after == '/') after++;
+ bool is_last = (!after || *after == '\0');
+
+ if (is_last && (flags & LOOKUP_PARENT)) {
+ result->de = current_de;
+ result->mnt = task->fs->root.mnt;
+ return 0;
+ }
+
+ dentry* child = walk_component(current_de, &component);
+
+ int64 err = reinterpret_cast<int64>(child);
+ if (err < 0 && err >= -4095) {
+ dput(current_de);
+ return static_cast<int>(err);
+ }
+
+ /* ---- Symlink following (Linux: step_into → pick_link) ----
+ * If the resolved component is a symlink, follow it.
+ * Linux limits this to MAXSYMLINKS (40); we use 8. */
+ constexpr int kMaxSymlinks = 8;
+ int symlink_depth = 0;
+ while (child->d_inode && S_ISLNK(child->d_inode->i_mode) &&
+ child->d_inode->i_link && symlink_depth < kMaxSymlinks) {
+ symlink_depth++;
+ const char* target = child->d_inode->i_link;
+ dput(child);
+
+ /* Absolute symlink: restart from root.
+ * Relative symlink: resolve from current parent. */
+ dentry* link_start;
+ if (target[0] == '/') {
+ link_start = dget(task->fs->root.de ? task->fs->root.de : g_root_dentry);
+ target++;
+ } else {
+ link_start = dget(current_de);
+ }
+
+ /* Walk each component of the symlink target */
+ dentry* link_cur = link_start;
+ const char* lp = target;
+ qstr lcomp;
+ while (lp && *lp) {
+ const char* lnext = next_component(lp, &lcomp);
+ if (!lnext && lcomp.len == 0) break;
+
+ dentry* lchild = walk_component(link_cur, &lcomp);
+ int64 lerr = reinterpret_cast<int64>(lchild);
+ if (lerr < 0 && lerr >= -4095) {
+ dput(link_cur);
+ dput(current_de);
+ return static_cast<int>(lerr);
+ }
+
+ dput(link_cur);
+ link_cur = lchild;
+ lp = lnext;
+ }
+
+ child = link_cur;
+ }
+
+ if (symlink_depth >= kMaxSymlinks) {
+ dput(child);
+ dput(current_de);
+ return -ELOOP;
+ }
+
+ if (!is_last && child->d_inode && !S_ISDIR(child->d_inode->i_mode)) {
+ dput(child);
+ dput(current_de);
+ return -ENOTDIR;
+ }
+
+ if (!is_last && !child->d_inode) {
+ dput(child);
+ dput(current_de);
+ return -ENOENT;
+ }
+
+ dput(current_de);
+ current_de = child;
+ p = next;
+ }
+
+ if ((flags & LOOKUP_DIRECTORY) && current_de->d_inode &&
+ !S_ISDIR(current_de->d_inode->i_mode)) {
+ dput(current_de);
+ return -ENOTDIR;
+ }
+
+ /* Negative dentry: the path was walked, but the final component
+ * does not exist (d_inode == NULL). Return -ENOENT. */
+ if (!current_de->d_inode) {
+ dput(current_de);
+ return -ENOENT;
+ }
+
+ result->de = current_de;
+ result->mnt = task->fs->root.mnt;
+ return 0;
+}
+
+int kern_path(const char* name, unsigned int flags, path* result)
+{
+ return filename_lookup(AT_FDCWD, name, flags, result);
+}
+
+dentry* lookup_one_len(const char* name, dentry* parent, int len)
+{
+ qstr q;
+ q.name = name;
+ q.len = static_cast<uint32>(len);
+ q.hash = full_name_hash(name, q.len);
+
+ return walk_component(parent, &q);
+}
+
+void namei_init() {}
diff --git a/na/fs/pipe.cc b/na/fs/pipe.cc
new file mode 100644
index 0000000..f7708f5
--- /dev/null
+++ b/na/fs/pipe.cc
@@ -0,0 +1,278 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * pipe.cc — Pipe implementation
+ *
+ * Implements unidirectional byte-stream pipes with blocking I/O.
+ * Each pipe has a 4KB circular buffer. Reads block when empty,
+ * writes block when full (unless O_NONBLOCK).
+ *
+ * When all writers close, read returns 0 (EOF).
+ * When all readers close, write returns -EPIPE.
+ */
+
+#include <lume/pipe.h>
+#include <lume/file.h>
+#include <lume/fdtable.h>
+#include <lume/errno.h>
+#include <lume/slab.h>
+#include <lume/fs_types.h>
+#include <lume/klog.h>
+#include <lume/sched.h>
+
+/* ================================================================== */
+/* pipe_inode_info lifecycle */
+/* ================================================================== */
+
+pipe_inode_info* alloc_pipe_info()
+{
+ auto* pi = new pipe_inode_info{};
+ if (!pi) return nullptr;
+
+ pi->rd_pos = 0;
+ pi->wr_pos = 0;
+ pi->len = 0;
+ pi->rd_wait.init("pipe_rd");
+ pi->wr_wait.init("pipe_wr");
+ pi->readers.store(0);
+ pi->writers.store(0);
+ pi->refcount.store(1);
+ return pi;
+}
+
+void free_pipe_info(pipe_inode_info* pipe)
+{
+ delete pipe;
+}
+
+static void pipe_get(pipe_inode_info* pi)
+{
+ pi->refcount.fetch_add(1);
+}
+
+static void pipe_put(pipe_inode_info* pi)
+{
+ if (pi->refcount.fetch_sub(1) == 1) {
+ free_pipe_info(pi);
+ }
+}
+
+/* ================================================================== */
+/* pipe_read */
+/* ================================================================== */
+
+static int64 pipe_read(file* fp, char* buf, uint64 count, int64* /*pos*/)
+{
+ pipe_inode_info* pi = static_cast<pipe_inode_info*>(fp->private_data);
+ if (!pi) return -EBADF;
+
+ while (true) {
+ pi->lock.acquire();
+
+ if (pi->len > 0) {
+ /* Data available — copy out */
+ int64 to_read = static_cast<int64>(count);
+ if (to_read > pi->len) to_read = pi->len;
+
+ for (int64 i = 0; i < to_read; i++) {
+ buf[i] = pi->buf[pi->rd_pos];
+ pi->rd_pos = (pi->rd_pos + 1) % PIPE_BUF_SIZE;
+ }
+ pi->len -= static_cast<int>(to_read);
+
+ pi->lock.release();
+
+ /* Wake any blocked writers */
+ pi->wr_wait.wake_all();
+ return to_read;
+ }
+
+ /* Buffer empty */
+ if (pi->writers.load() == 0) {
+ /* No writers left — EOF */
+ pi->lock.release();
+ return 0;
+ }
+
+ if (fp->f_flags & O_NONBLOCK) {
+ pi->lock.release();
+ return -EAGAIN;
+ }
+
+ pi->lock.release();
+
+ /* Sleep until data or writer closes */
+ pi->rd_wait.sleep();
+ }
+}
+
+/* ================================================================== */
+/* pipe_write */
+/* ================================================================== */
+
+static int64 pipe_write(file* fp, const char* buf, uint64 count, int64* /*pos*/)
+{
+ pipe_inode_info* pi = static_cast<pipe_inode_info*>(fp->private_data);
+ if (!pi) return -EBADF;
+
+ if (pi->readers.load() == 0) {
+ return -EPIPE; /* Broken pipe */
+ }
+
+ int64 written = 0;
+
+ while (static_cast<uint64>(written) < count) {
+ pi->lock.acquire();
+
+ if (pi->len < PIPE_BUF_SIZE) {
+ /* Space available — copy in */
+ int avail = PIPE_BUF_SIZE - pi->len;
+ int64 remaining = static_cast<int64>(count) - written;
+ int to_write = avail < static_cast<int>(remaining)
+ ? avail
+ : static_cast<int>(remaining);
+
+ for (int i = 0; i < to_write; i++) {
+ pi->buf[pi->wr_pos] = buf[written + i];
+ pi->wr_pos = (pi->wr_pos + 1) % PIPE_BUF_SIZE;
+ }
+ pi->len += to_write;
+ written += to_write;
+
+ pi->lock.release();
+
+ /* Wake any blocked readers */
+ pi->rd_wait.wake_all();
+ continue;
+ }
+
+ /* Buffer full */
+ if (pi->readers.load() == 0) {
+ pi->lock.release();
+ return written > 0 ? written : -EPIPE;
+ }
+
+ if (fp->f_flags & O_NONBLOCK) {
+ pi->lock.release();
+ return written > 0 ? written : -EAGAIN;
+ }
+
+ pi->lock.release();
+
+ /* Sleep until space or reader closes */
+ pi->wr_wait.sleep();
+ }
+
+ return written;
+}
+
+/* ================================================================== */
+/* pipe_release — called on last fput of a pipe fd */
+/* ================================================================== */
+
+static int pipe_read_release(inode* /*ip*/, file* fp)
+{
+ pipe_inode_info* pi = static_cast<pipe_inode_info*>(fp->private_data);
+ if (pi) {
+ pi->readers.fetch_sub(1);
+ /* Wake writers so they see readers==0 and get EPIPE */
+ pi->wr_wait.wake_all();
+ pipe_put(pi);
+ }
+ return 0;
+}
+
+static int pipe_write_release(inode* /*ip*/, file* fp)
+{
+ pipe_inode_info* pi = static_cast<pipe_inode_info*>(fp->private_data);
+ if (pi) {
+ pi->writers.fetch_sub(1);
+ /* Wake readers so they see writers==0 and get EOF */
+ pi->rd_wait.wake_all();
+ pipe_put(pi);
+ }
+ return 0;
+}
+
+/* ================================================================== */
+/* file_operations tables */
+/* ================================================================== */
+
+static file_operations pipe_read_fops;
+static file_operations pipe_write_fops;
+static bool pipe_fops_initialized = false;
+
+static void pipe_fops_init()
+{
+ if (pipe_fops_initialized) return;
+
+ pipe_read_fops = {};
+ pipe_read_fops.read = pipe_read;
+ pipe_read_fops.release = pipe_read_release;
+
+ pipe_write_fops = {};
+ pipe_write_fops.write = pipe_write;
+ pipe_write_fops.release = pipe_write_release;
+
+ pipe_fops_initialized = true;
+}
+
+/* ================================================================== */
+/* do_pipe2 — create a pipe, install two fds */
+/* ================================================================== */
+
+int do_pipe2(files_struct* files, int fds[2], int flags)
+{
+ pipe_fops_init();
+
+ pipe_inode_info* pi = alloc_pipe_info();
+ if (!pi) return -ENOMEM;
+
+ /* Create read-end file */
+ file* rd_fp = alloc_empty_file(O_RDONLY | (flags & O_NONBLOCK));
+ if (!rd_fp) {
+ free_pipe_info(pi);
+ return -ENFILE;
+ }
+ rd_fp->f_op = &pipe_read_fops;
+ rd_fp->private_data = pi;
+
+ /* Create write-end file */
+ file* wr_fp = alloc_empty_file(O_WRONLY | (flags & O_NONBLOCK));
+ if (!wr_fp) {
+ fput(rd_fp);
+ free_pipe_info(pi);
+ return -ENFILE;
+ }
+ wr_fp->f_op = &pipe_write_fops;
+ wr_fp->private_data = pi;
+
+ /* Bump pipe refcount for the two files */
+ pipe_get(pi); /* wr_fp gets second ref */
+ pi->readers.store(1);
+ pi->writers.store(1);
+
+ /* Allocate fds */
+ int rd_fd = alloc_fd(files, 0);
+ if (rd_fd < 0) {
+ fput(rd_fp);
+ fput(wr_fp);
+ return rd_fd;
+ }
+
+ int wr_fd = alloc_fd(files, 0);
+ if (wr_fd < 0) {
+ close_fd(files, rd_fd);
+ fput(wr_fp);
+ return wr_fd;
+ }
+
+ fd_install(files, rd_fd, rd_fp);
+ fd_install(files, wr_fd, wr_fp);
+
+ fds[0] = rd_fd;
+ fds[1] = wr_fd;
+
+ return 0;
+}
diff --git a/na/fs/ramfs/file.cc b/na/fs/ramfs/file.cc
new file mode 100644
index 0000000..6d9a070
--- /dev/null
+++ b/na/fs/ramfs/file.cc
@@ -0,0 +1,216 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * ramfs/file.cc — RamFS file operations (read/write via in-memory pages)
+ *
+ * Phase 1: simple flat buffer per-inode via i_private.
+ * Will migrate to page_cache integration later.
+ */
+
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/dcache.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/*
+ * Simplest possible in-memory file storage:
+ * i_private points to a dynamically grown buffer.
+ */
+struct ramfs_file_data {
+ char* buf;
+ uint64 capacity;
+};
+
+static ramfs_file_data* get_or_create_data(inode* ip)
+{
+ if (!ip->i_private) {
+ auto* data = new ramfs_file_data{};
+ data->buf = nullptr;
+ data->capacity = 0;
+ ip->i_private = data;
+ }
+ return static_cast<ramfs_file_data*>(ip->i_private);
+}
+
+static int ensure_capacity(ramfs_file_data* data, uint64 needed)
+{
+ if (needed <= data->capacity) return 0;
+
+ /* Round up to page size */
+ uint64 new_cap = (needed + 4095) & ~4095ULL;
+ char* new_buf = new char[new_cap];
+ if (!new_buf) return -ENOMEM;
+
+ /* Copy old data */
+ if (data->buf && data->capacity > 0) {
+ for (uint64 i = 0; i < data->capacity; i++)
+ new_buf[i] = data->buf[i];
+ }
+ /* Zero new region */
+ for (uint64 i = data->capacity; i < new_cap; i++)
+ new_buf[i] = 0;
+
+ delete[] data->buf;
+ data->buf = new_buf;
+ data->capacity = new_cap;
+ return 0;
+}
+
+/* ================================================================== */
+/* ramfs_read */
+/* ================================================================== */
+
+static int64 ramfs_read(file* fp, char* buf, uint64 count, int64* pos)
+{
+ inode* ip = file_inode(fp);
+ int64 offset = *pos;
+
+ if (offset < 0) return -EINVAL;
+ if (offset >= ip->i_size) return 0;
+
+ int64 avail = ip->i_size - offset;
+ if (static_cast<int64>(count) > avail)
+ count = static_cast<uint64>(avail);
+
+ auto* data = static_cast<ramfs_file_data*>(ip->i_private);
+ if (!data || !data->buf) return 0;
+
+ /* Copy to user buffer.
+ * TODO: use copy_to_user when called from syscall context */
+ for (uint64 i = 0; i < count; i++)
+ buf[i] = data->buf[offset + i];
+
+ *pos += count;
+ return static_cast<int64>(count);
+}
+
+/* ================================================================== */
+/* ramfs_write */
+/* ================================================================== */
+
+static int64 ramfs_write(file* fp, const char* buf, uint64 count, int64* pos)
+{
+ inode* ip = file_inode(fp);
+ int64 offset = *pos;
+
+ if (fp->f_flags & O_APPEND)
+ offset = ip->i_size;
+
+ if (offset < 0) return -EINVAL;
+
+ auto* data = get_or_create_data(ip);
+ uint64 end = static_cast<uint64>(offset) + count;
+
+ int err = ensure_capacity(data, end);
+ if (err) return err;
+
+ /* Copy from user buffer */
+ for (uint64 i = 0; i < count; i++)
+ data->buf[offset + i] = buf[i];
+
+ if (static_cast<int64>(end) > ip->i_size)
+ ip->i_size = end;
+
+ *pos = end;
+ mark_inode_dirty(ip);
+ return static_cast<int64>(count);
+}
+
+/* ================================================================== */
+/* ramfs_dir_iterate — getdents64 */
+/* ================================================================== */
+
+static int ramfs_dir_iterate(file* fp, dir_context* ctx)
+{
+ dentry* dir_de = file_dentry(fp);
+ inode* dir_ip = file_inode(fp);
+ int64 pos = ctx->pos;
+ int count = 0;
+
+ /* pos 0: emit "." */
+ if (pos == 0) {
+ if (!ctx->actor(ctx, ".", 1, pos, dir_ip->i_ino, DT_DIR))
+ return 0;
+ ctx->pos = 1;
+ pos = 1;
+ count++;
+ }
+
+ /* pos 1: emit ".." */
+ if (pos == 1) {
+ uint64 parent_ino = dir_de->d_parent->d_inode
+ ? dir_de->d_parent->d_inode->i_ino : dir_ip->i_ino;
+ if (!ctx->actor(ctx, "..", 2, pos, parent_ino, DT_DIR))
+ return 0;
+ ctx->pos = 2;
+ pos = 2;
+ count++;
+ }
+
+ /* pos >= 2: iterate children */
+ int64 child_idx = 2;
+ list_node* node;
+ list_for_each(node, &dir_de->d_children) {
+ dentry* child = list_entry(node, dentry, d_child);
+
+ if (!child->d_inode)
+ continue; /* skip negative dentries */
+
+ if (child_idx >= pos) {
+ uint8 d_type = DT_UNKNOWN;
+ if (S_ISDIR(child->d_inode->i_mode)) d_type = DT_DIR;
+ else if (S_ISREG(child->d_inode->i_mode)) d_type = DT_REG;
+ else if (S_ISCHR(child->d_inode->i_mode)) d_type = DT_CHR;
+ else if (S_ISBLK(child->d_inode->i_mode)) d_type = DT_BLK;
+ else if (S_ISLNK(child->d_inode->i_mode)) d_type = DT_LNK;
+ else if (S_ISFIFO(child->d_inode->i_mode)) d_type = DT_FIFO;
+
+ if (!ctx->actor(ctx, child->d_name.name,
+ static_cast<int>(child->d_name.len),
+ child_idx, child->d_inode->i_ino, d_type))
+ return 0;
+
+ ctx->pos = child_idx + 1;
+ count++;
+ }
+ child_idx++;
+ }
+
+ return count;
+}
+
+extern file_operations ramfs_file_operations;
+extern file_operations ramfs_dir_operations;
+
+file_operations ramfs_file_operations;
+file_operations ramfs_dir_operations;
+
+void ramfs_file_ops_init()
+{
+ ramfs_file_operations.llseek = nullptr;
+ ramfs_file_operations.read = ramfs_read;
+ ramfs_file_operations.write = ramfs_write;
+ ramfs_file_operations.iterate_shared = nullptr;
+ ramfs_file_operations.poll = nullptr;
+ ramfs_file_operations.unlocked_ioctl = nullptr;
+ ramfs_file_operations.mmap = nullptr;
+ ramfs_file_operations.open = nullptr;
+ ramfs_file_operations.flush = nullptr;
+ ramfs_file_operations.release = nullptr;
+ ramfs_file_operations.fsync = nullptr;
+
+ ramfs_dir_operations.llseek = nullptr;
+ ramfs_dir_operations.read = nullptr;
+ ramfs_dir_operations.write = nullptr;
+ ramfs_dir_operations.iterate_shared = ramfs_dir_iterate;
+ ramfs_dir_operations.poll = nullptr;
+ ramfs_dir_operations.unlocked_ioctl = nullptr;
+ ramfs_dir_operations.mmap = nullptr;
+ ramfs_dir_operations.open = nullptr;
+ ramfs_dir_operations.flush = nullptr;
+ ramfs_dir_operations.release = nullptr;
+ ramfs_dir_operations.fsync = nullptr;
+}
diff --git a/na/fs/ramfs/inode.cc b/na/fs/ramfs/inode.cc
new file mode 100644
index 0000000..c5e49bf
--- /dev/null
+++ b/na/fs/ramfs/inode.cc
@@ -0,0 +1,164 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * ramfs/inode.cc — RamFS inode operations
+ *
+ * A simple in-memory filesystem (like Linux tmpfs without swap).
+ * All data lives in memory via the page cache.
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+/* Forward declarations */
+extern file_operations ramfs_file_operations;
+extern file_operations ramfs_dir_operations;
+extern inode_operations ramfs_dir_inode_operations;
+
+/* ================================================================== */
+/* ramfs_get_inode — allocate + init an inode for ramfs */
+/* ================================================================== */
+
+inode* ramfs_get_inode(super_block* sb, const inode* dir, uint32 mode, uint32 rdev)
+{
+ inode* ip = new_inode(sb);
+ if (!ip) return nullptr;
+
+ inode_init_owner(ip, dir, mode);
+ /* TODO: set timestamps to current time when we have a time source */
+
+ switch (mode & S_IFMT) {
+ case S_IFREG:
+ ip->i_op = nullptr; /* regular files don't need inode_ops for Phase 1 */
+ ip->i_fop = &ramfs_file_operations;
+ break;
+ case S_IFDIR:
+ ip->i_op = &ramfs_dir_inode_operations;
+ ip->i_fop = &ramfs_dir_operations;
+ inc_nlink(ip); /* directories start with nlink=2 (. and parent's link) */
+ break;
+ case S_IFLNK:
+ /* Symlink: stub for Phase 1 */
+ break;
+ default:
+ /* Character/block devices: i_rdev */
+ ip->i_rdev = rdev;
+ break;
+ }
+
+ return ip;
+}
+
+/* ================================================================== */
+/* Directory inode operations */
+/* ================================================================== */
+
+static dentry* ramfs_lookup(inode* dir, dentry* de, unsigned int flags)
+{
+ (void)dir;
+ (void)flags;
+ /* For ramfs, everything is in dcache. If we get here, it means
+ * d_lookup() already failed, so this is a negative dentry.
+ * Just return nullptr to let the caller see de->d_inode == NULL. */
+ return nullptr;
+}
+
+static int ramfs_create(inode* dir, dentry* de, uint32 mode, bool excl)
+{
+ (void)excl;
+ inode* ip = ramfs_get_inode(dir->i_sb, dir, mode | S_IFREG, 0);
+ if (!ip) return -ENOMEM;
+
+ d_instantiate(de, ip);
+ dget(de);
+ dir->i_size += de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_mkdir(inode* dir, dentry* de, uint32 mode)
+{
+ inode* ip = ramfs_get_inode(dir->i_sb, dir, mode | S_IFDIR, 0);
+ if (!ip) return -ENOMEM;
+
+ d_instantiate(de, ip);
+ dget(de);
+ inc_nlink(dir);
+ dir->i_size += de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_rmdir(inode* dir, dentry* de)
+{
+ /* Check if directory is empty */
+ if (!list_empty(&de->d_children))
+ return -ENOTEMPTY;
+
+ drop_nlink(de->d_inode); /* remove . link */
+ drop_nlink(de->d_inode); /* remove parent's link */
+ drop_nlink(dir);
+ d_delete(de);
+ dir->i_size -= de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_unlink(inode* dir, dentry* de)
+{
+ drop_nlink(de->d_inode);
+ d_delete(de);
+ dir->i_size -= de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_link(dentry* old_de, inode* dir, dentry* new_de)
+{
+ inode* ip = old_de->d_inode;
+ if (!ip) return -ENOENT;
+
+ inc_nlink(ip);
+ ihold(ip);
+ d_instantiate(new_de, ip);
+ dget(new_de);
+ dir->i_size += new_de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+static int ramfs_mknod(inode* dir, dentry* de, uint32 mode, uint32 rdev)
+{
+ inode* ip = ramfs_get_inode(dir->i_sb, dir, mode, rdev);
+ if (!ip) return -ENOMEM;
+
+ d_instantiate(de, ip);
+ dget(de);
+ dir->i_size += de->d_name.len;
+ mark_inode_dirty(dir);
+ return 0;
+}
+
+inode_operations ramfs_dir_inode_operations;
+
+void ramfs_dir_inode_ops_init()
+{
+ ramfs_dir_inode_operations.lookup = ramfs_lookup;
+ ramfs_dir_inode_operations.create = ramfs_create;
+ ramfs_dir_inode_operations.link = ramfs_link;
+ ramfs_dir_inode_operations.unlink = ramfs_unlink;
+ ramfs_dir_inode_operations.symlink = nullptr;
+ ramfs_dir_inode_operations.mkdir = ramfs_mkdir;
+ ramfs_dir_inode_operations.rmdir = ramfs_rmdir;
+ ramfs_dir_inode_operations.mknod = ramfs_mknod;
+ ramfs_dir_inode_operations.rename = nullptr;
+ ramfs_dir_inode_operations.setattr = nullptr;
+ ramfs_dir_inode_operations.getattr = nullptr;
+ ramfs_dir_inode_operations.get_link = nullptr;
+ ramfs_dir_inode_operations.permission = nullptr;
+}
diff --git a/na/fs/ramfs/super.cc b/na/fs/ramfs/super.cc
new file mode 100644
index 0000000..6f929f2
--- /dev/null
+++ b/na/fs/ramfs/super.cc
@@ -0,0 +1,154 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * ramfs/super.cc — RamFS superblock: mount, fill_super, and init
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/mount.h>
+#include <lume/slab.h>
+#include <lume/kprintf.h>
+#include <lume/errno.h>
+#include <lume/fs_types.h>
+
+extern inode* ramfs_get_inode(super_block* sb, const inode* dir,
+ uint32 mode, uint32 rdev);
+
+/* Superblock allocation helper (defined in super.cc) */
+extern super_block* alloc_super(file_system_type* type);
+
+/* ================================================================== */
+/* ramfs super_operations */
+/* ================================================================== */
+
+static inode* ramfs_alloc_inode(super_block* sb)
+{
+ return new inode{};
+}
+
+static void ramfs_destroy_inode(inode* ip)
+{
+ /* Free ramfs_file_data if any */
+ if (ip->i_private) {
+ struct ramfs_file_data {
+ char* buf;
+ uint64 capacity;
+ };
+ auto* data = static_cast<ramfs_file_data*>(ip->i_private);
+ delete[] data->buf;
+ delete data;
+ }
+ delete ip;
+}
+
+static int ramfs_statfs(dentry* de, kstatfs* buf)
+{
+ /* Simple fake values */
+ buf->f_type = 0x858458f6; /* RAMFS_MAGIC */
+ buf->f_bsize = 4096;
+ buf->f_blocks = 0;
+ buf->f_bfree = 0;
+ buf->f_bavail = 0;
+ buf->f_files = 0;
+ buf->f_ffree = 0;
+ buf->f_namelen = 255;
+ buf->f_frsize = 4096;
+ buf->f_flags = 0;
+ return 0;
+}
+
+static super_operations ramfs_sops;
+
+static void ramfs_sops_init()
+{
+ ramfs_sops.alloc_inode = ramfs_alloc_inode;
+ ramfs_sops.destroy_inode = ramfs_destroy_inode;
+ ramfs_sops.free_inode = nullptr;
+ ramfs_sops.dirty_inode = nullptr;
+ ramfs_sops.write_inode = nullptr;
+ ramfs_sops.drop_inode = nullptr;
+ ramfs_sops.evict_inode = nullptr;
+ ramfs_sops.put_super = nullptr;
+ ramfs_sops.sync_fs = nullptr;
+ ramfs_sops.statfs = ramfs_statfs;
+ ramfs_sops.umount_begin = nullptr;
+}
+
+/* ================================================================== */
+/* ramfs_fill_super */
+/* ================================================================== */
+
+static int ramfs_fill_super(super_block* sb, void* data, int silent)
+{
+ sb->s_op = &ramfs_sops;
+ sb->s_magic = 0x858458f6;
+ sb->s_blocksize = 4096;
+ sb->s_blocksize_bits = 12;
+ sb->s_maxbytes = 0x7FFFFFFFFFFFFFFFLL;
+ sb->s_time_gran = 1;
+
+ /* Create root inode */
+ inode* root_ip = ramfs_get_inode(sb, nullptr, S_IFDIR | 0755, 0);
+ if (!root_ip) return -ENOMEM;
+
+ /* Create root dentry */
+ sb->s_root = d_make_root(root_ip);
+ if (!sb->s_root)
+ return -ENOMEM;
+
+ return 0;
+}
+
+/* ================================================================== */
+/* ramfs_mount */
+/* ================================================================== */
+
+static super_block* ramfs_mount(file_system_type* type, int flags,
+ const char* dev_name, void* data)
+{
+ super_block* sb = alloc_super(type);
+ if (!sb) return nullptr;
+
+ int err = ramfs_fill_super(sb, data, flags & SB_SILENT);
+ if (err) {
+ delete sb;
+ return nullptr;
+ }
+
+ return sb;
+}
+
+static void ramfs_kill_sb(super_block* sb)
+{
+ /* Phase 1: no cleanup needed for ramfs */
+ (void)sb;
+}
+
+/* ================================================================== */
+/* Filesystem type registration */
+/* ================================================================== */
+
+static file_system_type ramfs_fs_type;
+
+extern void ramfs_file_ops_init();
+extern void ramfs_dir_inode_ops_init();
+
+void ramfs_init()
+{
+ /* Runtime init to avoid un-relocated absolute pointers in .data */
+ ramfs_sops_init();
+ ramfs_file_ops_init();
+ ramfs_dir_inode_ops_init();
+
+ ramfs_fs_type.name = "ramfs";
+ ramfs_fs_type.fs_flags = FS_NO_DEVICE;
+ ramfs_fs_type.mount = ramfs_mount;
+ ramfs_fs_type.kill_sb = ramfs_kill_sb;
+ list_init(&ramfs_fs_type.fs_list);
+
+ register_filesystem(&ramfs_fs_type);
+}
+
diff --git a/na/fs/super.cc b/na/fs/super.cc
new file mode 100644
index 0000000..231b57c
--- /dev/null
+++ b/na/fs/super.cc
@@ -0,0 +1,95 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * super.cc — Superblock lifecycle + filesystem type registry
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/mount.h>
+#include <lume/klog.h>
+#include <lume/slab.h>
+#include <lume/errno.h>
+
+/* Global root filesystem entry points */
+vfsmount* g_root_mnt = nullptr;
+dentry* g_root_dentry = nullptr;
+
+/* ================================================================== */
+/* Filesystem type registry */
+/* ================================================================== */
+
+namespace {
+
+list_head g_fs_types;
+Spinlock g_fs_type_lock;
+
+} // namespace
+
+int register_filesystem(file_system_type* fs)
+{
+ LockGuard guard(g_fs_type_lock);
+ list_add_tail(&fs->fs_list, &g_fs_types);
+ printk("[vfs] registered filesystem: %s\n", fs->name);
+ return 0;
+}
+
+file_system_type* get_fs_type(const char* name)
+{
+ LockGuard guard(g_fs_type_lock);
+ list_node* pos;
+ list_for_each(pos, &g_fs_types) {
+ auto* fs = list_entry(pos, file_system_type, fs_list);
+ /* Simple strcmp */
+ const char* a = fs->name;
+ const char* b = name;
+ while (*a && *a == *b) { a++; b++; }
+ if (*a == '\0' && *b == '\0')
+ return fs;
+ }
+ return nullptr;
+}
+
+/* ================================================================== */
+/* Superblock allocation */
+/* ================================================================== */
+
+super_block* alloc_super(file_system_type* type)
+{
+ auto* sb = new super_block{};
+ if (!sb) return nullptr;
+
+ list_init(&sb->s_list);
+ list_init(&sb->s_inodes);
+ sb->s_type = type;
+ sb->s_op = nullptr;
+ sb->s_root = nullptr;
+ sb->s_fs_info = nullptr;
+ sb->s_blocksize = 4096;
+ sb->s_blocksize_bits = 12;
+ sb->s_maxbytes = 0x7FFFFFFFFFFFFFFFLL;
+ sb->s_time_gran = 1;
+ sb->s_time_min = 0;
+ sb->s_time_max = 0x7FFFFFFFFFFFFFFFLL;
+ sb->s_flags = 0;
+ sb->s_magic = 0;
+ sb->s_active.store(1);
+ sb->s_count = 1;
+ sb->s_max_links = 0;
+ sb->s_d_op = nullptr;
+ sb->s_mounts = nullptr;
+ sb->s_dev = 0;
+ for (int i = 0; i < 32; i++) sb->s_id[i] = 0;
+
+ return sb;
+}
+
+/* ================================================================== */
+/* Init */
+/* ================================================================== */
+
+void super_init()
+{
+ list_init(&g_fs_types);
+}
diff --git a/na/fs/vfs_init.cc b/na/fs/vfs_init.cc
new file mode 100644
index 0000000..ec56e88
--- /dev/null
+++ b/na/fs/vfs_init.cc
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * vfs_init.cc — Bootstrap the VFS: mount ramfs root, set global root
+ *
+ * Called from kernel_main() after slab_init() and ramfs_init().
+ * This is the moral equivalent of Linux's vfs_caches_init() +
+ * mnt_init() + init_rootfs().
+ */
+
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/file.h>
+#include <lume/mount.h>
+#include <lume/fdtable.h>
+#include <lume/fs_struct.h>
+#include <lume/klog.h>
+#include <lume/kprintf.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+
+/* ================================================================== */
+/* vfs_init — mount ramfs as root filesystem */
+/* ================================================================== */
+
+/*
+ * ramfs_mount is called internally via the file_system_type::mount
+ * callback registered by ramfs_init(). We look it up by name.
+ */
+extern file_system_type* get_fs_type(const char* name);
+extern super_block* alloc_super(file_system_type* type);
+
+void vfs_init()
+{
+ /* 1. Look up the ramfs filesystem type */
+ file_system_type* ramfs_type = get_fs_type("ramfs");
+ if (!ramfs_type || !ramfs_type->mount) {
+ printk(KERN_ERR "vfs_init: ramfs not registered!\n");
+ return;
+ }
+
+ /* 2. Mount ramfs */
+ super_block* sb = ramfs_type->mount(ramfs_type, 0, "ramfs", nullptr);
+ if (!sb || !sb->s_root) {
+ printk(KERN_ERR "vfs_init: failed to mount ramfs!\n");
+ return;
+ }
+
+ /* 3. Set global root */
+ g_root_dentry = sb->s_root;
+ g_root_mnt = nullptr; /* TODO: allocate a proper vfsmount when mount.cc lands */
+
+ dget(g_root_dentry); /* Hold a reference for the global root */
+
+ printk(KERN_INFO "[vfs] root filesystem mounted (ramfs)\n");
+
+ /* 4. Set up VFS state for the current (BSP idle) task,
+ * which will be inherited by all subsequently created tasks. */
+ TaskControlBlock* bsp = current_task();
+ if (bsp) {
+ if (!bsp->files) {
+ bsp->files = alloc_files_struct();
+ }
+ if (!bsp->fs) {
+ bsp->fs = alloc_fs_struct();
+ if (bsp->fs) {
+ bsp->fs->root.de = g_root_dentry;
+ bsp->fs->root.mnt = g_root_mnt;
+ bsp->fs->pwd.de = g_root_dentry;
+ bsp->fs->pwd.mnt = g_root_mnt;
+ dget(g_root_dentry); /* root ref */
+ dget(g_root_dentry); /* pwd ref */
+ }
+ }
+ printk(KERN_INFO "[vfs] BSP task VFS context initialized\n");
+ }
+}
diff --git a/na/include/lume/args.h b/na/include/lume/args.h
new file mode 100644
index 0000000..0562dc5
--- /dev/null
+++ b/na/include/lume/args.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef _LINUX_ARGS_H
+#define _LINUX_ARGS_H
+
+/*
+ * How do these macros work?
+ *
+ * In __COUNT_ARGS() _0 to _15 are just placeholders from the start
+ * in order to make sure _n is positioned over the correct number
+ * from 15 to 0 (depending on X, which is a variadic argument list).
+ * They serve no purpose other than occupying a position. Since each
+ * macro parameter must have a distinct identifier, those identifiers
+ * are as good as any.
+ *
+ * In COUNT_ARGS() we use actual integers, so __COUNT_ARGS() returns
+ * that as _n.
+ */
+
+/* This counts to 15. Any more, it will return 16th argument. */
+#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _n, X...) _n
+#define COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
+
+/* Concatenate two parameters, but allow them to be expanded beforehand. */
+#define __CONCAT(a, b) a ## b
+#define CONCATENATE(a, b) __CONCAT(a, b)
+
+#endif /* _LINUX_ARGS_H */
diff --git a/na/include/lume/atomic.h b/na/include/lume/atomic.h
index e6331c2..6e31129 100644
--- a/na/include/lume/atomic.h
+++ b/na/include/lume/atomic.h
@@ -51,6 +51,16 @@ public:
return __atomic_fetch_sub(&val_, delta, order);
}
+ /* Fetch-and-or, returns old value */
+ T fetch_or(T mask, int order = __ATOMIC_ACQ_REL) {
+ return __atomic_fetch_or(&val_, mask, order);
+ }
+
+ /* Fetch-and-and, returns old value */
+ T fetch_and(T mask, int order = __ATOMIC_ACQ_REL) {
+ return __atomic_fetch_and(&val_, mask, order);
+ }
+
/* Compare-and-swap. Returns true on success. */
bool compare_exchange(T& expected, T desired,
int success_order = __ATOMIC_ACQ_REL,
diff --git a/na/include/lume/boot_params.h b/na/include/lume/boot_params.h
deleted file mode 100644
index 7877468..0000000
--- a/na/include/lume/boot_params.h
+++ /dev/null
@@ -1,85 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#pragma once
-
-#include <lume/types.h>
-
-/*
- * Boot parameters shared between bootloader/firmware and kernel.
- */
-
-inline constexpr uint64 kBootParamsMagic = 0x4C554D45424F4F54ULL; /* LUMEBOOT */
-inline constexpr uint32 kBootParamsVersion = 2;
-
-enum BootMemType : uint32 {
- BOOT_MEM_USABLE = 1,
- BOOT_MEM_RESERVED = 2,
- BOOT_MEM_ACPI = 3,
- BOOT_MEM_MMIO = 4,
-};
-
-enum BootSeedSource : uint32 {
- BOOT_SEED_NONE = 0,
- BOOT_SEED_FDT = 1,
- BOOT_SEED_HW = 2,
- BOOT_SEED_CYCLE = 3,
-};
-
-enum BootImageFlags : uint32 {
- BOOT_IMAGE_COMPRESSED = 1u << 0,
- BOOT_IMAGE_RELOCATED = 1u << 1,
- BOOT_IMAGE_DECOMP_FAILED = 1u << 2,
- BOOT_IMAGE_RELOC_FAILED = 1u << 3,
-};
-
-struct BootMemRegion {
- uint64 base;
- uint64 size;
- uint32 type;
- uint32 reserved;
-};
-
-struct BootParams {
- uint64 magic;
- uint32 version;
- uint32 size;
-
- uint64 cmdline_ptr;
- uint32 cmdline_len;
- uint32 memmap_count;
- uint64 memmap_ptr;
- uint32 memmap_entry_size;
- uint32 reserved0;
-
- uint64 initrd_start;
- uint64 initrd_end;
-
- uint64 acpi_ptr;
- uint64 fdt_ptr;
-
- uint64 kaslr_seed;
- uint32 seed_source;
- uint32 flags;
-
- uint64 kimage_phys_base;
- uint64 kimage_size;
- uint64 kimage_reloc_offset;
- uint64 kimage_reloc_size;
- uint32 kimage_flags;
- uint32 kimage_comp;
-};
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-void boot_params_init_fdt(uint64 fdt_paddr);
-const BootParams* boot_params_get();
-void boot_params_set_image_info(uint64 phys_base, uint64 image_size,
- uint64 reloc_offset, uint64 reloc_size,
- uint32 comp, uint32 image_flags);
-
-#ifdef __cplusplus
-}
-#endif
diff --git a/na/include/lume/chardev.h b/na/include/lume/chardev.h
new file mode 100644
index 0000000..be81ab9
--- /dev/null
+++ b/na/include/lume/chardev.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * chardev.h — Character device abstraction
+ *
+ * Minimal Linux-style cdev registry: each character device has a
+ * major/minor number and a file_operations table.
+ */
+
+#include <lume/types.h>
+#include <lume/list.h>
+
+struct file_operations;
+
+/* ================================================================== */
+/* MKDEV / MAJOR / MINOR */
+/* ================================================================== */
+
+inline constexpr uint32 MKDEV(uint32 ma, uint32 mi) {
+ return (ma << 8) | (mi & 0xFF);
+}
+inline constexpr uint32 MAJOR(uint32 dev) { return dev >> 8; }
+inline constexpr uint32 MINOR(uint32 dev) { return dev & 0xFF; }
+
+/* ================================================================== */
+/* cdev — character device descriptor */
+/* ================================================================== */
+
+struct cdev {
+ const char* name;
+ uint32 dev; /* MKDEV(major, minor) */
+ const file_operations* fops;
+ list_node list; /* global cdev list */
+};
+
+/* ================================================================== */
+/* API */
+/* ================================================================== */
+
+void cdev_init();
+void cdev_register(cdev* dev);
+cdev* cdev_find(uint32 dev);
diff --git a/na/include/lume/clone.h b/na/include/lume/clone.h
new file mode 100644
index 0000000..156bae4
--- /dev/null
+++ b/na/include/lume/clone.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Linux-compatible clone(2) flags.
+ *
+ * Only the flags relevant to current and near-future use are defined.
+ * Reference: include/uapi/linux/sched.h
+ */
+
+#include <lume/types.h>
+
+/* Signal mask stored in low byte of clone flags */
+inline constexpr uint64 CSIGNAL = 0x000000ffULL;
+
+/* Sharing flags */
+inline constexpr uint64 CLONE_VM = 0x00000100ULL; // Share address space
+inline constexpr uint64 CLONE_FS = 0x00000200ULL; // Share fs info
+inline constexpr uint64 CLONE_FILES = 0x00000400ULL; // Share file descriptors
+inline constexpr uint64 CLONE_SIGHAND = 0x00000800ULL; // Share signal handlers
+inline constexpr uint64 CLONE_PIDFD = 0x00001000ULL; // pidfd
+inline constexpr uint64 CLONE_PTRACE = 0x00002000ULL; // Trace child
+inline constexpr uint64 CLONE_VFORK = 0x00004000ULL; // Parent blocks until child exits/execs
+inline constexpr uint64 CLONE_PARENT = 0x00008000ULL; // Same parent as cloner
+inline constexpr uint64 CLONE_THREAD = 0x00010000ULL; // Same thread group
+inline constexpr uint64 CLONE_NEWNS = 0x00020000ULL; // New mount namespace
+inline constexpr uint64 CLONE_SYSVSEM = 0x00040000ULL; // Share SysV semaphore undo
+inline constexpr uint64 CLONE_SETTLS = 0x00080000ULL; // Set TLS
+inline constexpr uint64 CLONE_PARENT_SETTID = 0x00100000ULL; // Write TID to parent
+inline constexpr uint64 CLONE_CHILD_CLEARTID = 0x00200000ULL; // Clear TID on child exit
+inline constexpr uint64 CLONE_DETACHED = 0x00400000ULL; // Unused, historical
+inline constexpr uint64 CLONE_CHILD_SETTID = 0x01000000ULL; // Write TID to child
diff --git a/na/include/lume/compiler.h b/na/include/lume/compiler.h
index fa05363..243f0de 100644
--- a/na/include/lume/compiler.h
+++ b/na/include/lume/compiler.h
@@ -1,27 +1,54 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Copyright (C) 2026 Richard Qin */
-#pragma once
+#ifndef _LUME_COMPILER_H
+#define _LUME_COMPILER_H
-/**
- * compiler.h - Centralized management of compiler-specific macros and built-in wrappers.
+/*
+ * Minimal compiler primitives used across LumeOS.
+ *
+ * Provides only what current code actually references:
+ * - READ_ONCE / WRITE_ONCE memory access opacity for concurrent code
+ * (rbtree.h, rbtree_augmented.h)
+ * - likely / unlikely branch hints for hot/cold paths
+ *
+ * Implementation mirrors Linux's semantics — same surface, same effect —
+ * without pulling in Linux's compiler_types.h cascade (KASAN/KCSAN/KMSAN
+ * sanitizer attribute machinery, BTF type tags, hardened-usercopy hooks).
+ * LumeOS doesn't build with those sanitizers, so the simpler form is
+ * sufficient and keeps the dependency graph flat.
*/
-/* Memory barrier macros */
-#define WRITE_ONCE(x, val) \
- do { \
- __asm__ volatile("" ::: "memory"); \
- (x) = (val); \
- __asm__ volatile("" ::: "memory"); \
- } while (0)
+/* Branch prediction hints */
+#define likely(x) __builtin_expect(!!(x), 1)
+#define unlikely(x) __builtin_expect(!!(x), 0)
-#define READ_ONCE(x) (*(volatile __typeof__(x) *)&(x))
+/* Compile-time barrier — prevent the compiler from reordering memory
+ * accesses across this point. Not a CPU fence; for CPU ordering use
+ * the architecture-specific fence intrinsics. */
+#define barrier() __asm__ __volatile__("" ::: "memory")
-#define barrier() __asm__ volatile("" ::: "memory")
+/*
+ * __always_inline — force the compiler to actually inline a function,
+ * overriding -fno-inline / -Os / size heuristics. Used by rbtree helper
+ * templates whose performance depends on the call being inlined into
+ * the iterating loop.
+ */
+#define __always_inline inline __attribute__((__always_inline__))
-/* Branch prediction hints */
-#define unlikely(x) __builtin_expect(!!(x), 0)
-#define likely(x) __builtin_expect(!!(x), 1)
+/*
+ * READ_ONCE / WRITE_ONCE — opaque single-access memory operations.
+ *
+ * The volatile cast prevents the compiler from:
+ * - merging adjacent reads/writes to the same location,
+ * - splitting a load into multiple narrower loads,
+ * - hoisting reads out of a loop (or sinking writes past one).
+ *
+ * Used in lock-free / cross-CPU shared data paths (rbtree color writes,
+ * future RCU pointers, atomic flag polls). Does NOT imply any CPU-side
+ * memory ordering — pair with smp_mb()/acquire/release for that.
+ */
+#define READ_ONCE(x) (*(const volatile __typeof__(x) *)&(x))
+#define WRITE_ONCE(x, val) (*(volatile __typeof__(x) *)&(x) = (val))
-/* Function attributes */
-#define __always_inline inline __attribute__((always_inline))
+#endif /* _LUME_COMPILER_H */
diff --git a/na/include/lume/compressed.h b/na/include/lume/compressed.h
deleted file mode 100644
index 8ca775d..0000000
--- a/na/include/lume/compressed.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#pragma once
-
-#include <lume/types.h>
-
-inline constexpr uint64 kCompressedKernelMagic = 0x4C554D45434F4D50ULL; /* LUMECOMP */
-inline constexpr uint32 kCompressedKernelVersion = 1;
-
-enum KernelCompressionType : uint32 {
- KERNEL_COMP_NONE = 0,
- KERNEL_COMP_LZ4 = 1,
-};
-
-enum CompressedHeaderFlags : uint16 {
- LUME_HDR_FLAG_STAGE3_DONE = 1u << 0,
-};
-
-struct CompressedKernelHeader {
- uint64 magic;
- uint32 version;
- uint16 header_size;
- uint16 flags;
- uint32 compression;
- uint32 reserved0;
- uint64 uncompressed_size;
- uint64 compressed_size;
- uint64 load_phys_base;
- uint64 reloc_offset;
- uint64 reloc_size;
- uint64 payload_offset;
- uint32 header_crc32;
- uint32 payload_crc32;
-};
-
-static_assert(sizeof(CompressedKernelHeader) == 80);
diff --git a/na/include/lume/config.h b/na/include/lume/config.h
index d3539c5..bde717a 100644
--- a/na/include/lume/config.h
+++ b/na/include/lume/config.h
@@ -37,7 +37,7 @@
#endif
#ifndef CONFIG_KERNEL_STACK_SIZE
-#define CONFIG_KERNEL_STACK_SIZE 8192
+#define CONFIG_KERNEL_STACK_SIZE 16384
#endif
#ifndef CONFIG_HZ
@@ -86,8 +86,15 @@ inline constexpr int kMaxCpus = CONFIG_NR_CPUS;
/* Virtual memory page size (4KB, matching SV39 base page). */
inline constexpr uint64 kPageSize = CONFIG_PAGE_SIZE;
+/* Page shift: log2(kPageSize). Used for PFN ↔ PA conversions. */
+inline constexpr int kPageShift = 12; // 4096 = 1 << 12
+
/* KASLR policy ID and logging level (build-time configurable) */
inline constexpr uint32 kKaslrPolicyId = CONFIG_KASLR_POLICY_ID;
inline constexpr int kKaslrLogLevel = CONFIG_KASLR_LOG_LEVEL;
+#include <arch/config.h>
+
+using arch::kPhysStart;
+
#endif /* __ASSEMBLER__ */ \ No newline at end of file
diff --git a/na/include/lume/console.h b/na/include/lume/console.h
index a4fdf3f..af187a6 100644
--- a/na/include/lume/console.h
+++ b/na/include/lume/console.h
@@ -3,7 +3,30 @@
#pragma once
+/*
+ * console.h — Kernel console abstraction
+ *
+ * Two-phase design following Linux:
+ *
+ * Phase 1 (early boot):
+ * early_putc() → direct UART MMIO write. Used before MMU and
+ * before the console registry is ready. kprintf() and panic
+ * use this path.
+ *
+ * Phase 2 (runtime):
+ * struct console registered via register_console(). printk/klog
+ * emit through the console driver chain. Multiple consoles can
+ * be registered (e.g., UART + framebuffer).
+ *
+ * Reference: Linux include/linux/console.h
+ */
+
#include <lume/types.h>
+#include <lume/list.h>
+
+/* ================================================================== */
+/* Early console (Phase 1 — direct UART access) */
+/* ================================================================== */
extern "C" void early_putc(char c);
extern "C" void early_puts(const char* s);
@@ -21,3 +44,34 @@ void console_init();
* instead of the bootstrap fixed base.
*/
void console_use_runtime_mapping();
+
+/* ================================================================== */
+/* struct console (Phase 2 — registered console drivers) */
+/* ================================================================== */
+
+inline constexpr int CON_PRINTBUFFER = (1 << 0); /* Replay klog on register */
+inline constexpr int CON_ENABLED = (1 << 1);
+inline constexpr int CON_BOOT = (1 << 2); /* Early boot console */
+
+struct console {
+ const char* name;
+ void (*write)(console* con, const char* s, unsigned int count);
+ int (*setup)(console* con, char* options);
+ int flags;
+ int index;
+ void* data; /* driver private data */
+ list_node list;
+};
+
+/* Register / unregister a console driver */
+void register_console(console* con);
+void unregister_console(console* con);
+
+/*
+ * Write to all registered consoles.
+ * Falls back to early_putc() if no console is registered.
+ */
+void console_write_all(const char* s, int count);
+
+/* Check if any console is registered */
+bool console_drivers_registered();
diff --git a/na/include/lume/container_of.h b/na/include/lume/container_of.h
new file mode 100644
index 0000000..a6889eb
--- /dev/null
+++ b/na/include/lume/container_of.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * container_of.h — Recover containing struct from member pointer
+ *
+ * Equivalent to Linux include/linux/container_of.h.
+ */
+
+#include <lume/types.h>
+
+#define container_of(ptr, type, member) \
+ reinterpret_cast<type*>( \
+ reinterpret_cast<uintptr>(ptr) - \
+ reinterpret_cast<uintptr>(&(reinterpret_cast<type*>(0)->member)))
diff --git a/na/include/lume/dcache.h b/na/include/lume/dcache.h
new file mode 100644
index 0000000..a31402e
--- /dev/null
+++ b/na/include/lume/dcache.h
@@ -0,0 +1,143 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * dcache.h — Directory entry cache
+ *
+ * Modeled after Linux include/linux/dcache.h.
+ * Reference: docs/vfs/03_dentry.md
+ */
+
+#include <lume/types.h>
+#include <lume/list.h>
+#include <lume/atomic.h>
+#include <lume/fs_types.h>
+#include <kernel/sync/spinlock.h>
+
+struct inode;
+struct super_block;
+struct dentry;
+struct dentry_operations;
+struct path;
+
+/* ================================================================== */
+/* qstr — quick string (name + precomputed hash + length) */
+/* ================================================================== */
+
+struct qstr {
+ union {
+ struct {
+ uint32 hash;
+ uint32 len;
+ };
+ uint64 hash_len;
+ };
+ const char* name;
+};
+
+uint32 full_name_hash(const char* name, uint32 len);
+
+/* ================================================================== */
+/* dentry */
+/* ================================================================== */
+
+inline constexpr int DNAME_INLINE_LEN = 40;
+
+struct dentry {
+ /* ---- RCU / lookup hot fields ---- */
+ unsigned int d_flags;
+ dentry* d_parent;
+ qstr d_name;
+ inode* d_inode;
+ unsigned char d_iname[DNAME_INLINE_LEN];
+
+ /* ---- Ref lookup ---- */
+ const dentry_operations* d_op;
+ super_block* d_sb;
+ unsigned long d_time;
+ void* d_fsdata;
+
+ /* ---- Refcount + lock ---- */
+ lume::atomic<int32> d_lockref;
+ Spinlock d_lock;
+
+ /* ---- Tree linkage ---- */
+ list_node d_lru;
+ list_node d_child; /* Linkage in parent->d_children */
+ list_head d_children; /* Children of this dentry */
+
+ /* ---- Inode alias ---- */
+ list_node d_alias; /* Linkage in inode->i_dentry */
+};
+
+/* ================================================================== */
+/* dentry_operations */
+/* ================================================================== */
+
+struct dentry_operations {
+ int (*d_revalidate)(dentry* de, unsigned int flags);
+ int (*d_hash)(const dentry* de, qstr* name);
+ int (*d_compare)(const dentry* parent, unsigned int len,
+ const char* str, const qstr* name);
+ int (*d_delete)(const dentry* de);
+ int (*d_init)(dentry* de);
+ void (*d_release)(dentry* de);
+ void (*d_iput)(dentry* de, inode* ip);
+};
+
+/* ================================================================== */
+/* Dentry helpers */
+/* ================================================================== */
+
+inline bool d_is_negative(const dentry* de) {
+ return (de->d_flags & DCACHE_ENTRY_TYPE) == DCACHE_MISS_TYPE;
+}
+inline bool d_is_positive(const dentry* de) {
+ return !d_is_negative(de);
+}
+inline bool d_is_dir(const dentry* de) {
+ return (de->d_flags & DCACHE_ENTRY_TYPE) == DCACHE_DIRECTORY_TYPE;
+}
+inline bool d_is_reg(const dentry* de) {
+ return (de->d_flags & DCACHE_ENTRY_TYPE) == DCACHE_REGULAR_TYPE;
+}
+inline bool d_is_symlink(const dentry* de) {
+ return (de->d_flags & DCACHE_ENTRY_TYPE) == DCACHE_SYMLINK_TYPE;
+}
+inline bool d_mountpoint(const dentry* de) {
+ return (de->d_flags & DCACHE_MOUNTED) != 0;
+}
+inline bool d_is_root(const dentry* de) {
+ return de == de->d_parent;
+}
+
+inline inode* d_inode_ptr(const dentry* de) {
+ return de->d_inode;
+}
+
+/* ================================================================== */
+/* Dcache API */
+/* ================================================================== */
+
+dentry* d_alloc(dentry* parent, const qstr* name);
+dentry* d_alloc_anon(super_block* sb);
+void d_instantiate(dentry* de, inode* ip);
+dentry* d_make_root(inode* root_inode);
+void d_add(dentry* de, inode* ip);
+dentry* d_lookup(const dentry* parent, const qstr* name);
+dentry* dget(dentry* de);
+void dput(dentry* de);
+void d_drop(dentry* de);
+void d_delete(dentry* de);
+char* d_path(const path* p, char* buf, int buflen);
+
+/* Set dentry type flags based on inode mode */
+void d_set_type(dentry* de, uint32 i_mode);
+
+/* ================================================================== */
+/* Dcache init */
+/* ================================================================== */
+
+void dcache_init();
diff --git a/na/include/lume/decompress.h b/na/include/lume/decompress.h
deleted file mode 100644
index 04082a9..0000000
--- a/na/include/lume/decompress.h
+++ /dev/null
@@ -1,11 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#pragma once
-
-#include <lume/types.h>
-
-/*
- * LZ4 block decompressor (no frame headers). Returns bytes written or -1.
- */
-int lz4_decompress(const uint8* src, uint32 src_len, uint8* dst, uint32 dst_cap);
diff --git a/na/include/lume/device.h b/na/include/lume/device.h
new file mode 100644
index 0000000..46e22e4
--- /dev/null
+++ b/na/include/lume/device.h
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Driver model: device, device_driver, bus_type.
+ *
+ * Mirrors Linux include/linux/device.h. Every hardware unit embeds a
+ * `device` and binds to a `device_driver` via a `bus_type` that knows
+ * how to match the two. Buses we will have:
+ * - platform: FDT-described memory-mapped devices (this commit)
+ * - virtio: virtio-mmio devices (P5 of storage_stack.md)
+ *
+ * Reference: docs/specs/storage_stack.md §4.3
+ */
+
+#include <lume/types.h>
+#include <lume/list.h>
+#include <lume/kobject.h>
+#include <kernel/sync/spinlock.h>
+
+struct bus_type;
+struct device_driver;
+
+struct device {
+ kobject kobj; // embedded; first member by convention
+ device* parent;
+ const char* init_name; // used until kobj.name is set
+ bus_type* bus;
+ device_driver* driver;
+ void* driver_data;
+ void* platform_data;
+ uint32 devt; // for char/block dev_t allocation
+ list_node bus_list; // node in bus->devices
+ list_node driver_list; // node in driver->devices
+};
+
+struct device_driver {
+ const char* name;
+ bus_type* bus;
+ int (*probe)(device*);
+ void (*remove)(device*);
+ list_node bus_node; // node in bus->drivers
+ list_node devices; // head of devices bound to this driver
+};
+
+struct bus_type {
+ const char* name;
+ int (*match)(device*, device_driver*);
+ /* If non-null, this is the bus-level probe dispatcher. It is called
+ * before drv->probe and is responsible for downcasting device/driver
+ * to the concrete bus type and forwarding (e.g. platform_dispatch). */
+ int (*probe)(device*);
+ list_node devices;
+ list_node drivers;
+ Spinlock lock;
+ bool registered;
+};
+
+int bus_register (bus_type* bus);
+
+/* device_initialize: kobject_init the embedded kobj with the generic
+ * device_ktype. device_register calls this automatically if the caller
+ * hasn't already. Separate entry point for callers that want a half-
+ * initialized device (so they can take refs before adding). */
+void device_initialize(device* dev);
+int device_register (device* dev);
+void device_unregister(device* dev);
+
+int driver_register (device_driver* drv);
+void driver_unregister(device_driver* drv);
+
+/* Convenience accessor — name from kobject if set, else init_name. */
+const char* dev_name(const device* dev);
diff --git a/na/include/lume/driver.h b/na/include/lume/driver.h
index c9d5669..a8bd787 100644
--- a/na/include/lume/driver.h
+++ b/na/include/lume/driver.h
@@ -4,30 +4,21 @@
#pragma once
/*
- * Driver Registration Subsystem
+ * driver.h — boot-time driver probing entry point.
*
- * Provides a linker-set based registration mechanism for hardware drivers.
- * Drivers define a probe function and register it with the REGISTER_DRIVER macro.
- * During boot, driver_probe_all() scans the Device Tree (FDT) and invokes the
- * probe function for any matching compatible strings.
+ * The actual driver-model primitives (kobject, device, bus_type,
+ * platform_device, platform_driver) live in their own headers:
+ * <lume/kobject.h>, <lume/device.h>, <lume/platform_device.h>.
*
- * This decouples the MI kernel initialization from MD driver specifics.
+ * This header just exposes the single boot entry point that
+ * kernel_main() calls after the slab allocator + FDT unflatten are
+ * ready. The implementation lives in drivers/base/platform.cc.
+ *
+ * Reference: docs/specs/storage_stack.md §4
*/
-#include <lume/types.h>
-
-class DeviceNode;
-
-using DriverProbeFunc = void (*)(DeviceNode* node);
-
-struct DriverEntry {
- const char* compatible;
- DriverProbeFunc probe;
-};
-
-#define REGISTER_DRIVER(compat_str, probe_fn) \
- static const DriverEntry __driver_##probe_fn \
- __attribute__((used, section(".drivers"))) = { compat_str, probe_fn }
-
-/* MI initialization function called from kernel_main() */
+/* Register the platform bus, register every linker-set platform_driver,
+ * walk the FDT, and instantiate a platform_device for each compatible
+ * match. Each device is bound to its driver via the platform bus
+ * match/probe path. Returns after all probes have run. */
void driver_probe_all();
diff --git a/na/include/lume/elf.h b/na/include/lume/elf.h
new file mode 100644
index 0000000..772ad97
--- /dev/null
+++ b/na/include/lume/elf.h
@@ -0,0 +1,150 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * ELF64 Type Definitions — Minimal set for loading executables
+ * AND for the kernel's own .rela.dyn relocation walk at boot.
+ *
+ * Follows the ELF specification (Tool Interface Standard, TIS).
+ * Only includes what's needed for static ELF64 executable loading.
+ *
+ * Reference: https://refspecs.linuxfoundation.org/elf/elf.pdf
+ */
+
+#include <lume/types.h>
+
+/* ================================================================== */
+/* Basic ELF types */
+/* ================================================================== */
+
+using Elf64_Addr = uint64;
+using Elf64_Off = uint64;
+using Elf64_Half = uint16;
+using Elf64_Word = uint32;
+using Elf64_Sword = int32;
+using Elf64_Xword = uint64;
+using Elf64_Sxword = int64;
+
+/* ================================================================== */
+/* ELF Header */
+/* ================================================================== */
+
+inline constexpr int EI_NIDENT = 16;
+
+struct Elf64_Ehdr {
+ uint8 e_ident[EI_NIDENT]; /* ELF identification */
+ Elf64_Half e_type; /* Object file type */
+ Elf64_Half e_machine; /* Machine type */
+ Elf64_Word e_version; /* Object file version */
+ Elf64_Addr e_entry; /* Entry point address */
+ Elf64_Off e_phoff; /* Program header table offset */
+ Elf64_Off e_shoff; /* Section header table offset */
+ Elf64_Word e_flags; /* Processor-specific flags */
+ Elf64_Half e_ehsize; /* ELF header size */
+ Elf64_Half e_phentsize; /* Program header entry size */
+ Elf64_Half e_phnum; /* Number of program headers */
+ Elf64_Half e_shentsize; /* Section header entry size */
+ Elf64_Half e_shnum; /* Number of section headers */
+ Elf64_Half e_shstrndx; /* Section name string table index */
+};
+
+/* e_ident[] indices */
+inline constexpr int EI_MAG0 = 0;
+inline constexpr int EI_MAG1 = 1;
+inline constexpr int EI_MAG2 = 2;
+inline constexpr int EI_MAG3 = 3;
+inline constexpr int EI_CLASS = 4;
+inline constexpr int EI_DATA = 5;
+inline constexpr int EI_VERSION = 6;
+inline constexpr int EI_OSABI = 7;
+
+/* e_ident[] magic */
+inline constexpr uint8 ELFMAG0 = 0x7f;
+inline constexpr uint8 ELFMAG1 = 'E';
+inline constexpr uint8 ELFMAG2 = 'L';
+inline constexpr uint8 ELFMAG3 = 'F';
+
+/* e_ident[EI_CLASS] */
+inline constexpr uint8 ELFCLASS64 = 2;
+
+/* e_ident[EI_DATA] */
+inline constexpr uint8 ELFDATA2LSB = 1; /* Little-endian */
+
+/* e_type */
+inline constexpr Elf64_Half ET_EXEC = 2; /* Executable */
+inline constexpr Elf64_Half ET_DYN = 3; /* Shared object (PIE) */
+
+/* e_machine */
+inline constexpr Elf64_Half EM_RISCV = 243;
+
+/* ================================================================== */
+/* Program Header */
+/* ================================================================== */
+
+struct Elf64_Phdr {
+ Elf64_Word p_type; /* Segment type */
+ Elf64_Word p_flags; /* Segment flags */
+ Elf64_Off p_offset; /* File offset */
+ Elf64_Addr p_vaddr; /* Virtual address */
+ Elf64_Addr p_paddr; /* Physical address (unused) */
+ Elf64_Xword p_filesz; /* Size in file */
+ Elf64_Xword p_memsz; /* Size in memory (>= p_filesz, diff is BSS) */
+ Elf64_Xword p_align; /* Alignment */
+};
+
+/* p_type values */
+inline constexpr Elf64_Word PT_NULL = 0;
+inline constexpr Elf64_Word PT_LOAD = 1;
+inline constexpr Elf64_Word PT_DYNAMIC = 2;
+inline constexpr Elf64_Word PT_INTERP = 3;
+inline constexpr Elf64_Word PT_NOTE = 4;
+inline constexpr Elf64_Word PT_PHDR = 6;
+inline constexpr Elf64_Word PT_GNU_STACK = 0x6474e551;
+
+/* p_flags values */
+inline constexpr Elf64_Word PF_X = 0x1; /* Execute */
+inline constexpr Elf64_Word PF_W = 0x2; /* Write */
+inline constexpr Elf64_Word PF_R = 0x4; /* Read */
+
+/* ================================================================== */
+/* Auxiliary Vector types */
+/* ================================================================== */
+
+struct Elf64_auxv_t {
+ uint64 a_type;
+ uint64 a_val;
+};
+
+inline constexpr uint64 AT_NULL = 0;
+inline constexpr uint64 AT_PHDR = 3;
+inline constexpr uint64 AT_PHENT = 4;
+inline constexpr uint64 AT_PHNUM = 5;
+inline constexpr uint64 AT_PAGESZ = 6;
+inline constexpr uint64 AT_ENTRY = 9;
+inline constexpr uint64 AT_UID = 11;
+inline constexpr uint64 AT_EUID = 12;
+inline constexpr uint64 AT_GID = 13;
+inline constexpr uint64 AT_EGID = 14;
+inline constexpr uint64 AT_RANDOM = 25;
+inline constexpr uint64 AT_BASE = 7; /* PIE load base address */
+
+/* ================================================================== */
+/* RELA relocation entry — used by the kernel's own .rela.dyn walk */
+/* at boot (relocate_kernel in arch/riscv/mm/init.cc). */
+/* ================================================================== */
+
+struct Elf64_Rela {
+ Elf64_Addr r_offset; /* link-time VA of the slot to patch */
+ Elf64_Xword r_info; /* r_sym << 32 | r_type */
+ Elf64_Sxword r_addend; /* link-time value of the target */
+};
+
+#define ELF64_R_TYPE(info) ((info) & 0xFFFFFFFFULL)
+
+/* RISC-V relocation types — only the ones a PIE-linked kernel emits
+ * into .rela.dyn under `-shared -Bsymbolic`. */
+inline constexpr uint32 R_RISCV_NONE = 0;
+inline constexpr uint32 R_RISCV_64 = 2;
+inline constexpr uint32 R_RISCV_RELATIVE = 3;
diff --git a/na/include/lume/errno.h b/na/include/lume/errno.h
index 089aa94..f3bf09f 100644
--- a/na/include/lume/errno.h
+++ b/na/include/lume/errno.h
@@ -1,151 +1,186 @@
-/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _LUME_ERRNO_H
+#define _LUME_ERRNO_H
+
/*
- * Standard error codes — asm-generic/errno-base.h + asm-generic/errno.h
+ * Error codes — numeric values aligned with Linux's asm-generic/errno-base.h,
+ * asm-generic/errno.h, and linux/errno.h (kernel-internal codes 512+).
+ *
+ * Userspace-visible codes (1..133) match POSIX / Linux exactly so a LumeOS
+ * binary returning -EINVAL to a user program receives the same number Linux
+ * would. Kernel-internal codes (>= 512) follow Linux's convention of being
+ * out of POSIX range; they are translated to a user-visible code (typically
+ * EINTR) before crossing into user mode.
*
- * LumeOS targets only asm-generic architectures (RISC-V, AArch64, LoongArch),
- * so a single unified file is sufficient. No per-arch overrides needed.
+ * Defined as plain #define so this header is usable from C, C++, and
+ * assembly (the macros expand to integer literals, no types).
*/
-#pragma once
+/* ===== Base errors (asm-generic/errno-base.h equivalent) ===== */
+#define EPERM 1 /* Operation not permitted */
+#define ENOENT 2 /* No such file or directory */
+#define ESRCH 3 /* No such process */
+#define EINTR 4 /* Interrupted system call */
+#define EIO 5 /* I/O error */
+#define ENXIO 6 /* No such device or address */
+#define E2BIG 7 /* Argument list too long */
+#define ENOEXEC 8 /* Exec format error */
+#define EBADF 9 /* Bad file number */
+#define ECHILD 10 /* No child processes */
+#define EAGAIN 11 /* Try again */
+#define ENOMEM 12 /* Out of memory */
+#define EACCES 13 /* Permission denied */
+#define EFAULT 14 /* Bad address */
+#define ENOTBLK 15 /* Block device required */
+#define EBUSY 16 /* Device or resource busy */
+#define EEXIST 17 /* File exists */
+#define EXDEV 18 /* Cross-device link */
+#define ENODEV 19 /* No such device */
+#define ENOTDIR 20 /* Not a directory */
+#define EISDIR 21 /* Is a directory */
+#define EINVAL 22 /* Invalid argument */
+#define ENFILE 23 /* File table overflow */
+#define EMFILE 24 /* Too many open files */
+#define ENOTTY 25 /* Not a typewriter */
+#define ETXTBSY 26 /* Text file busy */
+#define EFBIG 27 /* File too large */
+#define ENOSPC 28 /* No space left on device */
+#define ESPIPE 29 /* Illegal seek */
+#define EROFS 30 /* Read-only file system */
+#define EMLINK 31 /* Too many links */
+#define EPIPE 32 /* Broken pipe */
+#define EDOM 33 /* Math argument out of domain */
+#define ERANGE 34 /* Math result not representable */
-/* ====== errno-base.h (1-34) ====== */
+/* ===== Extended errors (asm-generic/errno.h equivalent) ===== */
+#define EDEADLK 35 /* Resource deadlock would occur */
+#define ENAMETOOLONG 36 /* File name too long */
+#define ENOLCK 37 /* No record locks available */
+#define ENOSYS 38 /* Invalid system call number */
+#define ENOTEMPTY 39 /* Directory not empty */
+#define ELOOP 40 /* Too many symbolic links encountered */
+#define EWOULDBLOCK EAGAIN /* Operation would block */
+#define ENOMSG 42 /* No message of desired type */
+#define EIDRM 43 /* Identifier removed */
+#define ECHRNG 44 /* Channel number out of range */
+#define EL2NSYNC 45 /* Level 2 not synchronized */
+#define EL3HLT 46 /* Level 3 halted */
+#define EL3RST 47 /* Level 3 reset */
+#define ELNRNG 48 /* Link number out of range */
+#define EUNATCH 49 /* Protocol driver not attached */
+#define ENOCSI 50 /* No CSI structure available */
+#define EL2HLT 51 /* Level 2 halted */
+#define EBADE 52 /* Invalid exchange */
+#define EBADR 53 /* Invalid request descriptor */
+#define EXFULL 54 /* Exchange full */
+#define ENOANO 55 /* No anode */
+#define EBADRQC 56 /* Invalid request code */
+#define EBADSLT 57 /* Invalid slot */
+#define EDEADLOCK EDEADLK
+#define EBFONT 59 /* Bad font file format */
+#define ENOSTR 60 /* Device not a stream */
+#define ENODATA 61 /* No data available */
+#define ETIME 62 /* Timer expired */
+#define ENOSR 63 /* Out of streams resources */
+#define ENONET 64 /* Machine is not on the network */
+#define ENOPKG 65 /* Package not installed */
+#define EREMOTE 66 /* Object is remote */
+#define ENOLINK 67 /* Link has been severed */
+#define EADV 68 /* Advertise error */
+#define ESRMNT 69 /* Srmount error */
+#define ECOMM 70 /* Communication error on send */
+#define EPROTO 71 /* Protocol error */
+#define EMULTIHOP 72 /* Multihop attempted */
+#define EDOTDOT 73 /* RFS specific error */
+#define EBADMSG 74 /* Not a data message */
+#define EOVERFLOW 75 /* Value too large for defined data type */
+#define ENOTUNIQ 76 /* Name not unique on network */
+#define EBADFD 77 /* File descriptor in bad state */
+#define EREMCHG 78 /* Remote address changed */
+#define ELIBACC 79 /* Cannot access a needed shared library */
+#define ELIBBAD 80 /* Accessing a corrupted shared library */
+#define ELIBSCN 81 /* .lib section in a.out corrupted */
+#define ELIBMAX 82 /* Linking in too many shared libraries */
+#define ELIBEXEC 83 /* Cannot exec a shared library directly */
+#define EILSEQ 84 /* Illegal byte sequence */
+#define ERESTART 85 /* Interrupted system call should be restarted */
+#define ESTRPIPE 86 /* Streams pipe error */
+#define EUSERS 87 /* Too many users */
+#define ENOTSOCK 88 /* Socket operation on non-socket */
+#define EDESTADDRREQ 89 /* Destination address required */
+#define EMSGSIZE 90 /* Message too long */
+#define EPROTOTYPE 91 /* Protocol wrong type for socket */
+#define ENOPROTOOPT 92 /* Protocol not available */
+#define EPROTONOSUPPORT 93 /* Protocol not supported */
+#define ESOCKTNOSUPPORT 94 /* Socket type not supported */
+#define EOPNOTSUPP 95 /* Operation not supported on transport endpoint */
+#define ENOTSUP EOPNOTSUPP
+#define EPFNOSUPPORT 96 /* Protocol family not supported */
+#define EAFNOSUPPORT 97 /* Address family not supported by protocol */
+#define EADDRINUSE 98 /* Address already in use */
+#define EADDRNOTAVAIL 99 /* Cannot assign requested address */
+#define ENETDOWN 100 /* Network is down */
+#define ENETUNREACH 101 /* Network is unreachable */
+#define ENETRESET 102 /* Network dropped connection because of reset */
+#define ECONNABORTED 103 /* Software caused connection abort */
+#define ECONNRESET 104 /* Connection reset by peer */
+#define ENOBUFS 105 /* No buffer space available */
+#define EISCONN 106 /* Transport endpoint is already connected */
+#define ENOTCONN 107 /* Transport endpoint is not connected */
+#define ESHUTDOWN 108 /* Cannot send after transport endpoint shutdown */
+#define ETOOMANYREFS 109 /* Too many references: cannot splice */
+#define ETIMEDOUT 110 /* Connection timed out */
+#define ECONNREFUSED 111 /* Connection refused */
+#define EHOSTDOWN 112 /* Host is down */
+#define EHOSTUNREACH 113 /* No route to host */
+#define EALREADY 114 /* Operation already in progress */
+#define EINPROGRESS 115 /* Operation now in progress */
+#define ESTALE 116 /* Stale file handle */
+#define EUCLEAN 117 /* Structure needs cleaning */
+#define ENOTNAM 118 /* Not a XENIX named type file */
+#define ENAVAIL 119 /* No XENIX semaphores available */
+#define EISNAM 120 /* Is a named type file */
+#define EREMOTEIO 121 /* Remote I/O error */
+#define EDQUOT 122 /* Quota exceeded */
+#define ENOMEDIUM 123 /* No medium found */
+#define EMEDIUMTYPE 124 /* Wrong medium type */
+#define ECANCELED 125 /* Operation Canceled */
+#define ENOKEY 126 /* Required key not available */
+#define EKEYEXPIRED 127 /* Key has expired */
+#define EKEYREVOKED 128 /* Key has been revoked */
+#define EKEYREJECTED 129 /* Key was rejected by service */
+#define EOWNERDEAD 130 /* Owner died (for robust mutexes) */
+#define ENOTRECOVERABLE 131 /* State not recoverable */
+#define ERFKILL 132 /* Operation not possible due to RF-kill */
+#define EHWPOISON 133 /* Memory page has hardware error */
-#define EPERM 1 /* Operation not permitted */
-#define ENOENT 2 /* No such file or directory */
-#define ESRCH 3 /* No such process */
-#define EINTR 4 /* Interrupted system call */
-#define EIO 5 /* I/O error */
-#define ENXIO 6 /* No such device or address */
-#define E2BIG 7 /* Argument list too long */
-#define ENOEXEC 8 /* Exec format error */
-#define EBADF 9 /* Bad file number */
-#define ECHILD 10 /* No child processes */
-#define EAGAIN 11 /* Try again */
-#define ENOMEM 12 /* Out of memory */
-#define EACCES 13 /* Permission denied */
-#define EFAULT 14 /* Bad address */
-#define ENOTBLK 15 /* Block device required */
-#define EBUSY 16 /* Device or resource busy */
-#define EEXIST 17 /* File exists */
-#define EXDEV 18 /* Cross-device link */
-#define ENODEV 19 /* No such device */
-#define ENOTDIR 20 /* Not a directory */
-#define EISDIR 21 /* Is a directory */
-#define EINVAL 22 /* Invalid argument */
-#define ENFILE 23 /* File table overflow */
-#define EMFILE 24 /* Too many open files */
-#define ENOTTY 25 /* Not a typewriter */
-#define ETXTBSY 26 /* Text file busy */
-#define EFBIG 27 /* File too large */
-#define ENOSPC 28 /* No space left on device */
-#define ESPIPE 29 /* Illegal seek */
-#define EROFS 30 /* Read-only file system */
-#define EMLINK 31 /* Too many links */
-#define EPIPE 32 /* Broken pipe */
-#define EDOM 33 /* Math argument out of domain of func */
-#define ERANGE 34 /* Math result not representable */
-
-/* ====== errno.h (35-133) ====== */
+/* ===== Kernel-internal errors (linux/errno.h equivalent, >= 512) =====
+ * These should never be seen by user programs. signal/syscall layer
+ * translates them to a user-visible code (typically EINTR) before
+ * returning to userspace.
+ */
+#define ERESTARTSYS 512
+#define ERESTARTNOINTR 513
+#define ERESTARTNOHAND 514 /* restart if no signal handler */
+#define ENOIOCTLCMD 515 /* No ioctl command */
+#define ERESTART_RESTARTBLOCK 516 /* restart by calling sys_restart_syscall */
+#define EPROBE_DEFER 517 /* Driver requests probe retry */
+#define EOPENSTALE 518 /* open() found a stale dentry */
+#define ENOPARAM 519 /* Parameter not supported */
-#define EDEADLK 35 /* Resource deadlock would occur */
-#define ENAMETOOLONG 36 /* File name too long */
-#define ENOLCK 37 /* No record locks available */
-#define ENOSYS 38 /* Invalid system call number */
-#define ENOTEMPTY 39 /* Directory not empty */
-#define ELOOP 40 /* Too many symbolic links encountered */
-#define EWOULDBLOCK EAGAIN /* Operation would block */
-#define ENOMSG 42 /* No message of desired type */
-#define EIDRM 43 /* Identifier removed */
-#define ECHRNG 44 /* Channel number out of range */
-#define EL2NSYNC 45 /* Level 2 not synchronized */
-#define EL3HLT 46 /* Level 3 halted */
-#define EL3RST 47 /* Level 3 reset */
-#define ELNRNG 48 /* Link number out of range */
-#define EUNATCH 49 /* Protocol driver not attached */
-#define ENOCSI 50 /* No CSI structure available */
-#define EL2HLT 51 /* Level 2 halted */
-#define EBADE 52 /* Invalid exchange */
-#define EBADR 53 /* Invalid request descriptor */
-#define EXFULL 54 /* Exchange full */
-#define ENOANO 55 /* No anode */
-#define EBADRQC 56 /* Invalid request code */
-#define EBADSLT 57 /* Invalid slot */
-#define EDEADLOCK EDEADLK
-#define EBFONT 59 /* Bad font file format */
-#define ENOSTR 60 /* Device not a stream */
-#define ENODATA 61 /* No data available */
-#define ETIME 62 /* Timer expired */
-#define ENOSR 63 /* Out of streams resources */
-#define ENONET 64 /* Machine is not on the network */
-#define ENOPKG 65 /* Package not installed */
-#define EREMOTE 66 /* Object is remote */
-#define ENOLINK 67 /* Link has been severed */
-#define EADV 68 /* Advertise error */
-#define ESRMNT 69 /* Srmount error */
-#define ECOMM 70 /* Communication error on send */
-#define EPROTO 71 /* Protocol error */
-#define EMULTIHOP 72 /* Multihop attempted */
-#define EDOTDOT 73 /* RFS specific error */
-#define EBADMSG 74 /* Not a data message */
-#define EOVERFLOW 75 /* Value too large for defined data type */
-#define ENOTUNIQ 76 /* Name not unique on network */
-#define EBADFD 77 /* File descriptor in bad state */
-#define EREMCHG 78 /* Remote address changed */
-#define ELIBACC 79 /* Can not access a needed shared library */
-#define ELIBBAD 80 /* Accessing a corrupted shared library */
-#define ELIBSCN 81 /* .lib section in a.out corrupted */
-#define ELIBMAX 82 /* Attempting to link in too many shared libraries */
-#define ELIBEXEC 83 /* Cannot exec a shared library directly */
-#define EILSEQ 84 /* Illegal byte sequence */
-#define ERESTART 85 /* Interrupted system call should be restarted */
-#define ESTRPIPE 86 /* Streams pipe error */
-#define EUSERS 87 /* Too many users */
-#define ENOTSOCK 88 /* Socket operation on non-socket */
-#define EDESTADDRREQ 89 /* Destination address required */
-#define EMSGSIZE 90 /* Message too long */
-#define EPROTOTYPE 91 /* Protocol wrong type for socket */
-#define ENOPROTOOPT 92 /* Protocol not available */
-#define EPROTONOSUPPORT 93 /* Protocol not supported */
-#define ESOCKTNOSUPPORT 94 /* Socket type not supported */
-#define EOPNOTSUPP 95 /* Operation not supported on transport endpoint */
-#define EPFNOSUPPORT 96 /* Protocol family not supported */
-#define EAFNOSUPPORT 97 /* Address family not supported by protocol */
-#define EADDRINUSE 98 /* Address already in use */
-#define EADDRNOTAVAIL 99 /* Cannot assign requested address */
-#define ENETDOWN 100 /* Network is down */
-#define ENETUNREACH 101 /* Network is unreachable */
-#define ENETRESET 102 /* Network dropped connection because of reset */
-#define ECONNABORTED 103 /* Software caused connection abort */
-#define ECONNRESET 104 /* Connection reset by peer */
-#define ENOBUFS 105 /* No buffer space available */
-#define EISCONN 106 /* Transport endpoint is already connected */
-#define ENOTCONN 107 /* Transport endpoint is not connected */
-#define ESHUTDOWN 108 /* Cannot send after transport endpoint shutdown */
-#define ETOOMANYREFS 109 /* Too many references: cannot splice */
-#define ETIMEDOUT 110 /* Connection timed out */
-#define ECONNREFUSED 111 /* Connection refused */
-#define EHOSTDOWN 112 /* Host is down */
-#define EHOSTUNREACH 113 /* No route to host */
-#define EALREADY 114 /* Operation already in progress */
-#define EINPROGRESS 115 /* Operation now in progress */
-#define ESTALE 116 /* Stale file handle */
-#define EUCLEAN 117 /* Structure needs cleaning */
-#define ENOTNAM 118 /* Not a XENIX named type file */
-#define ENAVAIL 119 /* No XENIX semaphores available */
-#define EISNAM 120 /* Is a named type file */
-#define EREMOTEIO 121 /* Remote I/O error */
-#define EDQUOT 122 /* Quota exceeded */
-#define ENOMEDIUM 123 /* No medium found */
-#define EMEDIUMTYPE 124 /* Wrong medium type */
-#define ECANCELED 125 /* Operation Canceled */
-#define ENOKEY 126 /* Required key not available */
-#define EKEYEXPIRED 127 /* Key has expired */
-#define EKEYREVOKED 128 /* Key has been revoked */
-#define EKEYREJECTED 129 /* Key was rejected by service */
+/* NFSv3 reserved range (521..531) */
+#define EBADHANDLE 521 /* Illegal NFS file handle */
+#define ENOTSYNC 522 /* Update synchronization mismatch */
+#define EBADCOOKIE 523 /* Cookie is stale */
+#define ENOTSUPP 524 /* Operation is not supported */
+#define ETOOSMALL 525 /* Buffer or request is too small */
+#define ESERVERFAULT 526 /* An untranslatable error occurred */
+#define EBADTYPE 527 /* Type not supported by server */
+#define EJUKEBOX 528 /* Request initiated, will not complete before timeout */
+#define EIOCBQUEUED 529 /* iocb queued, will get completion event */
+#define ERECALLCONFLICT 530 /* conflict with recalled state */
+#define ENOGRACE 531 /* NFS file lock reclaim refused */
-/* for robust mutexes */
-#define EOWNERDEAD 130 /* Owner died */
-#define ENOTRECOVERABLE 131 /* State not recoverable */
-#define ERFKILL 132 /* Operation not possible due to RF-kill */
-#define EHWPOISON 133 /* Memory page has hardware error */ \ No newline at end of file
+#endif /* _LUME_ERRNO_H */
diff --git a/na/include/lume/fdtable.h b/na/include/lume/fdtable.h
new file mode 100644
index 0000000..6159519
--- /dev/null
+++ b/na/include/lume/fdtable.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * fdtable.h — Per-task file descriptor table
+ *
+ * Modeled after Linux include/linux/fdtable.h.
+ * Reference: docs/vfs/05_fdtable.md
+ */
+
+#include <lume/types.h>
+#include <lume/atomic.h>
+#include <kernel/sync/spinlock.h>
+
+struct file;
+
+/* ================================================================== */
+/* files_struct — per-task open file table */
+/* ================================================================== */
+
+inline constexpr int NR_OPEN_DEFAULT = 256;
+
+struct files_struct {
+ lume::atomic<int32> count; /* Reference count (CLONE_FILES) */
+ Spinlock file_lock;
+
+ int next_fd; /* Hint for next free fd */
+ int max_fds; /* Current max (= NR_OPEN_DEFAULT) */
+ file* fd_array[NR_OPEN_DEFAULT];
+ uint64 close_on_exec_init[(NR_OPEN_DEFAULT + 63) / 64];
+ uint64 open_fds_init[(NR_OPEN_DEFAULT + 63) / 64];
+};
+
+/* ================================================================== */
+/* API */
+/* ================================================================== */
+
+files_struct* alloc_files_struct();
+void put_files_struct(files_struct* files);
+files_struct* dup_fd(files_struct* old_files);
+
+int alloc_fd(files_struct* files, unsigned int start);
+void fd_install(files_struct* files, int fd, file* fp);
+file* fcheck(files_struct* files, int fd);
+int close_fd(files_struct* files, int fd);
+int do_dup2(files_struct* files, int oldfd, int newfd, int flags);
+void do_close_on_exec(files_struct* files);
diff --git a/na/include/lume/file.h b/na/include/lume/file.h
new file mode 100644
index 0000000..349059b
--- /dev/null
+++ b/na/include/lume/file.h
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * file.h — Open file description + file_operations + path + vfsmount
+ *
+ * Modeled after Linux include/linux/fs.h struct file / file_operations.
+ * Reference: docs/vfs/04_file.md
+ */
+
+#include <lume/types.h>
+#include <lume/atomic.h>
+#include <lume/fs_types.h>
+#include <kernel/sync/spinlock.h>
+
+struct inode;
+struct dentry;
+struct super_block;
+struct address_space;
+struct dir_context;
+struct VmAreaStruct;
+struct kstat;
+
+/* ================================================================== */
+/* vfsmount (public part) */
+/* ================================================================== */
+
+struct vfsmount {
+ dentry* mnt_root;
+ super_block* mnt_sb;
+ int mnt_flags;
+};
+
+vfsmount* mntget(vfsmount* mnt);
+void mntput(vfsmount* mnt);
+
+/* ================================================================== */
+/* path */
+/* ================================================================== */
+
+struct path {
+ vfsmount* mnt;
+ dentry* de; /* Linux calls this 'dentry' but that conflicts in C++ */
+};
+
+void path_get(const path* p);
+void path_put(const path* p);
+
+/* ================================================================== */
+/* file */
+/* ================================================================== */
+
+struct file_operations;
+
+struct file {
+ Spinlock f_lock;
+ uint32 f_mode; /* FMODE_* */
+ const file_operations* f_op;
+ address_space* f_mapping;
+ void* private_data;
+ inode* f_inode;
+ unsigned int f_flags; /* O_* */
+
+ path f_path;
+
+ int64 f_pos;
+ lume::atomic<int32> f_count;
+};
+
+/* ================================================================== */
+/* Accessors */
+/* ================================================================== */
+
+inline inode* file_inode(const file* f) { return f->f_inode; }
+inline dentry* file_dentry(const file* f) { return f->f_path.de; }
+
+file* get_file(file* f);
+file* alloc_empty_file(int flags);
+void fput(file* fp);
+
+/* ================================================================== */
+/* dir_context (for iterate_shared / getdents64) */
+/* ================================================================== */
+
+struct dir_context;
+using filldir_t = bool (*)(dir_context* ctx, const char* name, int namelen,
+ int64 offset, uint64 ino, unsigned int d_type);
+
+struct dir_context {
+ filldir_t actor;
+ int64 pos;
+};
+
+/* ================================================================== */
+/* file_operations */
+/* ================================================================== */
+
+struct file_operations {
+ int64 (*llseek)(file* fp, int64 offset, int whence);
+ int64 (*read)(file* fp, char* buf, uint64 count, int64* pos);
+ int64 (*write)(file* fp, const char* buf, uint64 count, int64* pos);
+ int (*iterate_shared)(file* fp, dir_context* ctx);
+ uint32 (*poll)(file* fp, void* pt);
+ long (*unlocked_ioctl)(file* fp, unsigned int cmd, uint64 arg);
+ int (*mmap)(file* fp, VmAreaStruct* vma);
+ int (*open)(inode* ip, file* fp);
+ int (*flush)(file* fp);
+ int (*release)(inode* ip, file* fp);
+ int (*fsync)(file* fp, int64 start, int64 end, int datasync);
+};
+
+/* ================================================================== */
+/* VFS read/write/seek */
+/* ================================================================== */
+
+int64 vfs_read(file* fp, char* buf, uint64 count, int64* pos);
+int64 vfs_write(file* fp, const char* buf, uint64 count, int64* pos);
+int64 vfs_llseek(file* fp, int64 offset, int whence);
+int vfs_fstat(file* fp, kstat* stat);
+
+/* Generic helpers for simple fs */
+int64 generic_file_llseek(file* fp, int64 offset, int whence);
+
+/* ================================================================== */
+/* path helpers */
+/* ================================================================== */
+
+inline bool path_equal(const path* a, const path* b) {
+ return a->mnt == b->mnt && a->de == b->de;
+}
diff --git a/na/include/lume/fs.h b/na/include/lume/fs.h
new file mode 100644
index 0000000..8dac552
--- /dev/null
+++ b/na/include/lume/fs.h
@@ -0,0 +1,310 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * fs.h — Core VFS data structures
+ *
+ * Defines super_block, inode, address_space, and their operations.
+ * Modeled after Linux include/linux/fs.h + include/linux/fs/super_types.h.
+ *
+ * Reference: docs/vfs/01_super_block.md, docs/vfs/02_inode.md
+ */
+
+#include <lume/types.h>
+#include <lume/list.h>
+#include <lume/atomic.h>
+#include <lume/fs_types.h>
+#include <kernel/sync/spinlock.h>
+
+/* Forward declarations */
+struct super_block;
+struct inode;
+struct dentry;
+struct file;
+struct vfsmount;
+struct path;
+struct kstat;
+struct iattr;
+struct file_operations;
+struct inode_operations;
+struct dentry_operations;
+struct dir_context;
+struct pipe_inode_info;
+struct VmAreaStruct;
+struct Page;
+
+/* ================================================================== */
+/* address_space_operations */
+/* ================================================================== */
+
+struct address_space_operations {
+ int (*read_folio)(file* fp, Page* page);
+ int (*write_begin)(struct address_space* mapping, int64 pos,
+ unsigned len, Page** pagep, void** fsdata);
+ int (*write_end)(struct address_space* mapping, int64 pos,
+ unsigned len, unsigned copied,
+ Page* page, void* fsdata);
+};
+
+/* ================================================================== */
+/* address_space */
+/* ================================================================== */
+
+struct address_space {
+ inode* host;
+ const address_space_operations* a_ops;
+ uint64 nrpages;
+ uint64 flags;
+ Spinlock i_private_lock;
+};
+
+/* ================================================================== */
+/* super_operations */
+/* ================================================================== */
+
+struct super_operations {
+ inode* (*alloc_inode)(super_block* sb);
+ void (*destroy_inode)(inode* ip);
+ void (*free_inode)(inode* ip);
+
+ void (*dirty_inode)(inode* ip, int flags);
+ int (*write_inode)(inode* ip, bool sync);
+ int (*drop_inode)(inode* ip);
+ void (*evict_inode)(inode* ip);
+ void (*put_super)(super_block* sb);
+ int (*sync_fs)(super_block* sb, int wait);
+ int (*statfs)(dentry* de, struct kstatfs* buf);
+ void (*umount_begin)(super_block* sb);
+};
+
+/* ================================================================== */
+/* file_system_type */
+/* ================================================================== */
+
+struct file_system_type {
+ const char* name;
+ int fs_flags;
+ super_block* (*mount)(file_system_type* type, int flags,
+ const char* dev_name, void* data);
+ void (*kill_sb)(super_block* sb);
+ list_node fs_list;
+};
+
+int register_filesystem(file_system_type* fs);
+file_system_type* get_fs_type(const char* name);
+
+/* ================================================================== */
+/* super_block */
+/* ================================================================== */
+
+struct mount;
+
+struct super_block {
+ list_node s_list;
+ uint64 s_dev;
+ uint8 s_blocksize_bits;
+ uint64 s_blocksize;
+ int64 s_maxbytes;
+ file_system_type* s_type;
+ const super_operations* s_op;
+
+ uint64 s_flags;
+ uint64 s_magic;
+ dentry* s_root;
+
+ char s_id[32];
+ void* s_fs_info;
+
+ uint32 s_time_gran;
+ int64 s_time_min;
+ int64 s_time_max;
+
+ Spinlock s_inode_list_lock;
+ list_head s_inodes;
+
+ lume::atomic<int32> s_active;
+ int s_count;
+
+ mount* s_mounts;
+ uint32 s_max_links;
+ const dentry_operations* s_d_op;
+};
+
+/* ================================================================== */
+/* inode */
+/* ================================================================== */
+
+struct inode {
+ /* ---- Path-walk hot fields ---- */
+ uint32 i_mode;
+ uint32 i_flags;
+ uint32 i_uid;
+ uint32 i_gid;
+
+ const inode_operations* i_op;
+ super_block* i_sb;
+ address_space* i_mapping;
+
+ /* ---- Stat data ---- */
+ uint64 i_ino;
+ uint32 i_nlink; /* Use set_nlink/inc_nlink/drop_nlink */
+ uint32 i_rdev;
+ int64 i_size;
+
+ int64 i_atime_sec;
+ int64 i_mtime_sec;
+ int64 i_ctime_sec;
+ uint32 i_atime_nsec;
+ uint32 i_mtime_nsec;
+ uint32 i_ctime_nsec;
+ uint32 i_generation;
+
+ Spinlock i_lock;
+ unsigned short i_bytes;
+ uint8 i_blkbits;
+ int64 i_blocks;
+
+ /* ---- State ---- */
+ uint32 i_state;
+
+ /* ---- Lists ---- */
+ list_node i_sb_list;
+ list_head i_dentry;
+
+ /* ---- Counters ---- */
+ lume::atomic<int32> i_count;
+ lume::atomic<int32> i_writecount;
+
+ /* ---- File ops ---- */
+ const file_operations* i_fop;
+
+ /* ---- Embedded address_space ---- */
+ address_space i_data;
+
+ /* ---- Type-specific ---- */
+ union {
+ pipe_inode_info* i_pipe;
+ char* i_link;
+ uint32 i_dir_seq;
+ };
+
+ void* i_private;
+};
+
+/* ================================================================== */
+/* inode_operations */
+/* ================================================================== */
+
+struct inode_operations {
+ dentry* (*lookup)(inode* dir, dentry* de, unsigned int flags);
+ int (*create)(inode* dir, dentry* de, uint32 mode, bool excl);
+ int (*link)(dentry* old_de, inode* dir, dentry* new_de);
+ int (*unlink)(inode* dir, dentry* de);
+ int (*symlink)(inode* dir, dentry* de, const char* target);
+ int (*mkdir)(inode* dir, dentry* de, uint32 mode);
+ int (*rmdir)(inode* dir, dentry* de);
+ int (*mknod)(inode* dir, dentry* de, uint32 mode, uint32 rdev);
+ int (*rename)(inode* old_dir, dentry* old_de,
+ inode* new_dir, dentry* new_de, unsigned int flags);
+ int (*setattr)(dentry* de, iattr* attr);
+ int (*getattr)(const path* p, kstat* stat, uint32 request_mask,
+ unsigned int query_flags);
+ const char* (*get_link)(dentry* de, inode* ip);
+ int (*permission)(inode* ip, int mask);
+};
+
+/* ================================================================== */
+/* Inode API */
+/* ================================================================== */
+
+inode* new_inode(super_block* sb);
+inode* iget(super_block* sb, uint64 ino);
+void iput(inode* ip);
+void ihold(inode* ip);
+void mark_inode_dirty(inode* ip);
+
+void set_nlink(inode* ip, uint32 nlink);
+void inc_nlink(inode* ip);
+void drop_nlink(inode* ip);
+void clear_nlink(inode* ip);
+
+void inode_init_owner(inode* ip, const inode* dir, uint32 mode);
+
+/* Generic helpers used by simple filesystems */
+void generic_fillattr(inode* ip, kstat* stat);
+
+/* ================================================================== */
+/* kstat (matches oscomp ABI layout exactly) */
+/* ================================================================== */
+
+struct kstat {
+ uint64 st_dev;
+ uint64 st_ino;
+ uint32 st_mode;
+ uint32 st_nlink;
+ uint32 st_uid;
+ uint32 st_gid;
+ uint64 st_rdev;
+ uint64 __pad;
+ int64 st_size;
+ int32 st_blksize;
+ int32 __pad2;
+ int64 st_blocks;
+ int64 st_atime_sec;
+ int64 st_atime_nsec;
+ int64 st_mtime_sec;
+ int64 st_mtime_nsec;
+ int64 st_ctime_sec;
+ int64 st_ctime_nsec;
+ uint32 __unused[2];
+};
+
+struct kstatfs {
+ int64 f_type;
+ int64 f_bsize;
+ uint64 f_blocks;
+ uint64 f_bfree;
+ uint64 f_bavail;
+ uint64 f_files;
+ uint64 f_ffree;
+ int32 f_fsid[2];
+ int64 f_namelen;
+ int64 f_frsize;
+ int64 f_flags;
+ int64 f_spare[4];
+};
+
+/* ================================================================== */
+/* iattr (for setattr) */
+/* ================================================================== */
+
+struct iattr {
+ unsigned int ia_valid; /* ATTR_* flags */
+ uint32 ia_mode;
+ uint32 ia_uid;
+ uint32 ia_gid;
+ int64 ia_size;
+ int64 ia_atime_sec;
+ int64 ia_atime_nsec;
+ int64 ia_mtime_sec;
+ int64 ia_mtime_nsec;
+ int64 ia_ctime_sec;
+ int64 ia_ctime_nsec;
+};
+
+/* ATTR_* flags */
+inline constexpr uint32 ATTR_MODE = (1U << 0);
+inline constexpr uint32 ATTR_UID = (1U << 1);
+inline constexpr uint32 ATTR_GID = (1U << 2);
+inline constexpr uint32 ATTR_SIZE = (1U << 3);
+inline constexpr uint32 ATTR_ATIME = (1U << 4);
+inline constexpr uint32 ATTR_MTIME = (1U << 5);
+inline constexpr uint32 ATTR_CTIME = (1U << 6);
+
+/* ================================================================== */
+/* VFS init */
+/* ================================================================== */
+
+void vfs_init();
diff --git a/na/include/lume/fs_struct.h b/na/include/lume/fs_struct.h
new file mode 100644
index 0000000..ba643b5
--- /dev/null
+++ b/na/include/lume/fs_struct.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * fs_struct.h — Per-task filesystem context (cwd, root)
+ *
+ * Modeled after Linux include/linux/fs_struct.h.
+ * Reference: docs/vfs/05_fdtable.md
+ */
+
+#include <lume/types.h>
+#include <lume/atomic.h>
+#include <lume/file.h> /* struct path */
+#include <kernel/sync/spinlock.h>
+
+/* ================================================================== */
+/* fs_struct */
+/* ================================================================== */
+
+struct fs_struct {
+ lume::atomic<int32> count; /* Reference count (CLONE_FS) */
+ Spinlock lock;
+ int umask; /* File creation mask */
+ path root; /* Root directory */
+ path pwd; /* Current working directory */
+};
+
+/* ================================================================== */
+/* API */
+/* ================================================================== */
+
+fs_struct* alloc_fs_struct();
+void put_fs_struct(fs_struct* fs);
+fs_struct* copy_fs_struct(const fs_struct* old_fs);
+
+void set_fs_root(fs_struct* fs, const path* p);
+void set_fs_pwd(fs_struct* fs, const path* p);
diff --git a/na/include/lume/fs_types.h b/na/include/lume/fs_types.h
new file mode 100644
index 0000000..767f37c
--- /dev/null
+++ b/na/include/lume/fs_types.h
@@ -0,0 +1,197 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * fs_types.h — File mode constants, open flags, seek constants
+ *
+ * All values match Linux asm-generic so musl/busybox binaries work.
+ * Reference: docs/vfs/02_inode.md, docs/vfs/04_file.md
+ */
+
+#include <lume/types.h>
+
+/* ================================================================== */
+/* S_IF* — file type in inode::i_mode upper bits */
+/* ================================================================== */
+
+inline constexpr uint32 S_IFMT = 0170000;
+inline constexpr uint32 S_IFSOCK = 0140000;
+inline constexpr uint32 S_IFLNK = 0120000;
+inline constexpr uint32 S_IFREG = 0100000;
+inline constexpr uint32 S_IFBLK = 0060000;
+inline constexpr uint32 S_IFDIR = 0040000;
+inline constexpr uint32 S_IFCHR = 0020000;
+inline constexpr uint32 S_IFIFO = 0010000;
+
+inline constexpr uint32 S_ISUID = 04000;
+inline constexpr uint32 S_ISGID = 02000;
+inline constexpr uint32 S_ISVTX = 01000;
+
+inline constexpr uint32 S_IRWXU = 00700;
+inline constexpr uint32 S_IRUSR = 00400;
+inline constexpr uint32 S_IWUSR = 00200;
+inline constexpr uint32 S_IXUSR = 00100;
+inline constexpr uint32 S_IRWXG = 00070;
+inline constexpr uint32 S_IRGRP = 00040;
+inline constexpr uint32 S_IWGRP = 00020;
+inline constexpr uint32 S_IXGRP = 00010;
+inline constexpr uint32 S_IRWXO = 00007;
+inline constexpr uint32 S_IROTH = 00004;
+inline constexpr uint32 S_IWOTH = 00002;
+inline constexpr uint32 S_IXOTH = 00001;
+
+inline bool S_ISREG(uint32 m) { return (m & S_IFMT) == S_IFREG; }
+inline bool S_ISDIR(uint32 m) { return (m & S_IFMT) == S_IFDIR; }
+inline bool S_ISCHR(uint32 m) { return (m & S_IFMT) == S_IFCHR; }
+inline bool S_ISBLK(uint32 m) { return (m & S_IFMT) == S_IFBLK; }
+inline bool S_ISFIFO(uint32 m) { return (m & S_IFMT) == S_IFIFO; }
+inline bool S_ISLNK(uint32 m) { return (m & S_IFMT) == S_IFLNK; }
+inline bool S_ISSOCK(uint32 m) { return (m & S_IFMT) == S_IFSOCK; }
+
+/* ================================================================== */
+/* O_* — open(2) flags */
+/* ================================================================== */
+
+inline constexpr int O_RDONLY = 00;
+inline constexpr int O_WRONLY = 01;
+inline constexpr int O_RDWR = 02;
+inline constexpr int O_ACCMODE = 03;
+inline constexpr int O_CREAT = 0100;
+inline constexpr int O_EXCL = 0200;
+inline constexpr int O_NOCTTY = 0400;
+inline constexpr int O_TRUNC = 01000;
+inline constexpr int O_APPEND = 02000;
+inline constexpr int O_NONBLOCK = 04000;
+inline constexpr int O_DSYNC = 010000;
+inline constexpr int O_DIRECTORY = 0200000;
+inline constexpr int O_NOFOLLOW = 0400000;
+inline constexpr int O_CLOEXEC = 02000000;
+
+/* ================================================================== */
+/* AT_* — *at() syscall dirfd constants */
+/* ================================================================== */
+
+inline constexpr int AT_FDCWD = -100;
+inline constexpr int AT_REMOVEDIR = 0x200;
+inline constexpr int AT_SYMLINK_NOFOLLOW = 0x100;
+inline constexpr int AT_EMPTY_PATH = 0x1000;
+
+/* ================================================================== */
+/* SEEK_* — lseek(2) whence */
+/* ================================================================== */
+
+inline constexpr int SEEK_SET = 0;
+inline constexpr int SEEK_CUR = 1;
+inline constexpr int SEEK_END = 2;
+
+/* ================================================================== */
+/* FMODE_* — internal file mode flags */
+/* ================================================================== */
+
+inline constexpr uint32 FMODE_READ = (1U << 0);
+inline constexpr uint32 FMODE_WRITE = (1U << 1);
+inline constexpr uint32 FMODE_LSEEK = (1U << 2);
+inline constexpr uint32 FMODE_PREAD = (1U << 3);
+inline constexpr uint32 FMODE_PWRITE = (1U << 4);
+inline constexpr uint32 FMODE_EXEC = (1U << 5);
+inline constexpr uint32 FMODE_CAN_READ = (1U << 17);
+inline constexpr uint32 FMODE_CAN_WRITE = (1U << 18);
+inline constexpr uint32 FMODE_OPENED = (1U << 19);
+inline constexpr uint32 FMODE_CREATED = (1U << 20);
+inline constexpr uint32 FMODE_STREAM = (1U << 21);
+
+/* ================================================================== */
+/* MAY_* — permission check flags */
+/* ================================================================== */
+
+inline constexpr int MAY_EXEC = 0x00000001;
+inline constexpr int MAY_WRITE = 0x00000002;
+inline constexpr int MAY_READ = 0x00000004;
+inline constexpr int MAY_OPEN = 0x00000020;
+inline constexpr int MAY_CHDIR = 0x00000040;
+
+/* ================================================================== */
+/* DT_* — directory entry types (for getdents64) */
+/* ================================================================== */
+
+inline constexpr uint8 DT_UNKNOWN = 0;
+inline constexpr uint8 DT_FIFO = 1;
+inline constexpr uint8 DT_CHR = 2;
+inline constexpr uint8 DT_DIR = 4;
+inline constexpr uint8 DT_BLK = 6;
+inline constexpr uint8 DT_REG = 8;
+inline constexpr uint8 DT_LNK = 10;
+inline constexpr uint8 DT_SOCK = 12;
+
+/* ================================================================== */
+/* SB_* — super_block flags */
+/* ================================================================== */
+
+inline constexpr uint64 SB_RDONLY = (1ULL << 0);
+inline constexpr uint64 SB_NOSUID = (1ULL << 1);
+inline constexpr uint64 SB_NODEV = (1ULL << 2);
+inline constexpr uint64 SB_NOEXEC = (1ULL << 3);
+inline constexpr uint64 SB_SYNCHRONOUS = (1ULL << 4);
+inline constexpr uint64 SB_NOATIME = (1ULL << 10);
+inline constexpr uint64 SB_NODIRATIME = (1ULL << 11);
+inline constexpr uint64 SB_SILENT = (1ULL << 15);
+inline constexpr uint64 SB_KERNMOUNT = (1ULL << 22);
+inline constexpr uint64 SB_LAZYTIME = (1ULL << 25);
+inline constexpr uint64 SB_ACTIVE = (1ULL << 30);
+
+/* ================================================================== */
+/* MNT_* — mount flags */
+/* ================================================================== */
+
+inline constexpr int MNT_NOSUID = 0x01;
+inline constexpr int MNT_NODEV = 0x02;
+inline constexpr int MNT_NOEXEC = 0x04;
+inline constexpr int MNT_NOATIME = 0x08;
+inline constexpr int MNT_RELATIME = 0x20;
+inline constexpr int MNT_READONLY = 0x40;
+inline constexpr int MNT_INTERNAL = 0x4000;
+
+/* ================================================================== */
+/* Inode state flags */
+/* ================================================================== */
+
+inline constexpr uint32 I_NEW = (1U << 0);
+inline constexpr uint32 I_DIRTY_SYNC = (1U << 4);
+inline constexpr uint32 I_DIRTY_DATASYNC = (1U << 5);
+inline constexpr uint32 I_DIRTY_PAGES = (1U << 6);
+inline constexpr uint32 I_WILL_FREE = (1U << 7);
+inline constexpr uint32 I_FREEING = (1U << 8);
+inline constexpr uint32 I_CLEAR = (1U << 9);
+inline constexpr uint32 I_REFERENCED = (1U << 10);
+inline constexpr uint32 I_DIRTY_TIME = (1U << 12);
+inline constexpr uint32 I_CREATING = (1U << 15);
+
+inline constexpr uint32 I_DIRTY_INODE = I_DIRTY_SYNC | I_DIRTY_DATASYNC;
+inline constexpr uint32 I_DIRTY = I_DIRTY_INODE | I_DIRTY_PAGES;
+
+/* ================================================================== */
+/* DCACHE_* — dentry flags */
+/* ================================================================== */
+
+inline constexpr uint32 DCACHE_OP_HASH = (1U << 0);
+inline constexpr uint32 DCACHE_OP_COMPARE = (1U << 1);
+inline constexpr uint32 DCACHE_OP_REVALIDATE = (1U << 2);
+inline constexpr uint32 DCACHE_DISCONNECTED = (1U << 5);
+inline constexpr uint32 DCACHE_REFERENCED = (1U << 6);
+inline constexpr uint32 DCACHE_CANT_MOUNT = (1U << 8);
+inline constexpr uint32 DCACHE_MOUNTED = (1U << 15);
+inline constexpr uint32 DCACHE_ENTRY_TYPE = (7U << 19);
+inline constexpr uint32 DCACHE_MISS_TYPE = (0U << 19);
+inline constexpr uint32 DCACHE_DIRECTORY_TYPE = (2U << 19);
+inline constexpr uint32 DCACHE_REGULAR_TYPE = (4U << 19);
+inline constexpr uint32 DCACHE_SPECIAL_TYPE = (5U << 19);
+inline constexpr uint32 DCACHE_SYMLINK_TYPE = (6U << 19);
+
+/* ================================================================== */
+/* FS_* — file_system_type flags */
+/* ================================================================== */
+
+inline constexpr int FS_REQUIRES_DEV = 1;
+inline constexpr int FS_NO_DEVICE = 2;
diff --git a/na/include/lume/init.h b/na/include/lume/init.h
new file mode 100644
index 0000000..541ab96
--- /dev/null
+++ b/na/include/lume/init.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _LUME_INIT_H
+#define _LUME_INIT_H
+
+/*
+ * Boot-time section markers.
+ *
+ * Mirrors the subset of Linux include/linux/init.h that LumeOS uses:
+ *
+ * __init mark a function as boot-only — emitted into .init.text,
+ * which the kernel can later free once setup_arch /
+ * start_kernel have finished (LumeOS doesn't yet reclaim
+ * this memory, but the segment lives so future versions
+ * can without source changes).
+ * __initdata same for data, emitted into .init.data.
+ * asmlinkage ABI marker for C functions called from assembly. On
+ * RISC-V the default C ABI matches the assembly calling
+ * convention, so this is empty (matches Linux RISC-V).
+ *
+ * No attempt is made to mirror Linux's COMMAND_LINE_SIZE / saved_command_line
+ * machinery — that belongs in a future <lume/setup.h>.
+ */
+
+#define __init __attribute__((section(".init.text")))
+#define __initdata __attribute__((section(".init.data")))
+
+#ifndef asmlinkage
+#define asmlinkage /* RISC-V: empty, ABI already matches */
+#endif
+
+#endif /* _LUME_INIT_H */
diff --git a/na/include/lume/kbuild.h b/na/include/lume/kbuild.h
new file mode 100644
index 0000000..4c3e12a
--- /dev/null
+++ b/na/include/lume/kbuild.h
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _LUME_KBUILD_H
+#define _LUME_KBUILD_H
+
+/*
+ * Helpers for arch/<arch>/kernel/asm-offsets.c — 1:1 with Linux
+ * include/linux/kbuild.h.
+ *
+ * Trick: each DEFINE/OFFSET emits an inline-asm ".ascii" directive
+ * containing a literal "->" prefix. When the file is compiled with
+ *
+ * $(CC) -S asm-offsets.c -o asm-offsets.s
+ *
+ * the resulting assembly contains lines like
+ *
+ * ->KERNEL_MAP_VIRT_ADDR $0 offsetof(struct kernel_mapping, virt_addr)
+ *
+ * scripts/extract-asm-offsets.sed then rewrites those into proper
+ * `#define KERNEL_MAP_VIRT_ADDR 0` lines for inclusion from head.S.
+ *
+ * The benefit over hand-written #defines is that struct layout changes
+ * are caught at build time: rearrange a field and the asm offset
+ * changes automatically.
+ */
+
+#define DEFINE(sym, val) \
+ asm volatile("\n.ascii \"->" #sym " %0 " #val "\"" : : "i" (val))
+
+#define BLANK() \
+ asm volatile("\n.ascii \"->\"" : : )
+
+#define OFFSET(sym, str, mem) \
+ DEFINE(sym, offsetof(struct str, mem))
+
+#define COMMENT(x) \
+ asm volatile("\n.ascii \"->#" x "\"")
+
+#endif /* _LUME_KBUILD_H */
diff --git a/na/include/lume/kernel_map.h b/na/include/lume/kernel_map.h
new file mode 100644
index 0000000..7eafe88
--- /dev/null
+++ b/na/include/lume/kernel_map.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _LUME_KERNEL_MAP_H
+#define _LUME_KERNEL_MAP_H
+
+/*
+ * struct kernel_mapping — where the kernel image lives in physical and
+ * virtual memory. Field names and order match Linux's
+ * arch/riscv/include/asm/page.h struct kernel_mapping so that:
+ *
+ * 1. arch/riscv/kernel/head.S can do `la a1, kernel_map` then
+ * `REG_L a1, KERNEL_MAP_VIRT_ADDR(a1)` and read the virtual
+ * address. The KERNEL_MAP_* asm constants come from
+ * <generated/asm-offsets.h>, produced by arch/riscv/kernel/
+ * asm-offsets.c at build time — re-order the struct and the
+ * asm offsets follow automatically.
+ *
+ * 2. arch/riscv/mm/init.c (Linux origin) populates the same fields
+ * with the same semantics so no source edits are needed.
+ */
+
+#ifndef __ASSEMBLY__
+
+#include <lume/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct kernel_mapping {
+ unsigned long virt_addr;
+ unsigned long virt_offset;
+ uintptr_t phys_addr;
+ uintptr_t size;
+ unsigned long va_pa_offset;
+ unsigned long page_offset;
+ unsigned long va_kernel_pa_offset;
+};
+
+extern struct kernel_mapping kernel_map;
+
+#ifdef __cplusplus
+}
+#endif
+
+#else /* __ASSEMBLY__ */
+
+/* Assembly sees only the field offsets, picked up from the generated
+ * header. Including the C struct above would be a parse error. */
+#include <generated/asm-offsets.h>
+
+#endif /* __ASSEMBLY__ */
+
+#endif /* _LUME_KERNEL_MAP_H */
diff --git a/na/include/lume/klog.h b/na/include/lume/klog.h
index b63810f..69ab269 100644
--- a/na/include/lume/klog.h
+++ b/na/include/lume/klog.h
@@ -51,7 +51,11 @@ enum KlogSubsys : uint8 {
KLOG_SUBSYS_FS = 8,
KLOG_SUBSYS_DRIVER = 9,
KLOG_SUBSYS_TEST = 10,
- KLOG_SUBSYS_MAX = 11,
+ KLOG_SUBSYS_SYSCALL = 11, /* per-syscall entry trace */
+ KLOG_SUBSYS_EXEC = 12, /* ELF load / exec internals */
+ KLOG_SUBSYS_SIGNAL = 13, /* signal delivery trace */
+ KLOG_SUBSYS_TTY = 14, /* tty / job-control trace */
+ KLOG_SUBSYS_MAX = 15,
};
/* Default: show everything up to INFO. DEBUG is compile-gated. */
@@ -165,6 +169,15 @@ void klog_stats(KlogStats* out);
#define pr_debug(fmt, ...) do {} while (0)
#endif
+/* pr_debug_s: subsystem-tagged debug. Always compiled in (subsys is the gate);
+ * level KLOG_DEBUG ensures messages stay in the ring buffer but are filtered
+ * out of the console by default (console_level = KLOG_INFO). Toggle a subsys
+ * with klog_enable_subsys()/klog_disable_subsys() at runtime — or, to make
+ * the messages also reach the console for debugging, raise console_level
+ * via klog_set_level(KLOG_DEBUG). */
+#define pr_debug_s(subsys, fmt, ...) \
+ klog_ex(KLOG_DEBUG, subsys, fmt, ##__VA_ARGS__)
+
/* Linux kernel loglevel prefixes */
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0"
diff --git a/na/include/lume/kobject.h b/na/include/lume/kobject.h
new file mode 100644
index 0000000..8581d6e
--- /dev/null
+++ b/na/include/lume/kobject.h
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * kobject — generic refcounted object with parent/child relationship.
+ *
+ * Mirrors Linux include/linux/kobject.h. Embedded into device, gendisk,
+ * request_queue, block_device, etc. Each kobject has a kobj_type that
+ * provides the release callback called when the last reference drops.
+ *
+ * sysfs integration is not yet implemented — the sd/sysfs_ops fields are
+ * reserved for future use.
+ *
+ * Reference: docs/specs/storage_stack.md §4.2
+ */
+
+#include <lume/types.h>
+#include <lume/list.h>
+#include <lume/kref.h>
+#include <kernel/sync/spinlock.h>
+
+struct kobj_type;
+
+struct kobject {
+ const char* name;
+ kobject* parent;
+ kref refcount;
+ const kobj_type* ktype;
+ list_node entry; // node in parent->children
+ list_node children; // head of children list
+ Spinlock lock;
+ bool state_in_sysfs; // reserved, always false for now
+};
+
+struct kobj_type {
+ void (*release)(kobject*);
+ /* sysfs_ops + default_attrs: reserved for future sysfs implementation. */
+};
+
+/* Initialize a kobject in place; refcount starts at 1.
+ * Caller must provide a non-null ktype with a valid release(). */
+void kobject_init(kobject* k, const kobj_type* type);
+
+/* Link `k` under `parent` (which may be null for a detached root).
+ * Caller passes a stable `name` pointer (e.g. a static string or an
+ * already-strdup'd buffer); kobject does not own the name string. */
+int kobject_add(kobject* k, kobject* parent, const char* name);
+
+/* Unlink from parent. Does NOT drop the final ref — caller decides
+ * when to kobject_put(). */
+void kobject_del(kobject* k);
+
+/* Bump / drop reference. */
+void kobject_get(kobject* k);
+void kobject_put(kobject* k);
diff --git a/na/include/lume/kref.h b/na/include/lume/kref.h
new file mode 100644
index 0000000..13d53f3
--- /dev/null
+++ b/na/include/lume/kref.h
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * kref — atomic reference counting.
+ *
+ * Mirrors Linux include/linux/kref.h. Use kref_init() to set count=1,
+ * kref_get() / kref_put() to add/drop refs. kref_put() invokes the
+ * release callback when the final ref is dropped and returns 1 in that
+ * case.
+ *
+ * Reference: docs/specs/storage_stack.md §4.1
+ */
+
+#include <lume/types.h>
+#include <lume/atomic.h>
+
+struct kref {
+ lume::atomic<int32> refcount;
+};
+
+inline void kref_init(kref* k)
+{
+ k->refcount.store(1, __ATOMIC_RELEASE);
+}
+
+inline void kref_get(kref* k)
+{
+ k->refcount.fetch_add(1, __ATOMIC_RELAXED);
+}
+
+inline int kref_put(kref* k, void (*release)(kref*))
+{
+ if (k->refcount.fetch_sub(1, __ATOMIC_ACQ_REL) == 1) {
+ release(k);
+ return 1;
+ }
+ return 0;
+}
+
+inline int32 kref_read(const kref* k)
+{
+ return k->refcount.load(__ATOMIC_RELAXED);
+}
diff --git a/na/include/lume/libfdt.h b/na/include/lume/libfdt.h
new file mode 100644
index 0000000..e3496e9
--- /dev/null
+++ b/na/include/lume/libfdt.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _INCLUDE_LIBFDT_H_
+#define _INCLUDE_LIBFDT_H_
+
+#include <lume/libfdt_env.h>
+#include "../../scripts/dtc/libfdt/libfdt.h"
+
+#endif /* _INCLUDE_LIBFDT_H_ */
diff --git a/na/include/lume/libfdt_env.h b/na/include/lume/libfdt_env.h
new file mode 100644
index 0000000..a95fb1d
--- /dev/null
+++ b/na/include/lume/libfdt_env.h
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef LIBFDT_ENV_H
+#define LIBFDT_ENV_H
+
+/*
+ * Kernel-side adapter for the standalone libfdt sources in
+ * scripts/dtc/libfdt/. libfdt's *.c files start with
+ * #include "libfdt_env.h"
+ * and expect this header to provide:
+ * - byte-swap helpers (cpu_to_fdt16/32/64, fdt16/32/64_to_cpu)
+ * - fdt16/32/64_t typedefs (just plain uint types — endianness is
+ * handled in the helpers above)
+ * - string / mem helpers (memcpy, memmove, memcmp, strlen, strcmp,
+ * strchr, strnlen, ...)
+ * - INT_MAX, UINT32_MAX (libfdt bounds-checks props with these)
+ * - size_t / uintptr_t (everywhere)
+ *
+ * LumeOS-native: no Linux <linux/...> includes. Provides the surface
+ * libfdt needs and nothing else.
+ */
+
+#include <lume/types.h> /* uint16_t / uint32_t / uint64_t / size_t */
+#include <lume/limits.h> /* INT_MAX */
+#include <string.h> /* libc-shim memcpy / memcmp / strlen ... */
+
+/* libfdt uses these names; both come from <lume/limits.h>. */
+#define INT32_MAX 2147483647
+#define UINT32_MAX 4294967295U
+
+/* libfdt's "FDT word" types — opaque big-endian-stored u16/u32/u64.
+ * The byte-swap helpers below are the only access path. */
+typedef uint16_t fdt16_t;
+typedef uint32_t fdt32_t;
+typedef uint64_t fdt64_t;
+
+/*
+ * Endianness helpers. FDT (Device Tree Blob) is big-endian by spec.
+ * LumeOS runs on RISC-V (little-endian) and x86 (little-endian); we
+ * unconditionally byte-swap.
+ *
+ * Open-coded shifts rather than __builtin_bswapN — on RISC-V without
+ * the Zbb extension, gcc lowers __builtin_bswap32 to a libgcc call
+ * (__bswapsi2 / __bswapdi2) which isn't present in a freestanding
+ * kernel link. Mirrors Linux's scripts/dtc/libfdt/libfdt_env.h
+ * which uses ___constant_swab32-style explicit shifts. */
+static inline uint16_t __lumefdt_bswap16(uint16_t x)
+{
+ return (uint16_t)((x << 8) | (x >> 8));
+}
+static inline uint32_t __lumefdt_bswap32(uint32_t x)
+{
+ return ((x & 0x000000FFu) << 24) |
+ ((x & 0x0000FF00u) << 8) |
+ ((x & 0x00FF0000u) >> 8) |
+ ((x & 0xFF000000u) >> 24);
+}
+static inline uint64_t __lumefdt_bswap64(uint64_t x)
+{
+ return ((x & 0x00000000000000FFull) << 56) |
+ ((x & 0x000000000000FF00ull) << 40) |
+ ((x & 0x0000000000FF0000ull) << 24) |
+ ((x & 0x00000000FF000000ull) << 8) |
+ ((x & 0x000000FF00000000ull) >> 8) |
+ ((x & 0x0000FF0000000000ull) >> 24) |
+ ((x & 0x00FF000000000000ull) >> 40) |
+ ((x & 0xFF00000000000000ull) >> 56);
+}
+
+#define fdt16_to_cpu(x) __lumefdt_bswap16((uint16_t)(x))
+#define cpu_to_fdt16(x) ((fdt16_t)__lumefdt_bswap16((uint16_t)(x)))
+#define fdt32_to_cpu(x) __lumefdt_bswap32((uint32_t)(x))
+#define cpu_to_fdt32(x) ((fdt32_t)__lumefdt_bswap32((uint32_t)(x)))
+#define fdt64_to_cpu(x) __lumefdt_bswap64((uint64_t)(x))
+#define cpu_to_fdt64(x) ((fdt64_t)__lumefdt_bswap64((uint64_t)(x)))
+
+#endif /* LIBFDT_ENV_H */
diff --git a/na/include/lume/limits.h b/na/include/lume/limits.h
new file mode 100644
index 0000000..a275a71
--- /dev/null
+++ b/na/include/lume/limits.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _LUME_LIMITS_H
+#define _LUME_LIMITS_H
+
+/*
+ * Numeric limit constants — minimal subset needed by LumeOS kernel code.
+ *
+ * Values match the standard <limits.h> on all 64-bit LP64 targets, which
+ * is the only ABI LumeOS supports. Defined as plain integer literals so
+ * this header is usable from C, C++, and assembly without dragging in
+ * the toolchain's <limits.h> (which on hosted builds references libc
+ * internals we don't link against).
+ */
+
+/* Signed/unsigned char */
+#define SCHAR_MIN (-128)
+#define SCHAR_MAX 127
+#define UCHAR_MAX 255
+#define CHAR_MIN SCHAR_MIN
+#define CHAR_MAX SCHAR_MAX
+
+/* Short */
+#define SHRT_MIN (-32768)
+#define SHRT_MAX 32767
+#define USHRT_MAX 65535
+
+/* Int */
+#define INT_MIN (-INT_MAX - 1)
+#define INT_MAX 2147483647
+#define UINT_MAX 4294967295U
+
+/* Long (LP64: 64-bit) */
+#define LONG_MIN (-LONG_MAX - 1L)
+#define LONG_MAX 9223372036854775807L
+#define ULONG_MAX 18446744073709551615UL
+
+/* Long long */
+#define LLONG_MIN (-LLONG_MAX - 1LL)
+#define LLONG_MAX 9223372036854775807LL
+#define ULLONG_MAX 18446744073709551615ULL
+
+/* size_t (LP64) */
+#define SIZE_MAX ULONG_MAX
+#define SSIZE_MAX LONG_MAX
+
+#endif /* _LUME_LIMITS_H */
diff --git a/na/include/lume/list.h b/na/include/lume/list.h
index 7c020a0..ed1202a 100644
--- a/na/include/lume/list.h
+++ b/na/include/lume/list.h
@@ -4,6 +4,7 @@
#pragma once
#include <lume/types.h>
+#include <lume/container_of.h>
/**
* @brief The node to be embedded in a struct to make it linkable.
@@ -289,4 +290,57 @@ public:
private:
list_node head_;
-}; \ No newline at end of file
+};
+
+/* ========================================================================
+ * Linux-compatible C-style list macros (for VFS subsystem)
+ * ======================================================================== */
+
+using list_head = list_node;
+
+inline void list_init(list_node* head) {
+ head->prev = head;
+ head->next = head;
+}
+
+inline bool list_empty(const list_node* head) {
+ return head->next == head;
+}
+
+inline void __list_add(list_node* node, list_node* p, list_node* n) {
+ n->prev = node;
+ node->next = n;
+ node->prev = p;
+ p->next = node;
+}
+
+inline void list_add(list_node* node, list_node* head) {
+ __list_add(node, head, head->next);
+}
+
+inline void list_add_tail(list_node* node, list_node* head) {
+ __list_add(node, head->prev, head);
+}
+
+inline void list_del(list_node* node) {
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ node->next = nullptr;
+ node->prev = nullptr;
+}
+
+inline void list_del_init(list_node* node) {
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ list_init(node);
+}
+
+#define list_entry(ptr, type, member) container_of(ptr, type, member)
+
+#define list_for_each(pos, head) \
+ for ((pos) = (head)->next; (pos) != (head); (pos) = (pos)->next)
+
+#define list_for_each_safe(pos, tmp, head) \
+ for ((pos) = (head)->next, (tmp) = (pos)->next; \
+ (pos) != (head); \
+ (pos) = (tmp), (tmp) = (pos)->next)
diff --git a/na/include/lume/lumeos.lds.h b/na/include/lume/lumeos.lds.h
index a6b2b5f..58d2f72 100644
--- a/na/include/lume/lumeos.lds.h
+++ b/na/include/lume/lumeos.lds.h
@@ -15,36 +15,100 @@
#ifndef _LUME_LUMEOS_LDS_H
#define _LUME_LUMEOS_LDS_H
-/* ===== Text Section: executable code ===== */
+/* ===== Text Section: executable code =====
+ *
+ * .init.text is merged into the main .text segment so PC-relative `la`
+ * from regular code can call __init helpers without a thunk. Linux
+ * keeps a separate .init.text only to reclaim the pages after boot —
+ * LumeOS doesn't yet reclaim, so the merge is harmless and avoids an
+ * extra PT_LOAD entry. */
#define TEXT_SECTION \
.text : AT(ADDR(.text) - PAGE_OFFSET) { \
_stext = .; \
*(.text .text.*) \
+ *(.init.text .init.text.*) \
*(.fixup) \
_etext = .; \
} = 0
-/* ===== Read-Only Data + C++ init_array ===== */
+/* ===== Read-Only Data + C++ init_array + initramfs =====
+ *
+ * Layout invariants:
+ * - extable / init_array / driver_array / initramfs all live INSIDE
+ * .rodata between _srodata and _erodata so the VMM has one R-only
+ * mapping to manage.
+ * - The relocation table (.lume.reloc) is NOT in here — it lives in
+ * its own padded output section (see RELOC_SECTION) so that its
+ * two-pass-build size variance can't shift downstream offsets.
+ * (Putting it in .rodata caused a heisenbug where pre-link offsets
+ * used by gen_reloc.py didn't match post-link offsets after the
+ * reloc.o blob was inserted.)
+ */
#define RODATA_SECTION \
.rodata : AT(ADDR(.rodata) - PAGE_OFFSET) { \
_srodata = .; \
*(.rodata .rodata.*) \
+ *(.srodata .srodata.* .srodata.cst*) \
. = ALIGN(8); \
__extable_start = .; \
KEEP(*(.extable)) \
__extable_end = .; \
INIT_ARRAY \
DRIVER_ARRAY \
+ /* Embedded initramfs CPIO archive */ \
+ . = ALIGN(8); \
+ __initramfs_start = .; \
+ KEEP(*(.init.ramfs)) \
+ __initramfs_end = .; \
+ . = ALIGN(4096); \
_erodata = .; \
}
-/* ===== Relocation Table (optional, stays in file image) ===== */
-#define RELOC_SECTION \
- .reloc : AT(ADDR(.reloc) - PAGE_OFFSET) { \
- . = ALIGN(8); \
- __lume_reloc_start = .; \
- KEEP(*(.lume.reloc .lume.reloc.*)) \
- __lume_reloc_end = .; \
+/* ===== .rel.dyn / .rela.dyn — Linker-emitted RELA relocations =====
+ *
+ * Mirrors arch/riscv/kernel/vmlinux.lds.S §101-109: the .rela.dyn
+ * section sits BETWEEN init data and .rodata so it lands in a
+ * read-only PT_LOAD segment alongside .text/.rodata. The companion
+ * dynamic-linker bookkeeping (.dynamic, .dynsym, .dynstr, .hash,
+ * .gnu.hash) lives in the RW segment AFTER .data — Linux does the
+ * same under #ifdef CONFIG_RELOCATABLE (§139-147). */
+#define RELA_DYN_SECTION \
+ .rel.dyn : AT(ADDR(.rel.dyn) - PAGE_OFFSET) { \
+ *(.rel.dyn*) \
+ } \
+ .rela.dyn : AT(ADDR(.rela.dyn) - PAGE_OFFSET) ALIGN(8) {\
+ __rela_dyn_start = .; \
+ *(.rela .rela*) \
+ __rela_dyn_end = .; \
+ }
+
+/* ===== GOT — kept; everything else dynamic-linker is discarded =====
+ *
+ * Strict mirror of arch/riscv/kernel/vmlinux.lds.S in Linux: the only
+ * dynamic-linker artefact the kernel keeps is `.got` (it's referenced
+ * by every -fPIE compiled global access). `.plt .dynamic .dynsym
+ * .dynstr .hash .gnu.hash .interp .dynbss` are all sent to /DISCARD/
+ * via the DISCARDS macro below — the kernel never invokes a dynamic
+ * linker (head.S walks .rela.dyn directly).
+ *
+ * Keeping .plt was the cause of two distinct bugs:
+ * 1. PLT trampoline target for an undefined symbol (selftest_reloc
+ * after we removed test_reloc.cc) was unresolved → jumps to a
+ * slot that re-jumps to itself → instruction page fault.
+ * 2. .plt carries X permission; living in the same PT_LOAD as .data
+ * forces the load segment to RWX, which `ld` warns about.
+ *
+ * Sit in the same PT_LOAD as .data because .got is RW. */
+#define GOT_SECTION \
+ .got : AT(ADDR(.got) - PAGE_OFFSET) { *(.got*) }
+
+/* ===== Embedded initramfs (CPIO archive) ===== */
+#define INITRAMFS_SECTION \
+ .init.ramfs : AT(ADDR(.init.ramfs) - PAGE_OFFSET) { \
+ . = ALIGN(8); \
+ __initramfs_start = .; \
+ KEEP(*(.init.ramfs)) \
+ __initramfs_end = .; \
}
/* ===== C++ Global Constructor Table ===== */
@@ -61,11 +125,16 @@
KEEP(*(.drivers)) \
PROVIDE(__drivers_end = .);
-/* ===== Initialized Read-Write Data ===== */
+/* ===== Initialized Read-Write Data =====
+ *
+ * .init.data is merged into .data for the same reason TEXT_SECTION
+ * absorbs .init.text — LumeOS doesn't yet reclaim __init pages, so
+ * splitting them gains nothing while costing a PT_LOAD entry. */
#define DATA_SECTION \
.data : AT(ADDR(.data) - PAGE_OFFSET) { \
_sdata = .; \
*(.data .data.*) \
+ *(.init.data .init.data.*) \
ARCH_DATA_EXTRA \
_edata = .; \
}
@@ -80,12 +149,40 @@
_ebss = .; \
}
-/* ===== Discard Unwanted Sections ===== */
-#define DISCARDS \
- /DISCARD/ : { \
- *(.eh_frame .eh_frame_hdr) \
- *(.note .note.*) \
- *(.comment) \
+/* ===== Discard Unwanted Sections =====
+ *
+ * Mirrors arch/riscv/kernel/vmlinux.lds.S in Linux:
+ * /DISCARD/ : {
+ * *(.dynbss)
+ * *(.dynstr*)
+ * *(.dynamic*)
+ * *(.plt*)
+ * *(.interp*)
+ * *(.eh_frame)
+ * *(.note.GNU-stack)
+ * }
+ *
+ * Why each is safe to discard:
+ * .plt — -Bsymbolic resolves all internal refs at link time;
+ * any remaining PLT entry is an undefined symbol that
+ * the kernel would have crashed on anyway.
+ * .dynamic etc. — kernel doesn't invoke a dynamic linker; head.S
+ * walks __rela_dyn_start..__rela_dyn_end manually.
+ * .interp — no PT_INTERP at runtime.
+ * .eh_frame* — no C++ exceptions in kernel (-fno-exceptions).
+ * .note.* — boot-time identification metadata, unused. */
+#define DISCARDS \
+ /DISCARD/ : { \
+ *(.dynbss) \
+ *(.dynstr*) \
+ *(.dynamic*) \
+ *(.dynsym*) \
+ *(.hash*) *(.gnu.hash*) \
+ *(.plt*) \
+ *(.interp*) \
+ *(.eh_frame .eh_frame_hdr) \
+ *(.note .note.*) \
+ *(.comment) \
}
/* ===== Kernel Image Boundary ===== */
diff --git a/na/include/lume/mm_layout.h b/na/include/lume/mm_layout.h
new file mode 100644
index 0000000..7744de0
--- /dev/null
+++ b/na/include/lume/mm_layout.h
@@ -0,0 +1,73 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * mm_layout.h — User/Kernel address space boundary definitions
+ *
+ * Single source of truth for the user/kernel VA split.
+ * All modules (VMM, uaccess, page fault, tests) MUST include this
+ * header instead of defining their own constants.
+ *
+ * Layout (SV39, RISC-V):
+ * [0x0000_0000_0000, kUserVaLimit) — User space
+ * [kUserVaLimit, 0xFFFF_FFFF_FFFF) — Kernel space (direct map, vmemmap, etc.)
+ *
+ * Reference: docs/specs/linux_abi_mm_plan.md §5
+ */
+
+#include <lume/types.h>
+#include <lume/config.h>
+
+/* User VA range: [kUserVaBase, kUserVaLimit)
+ * kUserVaBase avoids mapping the null page (catch NULL derefs).
+ *
+ * SV39: user space is the lower half [0, 2^38). The upper half
+ * [0xFFFFFFC000000000, 0xFFFFFFFFFFFFFFFF] is kernel space.
+ * Addresses between 2^38 and 0xFFFFFFC000000000 are non-canonical
+ * and will always fault — they must never be used.
+ *
+ * This value MUST match the page table mode selected by the MD layer.
+ * If we ever support SV48/SV57, update this accordingly. */
+inline constexpr uint64 kUserVaBase = kPageSize;
+inline constexpr uint64 kUserVaLimit = 0x4000000000ULL; /* 2^38 = 256 GB */
+
+/* Kernel VA starts at the sign-extended upper half */
+inline constexpr uint64 kKernelVaBase = 0xFFFFFFC000000000ULL;
+
+/* User stack: sits at the top of the user VA range.
+ * Matches Linux STACK_TOP = TASK_SIZE = kUserVaLimit.
+ * One guard page below the top, 8 MB stack. */
+inline constexpr uint64 kUserStackSize = 8ULL * 1024 * 1024;
+inline constexpr uint64 kUserStackTop = kUserVaLimit - kPageSize; /* guard page */
+inline constexpr uint64 kUserStackBase = kUserStackTop - kUserStackSize;
+
+/*
+ * Check whether an address range [addr, addr+len) is entirely
+ * within user space. Returns true if valid.
+ */
+inline bool is_user_range(uint64 addr, uint64 len)
+{
+ if (addr < kUserVaBase) {
+ return false;
+ }
+ if (addr >= kUserVaLimit) {
+ return false;
+ }
+ if (addr + len < addr) {
+ return false; // Overflow
+ }
+ if (addr + len > kUserVaLimit) {
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Check whether a single address is in user space.
+ */
+inline bool is_user_address(uint64 addr)
+{
+ return addr >= kUserVaBase && addr < kUserVaLimit;
+}
diff --git a/na/include/lume/mount.h b/na/include/lume/mount.h
new file mode 100644
index 0000000..13f8e62
--- /dev/null
+++ b/na/include/lume/mount.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * mount.h — Internal mount structure
+ */
+
+#include <lume/types.h>
+#include <lume/list.h>
+#include <lume/atomic.h>
+#include <lume/file.h>
+
+struct dentry;
+struct super_block;
+struct file_system_type;
+
+/* ================================================================== */
+/* mount (VFS-internal) */
+/* ================================================================== */
+
+struct mount {
+ vfsmount mnt;
+ mount* mnt_parent;
+ dentry* mnt_mountpoint;
+ list_node mnt_child;
+ list_head mnt_mounts;
+ list_node mnt_list;
+ lume::atomic<int32> mnt_count;
+};
+
+inline mount* real_mount(vfsmount* m) {
+ return reinterpret_cast<mount*>(m);
+}
+
+/* ================================================================== */
+/* Mount API */
+/* ================================================================== */
+
+int do_mount(const char* dev, const char* dir,
+ const char* fstype, uint64 flags, void* data);
+int do_umount(vfsmount* mnt, int flags);
+
+vfsmount* vfs_kern_mount(file_system_type* type, int flags,
+ const char* name, void* data);
+
+/* Global root */
+extern vfsmount* g_root_mnt;
+extern dentry* g_root_dentry;
+
+void mount_init();
diff --git a/na/include/lume/mutex.h b/na/include/lume/mutex.h
new file mode 100644
index 0000000..9719aa6
--- /dev/null
+++ b/na/include/lume/mutex.h
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Mutex — Linux-style sleeping mutex (no priority inheritance).
+ *
+ * Semantics mirror Linux kernel/locking/mutex.c:
+ * - Single owner (nullptr when free).
+ * - FIFO wait list — first waiter is the next owner.
+ * - Recursive acquire by the same task is FORBIDDEN (panic).
+ * - Owner MUST be the one to unlock.
+ * - lock() may sleep; MUST NOT be called from IRQ context, with
+ * preemption disabled, or while holding a spinlock.
+ * - try_lock() never sleeps; may be called from any context.
+ *
+ * Priority inheritance lives in a separate primitive (RtMutex, future)
+ * just like Linux splits struct mutex / struct rt_mutex.
+ *
+ * Reference: docs/specs/mutex.md
+ */
+
+#include <lume/types.h>
+#include <lume/list.h>
+#include <kernel/sync/spinlock.h>
+
+struct TaskControlBlock;
+
+class Mutex {
+public:
+ void init(const char* name);
+
+ /* Blocking acquire (uninterruptible). */
+ void lock();
+
+ /* Non-blocking attempt. Returns true if acquired. */
+ bool try_lock();
+
+ /* Release; only the owner may call (else panic). */
+ void unlock();
+
+ bool is_locked() const { return owner_ != nullptr; }
+ bool is_owned_by_current() const;
+ TaskControlBlock* owner() const { return owner_; }
+
+private:
+ TaskControlBlock* owner_ = nullptr;
+ Spinlock wait_lock_;
+ list_node wait_list_{};
+ const char* name_ = nullptr;
+};
+
+/* RAII guard. */
+class MutexGuard {
+public:
+ explicit MutexGuard(Mutex& m) : m_(m) { m_.lock(); }
+ ~MutexGuard() { m_.unlock(); }
+ MutexGuard(const MutexGuard&) = delete;
+ MutexGuard& operator=(const MutexGuard&) = delete;
+private:
+ Mutex& m_;
+};
diff --git a/na/include/lume/namei.h b/na/include/lume/namei.h
new file mode 100644
index 0000000..1764693
--- /dev/null
+++ b/na/include/lume/namei.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * namei.h — Pathname lookup (path resolution)
+ */
+
+#include <lume/types.h>
+#include <lume/file.h> /* struct path */
+#include <lume/dcache.h> /* struct qstr */
+
+/* ================================================================== */
+/* LOOKUP_* flags */
+/* ================================================================== */
+
+inline constexpr uint32 LOOKUP_FOLLOW = 0x0001;
+inline constexpr uint32 LOOKUP_DIRECTORY = 0x0002;
+inline constexpr uint32 LOOKUP_CREATE = 0x0200;
+inline constexpr uint32 LOOKUP_EXCL = 0x0400;
+inline constexpr uint32 LOOKUP_PARENT = 0x0010;
+
+/* ================================================================== */
+/* Path lookup API */
+/* ================================================================== */
+
+int kern_path(const char* name, unsigned int flags, path* result);
+int filename_lookup(int dfd, const char* name, unsigned int flags, path* result);
+dentry* lookup_one_len(const char* name, dentry* parent, int len);
+
+void namei_init();
diff --git a/na/include/lume/page.h b/na/include/lume/page.h
index 7f3f8d1..61bff95 100644
--- a/na/include/lume/page.h
+++ b/na/include/lume/page.h
@@ -10,8 +10,21 @@
* structs (page_map) covers all physical memory discovered via FDT.
*
* Memory layout is compressed using a union: only one of free/slab
- * metadata is valid at a time, determined by `state`. The refcount
- * field is independent of the union to support COW sharing.
+ * metadata is valid at a time, determined by `type`.
+ *
+ * Fields:
+ * refcount — lifecycle reference count (atomic). Page cannot be
+ * freed while refcount > 0.
+ * mapcount — PTE mapping count (atomic). Number of page table
+ * entries pointing to this page. Used by COW and
+ * future reclaim.
+ * flags — page state bits (PG_*). Also serves as a bit-lock
+ * via PG_LOCKED for short critical sections.
+ * type — discriminant for the union (Free/Slab/Anon/File).
+ * mapping — pointer to reverse-mapping context. For anonymous
+ * pages, points to AnonVma (low bit set). For file
+ * pages, points to FileMapping (low bit clear).
+ * index — page offset within the mapping object.
*
* Reference: docs/specs/pmm.md §3.1
*/
@@ -20,40 +33,119 @@
#include <lume/list.h>
#include <lume/atomic.h>
-/* Forward declaration — defined in slab module (Phase 2) */
+/* Forward declarations */
class KmemCache;
-enum class PageState : uint8 {
+/* ================================================================== */
+/* Page type discriminant */
+/* ================================================================== */
+
+enum class PageType : uint8 {
Free, // In Buddy free list
PcpCached, // In Per-CPU cache (invisible to buddy coalescing)
- Allocated, // Handed out to Slab / VMM / user
+ Allocated, // Handed out but not yet typed (PMM default)
Slab, // Backing a Slab cache
+ Anon, // Anonymous user page
+ File, // File-backed page (page cache)
};
+/* ================================================================== */
+/* Page flags (stored as atomic uint32) */
+/* ================================================================== */
+
+inline constexpr uint32 kPgLocked = (1U << 0); // Bit-lock for short critical sections
+inline constexpr uint32 kPgReferenced = (1U << 1); // Accessed since last check
+inline constexpr uint32 kPgDirty = (1U << 2); // Modified, needs writeback
+inline constexpr uint32 kPgWriteback = (1U << 3); // IO writeback in progress
+inline constexpr uint32 kPgLru = (1U << 4); // On an LRU list
+inline constexpr uint32 kPgMapped = (1U << 5); // Has at least one PTE mapping
+inline constexpr uint32 kPgSwapcache = (1U << 6); // In swap cache (future)
+inline constexpr uint32 kPgCow = (1U << 7); // Involved in COW sharing
+
+/* ================================================================== */
+/* Page descriptor */
+/* ================================================================== */
+
struct Page {
- /*
- * Reference count (atomic).
- * Starts at 1 on allocation. Incremented by fork() for COW pages.
- * When decremented to 0, the page is returned to the free pool.
- */
- lume::atomic<uint32> refcount;
+ /* --- Counts (atomic, lock-free) --- */
+ lume::atomic<uint32> refcount; // Lifecycle ref: 0 = reclaimable
+ lume::atomic<uint32> mapcount; // PTE mapping count: 0 = no user mapping
+
+ /* --- State --- */
+ lume::atomic<uint32> flags; // PG_* bits (also serves as bit-lock)
+ PageType type; // Discriminant for union
+ uint8 order; // Buddy order (0–10, i.e. 4KB–4MB)
- PageState state; // Current usage state
- uint8 order; // Buddy order (0–10, i.e. 4KB–4MB)
+ /* --- Reverse mapping --- */
+ void* mapping; // AnonVma* (low bit set) or FileMapping*
+ uint64 index; // Page offset within mapping object
+ /* --- LRU (future) --- */
+ list_node lru; // LRU list linkage (for reclaim)
+
+ /* --- Subsystem-specific metadata --- */
union {
- /* Valid when state == Free */
+ /* Valid when type == Free */
struct {
- list_node free_link; // Link into Buddy or PCP free list
+ list_node free_link; // Link into Buddy or PCP free list
} free;
- /* Valid when state == Slab */
+ /* Valid when type == Slab */
struct {
- KmemCache* cache; // Owning slab cache
- uint32 obj_count; // Number of allocated objects on this page
- void* freelist; // Head of in-page free object linked list
+ KmemCache* cache; // Owning slab cache
+ uint32 obj_count; // Number of allocated objects on this page
+ void* freelist; // Head of in-page free object linked list
} slab;
- /* state == Allocated: no extra metadata (yet) */
+ /* type == Anon / File: no extra union metadata (rmap via mapping) */
};
};
+
+/* ================================================================== */
+/* Page bit-lock (lightweight, no sleep, used for rmap critical */
+/* sections and short metadata updates) */
+/* ================================================================== */
+
+inline bool page_trylock(Page* page)
+{
+ uint32 old = page->flags.fetch_or(kPgLocked);
+ return (old & kPgLocked) == 0;
+}
+
+inline void page_lock(Page* page)
+{
+ while (!page_trylock(page)) {
+ /* spin — acceptable for short critical sections only */
+ }
+}
+
+inline void page_unlock(Page* page)
+{
+ page->flags.fetch_and(~kPgLocked);
+}
+
+/* ================================================================== */
+/* Mapping pointer helpers (Linux convention: low bit = anon) */
+/* ================================================================== */
+
+inline bool page_is_anon(const Page* page)
+{
+ return (reinterpret_cast<uint64>(page->mapping) & 1ULL) != 0;
+}
+
+inline void* page_mapping_raw(const Page* page)
+{
+ return reinterpret_cast<void*>(
+ reinterpret_cast<uint64>(page->mapping) & ~1ULL);
+}
+
+inline void page_set_anon_mapping(Page* page, void* anon_vma)
+{
+ page->mapping = reinterpret_cast<void*>(
+ reinterpret_cast<uint64>(anon_vma) | 1ULL);
+}
+
+inline void page_set_file_mapping(Page* page, void* file_mapping)
+{
+ page->mapping = file_mapping;
+}
diff --git a/na/include/lume/page_cache.h b/na/include/lume/page_cache.h
new file mode 100644
index 0000000..abdc535
--- /dev/null
+++ b/na/include/lume/page_cache.h
@@ -0,0 +1,82 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * page_cache.h — Page Cache interface
+ *
+ * Provides a stable abstraction for caching physical pages indexed by
+ * (mapping, index) pairs. All file-backed fault paths should go
+ * through this interface so that upgrading the backing implementation
+ * (e.g. LRU eviction, writeback, radix tree) does not require
+ * changing any caller.
+ *
+ * Current implementation: simple open-addressing hash table.
+ * Future: radix tree + LRU lists + writeback integration.
+ *
+ * Terminology:
+ * mapping — opaque pointer identifying the backing object
+ * (FileMapping*, or any future cache-able object).
+ * index — page-granularity offset within the mapping.
+ *
+ * Reference: docs/specs/linux_abi_mm_plan.md §2.9, §3
+ */
+
+#include <lume/types.h>
+
+struct Page;
+
+/*
+ * Look up a cached page by (mapping, index).
+ * Returns the Page* if found (with refcount incremented), or nullptr.
+ * Caller must call page_cache_put() when done.
+ */
+Page* page_cache_get(void* mapping, uint64 index);
+
+/*
+ * Insert a page into the cache, associated with (mapping, index).
+ * The page's refcount is incremented by the cache.
+ * If a page already exists at (mapping, index), returns -EEXIST.
+ * Returns 0 on success, negative errno on failure.
+ */
+int page_cache_insert(void* mapping, uint64 index, Page* page);
+
+/*
+ * Release a reference obtained via page_cache_get().
+ * This decrements the page's refcount. If the page was removed
+ * from the cache and refcount drops to 0, the page is freed.
+ */
+void page_cache_put(Page* page);
+
+/*
+ * Remove a page from the cache by (mapping, index).
+ * Decrements the cache's reference on the page.
+ * Returns the removed Page* (caller does NOT own a new ref),
+ * or nullptr if not found.
+ */
+Page* page_cache_remove(void* mapping, uint64 index);
+
+/*
+ * Remove all pages associated with a given mapping.
+ * Used during file close / inode eviction / truncate.
+ * Each removed page has its cache reference decremented.
+ */
+void page_cache_invalidate(void* mapping);
+
+/*
+ * Look up a page; if not found, allocate one, insert it, and return it.
+ * The returned page has its refcount incremented (caller must put).
+ * Returns nullptr on OOM.
+ *
+ * This is the primary interface for file-backed fault paths:
+ * the caller gets a page and then fills it with file data if kPgUptodate
+ * is not set.
+ */
+Page* page_cache_get_or_alloc(void* mapping, uint64 index);
+
+/*
+ * Initialize the page cache subsystem.
+ * Called once during boot after slab_init().
+ */
+void page_cache_init();
diff --git a/na/include/lume/pgrp.h b/na/include/lume/pgrp.h
new file mode 100644
index 0000000..127f3f8
--- /dev/null
+++ b/na/include/lume/pgrp.h
@@ -0,0 +1,125 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Process Group / Session infrastructure.
+ *
+ * Linux models pgrp/session via struct pid + PIDTYPE_PGID/PIDTYPE_SID hlists.
+ * We use a simpler design that supports the same operations cheaply:
+ *
+ * - Each thread group owns one signal_struct (refcount-shared with
+ * CLONE_THREAD siblings).
+ * - signal_struct carries pgid/sid and is the linked-list node on a
+ * fixed-size pgrp hash table.
+ * - kill(pid<=0), tty_signal_pgrp, orphan checks all walk one bucket.
+ *
+ * Concurrency: g_pgrp_lock protects every bucket and the pgrp_link of
+ * every signal_struct on a bucket. signal_struct::lock is a finer-grain
+ * lock for fields that change without re-hashing (e.g. tty pointer).
+ *
+ * Reference: Linux include/linux/pid.h, kernel/pid.c
+ */
+
+#include <lume/types.h>
+#include <lume/list.h>
+#include <kernel/sync/spinlock.h>
+
+struct signal_struct;
+struct TaskControlBlock;
+
+/* ================================================================== */
+/* Subsystem init */
+/* ================================================================== */
+
+void pgrp_init();
+
+/* ================================================================== */
+/* signal_struct lifecycle */
+/* ================================================================== */
+
+/*
+ * Allocate a new signal_struct for `leader`, with sid=pgid=leader_pid,
+ * refcount=1, attached to pgrp `leader_pid`. Returns nullptr on OOM.
+ */
+signal_struct* alloc_signal_struct(int32 leader_pid,
+ TaskControlBlock* leader);
+
+/*
+ * Allocate a child signal_struct inheriting pgid/sid/tty from `parent`.
+ * The new struct is attached to the parent's pgrp and starts with
+ * refcount=1. Used by fork (non-CLONE_THREAD).
+ */
+signal_struct* fork_signal_struct(signal_struct* parent,
+ TaskControlBlock* child_leader);
+
+/* Increment refcount (CLONE_THREAD share). */
+void get_signal_struct(signal_struct* sig);
+
+/*
+ * Decrement refcount; on last drop, detach from pgrp hash and free.
+ * Pass the task that's dropping the reference so we can manage
+ * leader pointers if the leader exits while sub-threads remain.
+ */
+void put_signal_struct(signal_struct* sig);
+
+/* ================================================================== */
+/* Pgrp operations (assume signal_struct is allocated) */
+/* ================================================================== */
+
+/*
+ * Move a signal_struct from its current pgrp to `new_pgid`.
+ * Used by setpgid / setsid. Takes g_pgrp_lock internally.
+ */
+void change_pgid(signal_struct* sig, int32 new_pgid);
+
+/*
+ * Invoke `fn(sig)` for every signal_struct whose pgid == target.
+ * Holds g_pgrp_lock for the entire walk — fn must not block, sleep,
+ * or take locks that order before g_pgrp_lock.
+ *
+ * Template-y but kept in header to avoid a function-ptr indirection;
+ * keep callsites lean.
+ */
+template <typename F>
+void for_each_pgrp_member(int32 target_pgid, F&& fn);
+
+/*
+ * Count the number of signal_structs in `pgid`. Used by setsid
+ * preflight (can't create a session if you're already a pgrp leader
+ * with members) and orphan checks.
+ */
+int pgrp_member_count(int32 pgid);
+
+/* ================================================================== */
+/* Implementation details exposed for the template above */
+/* ================================================================== */
+
+constexpr int kPgrpHashShift = 6;
+constexpr int kPgrpHashSize = 1 << kPgrpHashShift; /* 64 buckets */
+constexpr int kPgrpHashMask = kPgrpHashSize - 1;
+
+extern list_node g_pgrp_hash[kPgrpHashSize];
+extern Spinlock g_pgrp_lock;
+
+inline int pgrp_hash_idx(int32 pgid) {
+ return static_cast<int>(static_cast<uint32>(pgid)) & kPgrpHashMask;
+}
+
+#include <lume/signal_types.h>
+
+template <typename F>
+void for_each_pgrp_member(int32 target_pgid, F&& fn)
+{
+ LockGuard guard(g_pgrp_lock);
+ list_node* bucket = &g_pgrp_hash[pgrp_hash_idx(target_pgid)];
+ list_node* node = bucket->next;
+ while (node != bucket) {
+ list_node* nxt = node->next; /* fn may not unlink, but be safe */
+ auto* sig = list_entry<signal_struct, &signal_struct::pgrp_link>(node);
+ if (sig->pgid == target_pgid)
+ fn(sig);
+ node = nxt;
+ }
+}
diff --git a/na/include/lume/pgtable_types.h b/na/include/lume/pgtable_types.h
new file mode 100644
index 0000000..e309a82
--- /dev/null
+++ b/na/include/lume/pgtable_types.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Page Table Entry Types — strongly typed wrappers.
+ *
+ * Layout and macro names mirror Linux's arch/riscv/include/asm/page.h
+ * and pgtable-64.h so that arch/riscv/mm/init.c can be copied from
+ * Linux without source edits. The wrapped struct prevents accidental
+ * mixing of a pte_t with a pgd_t or a raw physical address, which the
+ * previous bare-uint64 design in pmap.cc could not catch.
+ *
+ * Underlying width on RV64 LP64 is `unsigned long` = 64 bits, matching
+ * Linux exactly — assembly references (e.g. REG_S/REG_L on a pte_t*)
+ * see the same byte layout.
+ *
+ * SV39 uses 3 levels (pgd -> pmd -> pte); SV48 uses 4 (pgd -> pud ->
+ * pmd -> pte). p4d_t/pud_t are emitted unconditionally so that the
+ * higher-level abstractions in asm-generic/pgtable-nopud.h and friends
+ * (which Linux uses to collapse unused levels) work without #ifdef.
+ */
+
+#include <lume/types.h>
+
+typedef struct {
+ unsigned long pte;
+} pte_t;
+
+typedef struct {
+ unsigned long pmd;
+} pmd_t;
+
+typedef struct {
+ unsigned long pud;
+} pud_t;
+
+typedef struct {
+ unsigned long p4d;
+} p4d_t;
+
+typedef struct {
+ unsigned long pgd;
+} pgd_t;
+
+typedef struct {
+ unsigned long pgprot;
+} pgprot_t;
+
+typedef struct page *pgtable_t;
+
+/* Field accessors */
+#define pte_val(x) ((x).pte)
+#define pmd_val(x) ((x).pmd)
+#define pud_val(x) ((x).pud)
+#define p4d_val(x) ((x).p4d)
+#define pgd_val(x) ((x).pgd)
+#define pgprot_val(x) ((x).pgprot)
+
+/* Constructors (cast raw uint64 to wrapped type) */
+#define __pte(x) ((pte_t) { (x) })
+#define __pmd(x) ((pmd_t) { (x) })
+#define __pud(x) ((pud_t) { (x) })
+#define __p4d(x) ((p4d_t) { (x) })
+#define __pgd(x) ((pgd_t) { (x) })
+#define __pgprot(x) ((pgprot_t) { (x) })
+
+#define PTE_FMT "%016lx"
diff --git a/na/include/lume/pipe.h b/na/include/lume/pipe.h
new file mode 100644
index 0000000..c905c9a
--- /dev/null
+++ b/na/include/lume/pipe.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * pipe.h — Pipe data structure and API
+ *
+ * A pipe is a unidirectional byte stream backed by a fixed-size
+ * circular buffer. Two file structs (reader + writer) share a
+ * single pipe_inode_info.
+ */
+
+#include <lume/types.h>
+#include <lume/atomic.h>
+#include <lume/waitqueue.h>
+#include <kernel/sync/spinlock.h>
+
+struct file;
+struct files_struct;
+
+/* ================================================================== */
+/* pipe_inode_info */
+/* ================================================================== */
+
+inline constexpr int PIPE_BUF_SIZE = 4096;
+
+struct pipe_inode_info {
+ char buf[PIPE_BUF_SIZE];
+ int rd_pos; /* Read position in circular buffer */
+ int wr_pos; /* Write position in circular buffer */
+ int len; /* Bytes currently in buffer */
+ Spinlock lock;
+ WaitQueue rd_wait; /* Readers sleep here when empty */
+ WaitQueue wr_wait; /* Writers sleep here when full */
+
+ lume::atomic<int32> readers; /* Number of open read ends */
+ lume::atomic<int32> writers; /* Number of open write ends */
+ lume::atomic<int32> refcount; /* Total references */
+};
+
+/* ================================================================== */
+/* API */
+/* ================================================================== */
+
+/* Create a pipe, install read/write fds into the given files_struct.
+ * Returns 0 on success, negative errno on failure. */
+int do_pipe2(files_struct* files, int fds[2], int flags);
+
+/* Allocate a fresh pipe_inode_info */
+pipe_inode_info* alloc_pipe_info();
+void free_pipe_info(pipe_inode_info* pipe);
diff --git a/na/include/lume/platform_device.h b/na/include/lume/platform_device.h
new file mode 100644
index 0000000..ac3e527
--- /dev/null
+++ b/na/include/lume/platform_device.h
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * platform_device / platform_driver — FDT-described memory-mapped devices.
+ *
+ * Mirrors Linux include/linux/platform_device.h. The platform bus is
+ * the catch-all bus for devices that don't live on a real bus like PCI
+ * or USB — every device discovered via the FDT lands here.
+ *
+ * Reference: docs/specs/storage_stack.md §4.3
+ */
+
+#include <lume/types.h>
+#include <lume/device.h>
+
+class DeviceNode;
+
+/* Compatible-string -> driver-private data table (Linux's of_device_id). */
+struct of_device_id {
+ const char* compatible;
+ const void* data;
+};
+
+struct platform_device {
+ device dev;
+ DeviceNode* of_node; // FDT node this device came from
+ uint64 mmio_base; // first `reg` cell from FDT (0 if none)
+ uint64 mmio_size; // second `reg` cell (0 if none)
+ int irq; // first interrupt (-1 if none / not parsed)
+};
+
+struct platform_driver {
+ device_driver driver;
+ int (*probe)(platform_device*);
+ void (*remove)(platform_device*);
+ const of_device_id* of_match_table; // NULL-terminated array
+};
+
+extern bus_type platform_bus;
+
+void platform_bus_init();
+int platform_driver_register(platform_driver* drv);
+int platform_device_register(platform_device* pdev);
+
+/* Helper: pull current driver out of a probed device. */
+inline platform_driver* to_platform_driver(device_driver* drv)
+{
+ return container_of(drv, platform_driver, driver);
+}
+inline platform_device* to_platform_device(device* dev)
+{
+ return container_of(dev, platform_device, dev);
+}
+
+/* Linker-set registration macro — drop a pointer to your platform_driver
+ * into the .drivers section. driver_probe_all() iterates this set at
+ * boot. Use a unique token in CONCAT to avoid name collisions. */
+#define PLATFORM_DRIVER(_drv) \
+ extern platform_driver _drv; \
+ static platform_driver* const __pdrv_ptr_##_drv \
+ __attribute__((used, section(".drivers"))) = &_drv
diff --git a/na/include/lume/pmm.h b/na/include/lume/pmm.h
index f02a146..3e4b819 100644
--- a/na/include/lume/pmm.h
+++ b/na/include/lume/pmm.h
@@ -64,6 +64,20 @@ void pmm_free_pages(Page* page, uint8 order);
void page_incref(Page* page);
void page_decref(Page* page);
+/* --- Map counting (atomic, lock-free) --- */
+
+inline void page_inc_mapcount(Page* page) {
+ page->mapcount.fetch_add(1);
+}
+
+inline uint32 page_dec_mapcount(Page* page) {
+ return page->mapcount.fetch_sub(1) - 1; // Returns new value
+}
+
+inline uint32 page_mapcount(const Page* page) {
+ return page->mapcount.load(__ATOMIC_RELAXED);
+}
+
/* --- Address conversion --- */
// Convert a Page* to its physical address.
@@ -75,6 +89,20 @@ Page* pa_to_page(PhysAddr pa);
// Convert a Page* to a kernel virtual address.
VirtAddr page_to_va(const Page* page);
+/* --- PFN (Page Frame Number) abstraction --- */
+/*
+ * PFN is the fundamental unit for PA↔Page conversion.
+ * PA↔PFN converters already exist in addr.h (pa_to_pfn / pfn_to_pa).
+ * When sparse vmemmap lands, only pfn_to_page/page_to_pfn
+ * need to change — no callers are affected.
+ */
+
+// Convert PFN to Page*. PFN = pa / PAGE_SIZE.
+Page* pfn_to_page(uint64 pfn);
+
+// Convert Page* to PFN.
+uint64 page_to_pfn(const Page* page);
+
// Query PMM memory range (for VMM kernel mapping)
PhysAddr pmm_get_mem_base();
uint64 pmm_get_mem_size();
diff --git a/na/include/lume/poll.h b/na/include/lume/poll.h
new file mode 100644
index 0000000..7d5bca2
--- /dev/null
+++ b/na/include/lume/poll.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * poll.h — POSIX poll() / ppoll() infrastructure.
+ *
+ * Linux pattern (include/linux/poll.h):
+ * - file_operations->poll(file, poll_table) returns a bitmask of
+ * POLL* events that are currently true on this file.
+ * - As a side effect, the poll callback calls poll_wait(file, wq, pt)
+ * to register the caller on every wait queue that might fire when
+ * state changes. sys_ppoll() then sleeps on those queues until
+ * one of them fires or a signal/timeout arrives.
+ *
+ * Our minimal implementation supports a SINGLE wait queue per poll call
+ * (sufficient for nfds==1, which is the BusyBox interactive shell case).
+ * Multi-fd polls work but fall back to busy re-checking; a proper
+ * per-fd waiter-list upgrade can come later.
+ */
+
+#include <lume/types.h>
+
+struct file;
+class WaitQueue;
+
+/* ================================================================== */
+/* UAPI: pollfd struct + POLL* event bits (Linux asm-generic) */
+/* ================================================================== */
+
+struct pollfd {
+ int32 fd;
+ int16 events;
+ int16 revents;
+};
+
+inline constexpr int16 POLLIN = 0x0001;
+inline constexpr int16 POLLPRI = 0x0002;
+inline constexpr int16 POLLOUT = 0x0004;
+inline constexpr int16 POLLERR = 0x0008;
+inline constexpr int16 POLLHUP = 0x0010;
+inline constexpr int16 POLLNVAL = 0x0020;
+inline constexpr int16 POLLRDNORM = 0x0040;
+inline constexpr int16 POLLRDBAND = 0x0080;
+inline constexpr int16 POLLWRNORM = 0x0100;
+inline constexpr int16 POLLWRBAND = 0x0200;
+
+/* ================================================================== */
+/* Kernel-side poll_table */
+/* ================================================================== */
+
+/*
+ * Carried into file_operations->poll(). Drivers call poll_wait() with
+ * the wait queue they'd like the poller to sleep on; we record the
+ * first one. When `wq == nullptr` on entry, no real registration is
+ * needed (the caller is just probing readiness).
+ */
+struct poll_table {
+ WaitQueue* wq; /* first wait queue registered (single-wq design) */
+};
+
+/*
+ * Called from f_op->poll() implementations to register a wait queue.
+ * If `pt` is null we're in a probe-only pass — caller doesn't want to
+ * sleep, just to read current revents.
+ */
+void poll_wait(file* fp, WaitQueue* wq, poll_table* pt);
diff --git a/na/include/lume/ptrace.h b/na/include/lume/ptrace.h
new file mode 100644
index 0000000..687d103
--- /dev/null
+++ b/na/include/lume/ptrace.h
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * PtRegs — Linux-compatible user register context
+ *
+ * This structure MUST match the Linux RISC-V pt_regs layout exactly.
+ * It is the canonical representation for:
+ * - Signal frame construction (ucontext / sigcontext)
+ * - ptrace(GETREGS/SETREGS)
+ * - /proc/<pid>/regs
+ *
+ * The internal TrapFrame (arch/trapframe.h) has a DIFFERENT layout
+ * optimized for fast trap entry/exit. Conversion between the two
+ * is done via trapframe_to_pt_regs() / pt_regs_to_trapframe().
+ *
+ * Reference: Linux arch/riscv/include/asm/ptrace.h
+ */
+
+#include <lume/types.h>
+
+struct PtRegs {
+ uint64 epc; // Program counter at trap
+ uint64 ra; // x1
+ uint64 sp; // x2
+ uint64 gp; // x3
+ uint64 tp; // x4
+ uint64 t0; // x5
+ uint64 t1; // x6
+ uint64 t2; // x7
+ uint64 s0; // x8
+ uint64 s1; // x9
+ uint64 a0; // x10
+ uint64 a1; // x11
+ uint64 a2; // x12
+ uint64 a3; // x13
+ uint64 a4; // x14
+ uint64 a5; // x15
+ uint64 a6; // x16
+ uint64 a7; // x17
+ uint64 s2; // x18
+ uint64 s3; // x19
+ uint64 s4; // x20
+ uint64 s5; // x21
+ uint64 s6; // x22
+ uint64 s7; // x23
+ uint64 s8; // x24
+ uint64 s9; // x25
+ uint64 s10; // x26
+ uint64 s11; // x27
+ uint64 t3; // x28
+ uint64 t4; // x29
+ uint64 t5; // x30
+ uint64 t6; // x31
+ /* CSRs */
+ uint64 status; // sstatus
+ uint64 badaddr; // stval
+ uint64 cause; // scause
+ /* Syscall bookkeeping */
+ uint64 orig_a0; // a0 value before syscall (for restart)
+};
+
+static_assert(sizeof(PtRegs) == 36 * 8, "PtRegs must be 288 bytes (Linux ABI)");
+
+/*
+ * Convert between internal TrapFrame and Linux-ABI PtRegs.
+ * These are the ONLY places that know both layouts.
+ * Future TrapFrame restructuring only needs to update these two functions.
+ */
+namespace arch { struct TrapFrame; }
+
+void trapframe_to_pt_regs(PtRegs* dst, const arch::TrapFrame* src,
+ uint64 scause, uint64 stval);
+void pt_regs_to_trapframe(arch::TrapFrame* dst, const PtRegs* src);
diff --git a/na/include/lume/rbtree.h b/na/include/lume/rbtree.h
index 8bc8104..80b0c6c 100644
--- a/na/include/lume/rbtree.h
+++ b/na/include/lume/rbtree.h
@@ -10,16 +10,7 @@
#include <lume/compiler.h>
#include <lume/rbtree_types.h>
-
-/* ------------------------------------------------------------------ */
-/* Linux-compat macros for freestanding kernel */
-/* ------------------------------------------------------------------ */
-
-#ifndef container_of
-#define container_of(ptr, type, member) \
- reinterpret_cast<type *>( \
- reinterpret_cast<char *>(ptr) - __builtin_offsetof(type, member))
-#endif
+#include <lume/container_of.h>
/* ------------------------------------------------------------------ */
/* Core macros */
diff --git a/na/include/lume/relocate.h b/na/include/lume/relocate.h
deleted file mode 100644
index 120824f..0000000
--- a/na/include/lume/relocate.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#pragma once
-
-#include <lume/types.h>
-
-inline constexpr uint64 kRelocTableMagic = 0x4C554D45524C4F43ULL; /* LUMERLOC */
-inline constexpr uint16 kRelocTableVersion = 1;
-
-enum RelocArch : uint16 {
- RELOC_ARCH_RISCV = 1,
- RELOC_ARCH_AARCH64 = 2,
- RELOC_ARCH_X86_64 = 3,
-};
-
-enum RelocType : uint32 {
- RELOC_RISCV_RELATIVE = 1,
- RELOC_RISCV_64 = 2,
-};
-
-struct RelocTableHeader {
- uint64 magic;
- uint16 version;
- uint16 arch;
- uint32 entry_size;
- uint32 entry_count;
- uint32 reserved;
-};
-
-struct RelocEntry {
- uint64 offset;
- uint64 addend;
- uint32 type;
- uint32 reserved;
-};
-
-enum RelocStatus : uint32 {
- RELOC_OK = 0,
- RELOC_ERR_MAGIC,
- RELOC_ERR_VERSION,
- RELOC_ERR_ARCH,
- RELOC_ERR_SIZE,
- RELOC_ERR_RANGE,
- RELOC_ERR_UNSUPPORTED,
-};
-
-/* Apply relocations to the image loaded at load_base. */
-RelocStatus relocate_apply(uint64 load_base, uint64 image_size,
- uint64 link_base, const void* reloc_blob,
- uint64 reloc_size);
-
-const char* reloc_status_str(RelocStatus st);
diff --git a/na/include/lume/sched.h b/na/include/lume/sched.h
index 0f25e58..ab26bac 100644
--- a/na/include/lume/sched.h
+++ b/na/include/lume/sched.h
@@ -71,3 +71,32 @@ void sched_dequeue(TaskControlBlock* task);
/* Convert priority level to time slice length.
* Higher priority (lower number) → longer time slice. */
int32 priority_to_timeslice(uint8 priority);
+
+/* ========================================================================
+ * Task Migration & Load Balancing
+ * ======================================================================== */
+
+/*
+ * sched_migrate_task — Move a task from its current CPU to target_cpu.
+ *
+ * The task must NOT be the currently running task. It is dequeued from
+ * its old runqueue and enqueued onto the target. Both runqueue locks
+ * are acquired in cpu_id order to prevent deadlock.
+ *
+ * Returns 0 on success, -EINVAL if task is current or target is invalid.
+ */
+int sched_migrate_task(TaskControlBlock* task, uint8 target_cpu);
+
+/*
+ * sched_balance — Periodic load balancer.
+ *
+ * Called from sched_tick(). Checks if any CPU has significantly more
+ * runnable tasks and pulls work to the local runqueue. This is a
+ * simplified version of Linux's load_balance().
+ */
+void sched_balance();
+
+/*
+ * sched_nr_running — Get the number of runnable tasks on a CPU.
+ */
+uint32 sched_nr_running(uint32 cpu_id);
diff --git a/na/include/lume/setup.h b/na/include/lume/setup.h
new file mode 100644
index 0000000..f8e6cb6
--- /dev/null
+++ b/na/include/lume/setup.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _LUME_SETUP_H
+#define _LUME_SETUP_H
+
+/*
+ * Boot-time interfaces called between assembly and C.
+ *
+ * Mirrors the subset of Linux include/linux/setup.h relevant to LumeOS's
+ * Linux-aligned RISC-V boot flow:
+ *
+ * setup_vm Build early_pg_dir / trampoline_pg_dir before
+ * the MMU is enabled. Filled kernel_map for
+ * all subsequent code to consult.
+ * relocate_enable_mmu Switch SATP, jump to higher-half, apply
+ * .rela.dyn via relocate_kernel.
+ * relocate_kernel Walk .rela.dyn, patch R_RISCV_RELATIVE
+ * slots with addend + kaslr_offset.
+ *
+ * kernel_main (LumeOS's equivalent of Linux's start_kernel) is declared
+ * in kernel/main.cc and re-declared in arch-side glue — kept local to
+ * that file to avoid creating an artificial public API for the lone
+ * call site in head.S.
+ */
+
+#include <lume/types.h>
+#include <asm/pgtable.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void setup_vm(uintptr_t load_pa, uintptr_t dtb_pa);
+void relocate_enable_mmu(pgd_t *pgdir);
+void relocate_kernel(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _LUME_SETUP_H */
diff --git a/na/include/lume/signal.h b/na/include/lume/signal.h
index 5ff6ed0..05b7542 100644
--- a/na/include/lume/signal.h
+++ b/na/include/lume/signal.h
@@ -4,5 +4,57 @@
#pragma once
/*
- * Signal subsystem interface.
+ * Signal Infrastructure — Core signal delivery API
+ *
+ * Provides the interface between trap handling, signal delivery,
+ * and user-space signal handler execution.
+ *
+ * Reference: Linux include/linux/sched/signal.h
*/
+
+#include <lume/types.h>
+
+struct TaskControlBlock;
+struct sighand_struct;
+namespace arch { struct TrapFrame; }
+
+/* ------------------------------------------------------------------
+ * sighand_struct lifecycle (Linux kernel/fork.c:copy_sighand pattern)
+ * ------------------------------------------------------------------ */
+
+sighand_struct* alloc_sighand();
+sighand_struct* copy_sighand(sighand_struct* parent);
+void get_sighand(sighand_struct* sh);
+void put_sighand(sighand_struct* sh);
+
+/*
+ * force_sig_fault — Deliver a fault-induced signal to the current task.
+ *
+ * Called from trap handler when a user-mode exception cannot be resolved.
+ * Bypasses the blocked mask and enqueues the signal.
+ *
+ * @signo: Signal number (SIGSEGV, SIGBUS, SIGILL, SIGFPE, etc.)
+ * @code: si_code for siginfo_t (e.g., SEGV_MAPERR, SEGV_ACCERR)
+ * @addr: Fault address (stval / badaddr)
+ */
+void force_sig_fault(int signo, int code, uint64 addr);
+
+/*
+ * do_signal — Process pending signals before returning to user mode.
+ *
+ * Called at the end of trap_handle() when origin == FromUser.
+ * If a signal is pending and a handler is registered, constructs
+ * a signal frame on the user stack and redirects epc.
+ *
+ * @tf: TrapFrame to modify if signal delivery is needed.
+ */
+void do_signal(arch::TrapFrame* tf);
+
+/*
+ * signal_pending — Check if the task has unblocked pending signals.
+ *
+ * Used by blocking syscalls (e.g., n_tty_read) to detect whether
+ * they should return -EINTR.
+ */
+bool signal_pending(TaskControlBlock* task);
+
diff --git a/na/include/lume/signal_types.h b/na/include/lume/signal_types.h
new file mode 100644
index 0000000..f66231b
--- /dev/null
+++ b/na/include/lume/signal_types.h
@@ -0,0 +1,263 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Signal Types — Linux-compatible structures for signal delivery
+ *
+ * Follows the RISC-V LP64 ABI layout so that user-space programs
+ * compiled with musl / glibc see the correct struct sizes.
+ *
+ * Reference: Linux include/uapi/asm-generic/signal.h
+ * Linux include/uapi/asm-generic/siginfo.h
+ */
+
+#include <lume/types.h>
+#include <lume/signal_nr.h>
+#include <lume/list.h>
+#include <kernel/sync/spinlock.h>
+
+/* ================================================================== */
+/* Signal set (64-bit bitmap) */
+/* ================================================================== */
+
+struct sigset_t {
+ uint64 sig[_NSIG_WORDS]; /* _NSIG_WORDS = 1 on LP64 */
+
+ void clear() { for (auto& s : sig) s = 0; }
+ void fill() { for (auto& s : sig) s = ~0ULL; }
+ void add(int signo) { sig[(signo-1)/64] |= (1ULL << ((signo-1)%64)); }
+ void del(int signo) { sig[(signo-1)/64] &= ~(1ULL << ((signo-1)%64)); }
+ bool has(int signo) const { return (sig[(signo-1)/64] >> ((signo-1)%64)) & 1; }
+ bool is_empty() const { for (auto s : sig) if (s) return false; return true; }
+
+ sigset_t operator&(const sigset_t& o) const {
+ sigset_t r;
+ for (int i = 0; i < _NSIG_WORDS; i++) r.sig[i] = sig[i] & o.sig[i];
+ return r;
+ }
+ sigset_t operator|(const sigset_t& o) const {
+ sigset_t r;
+ for (int i = 0; i < _NSIG_WORDS; i++) r.sig[i] = sig[i] | o.sig[i];
+ return r;
+ }
+ sigset_t operator~() const {
+ sigset_t r;
+ for (int i = 0; i < _NSIG_WORDS; i++) r.sig[i] = ~sig[i];
+ return r;
+ }
+};
+
+/* ================================================================== */
+/* siginfo_t (simplified, Linux UAPI compatible layout) */
+/* ================================================================== */
+
+struct kernel_siginfo {
+ int si_signo;
+ int si_errno;
+ int si_code;
+ int _pad0; /* alignment */
+
+ union {
+ /* kill() */
+ struct {
+ int32 _pid;
+ int32 _uid;
+ } _kill;
+
+ /* SIGCHLD */
+ struct {
+ int32 _pid;
+ int32 _uid;
+ int _status;
+ long _utime;
+ long _stime;
+ } _sigchld;
+
+ /* SIGILL, SIGFPE, SIGSEGV, SIGBUS, SIGTRAP */
+ struct {
+ uint64 _addr;
+ } _sigfault;
+
+ /* Pad to 128 bytes total (Linux SI_MAX_SIZE) */
+ char _pad[128 - 4*sizeof(int)];
+ } _sifields;
+};
+
+/* ================================================================== */
+/* Signal action (kernel representation) */
+/* ================================================================== */
+
+/* sa_flags bits (Linux UAPI) */
+inline constexpr uint64 SA_NOCLDSTOP = 0x00000001;
+inline constexpr uint64 SA_NOCLDWAIT = 0x00000002;
+inline constexpr uint64 SA_SIGINFO = 0x00000004;
+inline constexpr uint64 SA_RESTORER = 0x04000000;
+inline constexpr uint64 SA_ONSTACK = 0x08000000;
+inline constexpr uint64 SA_RESTART = 0x10000000;
+inline constexpr uint64 SA_NODEFER = 0x40000000;
+inline constexpr uint64 SA_RESETHAND = 0x80000000;
+
+/* Special handler values */
+inline constexpr uint64 SIG_DFL = 0;
+inline constexpr uint64 SIG_IGN = 1;
+inline constexpr uint64 SIG_ERR = static_cast<uint64>(-1);
+
+struct kernel_sigaction {
+ uint64 sa_handler; /* handler address or SIG_DFL/SIG_IGN */
+ uint64 sa_flags;
+ uint64 sa_restorer; /* sigreturn trampoline (SA_RESTORER) */
+ sigset_t sa_mask; /* signals blocked during handler */
+};
+
+/* ================================================================== */
+/* Per-task signal state */
+/* ================================================================== */
+
+/* Queued signal entry */
+struct sigqueue {
+ list_node list;
+ kernel_siginfo info;
+};
+
+/* Pending signal set + queue */
+struct sigpending {
+ list_node list; /* list of sigqueue entries */
+ sigset_t signal; /* bitmap of pending signals */
+
+ void init() {
+ list.init();
+ signal.clear();
+ }
+};
+
+/* Signal handler table (shared between threads via CLONE_SIGHAND).
+ *
+ * Linux pattern (kernel/fork.c:copy_sighand): one sighand_struct per
+ * thread group by default, shared across CLONE_SIGHAND siblings,
+ * refcounted, freed when the last referencing task exits. Its
+ * `siglock` guards BOTH the action table and every per-task `pending`
+ * list of every task that references this sighand. That single-lock
+ * model is what lets send/dequeue/discard race-free without per-list
+ * locks. */
+struct sighand_struct {
+ Spinlock siglock;
+ uint32 refcount; /* # tasks sharing this sighand (≥1) */
+ kernel_sigaction action[_NSIG];
+
+ void init() {
+ siglock.init("sighand");
+ refcount = 1;
+ for (auto& a : action) {
+ a.sa_handler = SIG_DFL;
+ a.sa_flags = 0;
+ a.sa_restorer = 0;
+ a.sa_mask.clear();
+ }
+ }
+};
+
+/* ================================================================== */
+/* signal_struct — per-thread-group shared state */
+/* ================================================================== */
+
+struct TaskControlBlock;
+struct tty_struct;
+
+/*
+ * Linux's signal_struct is the per-thread-group container holding:
+ * - Process group / session identity (pgid, sid)
+ * - Controlling terminal (tty)
+ * - Misc state shared across CLONE_THREAD siblings
+ *
+ * Refcount tracks the number of tasks in the thread group; when the last
+ * task in the group exits, the signal_struct is freed. Detachment from
+ * the pgrp hash also happens at that point.
+ *
+ * `pgrp_link` makes this signal_struct a member of one pgrp bucket; the
+ * pgrp subsystem walks the bucket to enumerate all thread groups whose
+ * pgid hashes there.
+ */
+struct signal_struct {
+ Spinlock lock;
+ uint32 refcount; /* # tasks sharing this (≥1) */
+ int32 pgid; /* process group id */
+ int32 sid; /* session id */
+ tty_struct* tty; /* controlling tty (or nullptr) */
+ int32 tty_old_pgrp; /* SIGHUP target on TIOCNOTTY */
+ TaskControlBlock* leader; /* thread-group leader (pid==tgid) */
+ list_node pgrp_link; /* on pgrp hash bucket */
+
+ void init(int32 leader_pid, TaskControlBlock* tg_leader) {
+ lock.init("signal");
+ refcount = 1;
+ pgid = leader_pid;
+ sid = leader_pid;
+ tty = nullptr;
+ tty_old_pgrp = 0;
+ leader = tg_leader;
+ pgrp_link.init();
+ }
+};
+
+/* ================================================================== */
+/* Default signal dispositions */
+/* ================================================================== */
+
+enum SigDefaultAction {
+ SIG_ACT_TERM, /* Terminate */
+ SIG_ACT_IGN, /* Ignore */
+ SIG_ACT_CORE, /* Terminate + core dump */
+ SIG_ACT_STOP, /* Stop process */
+ SIG_ACT_CONT, /* Continue if stopped */
+};
+
+/* Returns the default action for a standard signal */
+inline SigDefaultAction sig_default_action(int sig)
+{
+ switch (sig) {
+ case SIGCHLD:
+ case SIGURG:
+ case SIGWINCH:
+ return SIG_ACT_IGN;
+
+ case SIGCONT:
+ return SIG_ACT_CONT;
+
+ case SIGSTOP:
+ case SIGTSTP:
+ case SIGTTIN:
+ case SIGTTOU:
+ return SIG_ACT_STOP;
+
+ case SIGQUIT:
+ case SIGILL:
+ case SIGTRAP:
+ case SIGABRT:
+ case SIGBUS:
+ case SIGFPE:
+ case SIGSEGV:
+ case SIGXCPU:
+ case SIGXFSZ:
+ case SIGSYS:
+ return SIG_ACT_CORE;
+
+ default:
+ return SIG_ACT_TERM;
+ }
+}
+
+/* SIG_KERNEL_ONLY: cannot be caught or ignored */
+inline bool sig_kernel_only(int sig)
+{
+ return sig == SIGKILL || sig == SIGSTOP;
+}
+
+/* ================================================================== */
+/* SigprocMask how values (Linux UAPI) */
+/* ================================================================== */
+
+inline constexpr int SIG_BLOCK = 0;
+inline constexpr int SIG_UNBLOCK = 1;
+inline constexpr int SIG_SETMASK = 2;
diff --git a/na/include/lume/smp.h b/na/include/lume/smp.h
new file mode 100644
index 0000000..a5292b6
--- /dev/null
+++ b/na/include/lume/smp.h
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * smp.h — Machine-Independent SMP primitives
+ *
+ * Provides cross-CPU communication (IPI) and TLB coordination.
+ * The MI interface calls through to arch-specific backends
+ * (e.g. SBI on RISC-V, APIC on x86).
+ *
+ * Reference: docs/specs/linux_abi_mm_plan.md §SMP
+ */
+
+#include <lume/types.h>
+#include <lume/atomic.h>
+#include <lume/config.h>
+
+/*
+ * IPI reasons — each bit in the per-CPU pending mask
+ * corresponds to one reason. Multiple reasons can be
+ * pending simultaneously.
+ */
+enum IpiReason : uint32 {
+ IPI_RESCHEDULE = (1U << 0), /* Target should call schedule() */
+ IPI_TLB_SHOOTDOWN = (1U << 1), /* Target should flush TLB */
+ IPI_CALL_FUNCTION = (1U << 2), /* Target should run a function */
+ IPI_HALT = (1U << 3), /* Target should halt */
+};
+
+/*
+ * Per-CPU IPI pending state.
+ * Written by sender (atomically), consumed by receiver in IRQ context.
+ */
+struct IpiData {
+ lume::atomic<uint32> pending; /* Bitmask of IpiReason */
+
+ /* TLB shootdown payload */
+ uint64 tlb_start;
+ uint64 tlb_end;
+};
+
+/* Per-CPU IPI data array (BSS zero-initialized) */
+extern IpiData g_ipi_data[kMaxCpus];
+
+/*
+ * smp_send_ipi — Send an IPI to a set of CPUs
+ *
+ * @cpu_mask: bitmask of target CPU IDs (bit N = CPU N)
+ * @reason: reason bits to set in the target's pending mask
+ *
+ * Safe to call from any context (interrupt or process).
+ */
+void smp_send_ipi(uint64 cpu_mask, uint32 reason);
+
+/*
+ * smp_send_reschedule — Convenience: send reschedule IPI to one CPU
+ */
+inline void smp_send_reschedule(uint8 cpu_id)
+{
+ smp_send_ipi(1ULL << cpu_id, IPI_RESCHEDULE);
+}
+
+/*
+ * smp_init — Initialize SMP subsystem (called from BSP after trap_init)
+ */
+void smp_init();
+
+/*
+ * smp_handle_ipi — Called from trap handler on receiving S-mode
+ * software interrupt. Processes all pending IPI reasons.
+ */
+void smp_handle_ipi();
diff --git a/na/include/lume/stdarg.h b/na/include/lume/stdarg.h
new file mode 100644
index 0000000..c8dc7f4
--- /dev/null
+++ b/na/include/lume/stdarg.h
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef _LINUX_STDARG_H
+#define _LINUX_STDARG_H
+
+typedef __builtin_va_list va_list;
+#define va_start(v, l) __builtin_va_start(v, l)
+#define va_end(v) __builtin_va_end(v)
+#define va_arg(v, T) __builtin_va_arg(v, T)
+#define va_copy(d, s) __builtin_va_copy(d, s)
+
+#endif
diff --git a/na/include/lume/stringify.h b/na/include/lume/stringify.h
new file mode 100644
index 0000000..0e84cbe
--- /dev/null
+++ b/na/include/lume/stringify.h
@@ -0,0 +1,14 @@
+#ifndef __LINUX_STRINGIFY_H
+#define __LINUX_STRINGIFY_H
+
+/* Indirect stringification. Doing two levels allows the parameter to be a
+ * macro itself. For example, compile with -DFOO=bar, __stringify(FOO)
+ * converts to "bar".
+ */
+
+#define __stringify_1(x...) #x
+#define __stringify(x...) __stringify_1(x)
+
+#define FILE_LINE __FILE__ ":" __stringify(__LINE__)
+
+#endif /* !__LINUX_STRINGIFY_H */
diff --git a/na/include/lume/syscall_nr.h b/na/include/lume/syscall_nr.h
index 1b677d4..29e3904 100644
--- a/na/include/lume/syscall_nr.h
+++ b/na/include/lume/syscall_nr.h
@@ -47,11 +47,28 @@ inline constexpr int SYS_writev = 66;
inline constexpr int SYS_ppoll = 73;
inline constexpr int SYS_fstatat = 79;
inline constexpr int SYS_fstat = 80;
+inline constexpr int SYS_getdents64 = 61;
+
+inline constexpr int SYS_fcntl = 25;
+inline constexpr int SYS_linkat = 37;
+inline constexpr int SYS_mount = 40;
+inline constexpr int SYS_umount2 = 39;
inline constexpr int SYS_kill = 129;
+inline constexpr int SYS_tkill = 130;
+inline constexpr int SYS_tgkill = 131;
+inline constexpr int SYS_sigaltstack = 132;
inline constexpr int SYS_rt_sigaction = 134;
inline constexpr int SYS_rt_sigprocmask = 135;
inline constexpr int SYS_rt_sigreturn = 139;
+inline constexpr int SYS_reboot = 142;
+inline constexpr int SYS_setpgid = 154;
+inline constexpr int SYS_getpgid = 155;
+inline constexpr int SYS_getsid = 156;
+inline constexpr int SYS_setsid = 157;
+inline constexpr int SYS_prctl = 167;
+inline constexpr int SYS_geteuid = 175;
+inline constexpr int SYS_getegid = 177;
inline constexpr int SYS_futex = 98;
inline constexpr int SYS_nanosleep = 101;
@@ -59,3 +76,7 @@ inline constexpr int SYS_clock_gettime = 113;
inline constexpr int SYS_epoll_create1 = 20;
inline constexpr int SYS_epoll_ctl = 21;
inline constexpr int SYS_epoll_pwait = 22;
+
+inline constexpr int SYS_rt_sigtimedwait = 137;
+inline constexpr int SYS_socket = 198;
+inline constexpr int SYS_sendto = 206;
diff --git a/na/include/lume/task.h b/na/include/lume/task.h
index c87938c..e959147 100644
--- a/na/include/lume/task.h
+++ b/na/include/lume/task.h
@@ -17,7 +17,9 @@
#include <lume/list.h>
#include <arch/context.h>
#include <arch/trapframe.h>
+#include <arch/fpstate.h>
#include <lume/waitqueue.h>
+#include <lume/signal_types.h>
/* Forward declarations */
struct VmSpace;
@@ -33,6 +35,7 @@ enum class TaskState : uint8 {
Sleeping = 2, // Blocked on a WaitQueue
Zombie = 3, // Exited, waiting for parent wait()
Dead = 4, // Fully reclaimed, TCB can be freed
+ Stopped = 5, // Job control: SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU
};
/* ========================================================================
@@ -43,13 +46,15 @@ inline constexpr uint8 TF_KTHREAD = 0x01; // Kernel thread (never enters user m
inline constexpr uint8 TF_IDLE = 0x02; // Idle thread
inline constexpr uint8 TF_INIT = 0x04; // init process (PID 1)
-inline constexpr int kExitCodeSignalFlag = 0x100;
-
/* ========================================================================
* Kernel Stack
* ======================================================================== */
inline constexpr uint64 kKernelStackSize = CONFIG_KERNEL_STACK_SIZE;
+inline constexpr int kKernelStackOrder = (kKernelStackSize <= 4096) ? 0
+ : (kKernelStackSize <= 8192) ? 1
+ : (kKernelStackSize <= 16384) ? 2
+ : (kKernelStackSize <= 32768) ? 3 : 4;
inline constexpr uint64 kStackCanary = 0xDEAD'BEEF'CAFE'BABEULL;
/* ========================================================================
@@ -66,6 +71,7 @@ struct TaskControlBlock {
int32 time_slice; // Remaining time slice (ticks)
int32 pid; // Unique task ID (Linux: tid)
int32 tgid; // Thread group ID (Linux: pid)
+ int32 preempt_count; // >0: preemption disabled (holding locks etc.)
list_node run_link; // RunQueue list node
list_node all_link; // Global task list node
@@ -73,6 +79,7 @@ struct TaskControlBlock {
/* ===== Context ===== */
arch::Context context; // Callee-saved registers (scheduler switch)
arch::TrapFrame* trapframe; // Trap frame (user-mode entry/exit)
+ arch::FpState fpstate; // FPU register state (f0-f31 + fcsr)
/* ===== Kernel Stack ===== */
uint64 kstack_pa; // Physical address
@@ -81,6 +88,11 @@ struct TaskControlBlock {
/* ===== Address Space ===== */
VmSpace* mm; // nullptr = kernel thread
+ /* ===== Per-task lock =====
+ * Protects: parent, children, sibling, exit_code, state transitions.
+ * Lock ordering: task->lock -> g_task_list_lock (never reverse). */
+ Spinlock lock;
+
/* ===== Task Hierarchy ===== */
TaskControlBlock* parent;
list_node children; // Children list head
@@ -89,11 +101,34 @@ struct TaskControlBlock {
/* ===== Exit ===== */
int32 exit_code;
+ int32 exit_signal; // Linux: signal sent to parent on exit.
+ // SIGCHLD = notify parent, become zombie
+ // -1 = auto-reap (kthreads)
+ uint64 clear_child_tid; // CLONE_CHILD_CLEARTID: write 0 here on exit
+
+ /* ===== Job control reporting =====
+ * stopped_code: 0 means "no pending stop notification"; non-zero is the
+ * Linux wstatus value to hand back from wait4(WUNTRACED). Cleared
+ * after the parent consumes it via wait4 (or task termination).
+ * notify_continued: set when SIGCONT resumes us; consumed by
+ * wait4(WCONTINUED). */
+ int32 stopped_code;
+ bool notify_continued;
/* ===== Scheduler class metadata ===== */
const SchedClass* sched_class;
void* sched_entity;
+ /* ===== VFS ===== */
+ struct files_struct* files; // Per-task fd table (shared via CLONE_FILES)
+ struct fs_struct* fs; // cwd/root (shared via CLONE_FS)
+
+ /* ===== Signals ===== */
+ struct sigpending pending; // Per-task pending signal queue
+ sigset_t blocked; // Blocked signal mask
+ struct sighand_struct* sighand; // Signal handler table (shared via CLONE_SIGHAND)
+ struct signal_struct* signal; // Thread-group identity: pgid/sid/ctty (shared via CLONE_THREAD)
+
/* ===== Debug ===== */
const char* name;
};
diff --git a/na/include/lume/timer.h b/na/include/lume/timer.h
index e7de2f1..9bdddd5 100644
--- a/na/include/lume/timer.h
+++ b/na/include/lume/timer.h
@@ -9,9 +9,24 @@
* MI Timer Subsystem
*/
+#include <lume/list.h>
+
+struct TimerList {
+ list_node entry;
+ uint64 expires;
+ void (*function)(TimerList*);
+ void* data;
+
+ void init(void (*cb)(TimerList*)) {
+ entry.init();
+ function = cb;
+ data = nullptr;
+ expires = 0;
+ }
+};
+
extern uint64 g_jiffies;
-// BSP global initialization
void timer_init();
// AP per-CPU initialization
@@ -22,3 +37,7 @@ void timer_tick();
// Get configured timebase frequency
uint64 timer_get_freq();
+
+void add_timer(TimerList* timer);
+int del_timer(TimerList* timer);
+int mod_timer(TimerList* timer, uint64 expires);
diff --git a/na/include/lume/tlb.h b/na/include/lume/tlb.h
new file mode 100644
index 0000000..b4d51ea
--- /dev/null
+++ b/na/include/lume/tlb.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * tlb.h — Machine-Independent TLB management interface
+ *
+ * All TLB invalidation in the kernel MUST go through these functions.
+ * Direct use of arch::mmu::flush_tlb_*() is prohibited outside of
+ * this file's implementation.
+ *
+ * Current implementation: local CPU sfence.vma only (single-core).
+ * When SMP IPI infrastructure is available, tlb_shootdown_range()
+ * and tlb_shootdown_all() will broadcast invalidation to remote
+ * CPUs via IPI — no caller changes required.
+ *
+ * Reference: docs/specs/linux_abi_mm_plan.md §4
+ */
+
+#include <lume/types.h>
+
+struct VmSpace;
+
+/*
+ * Invalidate TLB entries for a VA range [start, end) on ALL CPUs
+ * that may have cached translations for the given address space.
+ *
+ * @mm: address space (used to determine which CPUs to IPI;
+ * nullptr means kernel address space = all CPUs).
+ * @start: first VA to invalidate (page-aligned).
+ * @end: one-past-last VA to invalidate (page-aligned).
+ *
+ * If (end - start) exceeds a threshold, falls back to full flush.
+ */
+void tlb_shootdown_range(VmSpace* mm, uint64 start, uint64 end);
+
+/*
+ * Invalidate a single page's TLB entry on ALL CPUs.
+ * Convenience wrapper around tlb_shootdown_range().
+ */
+void tlb_shootdown_page(VmSpace* mm, uint64 va);
+
+/*
+ * Full TLB flush on ALL CPUs.
+ * Used after page table switch, large-scale munmap, etc.
+ */
+void tlb_shootdown_all();
diff --git a/na/include/lume/tty.h b/na/include/lume/tty.h
new file mode 100644
index 0000000..34cf7bb
--- /dev/null
+++ b/na/include/lume/tty.h
@@ -0,0 +1,165 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * tty.h — TTY subsystem core structures
+ *
+ * tty_struct is the central object representing an open terminal.
+ * It connects a hardware driver (tty_driver) with a line discipline
+ * (tty_ldisc) to implement the full terminal abstraction.
+ *
+ * Architecture:
+ * user read/write → tty_core → tty_ldisc (n_tty) → tty_driver (uart/pty)
+ *
+ * Reference: Linux include/linux/tty.h, include/linux/tty_ldisc.h
+ */
+
+#include <lume/types.h>
+#include <lume/tty_ioctl.h>
+#include <lume/waitqueue.h>
+#include <kernel/sync/spinlock.h>
+
+struct tty_struct;
+struct file;
+struct file_operations;
+struct inode;
+
+/* ================================================================== */
+/* tty_driver — hardware/pseudo-terminal driver interface */
+/* ================================================================== */
+
+struct tty_driver_ops {
+ int (*open)(tty_struct* tty);
+ void (*close)(tty_struct* tty);
+ int (*write)(tty_struct* tty, const char* buf, int count);
+ int (*write_room)(tty_struct* tty);
+ void (*put_char)(tty_struct* tty, char ch);
+ void (*flush_chars)(tty_struct* tty);
+ int (*chars_in_buffer)(tty_struct* tty);
+ void (*flush_buffer)(tty_struct* tty);
+ int (*ioctl)(tty_struct* tty, unsigned int cmd, uint64 arg);
+ void (*throttle)(tty_struct* tty);
+ void (*unthrottle)(tty_struct* tty);
+ void (*hangup)(tty_struct* tty);
+};
+
+struct tty_driver {
+ const char* name; /* "ttyS", "ttySIF", "pts" */
+ int major;
+ int minor_start;
+ int num; /* number of devices */
+ const tty_driver_ops* ops;
+};
+
+/* ================================================================== */
+/* tty_ldisc — line discipline interface */
+/* ================================================================== */
+
+struct tty_ldisc_ops {
+ const char* name;
+ int num; /* N_TTY = 0 */
+
+ int (*open)(tty_struct* tty);
+ void (*close)(tty_struct* tty);
+ int (*read)(tty_struct* tty, file* fp, char* buf, uint64 count);
+ int (*write)(tty_struct* tty, file* fp, const char* buf, uint64 count);
+ void (*receive_buf)(tty_struct* tty, const char* buf, int count);
+ int (*ioctl)(tty_struct* tty, unsigned int cmd, uint64 arg);
+ void (*flush_buffer)(tty_struct* tty);
+ int (*chars_in_buffer)(tty_struct* tty);
+ void (*set_termios)(tty_struct* tty, const termios* old);
+ void (*hangup)(tty_struct* tty);
+};
+
+struct tty_ldisc {
+ const tty_ldisc_ops* ops;
+ void* data; /* ldisc private data (n_tty_data*) */
+};
+
+/* ================================================================== */
+/* tty_struct — central TTY object */
+/* ================================================================== */
+
+inline constexpr int TTY_BUF_SIZE = 4096;
+
+/* TTY flags */
+inline constexpr uint32 TTY_THROTTLED = (1 << 0);
+inline constexpr uint32 TTY_IO_ERROR = (1 << 1);
+inline constexpr uint32 TTY_CLOSING = (1 << 2);
+inline constexpr uint32 TTY_HUPPING = (1 << 3);
+
+struct tty_struct {
+ int index; /* device index */
+ const tty_driver* driver; /* hardware driver */
+ tty_ldisc ldisc; /* current line discipline */
+ tty_struct* link; /* PTY peer (master ↔ slave) */
+
+ termios termios_data; /* terminal settings */
+ winsize ws; /* window size */
+
+ int32 pgrp; /* foreground process group */
+ int32 session; /* controlling session */
+
+ /* Output buffer (tty_core → driver) */
+ char write_buf[TTY_BUF_SIZE];
+ int write_cnt;
+
+ /* Synchronization */
+ WaitQueue read_wait;
+ WaitQueue write_wait;
+ Spinlock lock;
+
+ uint32 flags;
+ int open_count;
+
+ /* Private driver data */
+ void* driver_data;
+};
+
+/* ================================================================== */
+/* TTY core API */
+/* ================================================================== */
+
+/* Initialize the TTY subsystem (creates /dev/console TTY) */
+void tty_init();
+
+/* Allocate and initialize a new tty_struct */
+tty_struct* tty_alloc(const tty_driver* driver, int index);
+
+/* Open/close a TTY (called from chardev open/close) */
+int tty_open(tty_struct* tty);
+void tty_close(tty_struct* tty);
+
+/* Core I/O (called from file_operations) */
+int64 tty_read(tty_struct* tty, file* fp, char* buf, uint64 count);
+int64 tty_write(tty_struct* tty, file* fp, const char* buf, uint64 count);
+long tty_ioctl(tty_struct* tty, file* fp, unsigned int cmd, uint64 arg);
+
+/* Called by hardware driver when bytes arrive */
+void tty_receive_buf(tty_struct* tty, const char* buf, int count);
+
+/* Signal helpers */
+void tty_signal_pgrp(tty_struct* tty, int sig);
+
+/* file_operations for TTY devices */
+extern file_operations tty_fops;
+
+/* Global console TTY */
+extern tty_struct* g_console_tty;
+
+/* ================================================================== */
+/* N_TTY line discipline */
+/* ================================================================== */
+
+extern const tty_ldisc_ops n_tty_ops;
+
+/* ================================================================== */
+/* UART TTY driver */
+/* ================================================================== */
+
+extern const tty_driver uart_tty_driver;
+
+/* Initialize UART as a TTY driver (registers RX IRQ) */
+void uart_tty_init(tty_struct* tty);
diff --git a/na/include/lume/tty_ioctl.h b/na/include/lume/tty_ioctl.h
new file mode 100644
index 0000000..87ba59b
--- /dev/null
+++ b/na/include/lume/tty_ioctl.h
@@ -0,0 +1,202 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * tty_ioctl.h — Terminal I/O control constants and structures
+ *
+ * Linux-compatible termios, winsize, and TIOC* ioctl definitions.
+ * Values match the RISC-V/generic Linux ABI.
+ *
+ * Reference: include/uapi/asm-generic/termbits.h,
+ * include/uapi/asm-generic/ioctls.h
+ */
+
+#include <lume/types.h>
+
+/* ================================================================== */
+/* termios c_cc indices */
+/* ================================================================== */
+
+inline constexpr int VINTR = 0;
+inline constexpr int VQUIT = 1;
+inline constexpr int VERASE = 2;
+inline constexpr int VKILL = 3;
+inline constexpr int VEOF = 4;
+inline constexpr int VTIME = 5;
+inline constexpr int VMIN = 6;
+inline constexpr int VSWTC = 7;
+inline constexpr int VSTART = 8;
+inline constexpr int VSTOP = 9;
+inline constexpr int VSUSP = 10;
+inline constexpr int VEOL = 11;
+inline constexpr int VREPRINT = 12;
+inline constexpr int VDISCARD = 13;
+inline constexpr int VWERASE = 14;
+inline constexpr int VLNEXT = 15;
+inline constexpr int VEOL2 = 16;
+inline constexpr int NCCS = 19;
+
+/* ================================================================== */
+/* c_iflag — input mode flags */
+/* ================================================================== */
+
+inline constexpr uint32 IGNBRK = 0000001;
+inline constexpr uint32 BRKINT = 0000002;
+inline constexpr uint32 IGNPAR = 0000004;
+inline constexpr uint32 PARMRK = 0000010;
+inline constexpr uint32 INPCK = 0000020;
+inline constexpr uint32 ISTRIP = 0000040;
+inline constexpr uint32 INLCR = 0000100;
+inline constexpr uint32 IGNCR = 0000200;
+inline constexpr uint32 ICRNL = 0000400;
+inline constexpr uint32 IUCLC = 0001000;
+inline constexpr uint32 IXON = 0002000;
+inline constexpr uint32 IXANY = 0004000;
+inline constexpr uint32 IXOFF = 0010000;
+inline constexpr uint32 IMAXBEL = 0020000;
+inline constexpr uint32 IUTF8 = 0040000;
+
+/* ================================================================== */
+/* c_oflag — output mode flags */
+/* ================================================================== */
+
+inline constexpr uint32 OPOST = 0000001;
+inline constexpr uint32 OLCUC = 0000002;
+inline constexpr uint32 ONLCR = 0000004;
+inline constexpr uint32 OCRNL = 0000010;
+inline constexpr uint32 ONOCR = 0000020;
+inline constexpr uint32 ONLRET = 0000040;
+inline constexpr uint32 OFILL = 0000100;
+inline constexpr uint32 OFDEL = 0000200;
+
+/* ================================================================== */
+/* c_cflag — control mode flags */
+/* ================================================================== */
+
+inline constexpr uint32 CSIZE = 0000060;
+inline constexpr uint32 CS5 = 0000000;
+inline constexpr uint32 CS6 = 0000020;
+inline constexpr uint32 CS7 = 0000040;
+inline constexpr uint32 CS8 = 0000060;
+inline constexpr uint32 CSTOPB = 0000100;
+inline constexpr uint32 CREAD = 0000200;
+inline constexpr uint32 PARENB = 0000400;
+inline constexpr uint32 PARODD = 0001000;
+inline constexpr uint32 HUPCL = 0002000;
+inline constexpr uint32 CLOCAL = 0004000;
+
+/* Baud rates (stored in c_cflag masked with CBAUD) */
+inline constexpr uint32 CBAUD = 0010017;
+inline constexpr uint32 B0 = 0000000;
+inline constexpr uint32 B9600 = 0000015;
+inline constexpr uint32 B19200 = 0000016;
+inline constexpr uint32 B38400 = 0000017;
+inline constexpr uint32 B115200 = 0010002;
+
+/* ================================================================== */
+/* c_lflag — local mode flags */
+/* ================================================================== */
+
+inline constexpr uint32 ISIG = 0000001;
+inline constexpr uint32 ICANON = 0000002;
+inline constexpr uint32 XCASE = 0000004;
+inline constexpr uint32 LECHO = 0000010; /* Named LECHO to avoid macro conflict */
+inline constexpr uint32 ECHOE = 0000020;
+inline constexpr uint32 ECHOK = 0000040;
+inline constexpr uint32 ECHONL = 0000100;
+inline constexpr uint32 NOFLSH = 0000200;
+inline constexpr uint32 TOSTOP = 0000400;
+inline constexpr uint32 ECHOCTL = 0001000;
+inline constexpr uint32 ECHOPRT = 0002000;
+inline constexpr uint32 ECHOKE = 0004000;
+inline constexpr uint32 IEXTEN = 0100000;
+
+/* Linux uses ECHO = 0x0008, we use LECHO to avoid reserved-name issues.
+ * Provide the standard name as well for ioctl compatibility. */
+inline constexpr uint32 ECHO = LECHO;
+
+/* ================================================================== */
+/* termios structure */
+/* ================================================================== */
+
+struct termios {
+ uint32 c_iflag; /* input mode flags */
+ uint32 c_oflag; /* output mode flags */
+ uint32 c_cflag; /* control mode flags */
+ uint32 c_lflag; /* local mode flags */
+ uint8 c_line; /* line discipline */
+ uint8 c_cc[NCCS]; /* control characters */
+};
+
+/* ================================================================== */
+/* winsize structure */
+/* ================================================================== */
+
+struct winsize {
+ uint16 ws_row;
+ uint16 ws_col;
+ uint16 ws_xpixel;
+ uint16 ws_ypixel;
+};
+
+/* ================================================================== */
+/* ioctl command numbers (Linux asm-generic/ioctls.h) */
+/* ================================================================== */
+
+inline constexpr uint32 TCGETS = 0x5401;
+inline constexpr uint32 TCSETS = 0x5402;
+inline constexpr uint32 TCSETSW = 0x5403;
+inline constexpr uint32 TCSETSF = 0x5404;
+inline constexpr uint32 TCGETA = 0x5405;
+inline constexpr uint32 TCSETA = 0x5406;
+inline constexpr uint32 TCSETAW = 0x5407;
+inline constexpr uint32 TCSETAF = 0x5408;
+inline constexpr uint32 TCSBRK = 0x5409;
+inline constexpr uint32 TCXONC = 0x540A;
+inline constexpr uint32 TCFLSH = 0x540B;
+inline constexpr uint32 TIOCEXCL = 0x540C;
+inline constexpr uint32 TIOCNXCL = 0x540D;
+inline constexpr uint32 TIOCSCTTY = 0x540E;
+inline constexpr uint32 TIOCGPGRP = 0x540F;
+inline constexpr uint32 TIOCSPGRP = 0x5410;
+inline constexpr uint32 TIOCOUTQ = 0x5411;
+inline constexpr uint32 TIOCSTI = 0x5412;
+inline constexpr uint32 TIOCGWINSZ = 0x5413;
+inline constexpr uint32 TIOCSWINSZ = 0x5414;
+inline constexpr uint32 TIOCMGET = 0x5415;
+inline constexpr uint32 TIOCMSET = 0x5418;
+inline constexpr uint32 FIONREAD = 0x541B;
+inline constexpr uint32 TIOCNOTTY = 0x5422;
+inline constexpr uint32 TIOCGPTLCK = 0x80045430;
+inline constexpr uint32 TIOCSPTLCK = 0x40045431;
+
+/* Default termios for a new TTY */
+inline void tty_default_termios(termios* t)
+{
+ t->c_iflag = ICRNL | IXON | IUTF8;
+ t->c_oflag = OPOST | ONLCR;
+ t->c_cflag = B115200 | CS8 | CREAD | HUPCL | CLOCAL;
+ t->c_lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN;
+ t->c_line = 0; /* N_TTY */
+
+ for (int i = 0; i < NCCS; i++)
+ t->c_cc[i] = 0;
+
+ t->c_cc[VINTR] = 003; /* Ctrl+C */
+ t->c_cc[VQUIT] = 034; /* Ctrl+\ */
+ t->c_cc[VERASE] = 0177; /* DEL */
+ t->c_cc[VKILL] = 025; /* Ctrl+U */
+ t->c_cc[VEOF] = 004; /* Ctrl+D */
+ t->c_cc[VSTART] = 021; /* Ctrl+Q */
+ t->c_cc[VSTOP] = 023; /* Ctrl+S */
+ t->c_cc[VSUSP] = 032; /* Ctrl+Z */
+ t->c_cc[VREPRINT] = 022; /* Ctrl+R */
+ t->c_cc[VWERASE] = 027; /* Ctrl+W */
+ t->c_cc[VLNEXT] = 026; /* Ctrl+V */
+ t->c_cc[VEOL] = 0;
+ t->c_cc[VEOL2] = 0;
+ t->c_cc[VMIN] = 1;
+ t->c_cc[VTIME] = 0;
+}
diff --git a/na/include/lume/types.h b/na/include/lume/types.h
index 1d5b896..2342359 100644
--- a/na/include/lume/types.h
+++ b/na/include/lume/types.h
@@ -1,35 +1,78 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Copyright (C) 2026 Richard Qin */
-#pragma once
+#ifndef _LUME_TYPES_H
+#define _LUME_TYPES_H
/*
* LumeOS Base Type Definitions
*
- * All kernel code should use these types instead of
- * stdint.h (which may not exist in freestanding mode).
+ * Three naming layers coexist so that source files imported from
+ * upstream projects (Linux kernel, libfdt) compile unchanged:
*
- * Kernel-wide configuration constants are in <lume/config.h>.
+ * LumeOS native uint8 / int8 / ... / uint64 / int64
+ * Linux kernel short names u8 / s8 / ... / u64 / s64
+ * POSIX / C99 (with _t) uint8_t / ... / uint64_t / intptr_t
+ *
+ * All three resolve to the same underlying compiler builtin, so they
+ * are binary-compatible.
+ *
+ * Declared with C `typedef` (not C++ `using`) so this header may be
+ * included from .c sources — required for libfdt shims and the
+ * arch/riscv/kernel/pi/ subsystem, which are pure C imports from
+ * Linux. C++ accepts `typedef` syntax identically.
*/
-using uint8 = unsigned char;
-using uint16 = unsigned short;
-using uint32 = unsigned int;
-using uint64 = unsigned long long;
+/* ===== LumeOS native names ===== */
+typedef unsigned char uint8;
+typedef unsigned short uint16;
+typedef unsigned int uint32;
+typedef unsigned long long uint64;
-using int8 = signed char;
-using int16 = signed short;
-using int32 = signed int;
-using int64 = signed long long;
+typedef signed char int8;
+typedef signed short int16;
+typedef signed int int32;
+typedef signed long long int64;
-typedef __SIZE_TYPE__ size_t;
-typedef __PTRDIFF_TYPE__ ptrdiff_t;
-using ssize_t = int64;
-using uintptr = uint64;
+typedef __SIZE_TYPE__ size_t;
+typedef __PTRDIFF_TYPE__ ptrdiff_t;
+typedef int64 ssize_t;
+typedef uint64 uintptr;
-#define offsetof(TYPE, MEMBER) __builtin_offsetof(TYPE, MEMBER)
+/* ===== Linux kernel short names ===== */
+typedef uint8 u8; typedef int8 s8;
+typedef uint16 u16; typedef int16 s16;
+typedef uint32 u32; typedef int32 s32;
+typedef uint64 u64; typedef int64 s64;
-/* Null pointer constant */
-#ifndef nullptr
-/* C++11 and later have nullptr built-in */
+/* ===== POSIX / C99 aliases =====
+ * Guarded so a later #include <stdint.h> from libc-shim during musl
+ * userspace builds is a no-op rather than a redefinition error. */
+#ifndef _STDINT_H
+# define _STDINT_H
+typedef uint8 uint8_t;
+typedef uint16 uint16_t;
+typedef uint32 uint32_t;
+typedef uint64 uint64_t;
+typedef int8 int8_t;
+typedef int16 int16_t;
+typedef int32 int32_t;
+typedef int64 int64_t;
+typedef uintptr uintptr_t;
+typedef int64 intptr_t;
#endif
+
+/* ===== Linux domain typedefs needed by RISC-V boot path ===== */
+typedef uint64 phys_addr_t;
+
+/* ===== libfdt on-wire types (big-endian). Real byte-swap helpers
+ * live in <lume/libfdt_env.h>. ===== */
+typedef uint16 fdt16_t;
+typedef uint32 fdt32_t;
+typedef uint64 fdt64_t;
+
+#ifndef offsetof
+# define offsetof(TYPE, MEMBER) __builtin_offsetof(TYPE, MEMBER)
+#endif
+
+#endif /* _LUME_TYPES_H */
diff --git a/na/include/lume/uuid.h b/na/include/lume/uuid.h
new file mode 100644
index 0000000..fcbd4a3
--- /dev/null
+++ b/na/include/lume/uuid.h
@@ -0,0 +1,207 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * UUID/GUID definition
+ *
+ * Copyright (C) 2010, 2016 Intel Corp.
+ * Huang Ying <ying.huang@intel.com>
+ */
+
+#pragma once
+
+#include <lume/types.h>
+#include <libc-shim/string.h>
+
+constexpr uint32 UUID_SIZE = 16;
+constexpr uint32 UUID_STRING_LEN = 36;
+
+static inline int uuid_hex_value(char c)
+{
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+ if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ }
+ if (c >= 'A' && c <= 'F') {
+ return c - 'A' + 10;
+ }
+ return -1;
+}
+
+static inline bool uuid_parse_raw(const char* uuid, uint8 out[UUID_SIZE])
+{
+ if (!uuid) {
+ return false;
+ }
+ if (strlen(uuid) != UUID_STRING_LEN) {
+ return false;
+ }
+ if (uuid[8] != '-' || uuid[13] != '-' || uuid[18] != '-' || uuid[23] != '-') {
+ return false;
+ }
+
+ int out_index = 0;
+ for (int i = 0; i < static_cast<int>(UUID_STRING_LEN); ) {
+ if (uuid[i] == '-') {
+ ++i;
+ continue;
+ }
+
+ int hi = uuid_hex_value(uuid[i]);
+ int lo = uuid_hex_value(uuid[i + 1]);
+ if (hi < 0 || lo < 0) {
+ return false;
+ }
+
+ out[out_index++] = static_cast<uint8>((hi << 4) | lo);
+ i += 2;
+ }
+
+ return out_index == static_cast<int>(UUID_SIZE);
+}
+
+struct uuid_base {
+ uint8 b[UUID_SIZE];
+
+ bool is_null() const
+ {
+ for (uint32 i = 0; i < UUID_SIZE; ++i) {
+ if (b[i] != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ bool operator==(const uuid_base& other) const
+ {
+ return memcmp(b, other.b, UUID_SIZE) == 0;
+ }
+
+ bool operator!=(const uuid_base& other) const
+ {
+ return !(*this == other);
+ }
+};
+
+struct uuid_t : public uuid_base {
+ explicit uuid_t(const uint8 in_b[UUID_SIZE])
+ {
+ memcpy(b, in_b, UUID_SIZE);
+ }
+
+ explicit constexpr uuid_t(uint32 a, uint16 b, uint16 c, uint8 d0, uint8 d1,
+ uint8 d2, uint8 d3, uint8 d4, uint8 d5,
+ uint8 d6, uint8 d7)
+ : uuid_base{{
+ static_cast<uint8>((a >> 24) & 0xff),
+ static_cast<uint8>((a >> 16) & 0xff),
+ static_cast<uint8>((a >> 8) & 0xff),
+ static_cast<uint8>(a & 0xff),
+ static_cast<uint8>((b >> 8) & 0xff),
+ static_cast<uint8>(b & 0xff),
+ static_cast<uint8>((c >> 8) & 0xff),
+ static_cast<uint8>(c & 0xff),
+ d0, d1, d2, d3, d4, d5, d6, d7
+ }}
+ {
+ }
+
+ constexpr uuid_t() : uuid_base{{0}} {}
+
+ static bool parse(const char* uuid, uuid_t& out)
+ {
+ uint8 tmp[UUID_SIZE];
+ if (!uuid_parse_raw(uuid, tmp)) {
+ return false;
+ }
+
+ memcpy(out.b, tmp, UUID_SIZE);
+ return true;
+ }
+
+ static bool is_valid(const char* uuid)
+ {
+ uint8 tmp[UUID_SIZE];
+ return uuid_parse_raw(uuid, tmp);
+ }
+
+ static constexpr uuid_t null()
+ {
+ return uuid_t();
+ }
+
+ void import_bytes(const uint8* src)
+ {
+ memcpy(b, src, UUID_SIZE);
+ }
+
+ void export_bytes(uint8* dst) const
+ {
+ memcpy(dst, b, UUID_SIZE);
+ }
+};
+
+struct guid_t : public uuid_base {
+ explicit guid_t(const uint8 in_b[UUID_SIZE])
+ {
+ memcpy(b, in_b, UUID_SIZE);
+ }
+
+ explicit constexpr guid_t(uint32 a, uint16 b, uint16 c, uint8 d0, uint8 d1,
+ uint8 d2, uint8 d3, uint8 d4, uint8 d5,
+ uint8 d6, uint8 d7)
+ : uuid_base{{
+ static_cast<uint8>(a & 0xff),
+ static_cast<uint8>((a >> 8) & 0xff),
+ static_cast<uint8>((a >> 16) & 0xff),
+ static_cast<uint8>((a >> 24) & 0xff),
+ static_cast<uint8>(b & 0xff),
+ static_cast<uint8>((b >> 8) & 0xff),
+ static_cast<uint8>(c & 0xff),
+ static_cast<uint8>((c >> 8) & 0xff),
+ d0, d1, d2, d3, d4, d5, d6, d7
+ }}
+ {
+ }
+
+ constexpr guid_t() : uuid_base{{0}} {}
+ static bool parse(const char* uuid, guid_t& out)
+ {
+ uint8 tmp[UUID_SIZE];
+ if (!uuid_parse_raw(uuid, tmp)) {
+ return false;
+ }
+
+ static constexpr uint8 guid_index[UUID_SIZE] = {
+ 3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15,
+ };
+
+ for (int i = 0; i < static_cast<int>(UUID_SIZE); ++i) {
+ out.b[guid_index[i]] = tmp[i];
+ }
+ return true;
+ }
+
+ static bool is_valid(const char* uuid)
+ {
+ uint8 tmp[UUID_SIZE];
+ return uuid_parse_raw(uuid, tmp);
+ }
+
+ static constexpr guid_t null()
+ {
+ return guid_t();
+ }
+
+ void import_bytes(const uint8* src)
+ {
+ memcpy(b, src, UUID_SIZE);
+ }
+
+ void export_bytes(uint8* dst) const
+ {
+ memcpy(dst, b, UUID_SIZE);
+ }
+};
+
diff --git a/na/include/lume/vmm.h b/na/include/lume/vmm.h
index 85988e4..9cbebf1 100644
--- a/na/include/lume/vmm.h
+++ b/na/include/lume/vmm.h
@@ -13,6 +13,7 @@
#include <lume/types.h>
#include <lume/rbtree.h>
+#include <lume/atomic.h>
#include <kernel/sync/spinlock.h>
/* ------------------------------------------------------------------ */
@@ -61,14 +62,25 @@ struct VmSpace {
uint64 root_pa; // SV39 root page table physical address
struct rb_root vma_tree; // Red-black tree of VmAreaStructs
Spinlock lock; // Protects vma_tree mutations
+ lume::atomic<int32> refcount; // Reference count (for CLONE_VM sharing)
+ lume::atomic<uint64> cpu_mask; // Bitmask of CPUs with this mm in satp
uint64 brk_start; // Start of heap (page-aligned)
uint64 brk_end; // Current heap end (page-aligned)
+ uint64 mmap_base; // Base for mmap (Linux: TASK_UNMAPPED_BASE = TASK_SIZE/3)
- // Create a brand new VmSpace (allocates root pagetable)
+ // Create a brand new VmSpace (allocates root pagetable, refcount=1)
static VmSpace* create();
- // Destroy VmSpace, unmapping all VMAs and freeing pagetables
+ // Increment reference count (for CLONE_VM)
+ void incref();
+
+ // Decrement reference count; destroys VmSpace when it reaches 0.
+ // Call this instead of destroy() directly.
+ void put();
+
+ // Destroy VmSpace, unmapping all VMAs and freeing pagetables.
+ // Internal — called by put() when refcount drops to 0.
void destroy();
};
diff --git a/na/include/lume/wait.h b/na/include/lume/wait.h
index 02012ab..865c1f2 100644
--- a/na/include/lume/wait.h
+++ b/na/include/lume/wait.h
@@ -11,12 +11,21 @@
/* waitpid options */
inline constexpr int WNOHANG = 1;
inline constexpr int WUNTRACED = 2;
+inline constexpr int WCONTINUED = 8;
+inline constexpr int WSTOPPED = WUNTRACED; /* POSIX alias */
+inline constexpr int WEXITED = 4; /* POSIX */
+inline constexpr int WNOWAIT = 0x01000000; /* POSIX: peek, don't reap */
/* Exit status encoding (Linux-compatible) */
-static inline int WEXITSTATUS(int s) { return (s >> 8) & 0xFF; }
-static inline bool WIFEXITED(int s) { return (s & 0x7F) == 0; }
-static inline bool WIFSIGNALED(int s){ return (s & 0x7F) != 0 && (s & 0x7F) != 0x7F; }
-static inline int WTERMSIG(int s) { return s & 0x7F; }
+static inline int WEXITSTATUS(int s) { return (s >> 8) & 0xFF; }
+static inline bool WIFEXITED(int s) { return (s & 0x7F) == 0; }
+static inline bool WIFSIGNALED(int s) { return (s & 0x7F) != 0 && (s & 0x7F) != 0x7F; }
+static inline int WTERMSIG(int s) { return s & 0x7F; }
+static inline bool WIFSTOPPED(int s) { return (s & 0xFF) == 0x7F; }
+static inline int WSTOPSIG(int s) { return (s >> 8) & 0xFF; }
+static inline bool WIFCONTINUED(int s){ return s == 0xFFFF; }
/* Encode exit code into wstatus (normal exit) */
-static inline int W_EXITCODE(int code) { return (code & 0xFF) << 8; }
+static inline int W_EXITCODE(int code) { return (code & 0xFF) << 8; }
+static inline int W_STOPCODE(int sig) { return ((sig & 0xFF) << 8) | 0x7F; }
+static inline int W_CONTINUED() { return 0xFFFF; }
diff --git a/na/include/lume/waitqueue.h b/na/include/lume/waitqueue.h
index dd5edfe..3720cb7 100644
--- a/na/include/lume/waitqueue.h
+++ b/na/include/lume/waitqueue.h
@@ -6,11 +6,24 @@
/*
* WaitQueue — blocking synchronization primitive
*
- * A thread calls wq.sleep() to block itself until another thread
- * (or interrupt handler) calls wq.wake_one() or wq.wake_all().
+ * Supports two usage patterns:
*
- * Implementation: intrusive doubly-linked list of sleeping tasks,
- * protected by an internal spinlock.
+ * 1. Simple (legacy): wq.sleep() — combines prepare/schedule/finish.
+ * Suitable for simple cases where no external condition is checked.
+ *
+ * 2. Linux-style two-phase (recommended for correctness):
+ *
+ * wq.prepare_to_wait(curr, TaskState::Sleeping);
+ * if (!condition) {
+ * schedule();
+ * }
+ * wq.finish_wait(curr);
+ *
+ * This pattern prevents the "lost wakeup" race: the task is already
+ * on the wait list (and marked Sleeping) BEFORE checking the condition.
+ * If a wake_one/wake_all fires between the check and schedule(), the
+ * task's state will already be set back to Runnable, and schedule()
+ * will see state == Runnable and simply re-run the task.
*
* Reference: docs/specs/scheduler.md, docs/specs/task.md §5
*/
@@ -20,6 +33,7 @@
#include <kernel/sync/spinlock.h>
struct TaskControlBlock;
+enum class TaskState : uint8;
class WaitQueue {
public:
@@ -28,11 +42,26 @@ public:
lock_.init(name);
}
- /* Block the current task on this queue.
+ /*
+ * Phase 1: Add the task to the wait queue and set its state.
+ * The task is NOT yet descheduled — the caller must check the
+ * condition and call schedule() if the condition is still false.
+ *
+ * Must be called with interrupts disabled or preemption disabled.
+ */
+ void prepare_to_wait(TaskControlBlock* task, TaskState new_state);
+
+ /*
+ * Phase 3: Remove the task from the wait queue (if still on it)
+ * and ensure its state is Runnable. Called after schedule() returns
+ * (i.e. after the task has been woken up, or if the condition was
+ * true and schedule() was skipped).
+ */
+ void finish_wait(TaskControlBlock* task);
+
+ /* Legacy convenience: combines prepare_to_wait + schedule + finish_wait.
* If external_lock is provided, it is released AFTER the task is
- * added to the wait queue but BEFORE schedule() is called,
- * to avoid lost wakeups.
- * Returns after wake_one/wake_all unblocks this task. */
+ * added to the wait queue but BEFORE schedule() is called. */
void sleep(Spinlock* external_lock = nullptr);
/* Wake the first (oldest) sleeping task. Returns true if a task was woken. */
diff --git a/na/kernel/boot_params.cc b/na/kernel/boot_params.cc
deleted file mode 100644
index e5d4ee5..0000000
--- a/na/kernel/boot_params.cc
+++ /dev/null
@@ -1,45 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#include <lume/boot_params.h>
-
-static BootParams g_boot_params = {};
-
-static void boot_params_init_once()
-{
- if (g_boot_params.magic == kBootParamsMagic) {
- return;
- }
- g_boot_params = {};
- g_boot_params.magic = kBootParamsMagic;
- g_boot_params.version = kBootParamsVersion;
- g_boot_params.size = static_cast<uint32>(sizeof(BootParams));
-}
-
-void boot_params_init_fdt(uint64 fdt_paddr)
-{
- boot_params_init_once();
- g_boot_params.fdt_ptr = fdt_paddr;
- g_boot_params.seed_source = BOOT_SEED_NONE;
-}
-
-const BootParams* boot_params_get()
-{
- if (g_boot_params.magic != kBootParamsMagic) {
- return nullptr;
- }
- return &g_boot_params;
-}
-
-void boot_params_set_image_info(uint64 phys_base, uint64 image_size,
- uint64 reloc_offset, uint64 reloc_size,
- uint32 comp, uint32 image_flags)
-{
- boot_params_init_once();
- g_boot_params.kimage_phys_base = phys_base;
- g_boot_params.kimage_size = image_size;
- g_boot_params.kimage_reloc_offset = reloc_offset;
- g_boot_params.kimage_reloc_size = reloc_size;
- g_boot_params.kimage_comp = comp;
- g_boot_params.kimage_flags = image_flags;
-}
diff --git a/na/kernel/exec.cc b/na/kernel/exec.cc
new file mode 100644
index 0000000..616c0da
--- /dev/null
+++ b/na/kernel/exec.cc
@@ -0,0 +1,659 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * exec.cc — ELF64 binary loader and execve implementation
+ *
+ * Loads a static ELF64 executable into a fresh address space,
+ * constructs the user stack (argc/argv/envp/auxv), and redirects
+ * the current task's trapframe to begin executing at the ELF entry.
+ *
+ * Reference: Linux fs/binfmt_elf.c, fs/exec.c
+ */
+
+#include <lume/elf.h>
+#include <lume/types.h>
+#include <lume/task.h>
+#include <lume/vmm.h>
+#include <lume/addr.h>
+#include <lume/config.h>
+#include <lume/errno.h>
+#include <lume/pmm.h>
+#include <lume/kprintf.h>
+#include <lume/klog.h>
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/namei.h>
+#include <lume/fdtable.h>
+#include <lume/mm_layout.h>
+#include <lume/signal_types.h>
+#include <arch/pmap.h>
+#include <arch/trapframe.h>
+#include <arch/mmu.h>
+#include <arch/cpu.h>
+#include <arch/trap.h>
+
+/* Assembly entry points (trap_vector.S) */
+extern "C" void trap_vector();
+extern "C" [[noreturn]] void trap_dispatch(arch::TrapFrame*);
+extern "C" void userret(arch::TrapFrame* tf);
+
+/* ================================================================== */
+/* Constants */
+/* ================================================================== */
+
+/* User stack layout comes from mm_layout.h (MI layer).
+ * kUserStackTop, kUserStackBase, kUserStackSize are defined there,
+ * derived from kUserVaLimit so they automatically adapt to the
+ * page table mode (SV39/SV48). */
+
+/* ================================================================== */
+/* Helper: kernel_read — read from a file into a kernel buffer */
+/* ================================================================== */
+
+static int64 kernel_read(file* fp, void* buf, uint64 count, int64 offset)
+{
+ int64 pos = offset;
+ if (!fp->f_op || !fp->f_op->read) return -EINVAL;
+ return fp->f_op->read(fp, static_cast<char*>(buf), count, &pos);
+}
+
+/* ================================================================== */
+/* Helper: copy data to a user page via kernel direct map */
+/* ================================================================== */
+
+static void memcpy_kernel(void* dst, const void* src, uint64 n)
+{
+ auto* d = static_cast<uint8*>(dst);
+ auto* s = static_cast<const uint8*>(src);
+ for (uint64 i = 0; i < n; i++)
+ d[i] = s[i];
+}
+
+static void memset_kernel(void* dst, uint8 val, uint64 n)
+{
+ auto* d = static_cast<uint8*>(dst);
+ for (uint64 i = 0; i < n; i++)
+ d[i] = val;
+}
+
+static uint64 strlen_kernel(const char* s)
+{
+ uint64 n = 0;
+ while (s[n]) n++;
+ return n;
+}
+
+/* ================================================================== */
+/* Helper: allocate and map a page at a specific user VA */
+/* ================================================================== */
+
+static int map_page_at(VmSpace* mm, uint64 va, uint64 pte_perm)
+{
+ Page* pg = pmm_alloc_page();
+ if (!pg) return -ENOMEM;
+
+ uint64 pa = page_to_pa(pg).raw;
+
+ /* Zero the page */
+ void* kva = reinterpret_cast<void*>(pa_to_va(PhysAddr{pa}).raw);
+ memset_kernel(kva, 0, kPageSize);
+
+ int ret = pmap::map(mm->root_pa, va, pa, pte_perm);
+ if (ret != 0) {
+ pmm_free_page(pg);
+ return -ENOMEM;
+ }
+ page_inc_mapcount(pg);
+ return 0;
+}
+
+/* ================================================================== */
+/* load_elf_binary — parse ELF and map segments */
+/* ================================================================== */
+
+struct elf_load_info {
+ uint64 entry; /* ELF entry point (after base offset) */
+ uint64 phdr_addr; /* User VA where phdr was loaded */
+ uint16 phnum; /* Number of program headers */
+ uint16 phentsize; /* Size of each phdr */
+ uint64 brk_start; /* End of highest PT_LOAD segment (for brk) */
+ uint64 load_bias; /* PIE load bias (0 for ET_EXEC) */
+};
+
+static int load_elf_binary(file* fp, VmSpace* mm, elf_load_info* info)
+{
+ /* Read ELF header */
+ Elf64_Ehdr ehdr{};
+ int64 n = kernel_read(fp, &ehdr, sizeof(ehdr), 0);
+ if (n < static_cast<int64>(sizeof(ehdr))) {
+ printk(KERN_ERR "exec: failed to read ELF header (got %lld)\n", n);
+ return -ENOEXEC;
+ }
+
+ /* Validate magic */
+ if (ehdr.e_ident[EI_MAG0] != ELFMAG0 ||
+ ehdr.e_ident[EI_MAG1] != ELFMAG1 ||
+ ehdr.e_ident[EI_MAG2] != ELFMAG2 ||
+ ehdr.e_ident[EI_MAG3] != ELFMAG3) {
+ printk(KERN_ERR "exec: bad ELF magic\n");
+ return -ENOEXEC;
+ }
+
+ /* Validate class (64-bit) and endianness (little-endian) */
+ if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) return -ENOEXEC;
+ if (ehdr.e_ident[EI_DATA] != ELFDATA2LSB) return -ENOEXEC;
+
+ /* Validate type and machine */
+ if (ehdr.e_type != ET_EXEC && ehdr.e_type != ET_DYN) return -ENOEXEC;
+ if (ehdr.e_machine != EM_RISCV) return -ENOEXEC;
+
+ if (ehdr.e_phnum == 0 || ehdr.e_phentsize < sizeof(Elf64_Phdr))
+ return -ENOEXEC;
+
+ /* Read program headers */
+ uint64 phdr_total = static_cast<uint64>(ehdr.e_phnum) * ehdr.e_phentsize;
+ if (phdr_total > kPageSize) return -ENOEXEC; /* Sanity limit */
+
+ /* Allocate kernel buffer for program headers */
+ uint8 phdr_buf[512]; /* Max ~8 phdrs * 56 bytes each */
+ if (phdr_total > sizeof(phdr_buf)) return -ENOEXEC;
+
+ n = kernel_read(fp, phdr_buf, phdr_total, static_cast<int64>(ehdr.e_phoff));
+ if (n < static_cast<int64>(phdr_total)) return -ENOEXEC;
+
+ info->phnum = ehdr.e_phnum;
+ info->phentsize = ehdr.e_phentsize;
+ info->phdr_addr = 0;
+ info->brk_start = 0;
+ info->load_bias = 0;
+
+ /*
+ * PIE (ET_DYN): segments have p_vaddr starting near 0.
+ * We must choose a load base and add it to all addresses.
+ * Linux uses ELF_ET_DYN_BASE (~2/3 of TASK_SIZE); we use 0x400000.
+ */
+ constexpr uint64 kPieLoadBase = 0x400000;
+ uint64 load_bias = (ehdr.e_type == ET_DYN) ? kPieLoadBase : 0;
+ info->load_bias = load_bias;
+ info->entry = ehdr.e_entry + load_bias;
+
+ /* Process each PT_LOAD segment */
+ for (uint16 i = 0; i < ehdr.e_phnum; i++) {
+ auto* phdr = reinterpret_cast<Elf64_Phdr*>(
+ phdr_buf + i * ehdr.e_phentsize);
+
+ if (phdr->p_type == PT_PHDR) {
+ /* Remember where program headers are mapped */
+ info->phdr_addr = phdr->p_vaddr + load_bias;
+ continue;
+ }
+
+ if (phdr->p_type != PT_LOAD) continue;
+ if (phdr->p_memsz == 0) continue;
+
+ /* Compute permissions */
+ uint64 perm = pmap::PTE_V | pmap::PTE_U;
+ if (phdr->p_flags & PF_R) perm |= pmap::PTE_R;
+ if (phdr->p_flags & PF_W) perm |= pmap::PTE_W;
+ if (phdr->p_flags & PF_X) perm |= pmap::PTE_X;
+
+ /* Page-align the segment boundaries (apply load bias for PIE) */
+ uint64 seg_start = page_align_down(phdr->p_vaddr + load_bias);
+ uint64 seg_end = page_align_up(phdr->p_vaddr + phdr->p_memsz + load_bias);
+
+ /* Create VMA for this segment */
+ uint64 vm_perm = VM_USER;
+ if (phdr->p_flags & PF_R) vm_perm |= VM_READ;
+ if (phdr->p_flags & PF_W) vm_perm |= VM_WRITE;
+ if (phdr->p_flags & PF_X) vm_perm |= VM_EXEC;
+
+ int err = vmm_map_user(mm, seg_start, seg_end - seg_start, vm_perm);
+ if (err) {
+ printk(KERN_ERR "exec: vmm_map_user failed for segment %d: %d\n",
+ i, err);
+ return err;
+ }
+
+ /* Map and fill pages */
+ for (uint64 va = seg_start; va < seg_end; va += kPageSize) {
+ err = map_page_at(mm, va, perm);
+ if (err) return err;
+
+ /* Determine how much file data goes into this page */
+ uint64 file_seg_start = phdr->p_vaddr + load_bias;
+ uint64 file_seg_end = phdr->p_vaddr + phdr->p_filesz + load_bias;
+
+ uint64 page_start = va;
+ uint64 page_end = va + kPageSize;
+
+ /* Overlap between [page_start, page_end) and [file_seg_start, file_seg_end) */
+ if (page_start < file_seg_end && page_end > file_seg_start) {
+ uint64 copy_start = (page_start > file_seg_start) ? page_start : file_seg_start;
+ uint64 copy_end = (page_end < file_seg_end) ? page_end : file_seg_end;
+ uint64 copy_len = copy_end - copy_start;
+
+ /* File offset for this data */
+ uint64 file_off = phdr->p_offset + (copy_start - phdr->p_vaddr - load_bias);
+
+ /* Get kernel VA of the mapped page */
+ uint64 pa;
+ if (!pmap::lookup(mm->root_pa, va, &pa)) return -EFAULT;
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+
+ /* Read file data into the page */
+ uint64 page_offset = copy_start - va;
+ n = kernel_read(fp, kva + page_offset, copy_len,
+ static_cast<int64>(file_off));
+ if (n < static_cast<int64>(copy_len)) {
+ printk(KERN_ERR "exec: short read at offset %llu\n", file_off);
+ return -EIO;
+ }
+ }
+ /* BSS region (memsz > filesz) is already zeroed by map_page_at */
+ }
+
+ /* Track highest loaded address for brk */
+ if (seg_end > info->brk_start)
+ info->brk_start = seg_end;
+
+ /* Detect which PT_LOAD contains the program header table.
+ * Linux: fs/binfmt_elf.c line ~1209 */
+ if (info->phdr_addr == 0 &&
+ phdr->p_offset <= ehdr.e_phoff &&
+ ehdr.e_phoff < phdr->p_offset + phdr->p_filesz) {
+ info->phdr_addr = ehdr.e_phoff - phdr->p_offset
+ + phdr->p_vaddr + load_bias;
+ }
+ }
+
+ /* phdr_addr should have been set by the PT_LOAD loop above
+ * or by a PT_PHDR entry. If still zero, something is wrong. */
+ if (info->phdr_addr == 0) {
+ printk(KERN_ERR "exec: unable to determine AT_PHDR address\n");
+ return -ENOEXEC;
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* setup_user_stack — build argc/argv/envp/auxv on the user stack */
+/* ================================================================== */
+
+static int setup_user_stack(VmSpace* mm, const char* const* argv,
+ const char* const* envp,
+ const elf_load_info* info,
+ uint64* out_sp)
+{
+ /* Create stack VMA */
+ uint64 vm_perm = VM_USER | VM_READ | VM_WRITE;
+ int err = vmm_map_user(mm, kUserStackBase, kUserStackSize, vm_perm);
+ if (err) return err;
+
+ /* We only need the top few pages for initial stack content.
+ * Map 2 pages at the top for argc/argv/envp/auxv. */
+ uint64 perm = pmap::PTE_V | pmap::PTE_U | pmap::PTE_R | pmap::PTE_W;
+ for (uint64 va = kUserStackTop - 2 * kPageSize; va < kUserStackTop; va += kPageSize) {
+ err = map_page_at(mm, va, perm);
+ if (err) return err;
+ }
+
+ /* Count arguments and environment */
+ int argc = 0;
+ int envc = 0;
+ if (argv) { while (argv[argc]) argc++; }
+ if (envp) { while (envp[envc]) envc++; }
+
+ /*
+ * Build the stack from top down using kernel VA access.
+ * Layout (top to bottom):
+ * 1. String data (argv strings, envp strings)
+ * 2. Padding (16-byte align)
+ * 3. auxv[]
+ * 4. NULL (end of envp)
+ * 5. envp[] pointers
+ * 6. NULL (end of argv)
+ * 7. argv[] pointers
+ * 8. argc
+ */
+
+ /* First pass: calculate total string space */
+ uint64 str_total = 0;
+ for (int i = 0; i < argc; i++)
+ str_total += strlen_kernel(argv[i]) + 1;
+ for (int i = 0; i < envc; i++)
+ str_total += strlen_kernel(envp[i]) + 1;
+
+ /* String area starts at top of stack */
+ uint64 str_pos = kUserStackTop - str_total;
+ str_pos &= ~15ULL; /* 16-byte align */
+
+ /* Copy strings and record user addresses */
+ uint64 argv_addrs[32]; /* Max 32 args */
+ uint64 envp_addrs[32];
+ if (argc > 32) argc = 32;
+ if (envc > 32) envc = 32;
+
+ uint64 cur = str_pos;
+ for (int i = 0; i < argc; i++) {
+ uint64 len = strlen_kernel(argv[i]) + 1;
+ argv_addrs[i] = cur;
+
+ /* Copy string bytes to user stack via kernel VA */
+ uint64 pa;
+ if (!pmap::lookup(mm->root_pa, page_align_down(cur), &pa))
+ return -EFAULT;
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 off = cur - page_align_down(cur);
+ memcpy_kernel(kva + off, argv[i], len);
+ cur += len;
+ }
+ for (int i = 0; i < envc; i++) {
+ uint64 len = strlen_kernel(envp[i]) + 1;
+ envp_addrs[i] = cur;
+
+ uint64 pa;
+ if (!pmap::lookup(mm->root_pa, page_align_down(cur), &pa))
+ return -EFAULT;
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 off = cur - page_align_down(cur);
+ memcpy_kernel(kva + off, envp[i], len);
+ cur += len;
+ }
+
+ /* Build auxv, envp[], argv[], argc below the strings */
+ /* Count entries: argc + argv_ptrs + NULL + envp_ptrs + NULL + auxv (6 pairs + terminator) */
+ int auxv_count = 8; /* AT_PHDR, AT_PHENT, AT_PHNUM, AT_PAGESZ, AT_BASE, AT_ENTRY, AT_RANDOM, AT_NULL */
+ uint64 table_size = sizeof(uint64) * (1 + argc + 1 + envc + 1 + auxv_count * 2);
+ uint64 table_start = str_pos - table_size;
+ table_start &= ~15ULL;
+
+ /* Write the table entries.
+ * We need to handle page boundaries, but for simplicity we assume
+ * the table fits in the already-mapped pages. */
+ uint64 sp = table_start;
+
+ /* Helper: write a uint64 to user stack */
+ auto write_u64 = [&](uint64 addr, uint64 val) -> int {
+ uint64 pa;
+ if (!pmap::lookup(mm->root_pa, page_align_down(addr), &pa))
+ return -EFAULT;
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 off = addr - page_align_down(addr);
+ *reinterpret_cast<uint64*>(kva + off) = val;
+ return 0;
+ };
+
+ /* argc */
+ err = write_u64(sp, static_cast<uint64>(argc));
+ if (err) return err;
+ sp += 8;
+
+ /* argv[] */
+ for (int i = 0; i < argc; i++) {
+ err = write_u64(sp, argv_addrs[i]);
+ if (err) return err;
+ sp += 8;
+ }
+ err = write_u64(sp, 0); /* NULL terminator */
+ if (err) return err;
+ sp += 8;
+
+ /* envp[] */
+ for (int i = 0; i < envc; i++) {
+ err = write_u64(sp, envp_addrs[i]);
+ if (err) return err;
+ sp += 8;
+ }
+ err = write_u64(sp, 0); /* NULL terminator */
+ if (err) return err;
+ sp += 8;
+
+ /* auxv[] */
+ auto write_auxv = [&](uint64 type, uint64 val) -> int {
+ int r = write_u64(sp, type);
+ if (r) return r;
+ sp += 8;
+ r = write_u64(sp, val);
+ if (r) return r;
+ sp += 8;
+ return 0;
+ };
+
+ write_auxv(AT_PHDR, info->phdr_addr);
+ write_auxv(AT_PHENT, info->phentsize);
+ write_auxv(AT_PHNUM, info->phnum);
+ write_auxv(AT_PAGESZ, kPageSize);
+ write_auxv(AT_BASE, info->load_bias);
+ write_auxv(AT_ENTRY, info->entry);
+ write_auxv(AT_RANDOM, str_pos - 16); /* Point to 16 bytes of zeros for now */
+ write_auxv(AT_NULL, 0);
+
+ *out_sp = table_start;
+ return 0;
+}
+
+/* ================================================================== */
+/* do_execve — main entry point */
+/* ================================================================== */
+
+int do_execve(const char* pathname, const char* const* argv,
+ const char* const* envp)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return -EINVAL;
+
+ /* 1. Look up the file */
+ path p{};
+ int err = filename_lookup(AT_FDCWD, pathname, 0, &p);
+ if (err) {
+ printk(KERN_ERR "exec: file not found: %s (err=%d)\n", pathname, err);
+ return err;
+ }
+
+ inode* ip = p.de->d_inode;
+ if (!ip || !S_ISREG(ip->i_mode)) {
+ printk(KERN_ERR "exec: not a regular file: %s\n", pathname);
+ return -EACCES;
+ }
+
+ /* Open a kernel file handle */
+ file* fp = alloc_empty_file(O_RDONLY);
+ if (!fp) {
+ return -ENOMEM;
+ }
+ fp->f_inode = ip;
+ fp->f_path = p;
+ fp->f_op = ip->i_fop;
+ fp->f_pos = 0;
+
+ if (fp->f_op && fp->f_op->open) {
+ err = fp->f_op->open(ip, fp);
+ if (err) {
+ fput(fp);
+ return err;
+ }
+ }
+
+ /* 2. Create new address space */
+ VmSpace* new_mm = VmSpace::create();
+ if (!new_mm) {
+ fput(fp);
+ return -ENOMEM;
+ }
+
+ /* 3. Load ELF segments */
+ elf_load_info info{};
+ err = load_elf_binary(fp, new_mm, &info);
+ if (err) {
+ printk(KERN_ERR "exec: load_elf_binary failed: %d\n", err);
+ new_mm->put();
+ fput(fp);
+ dput(p.de);
+ return err;
+ }
+
+ /* 4. Set up user stack */
+ uint64 user_sp = 0;
+ err = setup_user_stack(new_mm, argv, envp, &info, &user_sp);
+ if (err) {
+ printk(KERN_ERR "exec: setup_user_stack failed: %d\n", err);
+ new_mm->put();
+ fput(fp);
+ return err;
+ }
+
+ /* Set brk */
+ new_mm->brk_start = page_align_up(info.brk_start);
+ new_mm->brk_end = new_mm->brk_start;
+
+ /* Set mmap base — Linux: arch_pick_mmap_layout() sets mm->mmap_base
+ * to TASK_UNMAPPED_BASE = PAGE_ALIGN(TASK_SIZE / 3).
+ * This ensures mmap(NULL) returns addresses well above the ELF
+ * text/data segments. */
+ new_mm->mmap_base = page_align_up(kUserVaLimit / 3);
+
+ fput(fp);
+
+ /* =============================================================== */
+ /* Point of no return — replace the current process */
+ /* =============================================================== */
+
+ /* 5a. Replace address space */
+ VmSpace* old_mm = curr->mm;
+ curr->mm = new_mm;
+ pmap::activate(new_mm->root_pa);
+ if (old_mm) old_mm->put();
+
+ /* 5a'. Transition from kernel thread to user process.
+ * Linux: begin_new_exec() in fs/exec.c:
+ * current->flags &= ~PF_KTHREAD; (line 1173)
+ * tsk->exit_signal = SIGCHLD; (line 1021)
+ * Without this, a kernel_execve'd task stays marked as a kthread
+ * and would be auto-reaped (exit_signal=-1) instead of becoming
+ * a proper zombie for parent wait(). */
+ curr->flags &= ~TF_KTHREAD;
+ curr->exit_signal = SIGCHLD;
+
+ /* 5b. Reset signal handlers to SIG_DFL (POSIX requirement).
+ *
+ * Linux fs/exec.c:flush_signal_handlers() runs under sighand->siglock.
+ * Even though after fork our sighand is no longer shared with the
+ * parent (we copy in task_clone), Linux still locks here — and so
+ * must we, because the same sighand may be shared with CLONE_SIGHAND
+ * sibling threads of the execing task. Holding siglock here also
+ * keeps the per-task blocked mask reset atomic w.r.t. concurrent
+ * sigprocmask / signal delivery on this very task. */
+ if (curr->sighand) {
+ LockGuard guard(curr->sighand->siglock);
+ for (int i = 0; i < _NSIG; i++) {
+ if (curr->sighand->action[i].sa_handler != SIG_IGN)
+ curr->sighand->action[i].sa_handler = SIG_DFL;
+ curr->sighand->action[i].sa_flags = 0;
+ curr->sighand->action[i].sa_mask.clear();
+ }
+ curr->blocked.clear();
+ } else {
+ curr->blocked.clear();
+ }
+
+ /* 5c. Close O_CLOEXEC file descriptors */
+ /* TODO: implement close-on-exec */
+
+ /* 6. Set up trapframe to enter user space */
+ arch::TrapFrame* tf = curr->trapframe;
+ if (!tf) {
+ printk(KERN_ERR "exec: no trapframe!\n");
+ task_kill_current(SIGKILL);
+ return -EINVAL; /* not reached */
+ }
+
+ /* Initialize trapframe for user-mode return.
+ * This is the MI equivalent of Linux's start_thread() + kernel
+ * metadata setup. All arch-specific details (sstatus, FP state,
+ * kernel_satp, kernel_sp, trap handler address, etc.) are handled
+ * by the MD layer. MI exec never touches arch registers. */
+ arch::trapframe_init_user(tf, info.entry, user_sp);
+
+ pr_debug_s(KLOG_SUBSYS_EXEC,
+ "exec: pid %d entry=%llx sp=%llx phdr=%llx\n",
+ curr->pid, info.entry, user_sp, info.phdr_addr);
+
+ /* Verify loaded data at entry point — debug-level, ring buffer only. */
+ {
+ uint64 pa;
+ if (pmap::lookup(curr->mm->root_pa, page_align_down(info.entry), &pa)) {
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 off = info.entry - page_align_down(info.entry);
+ pr_debug_s(KLOG_SUBSYS_EXEC,
+ "exec: entry bytes @%llx: %02x %02x %02x %02x %02x %02x %02x %02x\n",
+ info.entry,
+ kva[off+0], kva[off+1], kva[off+2], kva[off+3],
+ kva[off+4], kva[off+5], kva[off+6], kva[off+7]);
+ }
+ for (uint64 va = 0x10000; va < 0x14000; va += kPageSize) {
+ if (pmap::lookup(curr->mm->root_pa, va, &pa)) {
+ uint8* kva2 = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ pr_debug_s(KLOG_SUBSYS_EXEC,
+ "exec: page %llx: %02x%02x%02x%02x %02x%02x%02x%02x\n",
+ va, kva2[0], kva2[1], kva2[2], kva2[3],
+ kva2[4], kva2[5], kva2[6], kva2[7]);
+ } else {
+ pr_debug_s(KLOG_SUBSYS_EXEC,
+ "exec: page %llx: NOT MAPPED\n", va);
+ }
+ }
+ }
+
+ /* Flush instruction cache after loading executable code into memory.
+ * Without this, the I-cache may contain stale data for the pages
+ * we just loaded, causing the CPU to execute garbage. */
+ arch::mmu::flush_icache();
+
+ return 0; /* Will return to user mode via trap return */
+}
+
+/* ================================================================== */
+/* sys_execve_wrapper — syscall entry point */
+/* ================================================================== */
+
+int64 sys_execve_wrapper(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ const char* pathname = reinterpret_cast<const char*>(a0);
+ const char* const* argv = reinterpret_cast<const char* const*>(a1);
+ const char* const* envp = reinterpret_cast<const char* const*>(a2);
+
+ return do_execve(pathname, argv, envp);
+}
+
+/* ================================================================== */
+/* kernel_execve — call execve from kernel context (e.g. init) */
+/* ================================================================== */
+
+int kernel_execve(const char* pathname, const char* const* argv,
+ const char* const* envp)
+{
+ TaskControlBlock* curr = current_task();
+
+ int ret = do_execve(pathname, argv, envp);
+ if (ret != 0)
+ return ret;
+
+ /*
+ * do_execve succeeded: the trapframe is set up with entry point,
+ * user stack, and sstatus (SPP=0 for U-mode). We must NOT return
+ * normally — that would go back through the kthread return path
+ * and jump to address 0. Instead, jump directly to user mode via
+ * the same userret() path that fork_child_entry uses.
+ *
+ * Switch stvec from kernelvec to trap_vector so that traps from
+ * U-mode are handled by the user trap entry path (which loads
+ * kernel context from the trapframe via sscratch).
+ */
+ arch::trap::set_vector(reinterpret_cast<uint64>(trap_vector));
+ userret(curr->trapframe);
+ __builtin_unreachable();
+}
diff --git a/na/kernel/main.cc b/na/kernel/main.cc
index f28d078..db5843c 100644
--- a/na/kernel/main.cc
+++ b/na/kernel/main.cc
@@ -10,26 +10,50 @@
* BSP (cpu_id == 0) executes the full subsystem initialization DAG.
* APs spin on ap_boot_sync, then perform per-CPU init only.
*
+ * Boot DAG invariants (docs/specs/linux_abi_mm_plan.md §Boot Sequence):
+ * - trap_init() MUST precede any allocator (exception safety net)
+ * - sched_init() MUST precede timer_init() (software before hardware)
+ * - PID 1 (init) is spawned before entering idle loop
+ *
* Reference: docs/specs/global_init.md
*/
+#include <arch/config.h>
#include <arch/cpu.h>
+#include <arch/extable.h>
+#include <arch/sbi.h>
+#include <lume/addr.h>
#include <lume/atomic.h>
-#include <lume/boot_params.h>
#include <lume/console.h>
#include <lume/driver.h>
#include <lume/fdt.h>
+#include <lume/fdtable.h>
+#include <lume/fs_struct.h>
+#include <lume/fs_types.h>
#include <lume/irq.h>
+#include <lume/kernel_map.h>
#include <lume/klog.h>
#include <lume/kprintf.h>
#include <lume/memblock.h>
+#include <lume/mount.h>
+#include <lume/page_cache.h>
#include <lume/pmm.h>
#include <lume/random.h>
#include <lume/sched.h>
+#include <lume/smp.h>
+#include <lume/task.h>
+#include <lume/pid.h>
+#include <lume/pgrp.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
#include <lume/trap.h>
#include <lume/types.h>
-#include <arch/config.h>
-#include <arch/extable.h>
+
+// Declared by entry.S: the hart ID that won BSP election
+extern "C" uint32 bsp_hart_id;
+
+// Linker symbol: kernel entry point (physical start address)
+extern "C" char _start[];
// ============================================================
// Forward declarations for subsystem init functions.
@@ -42,11 +66,20 @@ void console_init(); // 1. UART ready; kprintf/panic can output
void fdt_init(uint64 fdt_paddr); // 2. Parse FDT, build device node table
void pmm_init(); // 3. Buddy allocator ready
void slab_init(); // 4. kmem_cache ready; operator new usable
-void vmm_init(); // 5. Fine-grained kernel page table; retire early_pgdir
-void task_init(); // 7. PID allocator + global task list
-void timer_init(); // 8. Configure timer interrupt (scheduler tick)
-void sched_init(); // 9. Create BSP Idle Task, init ready queues
-void syscall_init(); // 10. Register syscall jump table
+void vmm_init(); // 5. Fine-grained kernel page table; retire early_pgdir
+void task_init(); // 6. PID allocator + global task list
+void pgrp_init(); // 6.5. Process group hash table
+void sched_init(); // 7. Create BSP Idle Task, init ready queues
+void timer_init(); // 8. Configure timer interrupt (scheduler tick)
+void syscall_init(); // 9. Register syscall jump table
+void super_init(); // 10. VFS superblock registry
+void ramfs_init(); // 11. RamFS filesystem registration
+void vfs_init(); // 12. Mount root filesystem
+void cdev_init(); // 13. Character device registry
+void console_cdev_init(); // 14. Register console/null/zero cdevs
+void devfs_init(); // 15. Create /dev and populate nodes
+void tty_init(); // 15.5. TTY subsystem (console TTY + UART RX IRQ)
+void populate_rootfs(); // 15.6. Unpack embedded initramfs CPIO into ramfs
// Boot self-test runner
void selftest_run_all();
@@ -56,6 +89,102 @@ void vmm_init_ap(); // Switch to BSP-built kernel page table
void timer_init_ap(); // Start this CPU's timer interrupt
void sched_init_ap(); // Init per-CPU ready queue, create Idle Task
+// kthread_create from task.cc
+extern TaskControlBlock *kthread_create(const char *name, void (*entry)(void *),
+ void *arg, uint8 priority);
+
+// ============================================================
+// PID 1 — init thread
+// ============================================================
+static void init_thread_func(void *) {
+ printk(KERN_INFO "[init] PID 1 started\n");
+
+ /* Run post-SMP tests — APs are online by now */
+ extern void selftest_smp();
+ kprintf("\n========================================\n");
+ kprintf(" Post-SMP Test Suite\n");
+ kprintf("========================================\n");
+ selftest_smp();
+ kprintf("========================================\n");
+ kprintf(" Post-SMP tests PASSED\n");
+ kprintf("========================================\n\n");
+
+ /*
+ * Set up init's own VFS context (like Linux kernel_init).
+ * kthread_create does NOT inherit files/fs, so init creates its own.
+ */
+ TaskControlBlock *self = current_task();
+
+ if (!self->files) {
+ self->files = alloc_files_struct();
+ if (!self->files) {
+ printk(KERN_ERR "[init] failed to allocate files_struct\n");
+ goto out;
+ }
+ }
+ if (!self->fs) {
+ self->fs = alloc_fs_struct();
+ if (!self->fs) {
+ printk(KERN_ERR "[init] failed to allocate fs_struct\n");
+ goto out;
+ }
+ /* Point to the global root filesystem */
+ self->fs->root.de = g_root_dentry;
+ self->fs->root.mnt = g_root_mnt;
+ self->fs->pwd = self->fs->root;
+ }
+
+ /* Open /dev/console → fd 0 (stdin), dup to fd 1, 2 */
+ {
+ extern int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+ extern int64 sys_dup(uint64, uint64, uint64, uint64, uint64, uint64);
+
+ int64 fd0 =
+ sys_openat(static_cast<uint64>(-100), /* AT_FDCWD */
+ reinterpret_cast<uint64>("/dev/console"), O_RDWR, 0, 0, 0);
+ if (fd0 < 0) {
+ printk(KERN_ERR "[init] open /dev/console failed: %d\n",
+ static_cast<int>(fd0));
+ } else {
+ sys_dup(static_cast<uint64>(fd0), 0, 0, 0, 0, 0); /* fd 1 */
+ sys_dup(static_cast<uint64>(fd0), 0, 0, 0, 0, 0); /* fd 2 */
+ printk(KERN_INFO "[init] fd 0/1/2 -> /dev/console\n");
+ }
+ }
+
+ /*
+ * Try to exec /sbin/init (like Linux kernel_init).
+ * If not found, fall through to idle.
+ */
+ {
+ extern int kernel_execve(const char *, const char *const *,
+ const char *const *);
+
+ const char *init_paths[] = {"/sbin/init", "/etc/init", "/bin/init",
+ "/bin/sh", nullptr};
+ const char *argv[] = {"/sbin/init", nullptr};
+ const char *envp[] = {"HOME=/", "PATH=/sbin:/bin", nullptr};
+
+ for (int i = 0; init_paths[i]; i++) {
+ argv[0] = init_paths[i];
+ int ret = kernel_execve(init_paths[i], argv, envp);
+ if (ret == 0) {
+ /* Success — do_execve modified our trapframe.
+ * When this kthread returns through the trap return path,
+ * it will jump to user space. */
+ return;
+ }
+ printk(KERN_INFO "[init] exec %s failed: %d\n", init_paths[i], ret);
+ }
+
+ printk(KERN_ERR "[init] No init found! Kernel panic.\n");
+ }
+
+out:
+ extern void task_exit(int code);
+ task_exit(0);
+}
+
// ============================================================
// Second-stage synchronization barrier (C++ layer)
// AP cores spin here until BSP finishes all global init.
@@ -67,61 +196,46 @@ static lume::atomic<uint32> ap_boot_sync;
// ============================================================
extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
- if (cpu_id == 0) {
+ if (cpu_id == bsp_hart_id) {
// ========================================================
// BSP: Absolute single-core period — no locks needed.
// APs are spinning on ap_boot_sync.
// ========================================================
- boot_params_init_fdt(fdt_paddr);
- const BootParams* bp = boot_params_get();
- uint64 fdt_pa = bp ? bp->fdt_ptr : fdt_paddr;
+ uint64 fdt_pa = fdt_paddr;
+
+ // ---- Phase 1: Console + Exception Safety Net ----
- // Early console setup: discover UART via FDT before ANY output.
+ // 1. Early console setup: discover UART via FDT before ANY output.
console_early_init(fdt_pa);
klog_init();
printk("\n");
printk(KERN_INFO "LumeOS Kernel v0.1\n");
- printk(KERN_INFO "boot: BSP online, cpu_id=0\n");
+ printk(KERN_INFO "boot: BSP online, cpu_id=%llu\n", cpu_id);
printk(KERN_INFO "boot: higher-half kernel active\n");
- BOOT_STAGE(0, "firmware handoff");
- BOOT_STAGE(1, "bootloader handoff");
- BOOT_STAGE(2, "arch early asm complete");
- if (bp) {
- if (bp->kimage_flags & BOOT_IMAGE_DECOMP_FAILED) {
- BOOT_STAGE(3, "decompress failed");
- } else if (bp->kimage_flags & BOOT_IMAGE_COMPRESSED) {
- BOOT_STAGE(3, "decompress ok");
- } else {
- BOOT_STAGE(3, "decompress skipped");
- }
-
- if (bp->kimage_flags & BOOT_IMAGE_RELOC_FAILED) {
- BOOT_STAGE(3, "relocate failed");
- } else if (bp->kimage_flags & BOOT_IMAGE_RELOCATED) {
- BOOT_STAGE(3, "relocate ok");
- } else {
- BOOT_STAGE(3, "relocate skipped");
- }
-
- printk(KERN_INFO "boot: kimage phys=0x%llx size=%llu reloc=%llu\n",
- bp->kimage_phys_base,
- static_cast<unsigned long long>(bp->kimage_size),
- static_cast<unsigned long long>(bp->kimage_reloc_size));
- } else {
- BOOT_STAGE(3, "decompress/relocate unknown");
- }
- BOOT_STAGE(4, "early c init");
+ /* kernel_map filled by arch/riscv/mm/init.cc::setup_vm before MMU
+ * enable; relocate_kernel has already applied .rela.dyn. */
+ printk(KERN_INFO "boot: kimage phys=0x%llx virt=0x%llx size=%llu\n",
+ static_cast<unsigned long long>(kernel_map.phys_addr),
+ static_cast<unsigned long long>(kernel_map.virt_addr),
+ static_cast<unsigned long long>(kernel_map.size));
// --- Subsystem init DAG (strict top-down order) ---
- // 1. FDT parsing — make device tree available
+ // 2. Trap handler — install exception vector FIRST.
+ // Any fault from here on gets a proper panic backtrace
+ // instead of a silent hang. (Linux: trap_init is in start_kernel
+ // right after setup_arch.)
+ trap_init();
+ printk(KERN_INFO "init: trap_init done\n");
+
+ // 3. FDT parsing — make device tree available
fdt_init(fdt_pa);
printk(KERN_INFO "init: fdt_init done\n");
- // 2. KASLR — seed PRNG and randomise VA layout.
+ // 4. KASLR — seed PRNG and randomise VA layout.
// MUST happen before vmm_init() builds the kernel page table.
{
uint64 kaslr_seed = 0;
@@ -133,52 +247,122 @@ extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
// Sort exception table before setting RO permissions in vmm_init()
extable_init();
- // 3. Memblock allocator (Early boot PA allocator)
+ // ---- Phase 2: Memory Subsystem (strict cascade) ----
+
+ // 5. Memblock allocator (Early boot PA allocator)
memblock_init(fdt_pa);
printk(KERN_INFO "init: memblock_init done\n");
- // 4. Virtual memory manager — builds page table with KASLR'd bases
+ // 6. Virtual memory manager — builds page table with KASLR'd bases
vmm_init();
printk(KERN_INFO "init: vmm_init done\n");
- // 5. Physical memory manager (Buddy)
+ // 7. Physical memory manager (Buddy)
pmm_init();
printk(KERN_INFO "init: pmm_init done\n");
- // 6. Slab allocator — operator new usable after this
+ // 8. Slab allocator — operator new usable after this
slab_init();
printk(KERN_INFO "init: slab_init done\n");
- // 7. FDT Phase 2 — build DeviceNode object tree (needs Slab)
+ // 9. Page cache (minimal hash table, stable interface for file-backed mmap)
+ page_cache_init();
+
+ // 10. FDT Phase 2 — build DeviceNode object tree (needs Slab)
fdt_unflatten();
printk(KERN_INFO "init: fdt_unflatten done\n");
- // 6. Trap handler — set trap vector
- trap_init();
- printk(KERN_INFO "init: trap_init done\n");
+ // ---- Phase 3: Task & Scheduler (software state before hardware) ----
- // 6.5 Task management (PID allocator + global task list)
+ // 11. Task management (PID allocator + global task list)
task_init();
+ printk(KERN_INFO "init: task_init done\n");
- // 7. Hardware Drivers Discovery (Interrupt Controllers, etc)
+ pgrp_init();
+
+ sched_init();
+ printk(KERN_INFO "init: sched_init done\n");
+
+ // ---- Phase 3.5: VFS (needs slab + sched) ----
+ super_init();
+ ramfs_init();
+ vfs_init();
+ printk(KERN_INFO "init: vfs_init done (ramfs root mounted)\n");
+
+ // ---- Phase 3.55: Unpack initramfs into root ----
+ populate_rootfs();
+
+ // ---- Phase 3.6: Device nodes ----
+ cdev_init();
+ console_cdev_init();
+ devfs_init();
+ printk(KERN_INFO "init: devfs done\n");
+
+ // ---- Phase 4: Hardware Drivers & Interrupts ----
+
+ // 13. Hardware Drivers Discovery (Interrupt Controllers, etc)
driver_probe_all();
printk(KERN_INFO "init: driver_probe_all done\n");
- // 8. Timer interrupt (scheduler tick source)
+ // 13.5. TTY subsystem — needs PLIC (driver_probe_all) for UART RX IRQ
+ tty_init();
+ printk(KERN_INFO "init: tty_init done\n");
+
+ // 14. Timer interrupt (scheduler tick source)
+ // Safe now: scheduler is fully initialized.
timer_init();
printk(KERN_INFO "init: timer_init done\n");
- // 9. Scheduler — create Idle Task 0, init ready queues
- sched_init();
- printk(KERN_INFO "init: sched_init done\n");
-
- // 10. Syscall jump table
+ // 15. Syscall jump table
syscall_init();
+ printk(KERN_INFO "init: syscall_init done\n");
+
+ // 16. SMP / IPI infrastructure
+ smp_init();
+ arch::cpu::software_intr_on(); // Enable S-mode software interrupts for IPI
+ arch::cpu::external_intr_on(); // Enable S-mode external interrupts for PLIC (UART)
- // 10. Boot self-tests — verify subsystem correctness
+ // ---- Phase 5: Self-test & Launch ----
+
+ // 17. Boot self-tests — verify subsystem correctness.
+ // Tests that create kthreads must wait() for them (freeing PIDs).
selftest_run_all();
- // kernel_panic("Manual Panic Test", "triggered by developer"); // VERIFIED
- // OK
+
+ // 18. Spawn PID 1 (init) — the first persistent task.
+ // Like Linux's rest_init() → user_mode_thread(kernel_init).
+ // All test PIDs have been freed, so PID 1 is available.
+ TaskControlBlock *init_task =
+ kthread_create("init", init_thread_func, nullptr, 0);
+ if (init_task) {
+ init_task->flags |= TF_INIT;
+
+ /* Init is session and process-group leader: sid = pgid = pid.
+ * Allocate signal_struct AND sighand_struct eagerly so every
+ * descendant (created via task_clone) inherits a real identity
+ * and a real lock object — signal sending must take siglock and
+ * cannot tolerate a null sighand. */
+ init_task->signal = alloc_signal_struct(init_task->pid, init_task);
+ if (!init_task->signal) {
+ printk(KERN_ERR "boot: FAILED to allocate init signal_struct\n");
+ }
+ init_task->sighand = alloc_sighand();
+ if (!init_task->sighand) {
+ printk(KERN_ERR "boot: FAILED to allocate init sighand\n");
+ }
+
+ /* Publish g_init_task so reparenting paths can find the foster
+ * parent when other tasks exit before init takes over user space. */
+ extern TaskControlBlock* g_init_task;
+ g_init_task = init_task;
+
+ printk(KERN_INFO "boot: init thread created (pid=%d sid=%d pgid=%d)\n",
+ init_task->pid,
+ init_task->signal ? init_task->signal->sid : -1,
+ init_task->signal ? init_task->signal->pgid : -1);
+ } else {
+ printk(KERN_ERR "boot: FAILED to create init thread!\n");
+ }
+
BOOT_STAGE(5, "core kernel init");
printk(KERN_INFO "boot: BSP init complete, releasing APs\n");
@@ -187,9 +371,37 @@ extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
// guarantees all global data is visible to APs.
ap_boot_sync.store(1, __ATOMIC_RELEASE);
+ // Start AP harts via SBI HSM extension.
+ // OpenSBI v1.0+ parks non-boot harts in STOPPED state.
+ // They must be explicitly started with sbi_hart_start().
+ //
+ // kernel_map.phys_addr was filled by setup_vm in arch/riscv/mm/init.cc
+ // before MMU enable — it's the runtime PA at which the kernel was
+ // loaded. APs start at the same PA as the BSP.
+ {
+ uint64 start_pa = kernel_map.phys_addr;
+ if (start_pa == 0) {
+ /* Defensive fallback if setup_vm hasn't filled the field. */
+ start_pa =
+ boot_va_to_pa(virt_addr(reinterpret_cast<uint64>(_start))).raw;
+ }
+ uint32 num_harts = 4; // TODO: read from FDT /cpus
+ for (uint32 h = 0; h < num_harts; h++) {
+ if (h == static_cast<uint32>(cpu_id))
+ continue; // skip BSP
+ auto ret = arch::sbi::sbi_hart_start(h, start_pa, fdt_paddr);
+ if (ret.error == arch::sbi::SBI_SUCCESS) {
+ printk(KERN_INFO "smp: started hart %u at PA 0x%llx\n", h, start_pa);
+ } else {
+ printk(KERN_ERR "smp: failed to start hart %u (err=%ld)\n", h,
+ ret.error);
+ }
+ }
+ }
+
// BSP enables interrupts — first timer tick will preempt
- // into the scheduler loop.
- // arch::cpu::intr_on();
+ // into the scheduler loop, which will pick up PID 1.
+ arch::cpu::intr_on();
printk(KERN_INFO "boot: BSP entering idle loop\n");
@@ -197,22 +409,59 @@ extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
// ========================================================
// AP: spin until BSP finishes global init
// ========================================================
+ // DEBUG (commented out — uncomment for SMP debugging):
+ // arch::sbi::sbi_ecall(1, 0, '1'); // AP entered kernel_main
+
while (ap_boot_sync.load(__ATOMIC_ACQUIRE) == 0)
;
+ // arch::sbi::sbi_ecall(1, 0, '2'); // AP passed ap_boot_sync
+
+ // --- Per-CPU init (must happen before any printk!) ---
+ // early_pgdir doesn't map UART MMIO, so we MUST switch to
+ // BSP's kernel page table before any console output.
+ vmm_init_ap(); // Switch to BSP-built kernel page table
+
+ // arch::sbi::sbi_ecall(1, 0, '3'); // vmm_init_ap done
+
+ trap_init_ap(); // Install stvec on this CPU
+
+ // arch::sbi::sbi_ecall(1, 0, '4'); // trap_init_ap done
+
printk(KERN_INFO "boot: AP online, cpu_id=%llu\n", cpu_id);
- // --- Per-CPU init only (no global data mutation) ---
- // vmm_init_ap();
- // arch_irq_chip_init_ap();
- // timer_init_ap();
- // sched_init_ap();
+ sched_init_ap(); // Init per-CPU RunQueue, create Idle Task
+
+ // arch::sbi::sbi_ecall(1, 0, '5'); // sched_init_ap done
+
+ timer_init_ap(); // Start this CPU's timer interrupt
+ arch::cpu::software_intr_on(); // Enable S-mode software interrupts for IPI
+ arch::cpu::external_intr_on(); // Enable S-mode external interrupts for PLIC (UART)
- // AP enables interrupts — becomes Idle Task on this CPU
- // arch::cpu::intr_on();
+ // AP is now a fully functional scheduler CPU.
+ // Enable interrupts — first timer tick preempts into idle loop.
+ arch::cpu::intr_on();
+
+ printk(KERN_INFO "boot: AP cpu_id=%llu fully initialized\n", cpu_id);
+ }
+
+ // All CPUs reach here.
+ // The "current task" is the boot stub (bsp_task or ap_task, pid=-1).
+ // We must hand off to the scheduler's idle task by calling schedule().
+ // Without this, the BSP runs as bsp_task (O1 class, timeslice=20 ticks)
+ // instead of the idle task (which yields immediately when any task is
+ // runnable). That delays PID 1 by up to 200ms and can stall entirely
+ // if no timer tick fires before the first WFI.
+ {
+ TaskControlBlock *self = current_task();
+ if (self) {
+ self->state = TaskState::Dead; // Boot stub is done; don't re-enqueue
+ }
+ schedule(); // Switch to idle task (or init if it's already runnable)
}
- // All CPUs: idle loop (wfi until interrupt)
+ // Should not reach here — schedule() switches away permanently.
+ // But just in case, spin with WFI as a safety net.
while (true) {
arch::cpu::halt_until_interrupt();
}
diff --git a/na/kernel/module.mk b/na/kernel/module.mk
index 662f57e..754a14c 100644
--- a/na/kernel/module.mk
+++ b/na/kernel/module.mk
@@ -1,17 +1,26 @@
# kernel/module.mk
SRCS_CC += kernel/main.cc
-SRCS_CC += kernel/boot_params.cc
SRCS_CC += kernel/sync/spinlock.cc
+SRCS_CC += kernel/sync/mutex.cc
SRCS_CC += kernel/trap.cc
SRCS_CC += kernel/irq.cc
SRCS_CC += kernel/sched_core.cc
SRCS_CC += kernel/sched_class_o1.cc
SRCS_CC += kernel/timer.cc
SRCS_CC += kernel/pid.cc
+SRCS_CC += kernel/pgrp.cc
SRCS_CC += kernel/task.cc
SRCS_CC += kernel/syscall.cc
SRCS_CC += kernel/sys_process.cc
SRCS_CC += kernel/sys_mm.cc
SRCS_CC += kernel/sys_info.cc
+SRCS_CC += kernel/sys_fs.cc
SRCS_CC += kernel/uaccess.cc
SRCS_CC += kernel/waitqueue.cc
+SRCS_CC += kernel/signal.cc
+SRCS_CC += kernel/sys_signal.cc
+SRCS_CC += kernel/sys_misc.cc
+SRCS_CC += kernel/exec.cc
+SRCS_CC += kernel/poll.cc
+SRCS_CC += kernel/ptrace.cc
+SRCS_CC += kernel/smp.cc
diff --git a/na/kernel/pgrp.cc b/na/kernel/pgrp.cc
new file mode 100644
index 0000000..e5b382c
--- /dev/null
+++ b/na/kernel/pgrp.cc
@@ -0,0 +1,148 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Process Group / Session Implementation
+ *
+ * Hash table of signal_struct objects keyed by pgid. Lifecycle of
+ * signal_struct mirrors Linux: one per thread group, refcounted across
+ * CLONE_THREAD siblings, freed when the last task in the group exits.
+ *
+ * Reference: Linux kernel/pid.c, kernel/exit.c
+ */
+
+#include <lume/pgrp.h>
+#include <lume/signal_types.h>
+#include <lume/task.h>
+#include <lume/new.h>
+#include <lume/klog.h>
+
+/* ================================================================== */
+/* Hash table state */
+/* ================================================================== */
+
+list_node g_pgrp_hash[kPgrpHashSize];
+Spinlock g_pgrp_lock;
+
+void pgrp_init()
+{
+ for (auto& bucket : g_pgrp_hash) bucket.init();
+ g_pgrp_lock.init("pgrp");
+}
+
+/* ================================================================== */
+/* Internal helpers (must be called with g_pgrp_lock held) */
+/* ================================================================== */
+
+static void pgrp_attach_locked(signal_struct* sig, int32 pgid)
+{
+ sig->pgid = pgid;
+ list_node* bucket = &g_pgrp_hash[pgrp_hash_idx(pgid)];
+ sig->pgrp_link.prev = bucket->prev;
+ sig->pgrp_link.next = bucket;
+ bucket->prev->next = &sig->pgrp_link;
+ bucket->prev = &sig->pgrp_link;
+}
+
+static void pgrp_detach_locked(signal_struct* sig)
+{
+ /* Unlink only if currently on a list — init() makes prev/next point
+ * to self, which we treat as "not linked". */
+ if (sig->pgrp_link.next == &sig->pgrp_link) return;
+ sig->pgrp_link.prev->next = sig->pgrp_link.next;
+ sig->pgrp_link.next->prev = sig->pgrp_link.prev;
+ sig->pgrp_link.init();
+}
+
+/* ================================================================== */
+/* signal_struct allocation / refcount */
+/* ================================================================== */
+
+signal_struct* alloc_signal_struct(int32 leader_pid, TaskControlBlock* leader)
+{
+ auto* sig = new signal_struct;
+ if (!sig) return nullptr;
+ sig->init(leader_pid, leader);
+
+ LockGuard guard(g_pgrp_lock);
+ pgrp_attach_locked(sig, leader_pid);
+ return sig;
+}
+
+signal_struct* fork_signal_struct(signal_struct* parent,
+ TaskControlBlock* child_leader)
+{
+ if (!parent) return nullptr;
+
+ auto* sig = new signal_struct;
+ if (!sig) return nullptr;
+
+ /* Copy identity from parent, then override leader and attach. */
+ sig->lock.init("signal");
+ sig->refcount = 1;
+ sig->leader = child_leader;
+ sig->tty_old_pgrp = 0;
+ sig->pgrp_link.init();
+
+ {
+ LockGuard pg(parent->lock);
+ sig->pgid = parent->pgid;
+ sig->sid = parent->sid;
+ sig->tty = parent->tty;
+ }
+
+ LockGuard guard(g_pgrp_lock);
+ pgrp_attach_locked(sig, sig->pgid);
+ return sig;
+}
+
+void get_signal_struct(signal_struct* sig)
+{
+ if (!sig) return;
+ LockGuard guard(sig->lock);
+ sig->refcount++;
+}
+
+void put_signal_struct(signal_struct* sig)
+{
+ if (!sig) return;
+
+ uint32 remaining;
+ {
+ LockGuard guard(sig->lock);
+ remaining = --sig->refcount;
+ }
+ if (remaining != 0) return;
+
+ /* Last reference — detach from pgrp hash and free. */
+ {
+ LockGuard guard(g_pgrp_lock);
+ pgrp_detach_locked(sig);
+ }
+ delete sig;
+}
+
+/* ================================================================== */
+/* Pgrp membership change */
+/* ================================================================== */
+
+void change_pgid(signal_struct* sig, int32 new_pgid)
+{
+ if (!sig) return;
+ LockGuard guard(g_pgrp_lock);
+ if (sig->pgid == new_pgid) return;
+ pgrp_detach_locked(sig);
+ pgrp_attach_locked(sig, new_pgid);
+}
+
+int pgrp_member_count(int32 pgid)
+{
+ LockGuard guard(g_pgrp_lock);
+ int n = 0;
+ list_node* bucket = &g_pgrp_hash[pgrp_hash_idx(pgid)];
+ for (list_node* node = bucket->next; node != bucket; node = node->next) {
+ auto* sig = list_entry<signal_struct, &signal_struct::pgrp_link>(node);
+ if (sig->pgid == pgid) n++;
+ }
+ return n;
+}
diff --git a/na/kernel/pid.cc b/na/kernel/pid.cc
index 81b9494..04b846b 100644
--- a/na/kernel/pid.cc
+++ b/na/kernel/pid.cc
@@ -12,8 +12,10 @@ void PidAllocator::init()
{
for (int i = 0; i < kBitmapWords; i++)
bitmap_[i] = 0;
- next_hint_ = 2;
- bitmap_[0] |= 0x3ULL; /* Reserve PID 0 (idle) and 1 (init) */
+ next_hint_ = 1;
+ bitmap_[0] |= 0x1ULL; /* Reserve PID 0 (idle/swapper) only.
+ * PID 1 (init) is allocated normally — the first
+ * kthread_create() call gets it, matching Linux. */
}
int32 PidAllocator::alloc()
diff --git a/na/kernel/poll.cc b/na/kernel/poll.cc
new file mode 100644
index 0000000..3674491
--- /dev/null
+++ b/na/kernel/poll.cc
@@ -0,0 +1,250 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * sys_ppoll — POSIX ppoll() implementation.
+ *
+ * Linux fs/select.c:do_sys_poll() pattern, simplified for our needs:
+ * - Two-phase loop: "register-then-check". First pass calls every
+ * f_op->poll() with a real poll_table so drivers can register their
+ * wait queues; if any fd is already ready we return immediately.
+ * Otherwise schedule() until a wq fires (or timeout/signal), then
+ * re-check with poll_table=nullptr (probe-only).
+ *
+ * Limitations vs. Linux:
+ * - Only a single wait queue per ppoll call is tracked (the first one
+ * a driver hands to poll_wait). Multi-fd polls work, but only the
+ * first wq's wake actually re-runs the loop; other fds becoming
+ * ready won't be noticed until the timer fires or another wake.
+ * BusyBox interactive shell polls one fd (stdin), so this matches.
+ *
+ * Reference: Linux fs/select.c
+ */
+
+#include <lume/poll.h>
+#include <lume/file.h>
+#include <lume/fdtable.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+#include <lume/timer.h>
+#include <lume/waitqueue.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/uaccess.h>
+#include <lume/errno.h>
+#include <lume/klog.h>
+#include <lume/new.h>
+
+/* ================================================================== */
+/* poll_wait — called by file_operations->poll() implementations */
+/* ================================================================== */
+
+void poll_wait(file* /*fp*/, WaitQueue* wq, poll_table* pt)
+{
+ /* Probe-only pass: pt is null, drivers skip registration. */
+ if (!pt || !wq) return;
+ /* Single-wq design: keep the first one we see. A more complete
+ * implementation would chain entries onto a list. */
+ if (!pt->wq) pt->wq = wq;
+}
+
+/* ================================================================== */
+/* Linux-compatible timespec */
+/* ================================================================== */
+
+struct linux_timespec_pp {
+ int64 tv_sec;
+ int64 tv_nsec;
+};
+
+/*
+ * Convert a timespec into a deadline in jiffies (g_jiffies clock).
+ * Returns 0 if the input is null (= infinite wait), or a deadline
+ * jiffy value otherwise. Caller treats 0 deadline as "no timeout".
+ */
+static uint64 timespec_to_deadline(const linux_timespec_pp* ts)
+{
+ if (!ts) return 0;
+ /* g_jiffies advances at 100 Hz (HZ=100) — 1 jiffy = 10 ms */
+ uint64 ticks = static_cast<uint64>(ts->tv_sec) * 100ULL;
+ ticks += ts->tv_nsec / 10'000'000; /* 10 ms granularity */
+ /* Round up sub-jiffy nsec so a tiny timeout doesn't truncate to 0. */
+ if (ts->tv_nsec % 10'000'000 != 0) ticks++;
+ return g_jiffies + ticks;
+}
+
+/* ================================================================== */
+/* do_poll — one scan over all fds */
+/* ================================================================== */
+
+/*
+ * For each fd: look up the file, call its poll() callback. If `pt` is
+ * non-null, drivers register their wait queues onto it. Returns the
+ * number of fds that have any non-zero revents.
+ */
+static int do_poll_scan(pollfd* fds, uint32 nfds, poll_table* pt)
+{
+ TaskControlBlock* c = current_task();
+ files_struct* files = c ? c->files : nullptr;
+ if (!files) return 0;
+ int ready = 0;
+
+ for (uint32 i = 0; i < nfds; i++) {
+ if (fds[i].fd < 0) {
+ fds[i].revents = 0;
+ continue;
+ }
+ file* fp = fcheck(files, fds[i].fd);
+ if (!fp) {
+ fds[i].revents = POLLNVAL;
+ ready++;
+ continue;
+ }
+ uint32 mask = 0;
+ if (fp->f_op && fp->f_op->poll) {
+ mask = fp->f_op->poll(fp, pt);
+ } else {
+ /* No poll handler — treat as always readable/writable so
+ * a missing implementation doesn't permanently block sh. */
+ mask = POLLIN | POLLOUT;
+ }
+ /* Filter: revents always include error/hup; userland event bits
+ * only set if requested in events. */
+ uint32 always = POLLERR | POLLHUP | POLLNVAL;
+ uint32 want = static_cast<uint32>(fds[i].events) | always;
+ fds[i].revents = static_cast<int16>(mask & want);
+ if (fds[i].revents) ready++;
+ }
+ return ready;
+}
+
+/* ================================================================== */
+/* sys_ppoll */
+/* ================================================================== */
+
+/*
+ * a0 = struct pollfd __user *ufds
+ * a1 = nfds_t nfds
+ * a2 = const struct __kernel_timespec __user *tsp (nullable = infinite)
+ * a3 = const sigset_t __user *sigmask (nullable = no override)
+ * a4 = size_t sigsetsize
+ */
+int64 sys_ppoll(uint64 a0, uint64 a1, uint64 a2, uint64 a3,
+ uint64 a4, uint64 /*a5*/)
+{
+ uint64 ufds = a0;
+ uint32 nfds = static_cast<uint32>(a1);
+ uint64 utsp = a2;
+ uint64 usigmask = a3;
+ uint64 sigsetsize = a4;
+
+ if (nfds > 1024) return -EINVAL; /* Linux RLIMIT_NOFILE-ish cap */
+ if (usigmask && sigsetsize != sizeof(sigset_t)) return -EINVAL;
+
+ /* Copy timespec from user. Null pointer means "wait forever". */
+ bool have_timeout = (utsp != 0);
+ linux_timespec_pp ts{};
+ if (have_timeout) {
+ if (copy_from_user(&ts, utsp, sizeof(ts)) < 0) return -EFAULT;
+ if (ts.tv_sec < 0 || ts.tv_nsec < 0 || ts.tv_nsec >= 1'000'000'000)
+ return -EINVAL;
+ }
+ uint64 deadline = have_timeout ? timespec_to_deadline(&ts) : 0;
+
+ /* Apply temporary sigmask if requested. Linux semantics: the new
+ * mask is in effect for the duration of the call; on return we
+ * restore the old mask. If a signal interrupts us mid-wait, the
+ * original mask is restored before returning -EINTR. */
+ TaskControlBlock* curr = current_task();
+ sigset_t saved_mask{};
+ bool mask_swapped = false;
+ if (usigmask && curr && curr->sighand) {
+ sigset_t newmask{};
+ if (copy_from_user(&newmask, usigmask, sizeof(newmask)) < 0)
+ return -EFAULT;
+ newmask.del(SIGKILL);
+ newmask.del(SIGSTOP);
+ LockGuard guard(curr->sighand->siglock);
+ saved_mask = curr->blocked;
+ curr->blocked = newmask;
+ mask_swapped = true;
+ }
+
+ /* Copy pollfd array from user. Allocate a kernel buffer; for the
+ * common nfds<=8 case this is one slab object. */
+ pollfd* kfds = nullptr;
+ uint64 fds_bytes = static_cast<uint64>(nfds) * sizeof(pollfd);
+ int64 ret = 0;
+
+ if (nfds > 0) {
+ kfds = static_cast<pollfd*>(operator new[](fds_bytes));
+ if (!kfds) { ret = -ENOMEM; goto out_restore_mask; }
+ if (copy_from_user(kfds, ufds, fds_bytes) < 0) {
+ ret = -EFAULT; goto out_free;
+ }
+ }
+
+ /* The main poll loop. */
+ {
+ poll_table pt{};
+ pt.wq = nullptr;
+ int n_ready = do_poll_scan(kfds, nfds, &pt);
+
+ while (n_ready == 0) {
+ /* Timeout==0 means non-blocking probe. */
+ if (have_timeout && deadline <= g_jiffies) break;
+
+ /* No registered wait queue? Nothing to sleep on. This
+ * happens when no fd has a poll handler that calls
+ * poll_wait — fall back to a short reschedule loop so we
+ * don't burn the CPU. */
+ if (!pt.wq) {
+ schedule();
+ if (signal_pending(curr)) { ret = -EINTR; goto out_free; }
+ pt.wq = nullptr;
+ n_ready = do_poll_scan(kfds, nfds, &pt);
+ continue;
+ }
+
+ /* Standard prepare-check-schedule pattern. */
+ pt.wq->prepare_to_wait(curr, TaskState::Sleeping);
+
+ /* Re-probe with pt=nullptr (no extra registration) so we
+ * see any state change that arrived between the previous
+ * scan and prepare_to_wait. */
+ n_ready = do_poll_scan(kfds, nfds, nullptr);
+ if (n_ready > 0) {
+ pt.wq->finish_wait(curr);
+ break;
+ }
+ if (signal_pending(curr)) {
+ pt.wq->finish_wait(curr);
+ ret = -EINTR; goto out_free;
+ }
+
+ schedule();
+ pt.wq->finish_wait(curr);
+
+ /* Reset the registration and rescan — drivers may register
+ * fresh wait queues this pass. */
+ pt.wq = nullptr;
+ n_ready = do_poll_scan(kfds, nfds, &pt);
+ }
+ ret = n_ready;
+ }
+
+ /* Copy revents back to user (events bits stay as user wrote them). */
+ if (nfds > 0) {
+ if (copy_to_user(ufds, kfds, fds_bytes) < 0) ret = -EFAULT;
+ }
+
+out_free:
+ if (kfds) operator delete[](kfds);
+
+out_restore_mask:
+ if (mask_swapped && curr && curr->sighand) {
+ LockGuard guard(curr->sighand->siglock);
+ curr->blocked = saved_mask;
+ }
+ return ret;
+}
diff --git a/na/kernel/ptrace.cc b/na/kernel/ptrace.cc
new file mode 100644
index 0000000..3280c07
--- /dev/null
+++ b/na/kernel/ptrace.cc
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * PtRegs ↔ TrapFrame conversion
+ *
+ * This is the ONLY file that knows both the internal TrapFrame layout
+ * and the Linux-ABI PtRegs layout. When TrapFrame is restructured
+ * (e.g., to embed pt_regs directly), only this file needs to change.
+ */
+
+#include <lume/ptrace.h>
+#include <arch/trapframe.h>
+
+void trapframe_to_pt_regs(PtRegs* dst, const arch::TrapFrame* src,
+ uint64 scause, uint64 stval)
+{
+ dst->epc = src->epc;
+ dst->ra = src->ra;
+ dst->sp = src->sp;
+ dst->gp = src->gp;
+ dst->tp = src->tp;
+ dst->t0 = src->t[0];
+ dst->t1 = src->t[1];
+ dst->t2 = src->t[2];
+ dst->s0 = src->s[0];
+ dst->s1 = src->s[1];
+ dst->a0 = src->a[0];
+ dst->a1 = src->a[1];
+ dst->a2 = src->a[2];
+ dst->a3 = src->a[3];
+ dst->a4 = src->a[4];
+ dst->a5 = src->a[5];
+ dst->a6 = src->a[6];
+ dst->a7 = src->a[7];
+ dst->s2 = src->s[2];
+ dst->s3 = src->s[3];
+ dst->s4 = src->s[4];
+ dst->s5 = src->s[5];
+ dst->s6 = src->s[6];
+ dst->s7 = src->s[7];
+ dst->s8 = src->s[8];
+ dst->s9 = src->s[9];
+ dst->s10 = src->s[10];
+ dst->s11 = src->s[11];
+ dst->t3 = src->t[3];
+ dst->t4 = src->t[4];
+ dst->t5 = src->t[5];
+ dst->t6 = src->t[6];
+ dst->status = src->sstatus;
+ dst->badaddr = stval;
+ dst->cause = scause;
+ dst->orig_a0 = src->a[0]; /* Caller should set this before syscall dispatch */
+}
+
+void pt_regs_to_trapframe(arch::TrapFrame* dst, const PtRegs* src)
+{
+ dst->epc = src->epc;
+ dst->ra = src->ra;
+ dst->sp = src->sp;
+ dst->gp = src->gp;
+ dst->tp = src->tp;
+ dst->t[0] = src->t0;
+ dst->t[1] = src->t1;
+ dst->t[2] = src->t2;
+ dst->s[0] = src->s0;
+ dst->s[1] = src->s1;
+ dst->a[0] = src->a0;
+ dst->a[1] = src->a1;
+ dst->a[2] = src->a2;
+ dst->a[3] = src->a3;
+ dst->a[4] = src->a4;
+ dst->a[5] = src->a5;
+ dst->a[6] = src->a6;
+ dst->a[7] = src->a7;
+ dst->s[2] = src->s2;
+ dst->s[3] = src->s3;
+ dst->s[4] = src->s4;
+ dst->s[5] = src->s5;
+ dst->s[6] = src->s6;
+ dst->s[7] = src->s7;
+ dst->s[8] = src->s8;
+ dst->s[9] = src->s9;
+ dst->s[10] = src->s10;
+ dst->s[11] = src->s11;
+ dst->t[3] = src->t3;
+ dst->t[4] = src->t4;
+ dst->t[5] = src->t5;
+ dst->t[6] = src->t6;
+ dst->sstatus = src->status;
+ /* badaddr and cause are CSR values, not written back to TrapFrame */
+}
diff --git a/na/kernel/sched_class_o1.cc b/na/kernel/sched_class_o1.cc
index a8ba5df..28c693c 100644
--- a/na/kernel/sched_class_o1.cc
+++ b/na/kernel/sched_class_o1.cc
@@ -44,8 +44,13 @@ int32 priority_to_timeslice(uint8 priority)
static void o1_task_tick(RunQueue* rq, TaskControlBlock* curr)
{
- if (curr == rq->idle())
+ if (curr == rq->idle()) {
+ /* Idle task: if anything is runnable, yield immediately */
+ if (rq->nr_running() > 0) {
+ rq->set_need_resched(true);
+ }
return;
+ }
curr->time_slice--;
if (curr->time_slice <= 0) {
diff --git a/na/kernel/sched_core.cc b/na/kernel/sched_core.cc
index 85248ef..c0e068e 100644
--- a/na/kernel/sched_core.cc
+++ b/na/kernel/sched_core.cc
@@ -13,11 +13,15 @@
#include <lume/sched_class.h>
#include <lume/task.h>
#include <lume/pmm.h>
+#include <lume/vmm.h>
#include <lume/addr.h>
#include <lume/new.h>
#include <lume/kprintf.h>
#include <lume/klog.h>
+#include <lume/smp.h>
#include <arch/cpu.h>
+#include <arch/mmu.h>
+#include <arch/trap.h>
#include <arch/context.h>
#include <kernel/sched_internal.h>
@@ -37,11 +41,15 @@ static const SchedClass* g_sched_class_head = &g_sched_class_o1;
static void idle_thread_func(void* /*arg*/)
{
while (true) {
+ if (g_rq[arch::cpu::id()].need_resched()) {
+ schedule();
+ }
arch::cpu::intr_on();
arch::cpu::halt_until_interrupt();
}
}
+
/* ========================================================================
* Idle class (always returns idle task)
* ======================================================================== */
@@ -108,6 +116,9 @@ static const char* idle_name_for_cpu(uint32 cpu)
* Kernel thread entry — MI-layer function called by MD trampoline
* ======================================================================== */
+/* Forward declaration — C++ linkage (task.cc) */
+void task_exit(int code);
+
extern "C" void kthread_entry(void (*entry)(void*), void* arg)
{
RunQueue* rq = this_rq();
@@ -117,74 +128,38 @@ extern "C" void kthread_entry(void (*entry)(void*), void* arg)
entry(arg);
- kprintf("[sched] kthread exited\n");
- while (true) {
- arch::cpu::halt_until_interrupt();
- }
+ /* Entry function returned — clean exit (release PID, stack, TCB) */
+ task_exit(0);
+ __builtin_unreachable();
}
/* ========================================================================
* fork_child_entry — called from ret_from_fork assembly
* ======================================================================== */
extern "C" void userret(arch::TrapFrame* tf);
+extern "C" void trap_vector();
extern "C" void fork_child_entry(arch::TrapFrame* tf)
{
RunQueue* rq = this_rq();
rq->lock().release();
- /* Do NOT enable interrupts here! userret expects them disabled. */
-
- /* Jump to user space */
+ /* Set stvec to trap_vector for user-mode traps, then return to user */
+ arch::trap::set_vector(reinterpret_cast<uint64>(trap_vector));
userret(tf);
__builtin_unreachable();
}
/* ========================================================================
- * Helper: create a TCB with a kernel stack
+ * alloc_task — canonical TCB allocator (defined in task.cc)
+ *
+ * Single point of initialization for TaskControlBlock. Ensures all
+ * fields are properly zeroed when the struct gains new members.
* ======================================================================== */
-static TaskControlBlock* create_tcb(const char* name, uint8 priority,
- uint8 flags, uint8 cpu)
-{
- auto* task = new TaskControlBlock;
- if (!task) return nullptr;
-
- task->context = {};
-
- Page* stack_page = pmm_alloc_pages(1);
- if (!stack_page) {
- delete task;
- return nullptr;
- }
- task->kstack_pa = page_to_pa(stack_page).raw;
- task->kstack_va = pa_to_va(phys_addr(task->kstack_pa)).raw;
-
- *reinterpret_cast<uint64*>(task->kstack_va) = kStackCanary;
-
- task->name = name;
- task->priority = priority;
- task->time_slice = priority_to_timeslice(priority);
- task->state = TaskState::Runnable;
- task->cpu_id = cpu;
- task->flags = flags;
- task->exit_code = 0;
- task->parent = nullptr;
- task->mm = nullptr;
- task->trapframe = nullptr;
-
- task->run_link.init();
- task->all_link.init();
- task->children.init();
- task->sibling.init();
- task->wait_chld.init("wait_chld");
-
- task->sched_class = &g_sched_class_o1;
- task->sched_entity = &task->run_link;
-
- return task;
-}
+extern TaskControlBlock* alloc_task(const char* name, uint8 priority,
+ uint8 flags, uint8 cpu);
/* ========================================================================
* Public API: current_task()
@@ -212,6 +187,10 @@ void sched_enqueue(TaskControlBlock* task)
}
task->sched_class->enqueue_task(rq, task);
+
+ if (rq->current() == rq->idle() || task->priority < rq->current()->priority) {
+ rq->set_need_resched(true);
+ }
}
void sched_dequeue(TaskControlBlock* task)
@@ -262,6 +241,52 @@ void schedule()
rq->set_current(next);
rq->clear_need_resched();
+ /* Switch address space (satp) if the new task has a different mm.
+ * Kernel threads (mm == nullptr) borrow the previous task's mm,
+ * so no satp switch is needed — this matches Linux's lazy TLB behavior. */
+ {
+ VmSpace* prev_mm = prev ? prev->mm : nullptr;
+ VmSpace* next_mm = next->mm;
+ uint32 cpu = static_cast<uint32>(arch::cpu::id());
+
+ if (next_mm && next_mm != prev_mm) {
+ /* Switching TO a user process with a different address space.
+ * Linux: switch_mm_irqs_off() in arch/riscv/mm/context.c
+ * sets satp and flushes TLB. */
+ arch::mmu::set_page_table(next_mm->root_pa);
+ arch::mmu::flush_tlb_all();
+ next_mm->cpu_mask.fetch_or(1ULL << cpu, __ATOMIC_RELAXED);
+
+ /* Critical: update TrapFrame's kernel_satp so that trap_vector.S
+ * restores the correct page table on next trap entry.
+ * Without this, a process migrated to a new CPU would have its
+ * trap entry use a stale satp from when trapframe_init_user()
+ * was called, pointing to whatever page table was active then
+ * on the ORIGINAL CPU. */
+ if (next->trapframe) {
+ next->trapframe->kernel_satp = arch::mmu::read_satp();
+ }
+ }
+
+ if (prev_mm && prev_mm != next_mm) {
+ /* Leaving prev's address space — clear our bit */
+ prev_mm->cpu_mask.fetch_and(~(1ULL << cpu), __ATOMIC_RELAXED);
+ }
+ }
+
+ /* FPU lazy save/restore.
+ * Save prev's FP registers only if they were modified (FS=Dirty).
+ * Restore next's FP state for user tasks; disable FPU for kthreads. */
+ if (arch::fpu_state_dirty(arch::fpu_get_fs())) {
+ fpu_save(&prev->fpstate);
+ }
+ if (next->mm != nullptr) {
+ arch::fpu_set_fs(arch::kSstatusFS_Clean);
+ fpu_restore(&next->fpstate);
+ } else {
+ arch::fpu_set_fs(arch::kSstatusFS_Off);
+ }
+
arch::context_switch(&prev->context, &next->context);
rq->lock().release();
@@ -274,24 +299,37 @@ void schedule()
void sched_tick()
{
RunQueue* rq = this_rq();
- LockGuard guard(rq->lock());
-
- TaskControlBlock* curr = rq->current();
- if (!curr) return;
+ {
+ LockGuard guard(rq->lock());
+ TaskControlBlock* curr = rq->current();
+ if (!curr) return;
+ curr->sched_class->task_tick(rq, curr);
+ }
- curr->sched_class->task_tick(rq, curr);
+ /* Periodic load balancing — runs without holding rq lock */
+ sched_balance();
}
/* ========================================================================
- * sched_check_preempt() — called before returning to user mode
+ * sched_check_preempt() — called from trap return path
+ *
+ * Only preempts if:
+ * 1. need_resched flag is set (a higher-priority task is ready)
+ * 2. preempt_count == 0 (no spinlocks held, safe to context switch)
* ======================================================================== */
void sched_check_preempt()
{
RunQueue* rq = this_rq();
- if (rq->need_resched()) {
- schedule();
+ if (!rq->need_resched()) return;
+
+ TaskControlBlock* curr = current_task();
+ if (curr && curr->preempt_count > 0) {
+ /* Cannot preempt — caller is in a critical section.
+ * The preemption will fire when the last lock is released. */
+ return;
}
+ schedule();
}
/* ========================================================================
@@ -328,32 +366,37 @@ void sched_init()
g_sched_class_head = &g_sched_class_o1;
- TaskControlBlock* idle0 = create_tcb(idle_name_for_cpu(0), kIdlePriority,
- TF_KTHREAD | TF_IDLE, 0);
- if (!idle0) kernel_panic("sched_init: failed to create idle/0");
+ uint64 bsp_cpu = arch::cpu::id();
+ uint8 bsp_cpu8 = static_cast<uint8>(bsp_cpu);
+
+ TaskControlBlock* idle0 = alloc_task(idle_name_for_cpu(static_cast<uint32>(bsp_cpu)),
+ kIdlePriority,
+ TF_KTHREAD | TF_IDLE, bsp_cpu8);
+ if (!idle0) kernel_panic("sched_init: failed to create idle/bsp");
idle0->pid = 0;
idle0->tgid = 0;
idle0->sched_class = &g_sched_class_idle;
idle0->sched_entity = nullptr;
- uint64 stack_top = idle0->kstack_va + kKernelStackSize;
+ uint64 stack_top = idle0->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
idle0->context = {};
idle0->context.ra = reinterpret_cast<uint64>(kthread_entry_trampoline);
idle0->context.sp = stack_top;
idle0->context.s[0] = reinterpret_cast<uint64>(idle_thread_func);
idle0->context.s[1] = 0;
- g_rq[0].set_idle(idle0);
+ g_rq[bsp_cpu].set_idle(idle0);
- TaskControlBlock* bsp_task = create_tcb("kernel_main", 0, TF_KTHREAD, 0);
+ TaskControlBlock* bsp_task = alloc_task("kernel_main", 0, TF_KTHREAD, bsp_cpu8);
if (!bsp_task) kernel_panic("sched_init: failed to create bsp_task");
bsp_task->pid = -1;
bsp_task->tgid = -1;
bsp_task->state = TaskState::Running;
- g_rq[0].set_current(bsp_task);
+ g_rq[bsp_cpu].set_current(bsp_task);
}
/* ========================================================================
@@ -366,7 +409,7 @@ void sched_init_ap()
const char* name = idle_name_for_cpu(static_cast<uint32>(cpu));
- TaskControlBlock* idle = create_tcb(name, kIdlePriority,
+ TaskControlBlock* idle = alloc_task(name, kIdlePriority,
TF_KTHREAD | TF_IDLE,
static_cast<uint8>(cpu));
if (!idle) kernel_panic("sched_init_ap: failed to create idle");
@@ -376,7 +419,8 @@ void sched_init_ap()
idle->sched_class = &g_sched_class_idle;
idle->sched_entity = nullptr;
- uint64 stack_top = idle->kstack_va + kKernelStackSize;
+ uint64 stack_top = idle->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
idle->context = {};
idle->context.ra = reinterpret_cast<uint64>(kthread_entry_trampoline);
idle->context.sp = stack_top;
@@ -385,7 +429,7 @@ void sched_init_ap()
g_rq[cpu].set_idle(idle);
- TaskControlBlock* ap_task = create_tcb("ap_boot", 0, TF_KTHREAD,
+ TaskControlBlock* ap_task = alloc_task("ap_boot", 0, TF_KTHREAD,
static_cast<uint8>(cpu));
if (!ap_task) kernel_panic("sched_init_ap: failed to create ap_task");
@@ -395,3 +439,133 @@ void sched_init_ap()
g_rq[cpu].set_current(ap_task);
}
+
+/* ========================================================================
+ * sched_nr_running() — runnable task count for a CPU
+ * ======================================================================== */
+
+uint32 sched_nr_running(uint32 cpu_id)
+{
+ if (cpu_id >= kMaxCpus) return 0;
+ return g_rq[cpu_id].o1().nr_running();
+}
+
+/* ========================================================================
+ * sched_migrate_task() — move a task between CPUs
+ *
+ * Locks are acquired in cpu_id order to prevent ABBA deadlock.
+ * The task must be Runnable (in a runqueue) and not currently running.
+ * ======================================================================== */
+
+int sched_migrate_task(TaskControlBlock* task, uint8 target_cpu)
+{
+ if (!task || target_cpu >= kMaxCpus) return -22; /* -EINVAL */
+
+ uint8 src_cpu = task->cpu_id;
+ if (src_cpu == target_cpu) return 0; /* already there */
+
+ /* Cannot migrate idle or currently-running tasks */
+ if (task->flags & TF_IDLE) return -22;
+
+ RunQueue* src_rq = rq_for_cpu(src_cpu);
+ RunQueue* dst_rq = rq_for_cpu(target_cpu);
+
+ /* Ordered lock acquisition: always lock lower cpu_id first */
+ if (src_cpu < target_cpu) {
+ src_rq->lock().acquire();
+ dst_rq->lock().acquire();
+ } else {
+ dst_rq->lock().acquire();
+ src_rq->lock().acquire();
+ }
+
+ int ret = -22;
+
+ /* Double-check: task must not be current on either CPU */
+ if (task != src_rq->current() && task->state == TaskState::Runnable) {
+ /* Dequeue from source */
+ task->sched_class->dequeue_task(src_rq, task);
+
+ /* Update cpu_id and enqueue on destination */
+ task->cpu_id = target_cpu;
+ task->sched_class->enqueue_task(dst_rq, task);
+
+ /* Signal target CPU to reschedule */
+ dst_rq->set_need_resched(true);
+ ret = 0;
+ }
+
+ /* Release in reverse order */
+ if (src_cpu < target_cpu) {
+ dst_rq->lock().release();
+ src_rq->lock().release();
+ } else {
+ src_rq->lock().release();
+ dst_rq->lock().release();
+ }
+
+ /* If migration succeeded, send reschedule IPI to target */
+ if (ret == 0) {
+ smp_send_reschedule(target_cpu);
+ }
+
+ return ret;
+}
+
+/* ========================================================================
+ * sched_balance() — periodic load balancer (pull model)
+ *
+ * Called from sched_tick() context. Looks at all other CPUs and if any
+ * has 2+ more runnable tasks than the local CPU, pulls one task.
+ * This is a simplified Linux-style load_balance().
+ * ======================================================================== */
+
+void sched_balance()
+{
+ uint32 this_cpu = static_cast<uint32>(arch::cpu::id());
+ uint32 local_load = g_rq[this_cpu].o1().nr_running();
+
+ /* Only balance if we're idle or lightly loaded */
+ if (local_load > 1) return;
+
+ uint32 busiest_cpu = this_cpu;
+ uint32 busiest_load = local_load;
+
+ /* Find the busiest CPU */
+ for (uint32 cpu = 0; cpu < kMaxCpus; cpu++) {
+ if (cpu == this_cpu) continue;
+ uint32 load = g_rq[cpu].o1().nr_running();
+ if (load > busiest_load) {
+ busiest_load = load;
+ busiest_cpu = cpu;
+ }
+ }
+
+ /* Only pull if imbalance is >= 2 */
+ if (busiest_cpu == this_cpu || busiest_load < local_load + 2) return;
+
+ /* Try to pull one task from the busiest CPU */
+ RunQueue* busy_rq = rq_for_cpu(busiest_cpu);
+
+ /* Lock ordering: lower cpu_id first */
+ RunQueue* first = (this_cpu < busiest_cpu)
+ ? &g_rq[this_cpu] : busy_rq;
+ RunQueue* second = (this_cpu < busiest_cpu)
+ ? busy_rq : &g_rq[this_cpu];
+
+ first->lock().acquire();
+ second->lock().acquire();
+
+ /* Re-check after acquiring locks */
+ TaskControlBlock* victim = busy_rq->o1().pick_next();
+ if (victim && victim != busy_rq->current()
+ && !(victim->flags & TF_IDLE)) {
+ victim->sched_class->dequeue_task(busy_rq, victim);
+ victim->cpu_id = static_cast<uint8>(this_cpu);
+ victim->sched_class->enqueue_task(&g_rq[this_cpu], victim);
+ g_rq[this_cpu].set_need_resched(true);
+ }
+
+ second->lock().release();
+ first->lock().release();
+}
diff --git a/na/kernel/signal.cc b/na/kernel/signal.cc
new file mode 100644
index 0000000..53d26da
--- /dev/null
+++ b/na/kernel/signal.cc
@@ -0,0 +1,442 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Signal Delivery — Queue, Dequeue, Default Actions, and do_signal
+ *
+ * Implements the core signal infrastructure:
+ * - send_signal(): enqueue a signal to a task's pending set
+ * - force_sig_fault(): fault-induced signal (trap handler API)
+ * - do_signal(): called on return-to-user, delivers pending signals
+ * - next_signal(): dequeue the highest-priority deliverable signal
+ *
+ * Signal frame construction (arch-specific) is in arch/riscv/signal.cc.
+ *
+ * Reference: Linux kernel/signal.c
+ */
+
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+#include <lume/wait.h>
+#include <lume/console.h>
+#include <lume/kprintf.h>
+#include <lume/klog.h>
+#include <lume/new.h>
+#include <lume/errno.h>
+#include <arch/trapframe.h>
+
+/* Forward declaration: arch-specific signal frame setup */
+int setup_signal_frame(arch::TrapFrame* tf, int signo,
+ kernel_sigaction* ka, kernel_siginfo* info);
+
+/* ================================================================== */
+/* sighand_struct lifecycle (refcount-shared, Linux copy_sighand) */
+/* ================================================================== */
+
+sighand_struct* alloc_sighand()
+{
+ auto* sh = new sighand_struct;
+ if (sh) sh->init();
+ return sh;
+}
+
+sighand_struct* copy_sighand(sighand_struct* parent)
+{
+ /* Linux: kernel/fork.c:copy_sighand() when !CLONE_SIGHAND copies
+ * the action table verbatim so the child inherits dispositions
+ * without sharing future modifications. */
+ if (!parent) return alloc_sighand();
+ auto* sh = new sighand_struct;
+ if (!sh) return nullptr;
+ sh->init();
+ LockGuard guard(parent->siglock);
+ for (int i = 0; i < _NSIG; i++) sh->action[i] = parent->action[i];
+ return sh;
+}
+
+void get_sighand(sighand_struct* sh)
+{
+ if (!sh) return;
+ LockGuard guard(sh->siglock);
+ sh->refcount++;
+}
+
+void put_sighand(sighand_struct* sh)
+{
+ if (!sh) return;
+ uint32 remaining;
+ {
+ LockGuard guard(sh->siglock);
+ remaining = --sh->refcount;
+ }
+ if (remaining == 0) delete sh;
+}
+
+/* ================================================================== */
+/* next_signal — find lowest-numbered deliverable signal */
+/* ================================================================== */
+
+/* Manual count-trailing-zeros (no libgcc __ctzdi2 dependency) */
+static inline int sig_ctz64(uint64 x)
+{
+ if (x == 0) return 64;
+ int n = 0;
+ if ((x & 0xFFFFFFFF) == 0) { n += 32; x >>= 32; }
+ if ((x & 0x0000FFFF) == 0) { n += 16; x >>= 16; }
+ if ((x & 0x000000FF) == 0) { n += 8; x >>= 8; }
+ if ((x & 0x0000000F) == 0) { n += 4; x >>= 4; }
+ if ((x & 0x00000003) == 0) { n += 2; x >>= 2; }
+ if ((x & 0x00000001) == 0) { n += 1; }
+ return n;
+}
+
+static int next_signal(sigpending* pending, const sigset_t* mask)
+{
+ /* Standard signals: check bitmap & ~blocked */
+ for (int i = 0; i < _NSIG_WORDS; i++) {
+ uint64 bits = pending->signal.sig[i] & ~mask->sig[i];
+ if (bits != 0) {
+ int sig = i * 64 + sig_ctz64(bits) + 1;
+ return sig;
+ }
+ }
+ return 0;
+}
+
+/* ================================================================== */
+/* dequeue_signal — remove a signal from the pending set */
+/* ================================================================== */
+
+/*
+ * Dequeue the next deliverable signal. MUST be called with
+ * task->sighand->siglock held. Mirrors Linux's __dequeue_signal()
+ * (kernel/signal.c) which requires the same lock.
+ */
+static int dequeue_signal_locked(TaskControlBlock* task, kernel_siginfo* info)
+{
+ int sig = next_signal(&task->pending, &task->blocked);
+ if (sig == 0) return 0;
+
+ /* Clear the bitmap bit */
+ task->pending.signal.del(sig);
+
+ /* Search the queue for a matching sigqueue entry */
+ list_node* node = task->pending.list.next;
+ while (node != &task->pending.list) {
+ auto* q = list_entry<sigqueue, &sigqueue::list>(node);
+ if (q->info.si_signo == sig) {
+ /* Remove from queue */
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ if (info) *info = q->info;
+ delete q;
+ return sig;
+ }
+ node = node->next;
+ }
+
+ /* No queued entry — fill in minimal info */
+ if (info) {
+ info->si_signo = sig;
+ info->si_errno = 0;
+ info->si_code = 0; /* SI_USER */
+ }
+ return sig;
+}
+
+/* ================================================================== */
+/* send_signal — enqueue a signal to a task */
+/* ================================================================== */
+
+/*
+ * Drop a queued sigqueue entry with the given signo from `task`.
+ *
+ * MUST be called with task->sighand->siglock held — Linux's
+ * flush_sigqueue_mask() has the same precondition (kernel/signal.c).
+ */
+static bool discard_pending_locked(TaskControlBlock* task, int signo)
+{
+ bool any = false;
+ list_node* node = task->pending.list.next;
+ while (node != &task->pending.list) {
+ list_node* nxt = node->next;
+ auto* q = list_entry<sigqueue, &sigqueue::list>(node);
+ if (q->info.si_signo == signo) {
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ delete q;
+ any = true;
+ }
+ node = nxt;
+ }
+ task->pending.signal.del(signo);
+ return any;
+}
+
+static inline bool is_stop_sig(int sig)
+{
+ return sig == SIGSTOP || sig == SIGTSTP ||
+ sig == SIGTTIN || sig == SIGTTOU;
+}
+
+/*
+ * send_signal — queue a signal to a task.
+ *
+ * Acquires task->sighand->siglock for the full update (bitmap, queue,
+ * stop/cont side-effects, scheduler wake). Mirrors Linux's
+ * __send_signal_locked() which also requires this lock (see
+ * kernel/signal.c:1048 lockdep_assert_held).
+ *
+ * Returns 0 on success or a negative errno.
+ */
+int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task)
+{
+ if (sig < 1 || sig > _NSIG) return -EINVAL;
+ if (!task) return -ESRCH;
+ if (!task->sighand) return -ESRCH; /* eager-alloc invariant; tasks
+ * without sighand can't receive
+ * signals (idle / early kthreads) */
+
+ LockGuard guard(task->sighand->siglock);
+
+ /*
+ * Linux's prepare_signal(): a stop signal cancels any pending
+ * SIGCONT and vice versa, so the most recent decision wins.
+ */
+ if (is_stop_sig(sig)) {
+ discard_pending_locked(task, SIGCONT);
+ } else if (sig == SIGCONT) {
+ discard_pending_locked(task, SIGSTOP);
+ discard_pending_locked(task, SIGTSTP);
+ discard_pending_locked(task, SIGTTIN);
+ discard_pending_locked(task, SIGTTOU);
+
+ /* SIGCONT's resume effect happens atomically with delivery. */
+ if (task->state == TaskState::Stopped) {
+ task->state = TaskState::Runnable;
+ task->notify_continued = true;
+ task->stopped_code = 0;
+ sched_enqueue(task);
+ if (task->parent) task->parent->wait_chld.wake_all();
+ }
+ }
+
+ /* Already pending? For standard signals, just mark the bitmap */
+ if (sig < SIGRTMIN && task->pending.signal.has(sig))
+ return 0;
+
+ /* Enqueue a sigqueue entry */
+ auto* q = new sigqueue;
+ if (!q) {
+ /* Can't allocate — just set the bitmap (signal still delivered) */
+ task->pending.signal.add(sig);
+ return 0;
+ }
+
+ q->list.init();
+ if (info) {
+ q->info = *info;
+ } else {
+ q->info.si_signo = sig;
+ q->info.si_errno = 0;
+ q->info.si_code = 0; /* SI_USER */
+ }
+ q->info.si_signo = sig; /* ensure consistent */
+
+ /* Add to tail of queue */
+ list_node* head = &task->pending.list;
+ q->list.prev = head->prev;
+ q->list.next = head;
+ head->prev->next = &q->list;
+ head->prev = &q->list;
+
+ /* Set bitmap */
+ task->pending.signal.add(sig);
+
+ /* Wake the task if it's sleeping in an interruptible wait. */
+ if (task->state == TaskState::Sleeping) {
+ task->state = TaskState::Runnable;
+ sched_enqueue(task);
+ }
+
+ /* SIGKILL must run even if the target is Stopped. */
+ if (sig == SIGKILL && task->state == TaskState::Stopped) {
+ task->state = TaskState::Runnable;
+ task->stopped_code = 0;
+ sched_enqueue(task);
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* force_sig_fault — trap handler API */
+/* ================================================================== */
+
+void force_sig_fault(int signo, int code, uint64 addr)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return;
+
+ /* Kernel threads cannot handle signals — kill immediately */
+ if (curr->flags & TF_KTHREAD) {
+ pr_err("signal: kernel thread %s received signal %d\n",
+ curr->name ? curr->name : "(noname)", signo);
+ task_kill_current(signo);
+ return;
+ }
+
+ /* Sighand must already exist for user-mode tasks (allocated at
+ * fork / kernel_execve time). If it's somehow missing, the only
+ * safe action is to terminate. */
+ if (!curr->sighand) {
+ pr_err("signal: pid %d has no sighand, killing\n", curr->pid);
+ task_kill_current(signo);
+ return;
+ }
+
+ kernel_siginfo info{};
+ info.si_signo = signo;
+ info.si_code = code;
+ info._sifields._sigfault._addr = addr;
+
+ /* Force: bypass blocked mask for fatal signals (lock to keep
+ * blocked-mask updates consistent with sigprocmask). */
+ {
+ LockGuard guard(curr->sighand->siglock);
+ curr->blocked.del(signo);
+ }
+
+ send_signal(signo, &info, curr);
+}
+
+/* ================================================================== */
+/* signal_pending — check if unblocked signals are pending */
+/* ================================================================== */
+
+bool signal_pending(TaskControlBlock* task)
+{
+ /* Advisory check (matches Linux's recalc_sigpending() usage):
+ * callers re-validate under siglock before acting. The bitmap is
+ * a uint64 word; a torn read can only produce false-positives or
+ * false-negatives transiently, never a corrupt pointer. */
+ if (!task) return false;
+ sigset_t unblocked = task->pending.signal & ~task->blocked;
+ return !unblocked.is_empty();
+}
+
+/* ================================================================== */
+/* do_signal — called before returning to user mode */
+/* ================================================================== */
+
+void do_signal(arch::TrapFrame* tf)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return;
+
+ /* Kernel threads never deliver signals via frames */
+ if (curr->flags & TF_KTHREAD) return;
+
+ /* No sighand means no signal infrastructure for this task */
+ if (!curr->sighand) return;
+
+ while (true) {
+ kernel_siginfo info{};
+ kernel_sigaction ka_copy;
+ int sig;
+
+ /*
+ * Take siglock for dequeue + action snapshot. Action processing
+ * (default actions like SIG_ACT_TERM, or constructing a signal
+ * frame for a user handler) is done OUTSIDE the lock — it can
+ * touch user memory and even call schedule(). This matches
+ * Linux's get_signal() loop in kernel/signal.c: it takes
+ * siglock around dequeue_signal+ka_copy, drops it before
+ * dispatching, and re-acquires next iteration.
+ */
+ {
+ LockGuard guard(curr->sighand->siglock);
+ sig = dequeue_signal_locked(curr, &info);
+ if (sig == 0) break;
+ ka_copy = curr->sighand->action[sig - 1];
+ }
+ kernel_sigaction* ka = &ka_copy;
+
+ /* SIG_IGN: discard */
+ if (ka->sa_handler == SIG_IGN && !sig_kernel_only(sig))
+ continue;
+
+ /* SIG_DFL: apply default action */
+ if (ka->sa_handler == SIG_DFL) {
+ SigDefaultAction act = sig_default_action(sig);
+ switch (act) {
+ case SIG_ACT_IGN:
+ continue;
+ case SIG_ACT_TERM:
+ case SIG_ACT_CORE:
+ pr_debug_s(KLOG_SUBSYS_SIGNAL,
+ "signal: pid %d killed by signal %d\n",
+ curr->pid, sig);
+ task_kill_current(sig);
+ return; /* never reached */
+ case SIG_ACT_STOP: {
+ /*
+ * Enter Stopped state. Record the encoded wstatus so
+ * the parent's wait4(WUNTRACED) can observe and report.
+ * The schedule() below does NOT re-enqueue Stopped
+ * tasks — when SIGCONT eventually arrives, send_signal
+ * flips us back to Runnable and re-enqueues.
+ */
+ curr->stopped_code = W_STOPCODE(sig);
+ curr->state = TaskState::Stopped;
+ if (curr->parent)
+ curr->parent->wait_chld.wake_all();
+ pr_debug_s(KLOG_SUBSYS_SIGNAL,
+ "signal: pid %d stopped by sig %d\n",
+ curr->pid, sig);
+ schedule();
+ /*
+ * Resumed (either by SIGCONT or SIGKILL). Loop back to
+ * deliver any remaining pending signals before returning
+ * to user mode.
+ */
+ continue;
+ }
+ case SIG_ACT_CONT:
+ /* SIGCONT's resume side-effect already happened in
+ * send_signal(). The only thing left at handler time
+ * is to consume the signal (no-op for SIG_DFL). */
+ continue;
+ }
+ }
+
+ /* User handler: construct signal frame */
+ pr_debug_s(KLOG_SUBSYS_SIGNAL,
+ "signal: pid %d delivering sig %d handler=%llx\n",
+ curr->pid, sig, reinterpret_cast<uint64>(ka->sa_handler));
+ int ret = setup_signal_frame(tf, sig, ka, &info);
+ if (ret < 0) {
+ pr_err("signal: failed to set up frame for sig %d\n", sig);
+ task_kill_current(sig);
+ return;
+ }
+
+ /* Post-delivery bookkeeping under siglock — both the per-task
+ * blocked mask and the shared action table (SA_RESETHAND) are
+ * touched, so we must hold the same lock the senders use. */
+ {
+ LockGuard guard(curr->sighand->siglock);
+ if (!(ka->sa_flags & SA_NODEFER)) curr->blocked.add(sig);
+ curr->blocked = curr->blocked | ka->sa_mask;
+ if (ka->sa_flags & SA_RESETHAND) {
+ curr->sighand->action[sig - 1].sa_handler = SIG_DFL;
+ }
+ }
+
+ /* Only deliver one signal per return-to-user */
+ break;
+ }
+}
diff --git a/na/kernel/smp.cc b/na/kernel/smp.cc
new file mode 100644
index 0000000..1a79ac3
--- /dev/null
+++ b/na/kernel/smp.cc
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * smp.cc — SMP IPI implementation
+ *
+ * Sending side: atomically set reason bits in target CPU's IpiData,
+ * then invoke arch-specific IPI delivery (SBI ecall on RISC-V).
+ *
+ * Receiving side: smp_handle_ipi() is called from the trap handler
+ * when an S-mode software interrupt arrives. It atomically consumes
+ * all pending reason bits and dispatches to the appropriate handler.
+ *
+ * Reference: docs/specs/linux_abi_mm_plan.md §SMP §2.2
+ */
+
+#include <lume/smp.h>
+#include <lume/klog.h>
+#include <lume/sched.h>
+#include <arch/cpu.h>
+#include <arch/mmu.h>
+#include <arch/sbi.h>
+
+/* Per-CPU IPI data — BSS zero-initialized */
+IpiData g_ipi_data[kMaxCpus];
+
+/* ------------------------------------------------------------------ */
+/* smp_init */
+/* ------------------------------------------------------------------ */
+
+void smp_init()
+{
+ for (uint32 i = 0; i < kMaxCpus; i++) {
+ g_ipi_data[i].pending.store(0, __ATOMIC_RELAXED);
+ g_ipi_data[i].tlb_start = 0;
+ g_ipi_data[i].tlb_end = 0;
+ }
+
+ printk(KERN_INFO "smp: IPI framework initialized (%u CPUs max)\n",
+ static_cast<unsigned>(kMaxCpus));
+}
+
+/* ------------------------------------------------------------------ */
+/* smp_send_ipi — send IPI to target CPUs */
+/* ------------------------------------------------------------------ */
+
+void smp_send_ipi(uint64 cpu_mask, uint32 reason)
+{
+ if (cpu_mask == 0) return;
+
+ /* Set pending bits on each target CPU's IpiData */
+ for (uint32 cpu = 0; cpu < kMaxCpus; cpu++) {
+ if (cpu_mask & (1ULL << cpu)) {
+ g_ipi_data[cpu].pending.fetch_or(reason, __ATOMIC_RELEASE);
+ }
+ }
+
+ /* Deliver hardware interrupt via SBI.
+ * RISC-V SBI IPI extension sends S-mode software interrupt
+ * to the specified harts. hart_mask maps 1:1 to cpu_mask
+ * (assuming hartid == cpu_id, which holds for QEMU virt). */
+ arch::sbi::sbi_send_ipi(cpu_mask, 0);
+}
+
+/* ------------------------------------------------------------------ */
+/* smp_handle_ipi — called from trap handler on software interrupt */
+/* ------------------------------------------------------------------ */
+
+void smp_handle_ipi()
+{
+ uint32 cpu = static_cast<uint32>(arch::cpu::id());
+
+ /* Clear the S-mode software interrupt pending bit (sip.SSIP).
+ * On RISC-V, we must clear this in software, otherwise the
+ * interrupt will keep firing. */
+ arch::cpu::clear_sip_ssip();
+
+ /* Atomically consume all pending reasons */
+ uint32 reasons = g_ipi_data[cpu].pending.exchange(0, __ATOMIC_ACQUIRE);
+
+ if (reasons == 0) return; /* Spurious */
+
+ /* Dispatch each reason */
+
+ if (reasons & IPI_RESCHEDULE) {
+ /* Just need to return from interrupt — schedule() will be
+ * called on the return-from-trap path if need_resched is set.
+ * For now, set the flag by yielding on next opportunity. */
+ /* TODO: set need_resched flag when preemption tracking is added */
+ }
+
+ if (reasons & IPI_TLB_SHOOTDOWN) {
+ /* Execute local TLB flush with the parameters the sender stored */
+ uint64 start = g_ipi_data[cpu].tlb_start;
+ uint64 end = g_ipi_data[cpu].tlb_end;
+ if (start == 0 && end == 0) {
+ arch::mmu::flush_tlb_all();
+ } else {
+ for (uint64 va = start; va < end; va += kPageSize) {
+ arch::mmu::flush_tlb_page(va);
+ }
+ }
+ }
+
+ if (reasons & IPI_HALT) {
+ printk(KERN_INFO "smp: CPU %u halting by IPI\n", cpu);
+ arch::cpu::intr_off();
+ while (true) {
+ arch::cpu::halt_until_interrupt();
+ }
+ }
+}
diff --git a/na/kernel/sync/mutex.cc b/na/kernel/sync/mutex.cc
new file mode 100644
index 0000000..c4d3e9d
--- /dev/null
+++ b/na/kernel/sync/mutex.cc
@@ -0,0 +1,156 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Mutex — Linux-style sleeping mutex (no PI).
+ *
+ * Implementation notes (mirroring kernel/locking/mutex.c, simplified):
+ *
+ * - Fast path (uncontended): wait_lock_ -> see owner_==null -> claim -> unlock.
+ * - Slow path (contended): enqueue MutexWaiter on stack, sleep, race for
+ * ownership after each wake. unlock() wakes only the head waiter.
+ * - No optimistic spin, no handoff anti-starvation, no interruptible wait.
+ * These belong to follow-up work (see mutex.md §5).
+ *
+ * Reference: docs/specs/mutex.md
+ */
+
+#include <lume/mutex.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+#include <lume/klog.h>
+#include <arch/cpu.h>
+
+namespace {
+
+struct MutexWaiter {
+ list_node link;
+ TaskControlBlock* task;
+};
+
+} // namespace
+
+/* ------------------------------------------------------------------ */
+/* init */
+/* ------------------------------------------------------------------ */
+
+void Mutex::init(const char* name)
+{
+ owner_ = nullptr;
+ name_ = name;
+ wait_lock_.init(name != nullptr ? name : "mutex");
+ wait_list_.init();
+}
+
+/* ------------------------------------------------------------------ */
+/* try_lock — non-blocking, safe from any context */
+/* ------------------------------------------------------------------ */
+
+bool Mutex::try_lock()
+{
+ LockGuard g(wait_lock_);
+ if (owner_ != nullptr) {
+ return false;
+ }
+ owner_ = current_task();
+ return true;
+}
+
+/* ------------------------------------------------------------------ */
+/* lock — blocking, must be called in schedulable context */
+/* ------------------------------------------------------------------ */
+
+void Mutex::lock()
+{
+ TaskControlBlock* curr = current_task();
+
+ if (curr == nullptr) {
+ kernel_panic("Mutex::lock: no current task");
+ }
+ if (curr->preempt_count > 0) {
+ kernel_panic("Mutex::lock: preemption disabled (holding spinlock?)");
+ }
+
+ wait_lock_.acquire();
+
+ if (owner_ == nullptr) {
+ owner_ = curr;
+ wait_lock_.release();
+ return;
+ }
+
+ if (owner_ == curr) {
+ wait_lock_.release();
+ kernel_panic("Mutex::lock: recursive acquire by same task");
+ }
+
+ /* Slow path: enqueue ourselves and sleep until we are head + lock free. */
+ MutexWaiter waiter;
+ waiter.link.init();
+ waiter.task = curr;
+ list_add_tail(&waiter.link, &wait_list_);
+
+ for (;;) {
+ /* Set state inside wait_lock_ so unlock-side sees a coherent view:
+ * either we are on the list AND Sleeping (will be woken), or we
+ * already took ownership above (not in this loop). */
+ curr->state = TaskState::Sleeping;
+ wait_lock_.release();
+
+ schedule();
+
+ wait_lock_.acquire();
+
+ if (owner_ == nullptr && wait_list_.next == &waiter.link) {
+ owner_ = curr;
+ list_del_init(&waiter.link);
+ wait_lock_.release();
+ return;
+ }
+ /* Spurious wake / lost race — keep waiting. */
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* unlock */
+/* ------------------------------------------------------------------ */
+
+void Mutex::unlock()
+{
+ TaskControlBlock* curr = current_task();
+
+ wait_lock_.acquire();
+
+ if (owner_ != curr) {
+ wait_lock_.release();
+ kernel_panic("Mutex::unlock: not the owner");
+ }
+
+ owner_ = nullptr;
+
+ if (!wait_list_.is_empty()) {
+ list_node* first = wait_list_.next;
+ MutexWaiter* w = list_entry<MutexWaiter, &MutexWaiter::link>(first);
+ TaskControlBlock* victim = w->task;
+
+ /* Race-tolerant wake: only enqueue if still sleeping. If the
+ * waiter is already Runnable (e.g. it was already woken by a
+ * timer-driven schedule reentry and didn't yet retry the lock),
+ * sched_enqueue would double-link it. */
+ if (victim->state == TaskState::Sleeping) {
+ victim->state = TaskState::Runnable;
+ sched_enqueue(victim);
+ }
+ }
+
+ wait_lock_.release();
+}
+
+/* ------------------------------------------------------------------ */
+/* Debug accessors */
+/* ------------------------------------------------------------------ */
+
+bool Mutex::is_owned_by_current() const
+{
+ return owner_ == current_task();
+}
diff --git a/na/kernel/sync/spinlock.cc b/na/kernel/sync/spinlock.cc
index bca4684..f649fe7 100644
--- a/na/kernel/sync/spinlock.cc
+++ b/na/kernel/sync/spinlock.cc
@@ -17,6 +17,7 @@
#include "cpu_state.h"
#include <arch/cpu.h>
#include <lume/klog.h>
+#include <lume/task.h>
// ============================================================
// Per-CPU sync state (BSS zero-initialized)
@@ -88,6 +89,10 @@ void Spinlock::acquire() {
// Step 4: record owner
cpu_id_ = arch::cpu::id();
+
+ // Step 5: bump preempt_count — prevents schedule() from preempting
+ TaskControlBlock* curr = current_task();
+ if (curr) curr->preempt_count++;
}
void Spinlock::release() {
@@ -102,7 +107,11 @@ void Spinlock::release() {
// Step 3: release the lock with release semantics
locked_.store(0, __ATOMIC_RELEASE);
- // Step 4: restore interrupt state if this was the last lock
+ // Step 4: drop preempt_count
+ TaskControlBlock* curr = current_task();
+ if (curr) curr->preempt_count--;
+
+ // Step 5: restore interrupt state if this was the last lock
sync::pop_intr();
}
diff --git a/na/kernel/sys_fs.cc b/na/kernel/sys_fs.cc
new file mode 100644
index 0000000..1d26fdd
--- /dev/null
+++ b/na/kernel/sys_fs.cc
@@ -0,0 +1,679 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * sys_fs.cc — Filesystem-related syscall handlers
+ *
+ * Implements: openat, close, read, write, lseek, fstat, getdents64,
+ * dup, dup3, getcwd, chdir, mkdirat, unlinkat
+ */
+
+#include <lume/syscall.h>
+#include <lume/syscall_nr.h>
+#include <lume/errno.h>
+#include <lume/console.h>
+#include <lume/task.h>
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/dcache.h>
+#include <lume/namei.h>
+#include <lume/fdtable.h>
+#include <lume/fs_struct.h>
+#include <lume/mount.h>
+#include <lume/fs_types.h>
+#include <lume/klog.h>
+
+/* ================================================================== */
+/* Helpers */
+/* ================================================================== */
+
+static files_struct* current_files()
+{
+ TaskControlBlock* t = current_task();
+ return t ? t->files : nullptr;
+}
+
+static fs_struct* current_fs()
+{
+ TaskControlBlock* t = current_task();
+ return t ? t->fs : nullptr;
+}
+
+/* ================================================================== */
+/* sys_openat(dirfd, pathname, flags, mode) */
+/* ================================================================== */
+
+int64 sys_openat(uint64 a0, uint64 a1, uint64 a2,
+ uint64 a3, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int dirfd = static_cast<int>(a0);
+ const char* pathname = reinterpret_cast<const char*>(a1);
+ int flags = static_cast<int>(a2);
+ uint32 mode = static_cast<uint32>(a3);
+
+ files_struct* files = current_files();
+ if (!files) return -EMFILE;
+
+
+ /* Pathname lookup */
+ path p{};
+ int err = filename_lookup(dirfd, pathname, 0, &p);
+
+ if (err == -ENOENT && (flags & O_CREAT)) {
+ /* Create: look up the parent directory */
+ /* Find last '/' to split parent path / filename */
+ const char* last_slash = nullptr;
+ for (const char* s = pathname; *s; s++) {
+ if (*s == '/') last_slash = s;
+ }
+
+ if (!last_slash || last_slash == pathname) {
+ /* File in root or current directory */
+ path parent{};
+ if (last_slash == pathname) {
+ /* Absolute: parent is root */
+ parent.de = g_root_dentry;
+ parent.mnt = g_root_mnt;
+ } else {
+ /* Relative: parent is cwd */
+ fs_struct* fs = current_fs();
+ if (!fs) return -ENOENT;
+ parent = fs->pwd;
+ }
+
+ const char* name = last_slash ? last_slash + 1 : pathname;
+ inode* dir_ip = parent.de->d_inode;
+ if (!dir_ip || !dir_ip->i_op || !dir_ip->i_op->create) {
+ return -EACCES;
+ }
+
+ /* Create the inode via the directory's i_op->create */
+ qstr q{};
+ q.name = name;
+ q.len = 0;
+ for (const char* s = name; *s; s++) q.len++;
+ dentry* new_de = d_alloc(parent.de, &q);
+ if (!new_de) return -ENOMEM;
+
+
+ err = dir_ip->i_op->create(dir_ip, new_de, mode & ~S_IFMT, false);
+ if (err) {
+ /* TODO: free new_de */
+ return err;
+ }
+
+ p.de = new_de;
+ p.mnt = parent.mnt;
+ } else {
+ /* TODO: multi-component parent lookup */
+ return -ENOENT;
+ }
+ } else if (err) {
+ return err;
+ }
+
+ /* Allocate fd */
+ int fd = alloc_fd(files, 0);
+ if (fd < 0) {
+ return fd;
+ }
+
+ /* Allocate file object */
+ file* fp = alloc_empty_file(flags);
+ if (!fp) {
+ /* TODO: release fd */
+ return -ENFILE;
+ }
+
+ fp->f_inode = p.de->d_inode;
+ fp->f_path = p;
+ fp->f_op = p.de->d_inode->i_fop;
+ fp->f_pos = 0;
+
+ /* Chardev: look up cdev by i_rdev and replace f_op */
+ if (S_ISCHR(fp->f_inode->i_mode)) {
+ extern int devfs_chrdev_open(inode* ip, file* fp);
+ err = devfs_chrdev_open(fp->f_inode, fp);
+ if (err) {
+ fput(fp);
+ return err;
+ }
+ } else if (fp->f_op && fp->f_op->open) {
+ err = fp->f_op->open(fp->f_inode, fp);
+ if (err) {
+ fput(fp);
+ return err;
+ }
+ }
+
+ /* O_TRUNC for regular files */
+ if ((flags & O_TRUNC) && S_ISREG(fp->f_inode->i_mode)) {
+ fp->f_inode->i_size = 0;
+ }
+
+ fd_install(files, fd, fp);
+ return fd;
+}
+
+/* ================================================================== */
+/* sys_close(fd) */
+/* ================================================================== */
+
+int64 sys_close(uint64 a0, uint64 /*a1*/, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+ return close_fd(files, fd);
+}
+
+/* ================================================================== */
+/* sys_read(fd, buf, count) */
+/* ================================================================== */
+
+int64 sys_read(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ char* buf = reinterpret_cast<char*>(a1);
+ uint64 count = a2;
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ if (!(fp->f_mode & FMODE_READ)) return -EBADF;
+
+ return vfs_read(fp, buf, count, &fp->f_pos);
+}
+
+/* ================================================================== */
+/* sys_write(fd, buf, count) */
+/* ================================================================== */
+
+int64 sys_write(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ const char* buf = reinterpret_cast<const char*>(a1);
+ uint64 count = a2;
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ if (!(fp->f_mode & FMODE_WRITE)) return -EBADF;
+
+ return vfs_write(fp, buf, count, &fp->f_pos);
+}
+
+/* ================================================================== */
+/* sys_lseek(fd, offset, whence) */
+/* ================================================================== */
+
+int64 sys_lseek(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ int64 offset = static_cast<int64>(a1);
+ int whence = static_cast<int>(a2);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ return vfs_llseek(fp, offset, whence);
+}
+
+/* ================================================================== */
+/* sys_fstat(fd, statbuf) */
+/* ================================================================== */
+
+int64 sys_fstat_wrapper(uint64 a0, uint64 a1, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ auto* statbuf = reinterpret_cast<kstat*>(a1);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ return vfs_fstat(fp, statbuf);
+}
+
+/* ================================================================== */
+/* sys_getdents64(fd, dirp, count) */
+/* ================================================================== */
+
+struct linux_dirent64 {
+ uint64 d_ino;
+ int64 d_off;
+ uint16 d_reclen;
+ uint8 d_type;
+ char d_name[];
+};
+
+struct getdents_ctx {
+ dir_context ctx;
+ char* buf;
+ int count;
+ int written;
+ int error;
+};
+
+static bool filldir64(dir_context* ctx, const char* name, int namelen,
+ int64 offset, uint64 ino, unsigned int d_type)
+{
+ auto* gctx = container_of(ctx, getdents_ctx, ctx);
+
+ /* Calculate record length (rounded up to 8 bytes) */
+ int reclen = static_cast<int>(
+ (sizeof(linux_dirent64) + namelen + 1 + 7) & ~7ULL);
+
+ if (gctx->written + reclen > gctx->count) {
+ return false; /* Buffer full */
+ }
+
+ auto* dirent = reinterpret_cast<linux_dirent64*>(
+ gctx->buf + gctx->written);
+
+ dirent->d_ino = ino;
+ dirent->d_off = offset + 1;
+ dirent->d_reclen = static_cast<uint16>(reclen);
+ dirent->d_type = static_cast<uint8>(d_type);
+
+ /* Copy name */
+ for (int i = 0; i < namelen; i++) {
+ dirent->d_name[i] = name[i];
+ }
+ dirent->d_name[namelen] = '\0';
+
+ gctx->written += reclen;
+ return true;
+}
+
+int64 sys_getdents64(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ char* buf = reinterpret_cast<char*>(a1);
+ int count = static_cast<int>(a2);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ if (!fp->f_op || !fp->f_op->iterate_shared) {
+ return -ENOTDIR;
+ }
+
+ getdents_ctx gctx{};
+ gctx.ctx.actor = filldir64;
+ gctx.ctx.pos = fp->f_pos;
+ gctx.buf = buf;
+ gctx.count = count;
+ gctx.written = 0;
+ gctx.error = 0;
+
+ fp->f_op->iterate_shared(fp, &gctx.ctx);
+ fp->f_pos = gctx.ctx.pos;
+
+ return gctx.written;
+}
+
+/* ================================================================== */
+/* sys_dup(oldfd) */
+/* ================================================================== */
+
+int64 sys_dup(uint64 a0, uint64 /*a1*/, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int oldfd = static_cast<int>(a0);
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, oldfd);
+ if (!fp) return -EBADF;
+
+ int newfd = alloc_fd(files, 0);
+ if (newfd < 0) return newfd;
+
+ get_file(fp);
+ fd_install(files, newfd, fp);
+ return newfd;
+}
+
+/* ================================================================== */
+/* sys_dup3(oldfd, newfd, flags) */
+/* ================================================================== */
+
+int64 sys_dup3(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int oldfd = static_cast<int>(a0);
+ int newfd = static_cast<int>(a1);
+ int flags = static_cast<int>(a2);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ return do_dup2(files, oldfd, newfd, flags);
+}
+
+/* ================================================================== */
+/* sys_getcwd(buf, size) */
+/* ================================================================== */
+
+int64 sys_getcwd(uint64 a0, uint64 a1, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ char* buf = reinterpret_cast<char*>(a0);
+ uint64 size = a1;
+
+ if (!buf || size == 0) return -EINVAL;
+
+ fs_struct* fs = current_fs();
+ if (!fs) return -ENOENT;
+
+ char tmp[256];
+ char* result = d_path(&fs->pwd, tmp, sizeof(tmp));
+ if (!result) return -ERANGE;
+
+ /* Calculate length */
+ int len = 0;
+ for (const char* s = result; *s; s++) len++;
+ len++; /* Include NUL */
+
+ if (static_cast<uint64>(len) > size) return -ERANGE;
+
+ for (int i = 0; i < len; i++) {
+ buf[i] = result[i];
+ }
+
+ return len;
+}
+
+/* ================================================================== */
+/* sys_chdir(pathname) */
+/* ================================================================== */
+
+int64 sys_chdir(uint64 a0, uint64 /*a1*/, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ const char* pathname = reinterpret_cast<const char*>(a0);
+
+ path p{};
+ int err = filename_lookup(AT_FDCWD, pathname, LOOKUP_DIRECTORY, &p);
+ if (err) return err;
+
+ if (!S_ISDIR(p.de->d_inode->i_mode)) {
+ path_put(&p);
+ return -ENOTDIR;
+ }
+
+ fs_struct* fs = current_fs();
+ if (!fs) {
+ path_put(&p);
+ return -ENOENT;
+ }
+
+ set_fs_pwd(fs, &p);
+ path_put(&p);
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_mkdirat(dirfd, pathname, mode) */
+/* ================================================================== */
+
+int64 sys_mkdirat(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int dirfd = static_cast<int>(a0);
+ const char* pathname = reinterpret_cast<const char*>(a1);
+ uint32 mode = static_cast<uint32>(a2);
+
+ /* Walk to the parent directory */
+ /* For simplicity, support only "single component" paths for now */
+ path parent{};
+ int err;
+
+ if (pathname[0] == '/') {
+ parent.de = g_root_dentry;
+ parent.mnt = g_root_mnt;
+ pathname++; /* skip leading / */
+ } else {
+ fs_struct* fs = current_fs();
+ if (!fs) return -ENOENT;
+ parent = fs->pwd;
+ (void)dirfd; /* TODO: respect dirfd for AT_FDCWD check */
+ }
+
+ inode* dir_ip = parent.de->d_inode;
+ if (!dir_ip || !dir_ip->i_op || !dir_ip->i_op->mkdir) {
+ return -EPERM;
+ }
+
+ qstr q{};
+ q.name = pathname;
+ q.len = 0;
+ for (const char* s = pathname; *s; s++) q.len++;
+ dentry* new_de = d_alloc(parent.de, &q);
+ if (!new_de) return -ENOMEM;
+
+ err = dir_ip->i_op->mkdir(dir_ip, new_de, mode | S_IFDIR);
+ if (err) {
+ return err;
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_unlinkat(dirfd, pathname, flags) */
+/* ================================================================== */
+
+int64 sys_unlinkat(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ (void)a0; (void)a1; (void)a2;
+ /* TODO: implement unlink/rmdir */
+ return -ENOSYS;
+}
+
+/* ================================================================== */
+/* sys_writev(fd, iov, iovcnt) */
+/* ================================================================== */
+
+struct iovec {
+ void* iov_base;
+ uint64 iov_len;
+};
+
+int64 sys_writev(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ const iovec* iov = reinterpret_cast<const iovec*>(a1);
+ int iovcnt = static_cast<int>(a2);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+ if (!(fp->f_mode & FMODE_WRITE)) return -EBADF;
+
+ int64 total = 0;
+ for (int i = 0; i < iovcnt; i++) {
+ if (iov[i].iov_len == 0) continue;
+ int64 ret = vfs_write(fp, static_cast<const char*>(iov[i].iov_base),
+ iov[i].iov_len, &fp->f_pos);
+ if (ret < 0) {
+ return total > 0 ? total : ret;
+ }
+ total += ret;
+ if (static_cast<uint64>(ret) < iov[i].iov_len) break;
+ }
+ return total;
+}
+
+/* ================================================================== */
+/* sys_ioctl — dispatch to file_operations */
+/* ================================================================== */
+
+int64 sys_ioctl(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ unsigned int cmd = static_cast<unsigned int>(a1);
+ uint64 arg = a2;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->files) return -EBADF;
+
+ file* fp = fcheck(curr->files, fd);
+ if (!fp) return -EBADF;
+
+ if (fp->f_op && fp->f_op->unlocked_ioctl)
+ return fp->f_op->unlocked_ioctl(fp, cmd, arg);
+
+ return -ENOTTY;
+}
+
+/* ================================================================== */
+/* sys_pipe2(fds, flags) */
+/* ================================================================== */
+
+#include <lume/pipe.h>
+
+int64 sys_pipe2(uint64 a0, uint64 a1, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int* user_fds = reinterpret_cast<int*>(a0);
+ int flags = static_cast<int>(a1);
+
+ files_struct* files = current_files();
+ if (!files) return -EMFILE;
+
+ int fds[2];
+ int err = do_pipe2(files, fds, flags);
+ if (err) return err;
+
+ user_fds[0] = fds[0];
+ user_fds[1] = fds[1];
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_fcntl(fd, cmd, arg) */
+/* ================================================================== */
+
+/* fcntl commands — Linux ABI values */
+inline constexpr int F_DUPFD = 0;
+inline constexpr int F_GETFD = 1;
+inline constexpr int F_SETFD = 2;
+inline constexpr int F_GETFL = 3;
+inline constexpr int F_SETFL = 4;
+inline constexpr int F_DUPFD_CLOEXEC = 1030;
+
+inline constexpr int FD_CLOEXEC = 1;
+
+/* Bitmap helpers (duplicated from fd.cc — TODO: move to shared header) */
+static inline bool fcntl_bit_test(const uint64* bmap, int bit) {
+ return (bmap[bit / 64] >> (bit % 64)) & 1;
+}
+static inline void fcntl_bit_set(uint64* bmap, int bit) {
+ bmap[bit / 64] |= (1ULL << (bit % 64));
+}
+static inline void fcntl_bit_clear(uint64* bmap, int bit) {
+ bmap[bit / 64] &= ~(1ULL << (bit % 64));
+}
+
+int64 sys_fcntl(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ int cmd = static_cast<int>(a1);
+ uint64 arg = a2;
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ switch (cmd) {
+ case F_DUPFD:
+ case F_DUPFD_CLOEXEC: {
+ int new_fd = alloc_fd(files, static_cast<unsigned int>(arg));
+ if (new_fd < 0) return new_fd;
+ get_file(fp);
+ fd_install(files, new_fd, fp);
+ if (cmd == F_DUPFD_CLOEXEC) {
+ LockGuard guard(files->file_lock);
+ fcntl_bit_set(files->close_on_exec_init, new_fd);
+ }
+ return new_fd;
+ }
+
+ case F_GETFD: {
+ LockGuard guard(files->file_lock);
+ return fcntl_bit_test(files->close_on_exec_init, fd) ? FD_CLOEXEC : 0;
+ }
+
+ case F_SETFD: {
+ LockGuard guard(files->file_lock);
+ if (arg & FD_CLOEXEC)
+ fcntl_bit_set(files->close_on_exec_init, fd);
+ else
+ fcntl_bit_clear(files->close_on_exec_init, fd);
+ return 0;
+ }
+
+ case F_GETFL:
+ return fp->f_flags;
+
+ case F_SETFL:
+ /* Only allow changing O_APPEND, O_NONBLOCK, O_ASYNC */
+ fp->f_flags = (fp->f_flags & ~(O_APPEND | O_NONBLOCK)) |
+ (static_cast<unsigned int>(arg) & (O_APPEND | O_NONBLOCK));
+ return 0;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+/* ================================================================== */
+/* sys_socket (SYS_socket = 198) & sys_sendto (SYS_sendto = 206) */
+/* ================================================================== */
+
+int64 sys_socket(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ /*
+ * COMPATIBILITY STUB:
+ * LumeOS currently lacks a networking stack and Unix Domain Sockets (AF_UNIX).
+ * We return -EAFNOSUPPORT (Address family not supported by protocol) as expected
+ * by Linux applications (like BusyBox syslogd connecting to /dev/log).
+ */
+ return -EAFNOSUPPORT;
+}
+
+int64 sys_sendto(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ /* COMPATIBILITY STUB */
+ return -EAFNOSUPPORT;
+}
diff --git a/na/kernel/sys_misc.cc b/na/kernel/sys_misc.cc
new file mode 100644
index 0000000..48eb47b
--- /dev/null
+++ b/na/kernel/sys_misc.cc
@@ -0,0 +1,360 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Miscellaneous System Calls — busybox bootstrap set
+ *
+ * Each syscall follows the Linux kernel semantics exactly.
+ * Reference: Linux kernel/sys.c, kernel/signal.c, kernel/time/posix-timers.c
+ */
+
+#include <lume/types.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+#include <lume/kprintf.h>
+#include <lume/klog.h>
+#include <lume/pid.h>
+#include <lume/pgrp.h>
+#include <lume/signal_types.h>
+#include <lume/tty.h>
+#include <lume/uaccess.h>
+#include <arch/cpu.h>
+
+/* ======================================================================
+ * sys_reboot — Linux kernel/reboot.c
+ *
+ * Validates magic numbers and performs the requested action.
+ * For now, we only support LINUX_REBOOT_CMD_CAD_ON/OFF (ignore)
+ * and reject everything else with -EPERM (no CAP_SYS_BOOT).
+ * ====================================================================== */
+
+static constexpr uint32 LINUX_REBOOT_MAGIC1 = 0xfee1dead;
+static constexpr uint32 LINUX_REBOOT_MAGIC2 = 672274793; /* 0x28121969 */
+static constexpr uint32 LINUX_REBOOT_MAGIC2A = 85072278;
+static constexpr uint32 LINUX_REBOOT_MAGIC2B = 369367448;
+static constexpr uint32 LINUX_REBOOT_MAGIC2C = 537993216;
+
+int64 sys_reboot(uint64 magic1, uint64 magic2, uint64 cmd,
+ uint64, uint64, uint64)
+{
+ /* Linux: kernel/reboot.c — validate magic numbers first */
+ if (static_cast<uint32>(magic1) != LINUX_REBOOT_MAGIC1)
+ return -EINVAL;
+ if (static_cast<uint32>(magic2) != LINUX_REBOOT_MAGIC2 &&
+ static_cast<uint32>(magic2) != LINUX_REBOOT_MAGIC2A &&
+ static_cast<uint32>(magic2) != LINUX_REBOOT_MAGIC2B &&
+ static_cast<uint32>(magic2) != LINUX_REBOOT_MAGIC2C)
+ return -EINVAL;
+
+ /* We don't have CAP_SYS_BOOT yet — reject with -EPERM.
+ * This matches Linux behavior for unprivileged callers.
+ * Busybox handles this gracefully and continues. */
+ (void)cmd;
+ printk(KERN_INFO "reboot: cmd=%llu (not implemented, returning -EPERM)\n", cmd);
+ return -EPERM;
+}
+
+/* ======================================================================
+ * sys_prctl — Linux kernel/sys.c
+ *
+ * Process control. Most options are optional for early boot.
+ * We implement the minimum set busybox needs.
+ * ====================================================================== */
+
+/* prctl option constants (from <linux/prctl.h>) */
+static constexpr int PR_SET_NAME = 15;
+static constexpr int PR_GET_NAME = 16;
+static constexpr int PR_SET_PDEATHSIG = 1;
+static constexpr int PR_GET_PDEATHSIG = 2;
+
+int64 sys_prctl(uint64 option, uint64 arg2, uint64 arg3,
+ uint64 arg4, uint64 arg5, uint64)
+{
+ switch (static_cast<int>(option)) {
+ case PR_SET_PDEATHSIG:
+ /* Linux: set signal delivered when parent dies.
+ * Silently accept — we don't track pdeath_signal yet. */
+ return 0;
+ case PR_GET_PDEATHSIG: {
+ /* Linux: return current pdeath_signal (0 = none) */
+ int32 zero = 0;
+ if (copy_to_user(arg2, &zero, sizeof(zero)) != 0)
+ return -EFAULT;
+ return 0;
+ }
+ case PR_SET_NAME: {
+ /* Linux: set comm (task name), max 16 chars */
+ /* TODO: copy_from_user the string */
+ return 0;
+ }
+ case PR_GET_NAME: {
+ /* Linux: get comm (task name) */
+ TaskControlBlock* curr = current_task();
+ if (!curr) return -ESRCH;
+ const char* name = curr->name ? curr->name : "";
+ uint64 len = 0;
+ while (name[len] && len < 15) len++;
+ char buf[16] = {};
+ for (uint64 i = 0; i < len; i++) buf[i] = name[i];
+ if (copy_to_user(arg2, buf, 16) != 0)
+ return -EFAULT;
+ return 0;
+ }
+ default:
+ /* Unknown prctl — Linux returns -EINVAL for unrecognized options */
+ return -EINVAL;
+ }
+}
+
+/* ======================================================================
+ * Session / Process-Group syscalls
+ *
+ * All four operate on the caller's signal_struct (or a target task's).
+ * Identity lives in signal_struct, so these become straightforward reads
+ * and pgrp-hash updates. We mirror Linux kernel/sys.c semantics closely.
+ * ====================================================================== */
+
+static TaskControlBlock* find_task_by_pid(int32 pid)
+{
+ extern list_node g_all_tasks;
+ extern Spinlock g_task_list_lock;
+ LockGuard guard(g_task_list_lock);
+ list_node* node = g_all_tasks.next;
+ while (node != &g_all_tasks) {
+ auto* task = list_entry<TaskControlBlock, &TaskControlBlock::all_link>(node);
+ if (task->pid == pid) return task;
+ node = node->next;
+ }
+ return nullptr;
+}
+
+/*
+ * sys_setsid — Linux kernel/sys.c:ksys_setsid()
+ *
+ * Caller becomes leader of a new session+pgrp. Refused if a pgrp with
+ * id == caller pid already exists with members other than the caller —
+ * that's Linux's "process group id already exists" check, which prevents
+ * collisions when a non-leader tries to setsid.
+ */
+int64 sys_setsid(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ESRCH;
+
+ /* Detect collision: another pgrp already uses our pid as pgid. */
+ bool conflict = false;
+ for_each_pgrp_member(curr->pid, [&](signal_struct* sig) {
+ if (sig != curr->signal) conflict = true;
+ });
+ if (conflict) return -EPERM;
+
+ /* Move to new pgrp (creates one if missing), update sid, drop ctty. */
+ change_pgid(curr->signal, curr->pid);
+
+ /* Detach the controlling terminal. If this task is the session
+ * leader that owns the tty (tty->session == caller's old sid),
+ * also clear the tty's session/pgrp so the next session-leader
+ * that opens the device can claim it. Without this, tty->session
+ * permanently sticks to the abandoned sid and every subsequent
+ * tty_acquire_ctty() bails with "owned by another session" —
+ * which is exactly what kept BusyBox's child sh from picking the
+ * console back up after init's setsid().
+ *
+ * Linux's behavior is similar in spirit (disassociate_ctty when
+ * the leader exits), but it triggers from exit, not setsid. Our
+ * simpler "clear-on-give-up" matches the way our acquisition path
+ * is written. */
+ tty_struct* old_tty = nullptr;
+ int32 old_sid = 0;
+ {
+ LockGuard guard(curr->signal->lock);
+ old_tty = curr->signal->tty;
+ old_sid = curr->signal->sid;
+ curr->signal->sid = curr->pid;
+ curr->signal->tty = nullptr;
+ curr->signal->tty_old_pgrp = 0;
+ }
+ if (old_tty) {
+ LockGuard guard(old_tty->lock);
+ if (old_tty->session == old_sid) {
+ old_tty->session = 0;
+ old_tty->pgrp = 0;
+ }
+ }
+ return curr->pid;
+}
+
+/*
+ * sys_setpgid — Linux kernel/sys.c:ksys_setpgid()
+ *
+ * Move a process between groups within the same session. Allowed:
+ * - caller targeting self or a child
+ * - target is not a session leader
+ * - target stays in caller's session
+ * - target hasn't exec()'d (we don't track did_exec yet, so skip)
+ * - new pgid is either == target's pid (new group) or names a pgrp
+ * that already exists in the same session
+ */
+int64 sys_setpgid(uint64 pid, uint64 pgid, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ESRCH;
+
+ int32 target_pid = static_cast<int32>(pid);
+ int32 target_pgid = static_cast<int32>(pgid);
+
+ if (target_pid == 0) target_pid = curr->pid;
+ if (target_pgid == 0) target_pgid = target_pid;
+ if (target_pgid < 0) return -EINVAL;
+
+ TaskControlBlock* target = (target_pid == curr->pid)
+ ? curr
+ : find_task_by_pid(target_pid);
+ if (!target || !target->signal) return -ESRCH;
+
+ /* Target must be caller or a child of caller. */
+ if (target != curr && target->parent != curr) return -ESRCH;
+
+ /* Same session as caller. */
+ if (target->signal->sid != curr->signal->sid) return -EPERM;
+
+ /* Cannot move a session leader into a different pgrp. */
+ if (target->signal->sid == target->pid) return -EPERM;
+
+ /* If target_pgid != target_pid, the pgrp must already exist in
+ * the same session. pgrp existence == any signal_struct with
+ * pgid == target_pgid AND sid == caller's sid. */
+ if (target_pgid != target->pid) {
+ bool found = false;
+ for_each_pgrp_member(target_pgid, [&](signal_struct* sig) {
+ if (sig->sid == curr->signal->sid) found = true;
+ });
+ if (!found) return -EPERM;
+ }
+
+ change_pgid(target->signal, target_pgid);
+ return 0;
+}
+
+/*
+ * sys_getpgid — return process group ID of `pid` (0 = self).
+ */
+int64 sys_getpgid(uint64 pid, uint64, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ESRCH;
+
+ if (pid == 0) return curr->signal->pgid;
+
+ TaskControlBlock* target = find_task_by_pid(static_cast<int32>(pid));
+ if (!target || !target->signal) return -ESRCH;
+ return target->signal->pgid;
+}
+
+/*
+ * sys_getsid — return session ID of `pid` (0 = self).
+ */
+int64 sys_getsid(uint64 pid, uint64, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ESRCH;
+
+ if (pid == 0) return curr->signal->sid;
+
+ TaskControlBlock* target = find_task_by_pid(static_cast<int32>(pid));
+ if (!target || !target->signal) return -ESRCH;
+
+ /* Linux: -EPERM if target's session differs from caller's and we're
+ * in a different namespace. We have one namespace, so always allow. */
+ return target->signal->sid;
+}
+
+/* ======================================================================
+ * sys_geteuid / sys_getegid — Linux kernel/sys.c
+ *
+ * Return effective UID/GID. We run everything as root (0).
+ * ====================================================================== */
+
+int64 sys_geteuid(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ /* Linux: return current_euid(). We are always root. */
+ return 0;
+}
+
+int64 sys_getegid(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ /* Linux: return current_egid(). We are always root. */
+ return 0;
+}
+
+/* ======================================================================
+ * sys_sigaltstack — Linux kernel/signal.c
+ *
+ * Set/get alternate signal stack. For now, accept and ignore
+ * (busybox doesn't critically need it for shell operation).
+ * ====================================================================== */
+
+int64 sys_sigaltstack(uint64 uss_ptr, uint64 uoss_ptr,
+ uint64, uint64, uint64, uint64)
+{
+ /* Linux: if uoss != NULL, return current alt stack info.
+ * We don't have alt stacks yet — report SS_DISABLE. */
+ if (uoss_ptr != 0) {
+ /* stack_t { void* ss_sp; int ss_flags; size_t ss_size; }
+ * On RISC-V LP64: 8 + 4 + padding + 8 = 24 bytes typically,
+ * but the ABI packs it as {uint64, int32, pad, uint64}. */
+ struct {
+ uint64 ss_sp;
+ int32 ss_flags;
+ int32 _pad;
+ uint64 ss_size;
+ } oss = { 0, 2 /* SS_DISABLE */, 0, 0 };
+ if (copy_to_user(uoss_ptr, &oss, sizeof(oss)) != 0)
+ return -EFAULT;
+ }
+
+ /* uss: silently accept (we'll need it for proper signal handling later) */
+ (void)uss_ptr;
+ return 0;
+}
+
+/* ======================================================================
+ * sys_clock_gettime — Linux kernel/time/posix-timers.c
+ *
+ * Get the current time for a given clock.
+ * We use the RISC-V cycle counter as a monotonic source.
+ * ====================================================================== */
+
+static constexpr int CLOCK_REALTIME = 0;
+static constexpr int CLOCK_MONOTONIC = 1;
+
+int64 sys_clock_gettime(uint64 clk_id, uint64 tp_ptr,
+ uint64, uint64, uint64, uint64)
+{
+ if (tp_ptr == 0) return -EFAULT;
+
+ /* Read RISC-V time CSR (mtime mapped to SBI) */
+ uint64 ticks = arch::cpu::read_time();
+
+ /* Convert to seconds + nanoseconds.
+ * QEMU virt timebase-frequency is 10MHz. */
+ constexpr uint64 kTimebaseHz = 10'000'000ULL;
+ uint64 sec = ticks / kTimebaseHz;
+ uint64 nsec = (ticks % kTimebaseHz) * (1'000'000'000ULL / kTimebaseHz);
+
+ struct timespec64 {
+ int64 tv_sec;
+ int64 tv_nsec;
+ };
+
+ timespec64 ts;
+ ts.tv_sec = static_cast<int64>(sec);
+ ts.tv_nsec = static_cast<int64>(nsec);
+
+ (void)clk_id; /* We treat all clocks as monotonic for now */
+
+ if (copy_to_user(tp_ptr, &ts, sizeof(ts)) != 0)
+ return -EFAULT;
+
+ return 0;
+}
diff --git a/na/kernel/sys_mm.cc b/na/kernel/sys_mm.cc
index ff03e96..bfa067c 100644
--- a/na/kernel/sys_mm.cc
+++ b/na/kernel/sys_mm.cc
@@ -15,6 +15,7 @@
#include <lume/errno.h>
#include <lume/mman.h>
#include <lume/config.h>
+#include <lume/klog.h>
/* Page alignment helpers */
@@ -78,6 +79,9 @@ int64 sys_brk(uint64 new_brk, uint64, uint64, uint64, uint64, uint64)
int64 sys_mmap(uint64 addr, uint64 length, uint64 prot,
uint64 flags, uint64 fd, uint64 offset)
{
+ printk("[mmap] addr=%llx len=%llx prot=%llx flags=%llx fd=%lld off=%llx\n",
+ addr, length, prot, flags, static_cast<int64>(fd), offset);
+
if (length == 0) return -EINVAL;
length = page_align_up(length);
if (offset % kPageSize != 0) return -EINVAL;
@@ -89,8 +93,10 @@ int64 sys_mmap(uint64 addr, uint64 length, uint64 prot,
if (!mm) return -ENOMEM;
/* Phase 2: only anonymous private mappings */
- if (!(flags & MAP_ANONYMOUS))
+ if (!(flags & MAP_ANONYMOUS)) {
+ printk("[mmap] REJECTED: file-backed mmap not implemented\n");
return -ENOSYS; /* File-backed mmap is Phase 3 */
+ }
if (flags & MAP_SHARED)
return -ENOSYS; /* Shared mappings are Phase 3 */
diff --git a/na/kernel/sys_process.cc b/na/kernel/sys_process.cc
index 1759a81..438ad1b 100644
--- a/na/kernel/sys_process.cc
+++ b/na/kernel/sys_process.cc
@@ -11,6 +11,7 @@
* Reference: docs/specs/syscall.md §7.1
*/
+#include "lume/klog.h"
#include <lume/types.h>
#include <lume/task.h>
#include <lume/sched.h>
@@ -20,8 +21,10 @@
/* External: task lifecycle (kernel/task.cc) */
extern void task_exit(int code);
-extern void task_destroy(TaskControlBlock* task);
-extern int32 task_wait(int32* exit_code);
+extern int32 do_wait4(int32 wait_pid, int options, int32* exit_code);
+extern int32 task_clone(arch::TrapFrame* tf, uint64 clone_flags,
+ uint64 child_stack, uint64 parent_tidptr,
+ uint64 child_tidptr, uint64 tls);
extern int32 task_fork(arch::TrapFrame* tf);
/* ======================================================================
@@ -75,14 +78,10 @@ int64 sys_sched_yield_wrapper(uint64, uint64, uint64, uint64, uint64, uint64)
int64 sys_clone_wrapper(uint64 flags, uint64 child_stack,
uint64 ptid, uint64 ctid, uint64 tls, uint64)
{
- /* Phase A: Simple fork() support.
- * Ignore flags/stack for now and just clone the current task. */
- (void)flags; (void)child_stack; (void)ptid; (void)ctid; (void)tls;
-
arch::TrapFrame* tf = current_task()->trapframe;
if (!tf) return -EINVAL;
- return task_fork(tf);
+ return task_clone(tf, flags, child_stack, ptid, ctid, tls);
}
/* ========================================================================
@@ -92,23 +91,16 @@ int64 sys_clone_wrapper(uint64 flags, uint64 child_stack,
int64 sys_wait4_wrapper(uint64 pid, uint64 wstatus_ptr,
uint64 options, uint64, uint64, uint64)
{
- /* Phase A: Basic wait() support.
- * TODO: Implement pid-specific wait and WNOHANG in task_wait. */
- (void)pid; (void)options;
-
int32 exit_code = 0;
- int32 child_pid = task_wait(&exit_code);
+ int32 child_pid = do_wait4(static_cast<int32>(pid),
+ static_cast<int>(options),
+ &exit_code);
if (child_pid < 0) return child_pid;
+ /* exit_code is already in Linux wstatus format */
if (wstatus_ptr != 0) {
- int wstatus = 0;
- if (exit_code & kExitCodeSignalFlag) {
- wstatus = exit_code & 0x7F;
- } else {
- wstatus = W_EXITCODE(exit_code);
- }
- if (copy_to_user(wstatus_ptr, &wstatus, sizeof(int)) < 0)
+ if (copy_to_user(wstatus_ptr, &exit_code, sizeof(int)) < 0)
return -EFAULT;
}
@@ -122,9 +114,9 @@ int64 sys_wait4_wrapper(uint64 pid, uint64 wstatus_ptr,
int64 sys_set_tid_address(uint64 tidptr, uint64, uint64,
uint64, uint64, uint64)
{
- /* Phase 2: store the clear_child_tid pointer (needed for futex wakeup).
- * For now just return current tid. */
- (void)tidptr;
+ /* Store clear_child_tid pointer — on exit, kernel writes 0 here
+ * and does futex_wake (enables pthread_join). */
+ current_task()->clear_child_tid = tidptr;
return current_task()->pid;
}
@@ -141,3 +133,53 @@ int64 sys_getgid(uint64, uint64, uint64, uint64, uint64, uint64)
{
return 0; /* root */
}
+
+/* ======================================================================
+ * sys_nanosleep (SYS_nanosleep = 101)
+ * ====================================================================== */
+#include <lume/wait.h>
+#include <lume/kprintf.h>
+#include <lume/timer.h>
+
+struct linux_timespec {
+ int64 tv_sec;
+ int64 tv_nsec;
+};
+
+static void nanosleep_wakeup(TimerList* t) {
+ TaskControlBlock* task = static_cast<TaskControlBlock*>(t->data);
+ task->state = TaskState::Runnable;
+ sched_enqueue(task);
+}
+
+int64 sys_nanosleep(uint64 req_ptr, uint64 rem_ptr, uint64, uint64, uint64, uint64) {
+ if (req_ptr == 0) return -EINVAL;
+
+ linux_timespec req;
+ if (copy_from_user(&req, req_ptr, sizeof(req)) != 0) {
+ return -EFAULT;
+ }
+
+ uint64 target = *(volatile uint64*)&g_jiffies + (req.tv_sec * 100) + (req.tv_nsec / 10000000);
+
+ TaskControlBlock* curr = current_task();
+ curr->state = TaskState::Sleeping;
+
+ TimerList t;
+ t.init(nanosleep_wakeup);
+ t.expires = target;
+ t.data = curr;
+
+ add_timer(&t);
+
+ if (*(volatile uint64*)&g_jiffies < target) {
+ schedule();
+ } else {
+ curr->state = TaskState::Running;
+ }
+
+
+ del_timer(&t);
+
+ return 0;
+}
diff --git a/na/kernel/sys_signal.cc b/na/kernel/sys_signal.cc
new file mode 100644
index 0000000..8b459d5
--- /dev/null
+++ b/na/kernel/sys_signal.cc
@@ -0,0 +1,351 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * sys_signal.cc — Signal-related system calls
+ *
+ * Implements:
+ * sys_kill (SYS_kill = 129)
+ * sys_rt_sigaction (SYS_rt_sigaction = 134)
+ * sys_rt_sigprocmask (SYS_rt_sigprocmask = 135)
+ * sys_rt_sigreturn (SYS_rt_sigreturn = 139)
+ *
+ * Reference: Linux kernel/signal.c, arch/riscv/kernel/signal.c
+ */
+
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+#include <lume/uaccess.h>
+#include <lume/kprintf.h>
+#include <lume/klog.h>
+#include <lume/new.h>
+#include <lume/pgrp.h>
+
+/* From signal.cc */
+extern int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task);
+
+/* From task.cc */
+extern list_node g_all_tasks;
+extern Spinlock g_task_list_lock;
+
+/* ================================================================== */
+/* Helper: find task by PID */
+/* ================================================================== */
+
+static TaskControlBlock* find_task_by_pid(int32 pid)
+{
+ LockGuard guard(g_task_list_lock);
+ list_node* node = g_all_tasks.next;
+ while (node != &g_all_tasks) {
+ auto* task = list_entry<TaskControlBlock, &TaskControlBlock::all_link>(node);
+ if (task->pid == pid) return task;
+ node = node->next;
+ }
+ return nullptr;
+}
+
+/* ================================================================== */
+/* sys_kill — send a signal to a process */
+/* ================================================================== */
+
+/*
+ * Deliver `sig` to the thread-group leader represented by `sig_st`.
+ * sighand is allocated eagerly at fork/exec time, so we just trust
+ * that the leader has one; send_signal returns -ESRCH if not.
+ */
+static int deliver_to_group(signal_struct* sig_st, int sig,
+ kernel_siginfo* info)
+{
+ if (!sig_st || !sig_st->leader) return 0;
+ if (send_signal(sig, info, sig_st->leader) == 0) return 1;
+ return 0;
+}
+
+int64 sys_kill(uint64 a0, uint64 a1, uint64, uint64, uint64, uint64)
+{
+ int32 pid = static_cast<int32>(a0);
+ int sig = static_cast<int>(a1);
+
+ if (sig < 0 || sig > _NSIG) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr) return -ESRCH;
+
+ kernel_siginfo info{};
+ info.si_signo = sig;
+ info.si_code = 0; /* SI_USER */
+ info._sifields._kill._pid = curr->pid;
+ info._sifields._kill._uid = 0;
+
+ /* pid > 0: deliver to that thread group's leader. */
+ if (pid > 0) {
+ TaskControlBlock* target = find_task_by_pid(pid);
+ if (!target) return -ESRCH;
+ /* Group signal: prefer the thread-group leader if available. */
+ if (target->signal && target->signal->leader)
+ target = target->signal->leader;
+ if (sig == 0) return 0;
+ return send_signal(sig, &info, target);
+ }
+
+ /* pid == 0: every member of caller's process group. */
+ if (pid == 0) {
+ if (!curr->signal) return -ESRCH;
+ if (sig == 0) return 0;
+ int delivered = 0;
+ int32 target_pgid = curr->signal->pgid;
+ for_each_pgrp_member(target_pgid, [&](signal_struct* s) {
+ delivered += deliver_to_group(s, sig, &info);
+ });
+ return delivered > 0 ? 0 : -ESRCH;
+ }
+
+ /* pid == -1: every process the caller may signal, except init.
+ * We approximate "every" with "every thread-group leader on every
+ * pgrp bucket". This is rare in practice (BusyBox doesn't use it
+ * during normal shell operation). */
+ if (pid == -1) {
+ if (sig == 0) return 0;
+ int delivered = 0;
+ LockGuard guard(g_pgrp_lock);
+ for (auto& bucket : g_pgrp_hash) {
+ list_node* node = bucket.next;
+ while (node != &bucket) {
+ list_node* next = node->next;
+ auto* s = list_entry<signal_struct,
+ &signal_struct::pgrp_link>(node);
+ if (s->leader && s->leader->pid != 1)
+ delivered += deliver_to_group(s, sig, &info);
+ node = next;
+ }
+ }
+ return delivered > 0 ? 0 : -ESRCH;
+ }
+
+ /* pid < -1: every member of pgrp |pid|. */
+ int32 target_pgid = -pid;
+ if (sig == 0) {
+ bool any = false;
+ for_each_pgrp_member(target_pgid, [&](signal_struct*) { any = true; });
+ return any ? 0 : -ESRCH;
+ }
+ int delivered = 0;
+ for_each_pgrp_member(target_pgid, [&](signal_struct* s) {
+ delivered += deliver_to_group(s, sig, &info);
+ });
+ return delivered > 0 ? 0 : -ESRCH;
+}
+
+/* ================================================================== */
+/* sys_tkill / sys_tgkill — send a signal to a specific thread */
+/* ================================================================== */
+
+static int64 do_tkill(int32 tgid, int32 tid, int sig)
+{
+ if (sig < 0 || sig > _NSIG) return -EINVAL;
+ if (tid <= 0) return -EINVAL;
+ TaskControlBlock* target = find_task_by_pid(tid);
+ if (!target) return -ESRCH;
+ /* tgid == -1 from tkill means "any tgid"; otherwise enforce match. */
+ if (tgid != -1 && target->tgid != tgid) return -ESRCH;
+
+ if (sig == 0) return 0;
+
+ kernel_siginfo info{};
+ info.si_signo = sig;
+ info.si_code = 0; /* SI_TKILL would be -6; userland rarely checks */
+ info._sifields._kill._pid = current_task() ? current_task()->pid : 0;
+ info._sifields._kill._uid = 0;
+ return send_signal(sig, &info, target);
+}
+
+int64 sys_tkill(uint64 a0, uint64 a1, uint64, uint64, uint64, uint64)
+{
+ return do_tkill(-1,
+ static_cast<int32>(a0),
+ static_cast<int>(a1));
+}
+
+int64 sys_tgkill(uint64 a0, uint64 a1, uint64 a2, uint64, uint64, uint64)
+{
+ return do_tkill(static_cast<int32>(a0),
+ static_cast<int32>(a1),
+ static_cast<int>(a2));
+}
+
+/* ================================================================== */
+/* sys_rt_sigaction — get/set signal disposition */
+/* ================================================================== */
+
+/*
+ * User-space sigaction struct layout (RISC-V LP64):
+ * uint64 sa_handler;
+ * uint64 sa_flags;
+ * uint64 sa_restorer; (SA_RESTORER)
+ * sigset_t sa_mask; (8 bytes on LP64)
+ */
+struct user_sigaction {
+ uint64 sa_handler;
+ uint64 sa_flags;
+ uint64 sa_restorer;
+ sigset_t sa_mask;
+};
+
+int64 sys_rt_sigaction(uint64 a0, uint64 a1, uint64 a2, uint64 a3,
+ uint64, uint64)
+{
+ int sig = static_cast<int>(a0);
+ uint64 act_uaddr = a1; /* const struct sigaction __user * */
+ uint64 oact_uaddr = a2; /* struct sigaction __user * */
+ uint64 sigsetsize = a3;
+
+ if (sig < 1 || sig > _NSIG) return -EINVAL;
+ if (sig_kernel_only(sig)) return -EINVAL;
+ if (sigsetsize != sizeof(sigset_t)) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->sighand) return -EINVAL;
+
+ /* All reads/writes of the action table happen under siglock —
+ * Linux's do_sigaction() also takes the same lock (kernel/signal.c). */
+ user_sigaction old{};
+ user_sigaction act{};
+ bool have_new = (act_uaddr != 0);
+
+ if (have_new) {
+ int err = copy_from_user(&act, act_uaddr, sizeof(act));
+ if (err < 0) return -EFAULT;
+ }
+
+ {
+ LockGuard guard(curr->sighand->siglock);
+ kernel_sigaction* ksa = &curr->sighand->action[sig - 1];
+ old.sa_handler = ksa->sa_handler;
+ old.sa_flags = ksa->sa_flags;
+ old.sa_restorer = ksa->sa_restorer;
+ old.sa_mask = ksa->sa_mask;
+
+ if (have_new) {
+ ksa->sa_handler = act.sa_handler;
+ ksa->sa_flags = act.sa_flags;
+ ksa->sa_restorer = act.sa_restorer;
+ ksa->sa_mask = act.sa_mask;
+ ksa->sa_mask.del(SIGKILL);
+ ksa->sa_mask.del(SIGSTOP);
+ }
+ }
+
+ if (oact_uaddr != 0) {
+ int err = copy_to_user(oact_uaddr, &old, sizeof(old));
+ if (err < 0) return -EFAULT;
+ }
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_rt_sigprocmask — change signal blocked mask */
+/* ================================================================== */
+
+int64 sys_rt_sigprocmask(uint64 a0, uint64 a1, uint64 a2, uint64 a3,
+ uint64, uint64)
+{
+ int how = static_cast<int>(a0);
+ uint64 set_uaddr = a1;
+ uint64 oset_uaddr = a2;
+ uint64 sigsetsize = a3;
+
+ if (sigsetsize != sizeof(sigset_t)) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->sighand) return -EINVAL;
+
+ sigset_t newset{};
+ bool have_new = (set_uaddr != 0);
+ if (have_new) {
+ int err = copy_from_user(&newset, set_uaddr, sizeof(sigset_t));
+ if (err < 0) return -EFAULT;
+ newset.del(SIGKILL);
+ newset.del(SIGSTOP);
+ }
+
+ sigset_t old{};
+ {
+ /* Linux: kernel/signal.c:do_sigprocmask() also holds siglock
+ * across read+update so the snapshot is atomic vs. concurrent
+ * signal delivery. */
+ LockGuard guard(curr->sighand->siglock);
+ old = curr->blocked;
+ if (have_new) {
+ switch (how) {
+ case SIG_BLOCK:
+ curr->blocked = curr->blocked | newset;
+ break;
+ case SIG_UNBLOCK:
+ curr->blocked = curr->blocked & ~newset;
+ break;
+ case SIG_SETMASK:
+ curr->blocked = newset;
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+ }
+
+ if (oset_uaddr != 0) {
+ int err = copy_to_user(oset_uaddr, &old, sizeof(sigset_t));
+ if (err < 0) return -EFAULT;
+ }
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_rt_sigreturn — restore context after signal handler */
+/* ================================================================== */
+
+/* Arch-specific: implemented in arch/riscv/signal.cc */
+extern int64 arch_sigreturn(arch::TrapFrame* tf);
+
+int64 sys_rt_sigreturn(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->trapframe) return -EINVAL;
+
+ return arch_sigreturn(curr->trapframe);
+}
+
+/* ================================================================== */
+/* sys_rt_sigtimedwait (SYS_rt_sigtimedwait = 137) */
+/* ================================================================== */
+#include <lume/timer.h>
+#include <lume/sched.h>
+
+struct linux_timespec {
+ int64 tv_sec;
+ int64 tv_nsec;
+};
+
+int64 sys_rt_sigtimedwait(uint64 /* set_ptr */, uint64 /* info_ptr */, uint64 timeout_ptr, uint64 /* sigsetsize */, uint64, uint64)
+{
+ /*
+ * COMPATIBILITY STUB:
+ * LumeOS lacks full real-time signal queues and timed waiting mechanism.
+ * When BusyBox init runs out of children to reap, it calls this to wait
+ * for incoming signals (like SIGCHLD) with a timeout.
+ * We simulate the timeout by yielding the CPU, then return -EAGAIN (Try again)
+ * which tells user space that the timeout expired without receiving a signal.
+ */
+ if (timeout_ptr != 0) {
+ linux_timespec req;
+ if (copy_from_user(&req, timeout_ptr, sizeof(req)) == 0) {
+ uint64 target = g_jiffies + (req.tv_sec * 100) + (req.tv_nsec / 10000000);
+ while (g_jiffies < target) {
+ schedule();
+ }
+ }
+ }
+ return -EAGAIN;
+}
diff --git a/na/kernel/syscall.cc b/na/kernel/syscall.cc
index 4dfbb5d..a5b80ec 100644
--- a/na/kernel/syscall.cc
+++ b/na/kernel/syscall.cc
@@ -15,6 +15,8 @@
#include <lume/errno.h>
#include <lume/kprintf.h>
#include <lume/klog.h>
+#include <lume/console.h>
+#include <arch/cpu.h>
/* ======================================================================
* Jump table
@@ -30,12 +32,23 @@ static SyscallHandler sys_call_table[kNrSyscalls];
int64 syscall_dispatch(uint64 nr, uint64 args[6])
{
- if (nr >= static_cast<uint64>(kNrSyscalls))
+ if (nr >= static_cast<uint64>(kNrSyscalls)) {
+ kprintf("[syscall] nr=%llu out of range\n", nr);
return -ENOSYS;
+ }
+
+ /* Per-syscall entry trace. KLOG_DEBUG keeps it out of the console
+ * by default; enable via klog_set_level(KLOG_DEBUG) or read the
+ * ring buffer to investigate. Subsys mask gates this further. */
+ pr_debug_s(KLOG_SUBSYS_SYSCALL,
+ "syscall: nr=%llu a0=%llx a1=%llx a2=%llx a3=%llx\n",
+ nr, args[0], args[1], args[2], args[3]);
SyscallHandler handler = sys_call_table[nr];
- if (!handler)
+ if (!handler) {
+ kprintf("[syscall] unimplemented nr=%llu\n", nr);
return -ENOSYS;
+ }
return handler(args[0], args[1], args[2],
args[3], args[4], args[5]);
@@ -57,6 +70,7 @@ int64 sys_wait4_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
int64 sys_set_tid_address(uint64, uint64, uint64, uint64, uint64, uint64);
int64 sys_getuid(uint64, uint64, uint64, uint64, uint64, uint64);
int64 sys_getgid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_nanosleep(uint64, uint64, uint64, uint64, uint64, uint64);
/* sys_mm.cc */
int64 sys_brk(uint64, uint64, uint64, uint64, uint64, uint64);
@@ -67,6 +81,52 @@ int64 sys_mprotect(uint64, uint64, uint64, uint64, uint64, uint64);
/* sys_info.cc */
int64 sys_uname(uint64, uint64, uint64, uint64, uint64, uint64);
+/* sys_fs.cc */
+int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_lseek(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_fstat_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getdents64(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_dup(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_dup3(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getcwd(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_chdir(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_unlinkat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_writev(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_ioctl(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_ppoll(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_pipe2(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_fcntl(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_socket(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_sendto(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* sys_signal.cc */
+int64 sys_kill(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_tkill(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_tgkill(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_rt_sigaction(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_rt_sigprocmask(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_rt_sigreturn(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_rt_sigtimedwait(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* exec.cc */
+int64 sys_execve_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* sys_misc.cc */
+int64 sys_reboot(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_prctl(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_setsid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getsid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_setpgid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getpgid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_geteuid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getegid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_sigaltstack(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_clock_gettime(uint64, uint64, uint64, uint64, uint64, uint64);
+
/* ======================================================================
* syscall_init — register all handlers
* ====================================================================== */
@@ -95,5 +155,54 @@ void syscall_init()
sys_call_table[SYS_mprotect] = sys_mprotect;
sys_call_table[SYS_uname] = sys_uname;
- printk(KERN_INFO "init: syscall_init done (16 handlers registered)\n");
+ /* Phase 3: filesystem */
+ sys_call_table[SYS_openat] = sys_openat;
+ sys_call_table[SYS_close] = sys_close;
+ sys_call_table[SYS_read] = sys_read;
+ sys_call_table[SYS_write] = sys_write;
+ sys_call_table[SYS_lseek] = sys_lseek;
+ sys_call_table[SYS_fstat] = sys_fstat_wrapper;
+ sys_call_table[SYS_getdents64] = sys_getdents64;
+ sys_call_table[SYS_dup] = sys_dup;
+ sys_call_table[SYS_dup3] = sys_dup3;
+ sys_call_table[SYS_getcwd] = sys_getcwd;
+ sys_call_table[SYS_chdir] = sys_chdir;
+ sys_call_table[SYS_mkdirat] = sys_mkdirat;
+ sys_call_table[SYS_unlinkat] = sys_unlinkat;
+ sys_call_table[SYS_writev] = sys_writev;
+ sys_call_table[SYS_ioctl] = sys_ioctl;
+ sys_call_table[SYS_ppoll] = sys_ppoll;
+
+ /* Phase 4: signals */
+ sys_call_table[SYS_kill] = sys_kill;
+ sys_call_table[SYS_tkill] = sys_tkill;
+ sys_call_table[SYS_tgkill] = sys_tgkill;
+ sys_call_table[SYS_rt_sigaction] = sys_rt_sigaction;
+ sys_call_table[SYS_rt_sigprocmask] = sys_rt_sigprocmask;
+ sys_call_table[SYS_rt_sigreturn] = sys_rt_sigreturn;
+ sys_call_table[SYS_rt_sigtimedwait] = sys_rt_sigtimedwait;
+
+ /* Phase 5: exec */
+ sys_call_table[SYS_execve] = sys_execve_wrapper;
+
+ /* Phase 6: pipe + fcntl */
+ sys_call_table[SYS_pipe2] = sys_pipe2;
+ sys_call_table[SYS_fcntl] = sys_fcntl;
+
+ /* Phase 7: busybox bootstrap */
+ sys_call_table[SYS_reboot] = sys_reboot;
+ sys_call_table[SYS_prctl] = sys_prctl;
+ sys_call_table[SYS_setsid] = sys_setsid;
+ sys_call_table[SYS_getsid] = sys_getsid;
+ sys_call_table[SYS_setpgid] = sys_setpgid;
+ sys_call_table[SYS_getpgid] = sys_getpgid;
+ sys_call_table[SYS_geteuid] = sys_geteuid;
+ sys_call_table[SYS_getegid] = sys_getegid;
+ sys_call_table[SYS_sigaltstack] = sys_sigaltstack;
+ sys_call_table[SYS_clock_gettime] = sys_clock_gettime;
+ sys_call_table[SYS_nanosleep] = sys_nanosleep;
+ sys_call_table[SYS_socket] = sys_socket;
+ sys_call_table[SYS_sendto] = sys_sendto;
+
+ printk(KERN_INFO "init: syscall_init done (%d handlers registered)\n", 46);
}
diff --git a/na/kernel/task.cc b/na/kernel/task.cc
index 9f09d62..9f9b043 100644
--- a/na/kernel/task.cc
+++ b/na/kernel/task.cc
@@ -20,8 +20,18 @@
#include <lume/vmm.h>
#include <lume/errno.h>
#include <lume/pid.h>
+#include <lume/wait.h>
+#include <lume/clone.h>
+#include <lume/signal_nr.h>
#include <arch/cpu.h>
#include <arch/context.h>
+#include <lume/uaccess.h>
+#include <lume/fs_struct.h>
+#include <lume/fdtable.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/pgrp.h>
+#include <arch/pmap.h>
#include <kernel/sync/spinlock.h>
/* ======================================================================
@@ -57,11 +67,16 @@ void task_init()
}
/* ======================================================================
- * Internal: allocate TCB + kernel stack
+ * alloc_task — allocate TCB + kernel stack
+ *
+ * This is the SINGLE canonical TCB initializer. Both kthread_create
+ * (task.cc) and sched_init (sched_core.cc) must use this function.
+ * NEVER duplicate this logic — adding fields to TaskControlBlock
+ * requires updating only this function.
* ====================================================================== */
-static TaskControlBlock* alloc_task(const char* name, uint8 priority,
- uint8 flags)
+TaskControlBlock* alloc_task(const char* name, uint8 priority,
+ uint8 flags, uint8 cpu)
{
auto* task = new TaskControlBlock;
if (!task) return nullptr;
@@ -80,23 +95,45 @@ static TaskControlBlock* alloc_task(const char* name, uint8 priority,
/* Write stack canary at bottom */
*reinterpret_cast<uint64*>(task->kstack_va) = kStackCanary;
+ /* Place TrapFrame at the top of the kernel stack (like Linux pt_regs).
+ * ALL tasks — kernel threads and user processes — have a TrapFrame
+ * here. kthread_create sets the initial SP below the TrapFrame so
+ * the kthread function never touches this reserved area.
+ * kernel_execve / do_execve can later fill it in for user-mode return. */
+ uint64 tf_addr = task->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
+ task->trapframe = reinterpret_cast<arch::TrapFrame*>(tf_addr);
+ __builtin_memset(task->trapframe, 0, sizeof(arch::TrapFrame));
+
/* Basic fields */
task->name = name;
task->priority = priority;
task->time_slice = priority_to_timeslice(priority);
task->state = TaskState::Runnable;
- task->cpu_id = static_cast<uint8>(arch::cpu::id());
+ task->cpu_id = cpu;
task->flags = flags;
task->exit_code = 0;
+ task->exit_signal = 0;
+ task->preempt_count = 0;
+ task->clear_child_tid = 0;
+ task->stopped_code = 0;
+ task->notify_continued = false;
task->parent = nullptr;
task->mm = nullptr;
- task->trapframe = nullptr;
+ task->fpstate = {};
+ task->files = nullptr;
+ task->fs = nullptr;
+ task->pending.init();
+ task->blocked.clear();
+ task->sighand = nullptr;
+ task->signal = nullptr;
/* Init all list nodes */
task->run_link.init();
task->all_link.init();
task->children.init();
task->sibling.init();
+ task->lock.init("task");
task->wait_chld.init("wait_chld");
/* Default sched class */
@@ -143,7 +180,8 @@ TaskControlBlock* kthread_create(const char* name,
void* arg,
uint8 priority)
{
- TaskControlBlock* task = alloc_task(name, priority, TF_KTHREAD);
+ TaskControlBlock* task = alloc_task(name, priority, TF_KTHREAD,
+ static_cast<uint8>(arch::cpu::id()));
if (!task) return nullptr;
/* Allocate PID */
@@ -156,19 +194,30 @@ TaskControlBlock* kthread_create(const char* name,
}
task->tgid = task->pid;
- /* Set up context for kthread_entry_trampoline */
- uint64 stack_top = task->kstack_va + kKernelStackSize;
+ /* Kernel threads: exit_signal = -1 → auto-reap on exit.
+ * Linux: kernel_thread() passes (flags & ~CSIGNAL) → exit_signal = 0,
+ * but kthreads' parent (kthreadd) reaps them. We have no kthreadd,
+ * so we use -1 which makes exit_notify() call release_task() directly,
+ * identical to Linux's CLONE_THREAD autoreap path. */
+ task->exit_signal = -1;
+
+ /* Set up context for kthread_entry_trampoline.
+ * SP starts BELOW the TrapFrame reserved at the stack top.
+ * This ensures the kthread function never clobbers the TrapFrame area. */
+ uint64 stack_top = task->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
task->context = {};
task->context.ra = reinterpret_cast<uint64>(kthread_entry_trampoline);
task->context.sp = stack_top;
task->context.s[0] = reinterpret_cast<uint64>(entry);
task->context.s[1] = reinterpret_cast<uint64>(arg);
- /* Parent is kernel_task (PID 0) */
- task->parent = g_kernel_task;
- if (g_kernel_task) {
- LockGuard guard(g_task_list_lock);
- list_node* head = &g_kernel_task->children;
+ /* Parent is kernel_task (PID 0) or current caller */
+ TaskControlBlock* parent = g_kernel_task ? g_kernel_task : current_task();
+ task->parent = parent;
+ if (parent) {
+ LockGuard guard(parent->lock);
+ list_node* head = &parent->children;
task->sibling.prev = head->prev;
task->sibling.next = head;
head->prev->next = &task->sibling;
@@ -191,18 +240,32 @@ void task_exit(int code)
TaskControlBlock* curr = current_task();
if (!curr) kernel_panic("task_exit: no current task");
- curr->exit_code = code;
+ /* Linux ABI: normal exit encodes status as (code & 0xFF) << 8 */
+ curr->exit_code = (code & 0xFF) << 8;
- /* Release user address space if we're the last reference */
- if (curr->mm) {
- curr->mm->destroy();
+ /* CLONE_CHILD_CLEARTID / set_tid_address: write 0 to user tidptr.
+ * This is how pthread_join detects thread termination.
+ * TODO: futex_wake(clear_child_tid, 1) after writing 0. */
+ if (curr->clear_child_tid != 0 && curr->mm != nullptr) {
+ int32 zero = 0;
+ copy_to_user(curr->clear_child_tid, &zero, sizeof(zero));
+ curr->clear_child_tid = 0;
+ }
+
+ /* Release user address space (refcount-safe).
+ * MUST switch to kernel page table FIRST — otherwise we're running
+ * on a freed page table after put() deallocates it. */
+ if (curr->mm != nullptr) {
+ extern uint64 g_kernel_pgtbl;
+ pmap::activate(g_kernel_pgtbl);
+ curr->mm->put();
curr->mm = nullptr;
}
/* Reparent children to init (or kernel_task if no init yet) */
TaskControlBlock* foster = g_init_task ? g_init_task : g_kernel_task;
if (foster && !curr->children.is_empty()) {
- LockGuard guard(g_task_list_lock);
+ LockGuard guard(curr->lock);
list_node* child_node = curr->children.next;
while (child_node != &curr->children) {
@@ -226,7 +289,46 @@ void task_exit(int code)
curr->children.init();
}
- /* Transition to Zombie — TCB + kstack preserved for parent wait() */
+
+ /* ---- exit_notify: Linux kernel/exit.c:exit_notify() ----
+ * Determine whether this task should be auto-reaped or become zombie.
+ *
+ * Linux logic (simplified):
+ * - exit_signal == -1 (kthreads, sub-threads): autoreap = true
+ * - exit_signal >= 1 (user processes): notify parent, become zombie
+ *
+ * If autoreap, we call release_task() equivalent inline:
+ * free PID, remove from task list, then deschedule to Dead. */
+ bool autoreap = (curr->exit_signal <= 0);
+
+ if (autoreap) {
+ /* Auto-reap: release resources immediately (no zombie state).
+ * Linux: exit_notify() → tsk->exit_state = EXIT_DEAD → release_task() */
+ task_list_remove(curr);
+ g_pid_alloc.free(curr->pid);
+
+ /* Remove from parent's children list */
+ curr->sibling.prev->next = curr->sibling.next;
+ curr->sibling.next->prev = curr->sibling.prev;
+ curr->sibling.init();
+
+ /* Drop signal_struct ref: last task in the thread group also
+ * detaches us from the pgrp hash. */
+ if (curr->signal) {
+ put_signal_struct(curr->signal);
+ curr->signal = nullptr;
+ }
+ if (curr->sighand) {
+ put_sighand(curr->sighand);
+ curr->sighand = nullptr;
+ }
+
+ curr->state = TaskState::Dead;
+ schedule();
+ __builtin_unreachable();
+ }
+
+ /* User process path: become Zombie for parent wait() */
curr->state = TaskState::Zombie;
/* Wake parent->wait_chld queue */
@@ -242,8 +344,55 @@ void task_exit(int code)
void task_kill_current(int sig)
{
- int code = (sig & 0x7F) | kExitCodeSignalFlag;
- task_exit(code);
+ TaskControlBlock* curr = current_task();
+ if (!curr) kernel_panic("task_kill_current: no current task");
+
+ /* Linux ABI: signal-killed encodes status as (signo & 0x7F) */
+ curr->exit_code = sig & 0x7F;
+
+ /* Release user address space (refcount-safe).
+ * Switch to kernel page table first (same as task_exit). */
+ if (curr->mm) {
+ extern uint64 g_kernel_pgtbl;
+ pmap::activate(g_kernel_pgtbl);
+ curr->mm->put();
+ curr->mm = nullptr;
+ }
+
+ /* Reparent children (same as task_exit) */
+ TaskControlBlock* foster = g_init_task ? g_init_task : g_kernel_task;
+ if (foster && !curr->children.is_empty()) {
+ LockGuard guard(curr->lock);
+
+ list_node* child_node = curr->children.next;
+ while (child_node != &curr->children) {
+ list_node* next_sib = child_node->next;
+ auto* child = list_entry<TaskControlBlock,
+ &TaskControlBlock::sibling>(child_node);
+ child->parent = foster;
+
+ child_node->prev->next = child_node->next;
+ child_node->next->prev = child_node->prev;
+
+ list_node* fhead = &foster->children;
+ child_node->prev = fhead->prev;
+ child_node->next = fhead;
+ fhead->prev->next = child_node;
+ fhead->prev = child_node;
+
+ child_node = next_sib;
+ }
+ curr->children.init();
+ }
+
+ curr->state = TaskState::Zombie;
+
+ if (curr->parent) {
+ curr->parent->wait_chld.wake_all();
+ }
+
+ schedule();
+ __builtin_unreachable();
}
/* ======================================================================
@@ -263,6 +412,19 @@ void task_destroy(TaskControlBlock* task)
/* Free PID */
g_pid_alloc.free(task->pid);
+ /* Drop signal_struct + sighand refs. For zombies these are delayed
+ * until reap (here) so a waitpid() walker can still inspect
+ * pgid/sid; Linux defers __exit_signal/__cleanup_sighand to
+ * release_task() for the same reason. */
+ if (task->signal) {
+ put_signal_struct(task->signal);
+ task->signal = nullptr;
+ }
+ if (task->sighand) {
+ put_sighand(task->sighand);
+ task->sighand = nullptr;
+ }
+
/* Free kernel stack */
if (task->kstack_pa != 0) {
pmm_free_pages(pa_to_page(phys_addr(task->kstack_pa)), 1);
@@ -273,61 +435,155 @@ void task_destroy(TaskControlBlock* task)
}
/* ======================================================================
- * task_wait — sleep until a child exits, then reclaim it
- * Returns PID of exited child, or -ECHILD if no children exist.
+ * do_wait4 — wait for a child to exit (Linux-compatible)
+ *
+ * @wait_pid: >0 = specific child PID
+ * -1 = any child
+ * 0 = any child in same process group (TODO)
+ * <-1 = any child in specific process group (TODO)
+ * @options: WNOHANG = return immediately if no zombie
+ * @exit_code: output parameter (already in Linux wstatus format)
+ *
+ * Returns: PID of exited child, -ECHILD, or -EAGAIN (WNOHANG).
* ====================================================================== */
-int32 task_wait(int32* exit_code)
+int32 do_wait4(int32 wait_pid, int options, int32* exit_code)
{
TaskControlBlock* curr = current_task();
if (!curr) return -EINVAL;
while (true) {
TaskControlBlock* zombie = nullptr;
+ TaskControlBlock* report = nullptr; /* Stopped/Continued report */
+ int32 report_status = 0;
+ bool has_children = false;
+
+ /*
+ * Two-phase wait: put ourselves on the wait queue BEFORE
+ * checking the condition. This closes the race window where
+ * a child exits between our check and schedule().
+ *
+ * If a child exits and calls wake_all() while we're checking,
+ * our state will be flipped back to Runnable, and schedule()
+ * will simply return immediately.
+ */
+ curr->wait_chld.prepare_to_wait(curr, TaskState::Sleeping);
+
+ /* Scan children */
+ curr->lock.acquire();
- /* Look for a Zombie child */
- g_task_list_lock.acquire();
if (curr->children.is_empty()) {
- g_task_list_lock.release();
+ curr->lock.release();
+ curr->wait_chld.finish_wait(curr);
return -ECHILD;
}
+ has_children = true;
list_node* node = curr->children.next;
while (node != &curr->children) {
auto* child = list_entry<TaskControlBlock, &TaskControlBlock::sibling>(node);
+ bool matches = (wait_pid == -1) || (wait_pid == child->pid);
+ if (!matches) {
+ node = node->next;
+ continue;
+ }
+
+ /* Zombie reaping has priority — it frees resources. */
if (child->state == TaskState::Zombie) {
zombie = child;
break;
}
+
+ /* WUNTRACED: report Stopped child once per stop event. */
+ if ((options & WUNTRACED) && child->stopped_code != 0) {
+ report = child;
+ report_status = child->stopped_code;
+ break;
+ }
+
+ /* WCONTINUED: report a one-shot SIGCONT resume. */
+ if ((options & WCONTINUED) && child->notify_continued) {
+ report = child;
+ report_status = W_CONTINUED();
+ break;
+ }
+
node = node->next;
}
- g_task_list_lock.release();
+ curr->lock.release();
if (zombie) {
+ curr->wait_chld.finish_wait(curr);
int32 pid = zombie->pid;
- if (exit_code) {
- /* In a real kernel, we copy_to_user */
- *exit_code = zombie->exit_code;
- }
+ if (exit_code) *exit_code = zombie->exit_code;
task_destroy(zombie);
return pid;
}
- /* Sleep if no child has exited yet */
- curr->wait_chld.sleep(&g_task_list_lock);
+ if (report) {
+ /* Status-only report: clear the one-shot flag, leave the
+ * child alive. WNOWAIT (peek) would skip the clear, but
+ * we don't yet honour it. */
+ curr->wait_chld.finish_wait(curr);
+ int32 pid = report->pid;
+ if (exit_code) *exit_code = report_status;
+ if (W_CONTINUED() == report_status) {
+ report->notify_continued = false;
+ } else {
+ report->stopped_code = 0;
+ }
+ return pid;
+ }
+
+ /* No event to report */
+ if (!has_children) {
+ curr->wait_chld.finish_wait(curr);
+ return -ECHILD;
+ }
+ if (options & WNOHANG) {
+ curr->wait_chld.finish_wait(curr);
+ return 0;
+ }
+
+ /* Condition still false — deschedule.
+ * We are already on the wait queue and marked Sleeping. */
+ schedule();
+
+ /* Woken up — finish_wait removes us from queue, restores Runnable */
+ curr->wait_chld.finish_wait(curr);
+
+ /* Loop back to re-check the condition */
}
}
+/* Legacy wrapper for internal callers */
+int32 task_wait(int32* exit_code)
+{
+ return do_wait4(-1, 0, exit_code);
+}
+
/* ======================================================================
- * task_fork — clone current task
+ * task_clone — clone current task with Linux-compatible flags
+ *
+ * @tf: trap frame to copy to child
+ * @clone_flags: CLONE_VM | CLONE_THREAD | CSIGNAL etc.
+ * @child_stack: if non-zero, set as child's user SP (for pthread)
+ * @parent_tidptr: write child tid here in parent (CLONE_PARENT_SETTID)
+ * @child_tidptr: write child tid here in child (CLONE_CHILD_SETTID)
+ * @tls: TLS pointer (CLONE_SETTLS → tp register)
+ *
+ * Returns: child PID in parent, 0 in child, or negative errno.
* ====================================================================== */
-int32 task_fork(arch::TrapFrame* tf)
+int32 task_clone(arch::TrapFrame* tf, uint64 clone_flags,
+ uint64 child_stack, uint64 parent_tidptr,
+ uint64 child_tidptr, uint64 tls)
{
TaskControlBlock* curr = current_task();
if (!curr) return -EINVAL;
- TaskControlBlock* child = alloc_task("forked", curr->priority, 0);
+ TaskControlBlock* child = alloc_task("cloned", curr->priority, 0,
+ static_cast<uint8>(arch::cpu::id()));
if (!child) return -ENOMEM;
/* Allocate PID */
@@ -337,16 +593,35 @@ int32 task_fork(arch::TrapFrame* tf)
delete child;
return -ENOMEM;
}
- child->tgid = child->pid;
- /* Copy VmSpace */
- if (curr->mm) {
- child->mm = vmm_clone(curr->mm);
- if (!child->mm) {
- g_pid_alloc.free(child->pid);
- pmm_free_pages(pa_to_page(phys_addr(child->kstack_pa)), 1);
- delete child;
- return -ENOMEM;
+ /* exit_signal + tgid: from clone_flags.
+ * Linux: fork.c line 2418/2424 */
+ if (clone_flags & CLONE_THREAD) {
+ child->tgid = curr->tgid;
+ child->exit_signal = -1; /* Linux: sub-threads auto-reap */
+ } else {
+ child->tgid = child->pid;
+ child->exit_signal = static_cast<int32>(clone_flags & 0xFF); /* CSIGNAL */
+ if (child->exit_signal == 0) {
+ child->exit_signal = SIGCHLD; /* default for fork() */
+ }
+ }
+
+ /* Address space: CLONE_VM shares, otherwise deep copy */
+ if (clone_flags & CLONE_VM) {
+ if (curr->mm) {
+ curr->mm->incref();
+ child->mm = curr->mm;
+ }
+ } else {
+ if (curr->mm) {
+ child->mm = vmm_clone(curr->mm);
+ if (!child->mm) {
+ g_pid_alloc.free(child->pid);
+ pmm_free_pages(pa_to_page(phys_addr(child->kstack_pa)), 1);
+ delete child;
+ return -ENOMEM;
+ }
}
}
@@ -355,20 +630,94 @@ int32 task_fork(arch::TrapFrame* tf)
child->trapframe = reinterpret_cast<arch::TrapFrame*>(tf_addr);
*child->trapframe = *tf;
- /* Child returns 0 from fork */
+ /* Set Kernel SP */
+ child->trapframe->kernel_sp = reinterpret_cast<uint64>(child->trapframe);
+ child->trapframe->kernel_cpu_id = child->cpu_id;
+
+ /* Child returns 0 from fork/clone */
child->trapframe->a[0] = 0;
+ /* If caller specified a child stack, use it (pthread_create) */
+ if (child_stack != 0) {
+ child->trapframe->sp = child_stack;
+ }
+
+ /* CLONE_SETTLS: set thread pointer register */
+ if (clone_flags & CLONE_SETTLS) {
+ child->trapframe->tp = tls;
+ }
+
/* Set up context for ret_from_fork */
- uint64 stack_top = tf_addr; // Context goes below TrapFrame conceptually, but we just set SP
+ uint64 stack_top = tf_addr;
child->context = {};
child->context.ra = reinterpret_cast<uint64>(ret_from_fork);
child->context.sp = stack_top;
- child->context.s[0] = tf_addr; /* Passed to a0 in ret_from_fork */
+ child->context.s[0] = tf_addr;
+
+ /* ---- Copy fs_struct (Linux: copy_fs in fork.c) ----
+ * CLONE_FS: share the same fs_struct (increment refcount).
+ * Otherwise: allocate a new fs_struct copying root/pwd. */
+ if (curr->fs != nullptr) {
+ if (clone_flags & CLONE_FS) {
+ curr->fs->count.fetch_add(1, __ATOMIC_RELAXED);
+ child->fs = curr->fs;
+ } else {
+ child->fs = copy_fs_struct(curr->fs);
+ }
+ }
+
+ /* ---- Copy files_struct (Linux: copy_files in fork.c) ----
+ * CLONE_FILES: share the same fd table.
+ * Otherwise: duplicate the fd table. */
+ if (curr->files != nullptr) {
+ if (clone_flags & CLONE_FILES) {
+ curr->files->count.fetch_add(1, __ATOMIC_RELAXED);
+ child->files = curr->files;
+ } else {
+ child->files = dup_fd(curr->files);
+ }
+ }
+
+ /* ---- Copy sighand (Linux kernel/fork.c:copy_sighand) ----
+ * CLONE_SIGHAND: share signal handlers (incref parent's).
+ * Otherwise: deep copy of the action table; child gets its own
+ * sighand_struct (and its own siglock). Both branches MUST yield
+ * a non-null sighand — every user task needs one so signal paths
+ * can take the siglock without lazy-allocation races. */
+ if (clone_flags & CLONE_SIGHAND) {
+ if (!curr->sighand) {
+ /* CLONE_SIGHAND requires the parent to have a sighand. */
+ return -EINVAL;
+ }
+ get_sighand(curr->sighand);
+ child->sighand = curr->sighand;
+ } else {
+ child->sighand = copy_sighand(curr->sighand);
+ if (!child->sighand) {
+ /* OOM — same best-effort policy as fs/files/signal above.
+ * A separate cleanup-on-OOM pass is on the to-do list. */
+ }
+ }
+
+ /* ---- Copy signal_struct (Linux: copy_signal in fork.c) ----
+ * CLONE_THREAD shares the parent's signal_struct (incref).
+ * Otherwise allocate a new one that inherits pgid/sid/tty from the
+ * parent so the child stays in the same process group and session —
+ * this is what makes fork() participate in the existing job-control
+ * identity without explicit setpgid() calls. */
+ if (curr->signal != nullptr) {
+ if (clone_flags & CLONE_THREAD) {
+ get_signal_struct(curr->signal);
+ child->signal = curr->signal;
+ } else {
+ child->signal = fork_signal_struct(curr->signal, child);
+ }
+ }
/* Add to task tree */
child->parent = curr;
{
- LockGuard guard(g_task_list_lock);
+ LockGuard guard(curr->lock);
list_node* head = &curr->children;
child->sibling.prev = head->prev;
child->sibling.next = head;
@@ -380,9 +729,41 @@ int32 task_fork(arch::TrapFrame* tf)
task_list_add(child);
sched_enqueue(child);
+ /* CLONE_PARENT_SETTID: write child tid to parent's user address */
+ if ((clone_flags & CLONE_PARENT_SETTID) && parent_tidptr != 0) {
+ copy_to_user(parent_tidptr, &child->pid, sizeof(child->pid));
+ }
+
+ /* CLONE_CHILD_SETTID: write child tid into child's user address.
+ * Note: this runs in parent context, but the address is in the
+ * child's mm. For CLONE_VM (threads), parent and child share mm
+ * so this works directly. For fork, the page was just CoW-copied. */
+ if ((clone_flags & CLONE_CHILD_SETTID) && child_tidptr != 0) {
+ if (clone_flags & CLONE_VM) {
+ /* Same address space — write directly */
+ copy_to_user(child_tidptr, &child->pid, sizeof(child->pid));
+ }
+ /* For non-CLONE_VM fork, the child's mm is separate.
+ * The write would need to go into the child's pagetable.
+ * Linux handles this in the child's first schedule via
+ * a special flag. For now, threads (CLONE_VM) are the
+ * primary consumer. */
+ }
+
+ /* CLONE_CHILD_CLEARTID: kernel writes 0 to child_tidptr on exit */
+ if (clone_flags & CLONE_CHILD_CLEARTID) {
+ child->clear_child_tid = child_tidptr;
+ }
+
return child->pid;
}
+/* Legacy wrapper — plain fork() semantics */
+int32 task_fork(arch::TrapFrame* tf)
+{
+ return task_clone(tf, SIGCHLD, 0, 0, 0, 0);
+}
+
/* ======================================================================
* task_check_stack_canary
diff --git a/na/kernel/timer.cc b/na/kernel/timer.cc
index 01e8f9d..3cf03cf 100644
--- a/na/kernel/timer.cc
+++ b/na/kernel/timer.cc
@@ -7,10 +7,72 @@
#include <arch/cpu.h>
#include <arch/sbi.h>
+#include <kernel/sync/spinlock.h>
+
uint64 g_jiffies = 0;
static uint64 g_timer_freq = 10000000;
static uint64 g_tick_interval = 0;
+static list_node g_timer_list;
+static Spinlock g_timer_lock;
+
+
+void add_timer(TimerList* timer) {
+ LockGuard guard(g_timer_lock);
+
+ list_node* pos;
+ list_for_each(pos, &g_timer_list) {
+ TimerList* t = list_entry<TimerList, &TimerList::entry>(pos);
+ if (t->expires > timer->expires) {
+ break;
+ }
+ }
+
+ timer->entry.prev = pos->prev;
+ timer->entry.next = pos;
+ pos->prev->next = &timer->entry;
+ pos->prev = &timer->entry;
+}
+
+int del_timer(TimerList* timer) {
+ LockGuard guard(g_timer_lock);
+ if (timer->entry.next != nullptr && timer->entry.next != &timer->entry) {
+ timer->entry.prev->next = timer->entry.next;
+ timer->entry.next->prev = timer->entry.prev;
+ timer->entry.init();
+ return 1;
+ }
+ return 0;
+}
+
+int mod_timer(TimerList* timer, uint64 expires) {
+ int ret = del_timer(timer);
+ timer->expires = expires;
+ add_timer(timer);
+ return ret;
+}
+
+static void run_timers() {
+ g_timer_lock.acquire();
+
+ while (!g_timer_list.is_empty()) {
+ TimerList* t = list_entry<TimerList, &TimerList::entry>(g_timer_list.next);
+ if (t->expires > g_jiffies) {
+ break;
+ }
+
+ t->entry.prev->next = t->entry.next;
+ t->entry.next->prev = t->entry.prev;
+ t->entry.init();
+
+ g_timer_lock.release();
+ t->function(t);
+ g_timer_lock.acquire();
+ }
+
+ g_timer_lock.release();
+}
+
static void timer_set_next() {
arch::sbi::set_timer(arch::cpu::read_time() + g_tick_interval);
}
@@ -24,21 +86,33 @@ void timer_init() {
// Default 100 Hz (10ms per tick)
g_tick_interval = g_timer_freq / 100;
+ g_timer_lock.init("timer_list");
+ g_timer_list.init();
+
timer_set_next();
+
arch::cpu::timer_intr_on();
}
+
void timer_init_ap() {
timer_set_next();
arch::cpu::timer_intr_on();
}
+extern "C" uint32 bsp_hart_id;
+
void timer_tick() {
- g_jiffies++;
+ if (arch::cpu::id() == bsp_hart_id) {
+ g_jiffies++;
+ run_timers();
+ }
+
timer_set_next();
sched_tick();
}
+
uint64 timer_get_freq() {
return g_timer_freq;
}
diff --git a/na/kernel/trap.cc b/na/kernel/trap.cc
index 738b2db..2116d91 100644
--- a/na/kernel/trap.cc
+++ b/na/kernel/trap.cc
@@ -15,6 +15,7 @@
* Reference: docs/specs/trap.md §4
*/
+#include <arch/cpu.h>
#include <lume/trap.h>
#include <lume/irq.h>
#include <lume/console.h>
@@ -26,8 +27,11 @@
#include <lume/syscall.h>
#include <lume/task.h>
#include <lume/vmm.h>
+#include <lume/signal.h>
#include <lume/signal_nr.h>
+#include <lume/smp.h>
#include <arch/trapframe.h> /* TrapFrame: cross-layer data contract */
+#include <arch/extable.h> /* search_extable: kernel fault recovery */
/* ========================================================================
* trap_handle() — MI-layer dispatcher for traps
@@ -39,22 +43,18 @@ static TrapStats g_trap_stats = {};
static uint32 trap_cause_index(TrapCause cause)
{
- switch (cause) {
- case TrapCause::PageFaultLoad: return 0;
- case TrapCause::PageFaultStore: return 1;
- case TrapCause::PageFaultExec: return 2;
- case TrapCause::Syscall: return 3;
- case TrapCause::IllegalInst: return 4;
- case TrapCause::Breakpoint: return 5;
- case TrapCause::AccessFault: return 6;
- case TrapCause::AlignFault: return 7;
- case TrapCause::IrqTimer: return 8;
- case TrapCause::IrqSoftware: return 9;
- case TrapCause::IrqExternal: return 10;
- case TrapCause::Unknown:
- default:
- return 11;
+ uint32 raw = static_cast<uint32>(cause);
+
+ /* Exceptions 0-7 map directly to indices 0-7 */
+ if (raw <= 7) {
+ return raw;
+ }
+ /* IRQs 16-18 map to indices 8-10 */
+ if (raw >= 16 && raw <= 18) {
+ return raw - 16 + 8;
}
+ /* Unknown / out-of-range */
+ return 11;
}
static void trap_record(const TrapInfo& info)
@@ -100,10 +100,33 @@ void trap_handle(arch::TrapFrame* tf, const TrapInfo& info)
{
trap_record(info);
+ /* ---- Kernel exception handling ----
+ * Interrupts are always allowed through. For exceptions:
+ * - Page faults: handled in the switch below (extable fixup path)
+ * - Other exceptions: try extable, then panic.
+ *
+ * This replaces the old blanket kernel_panic, enabling
+ * copy_from_user/copy_to_user to safely fault on user pages. */
if (info.origin == TrapOrigin::FromKernel &&
info.cls != TrapClass::Interrupt) {
- kprintf("[trap] kernel exception\n");
- kernel_panic("kernel exception");
+
+ /* Page faults are dispatched in the switch — let them through */
+ bool is_page_fault =
+ (info.cause == TrapCause::PageFaultLoad ||
+ info.cause == TrapCause::PageFaultStore ||
+ info.cause == TrapCause::PageFaultExec);
+
+ if (!is_page_fault) {
+ /* Non-page-fault kernel exception: try extable fixup */
+ uint64 fixup = search_extable(info.epc);
+ if (fixup != 0) {
+ tf->epc = fixup;
+ return;
+ }
+ kprintf("[trap] fatal kernel exception: cause=%u epc=0x%llx val=0x%llx\n",
+ static_cast<unsigned>(info.cause), info.epc, info.val);
+ kernel_panic("unrecoverable kernel exception");
+ }
}
switch (info.cause) {
@@ -120,17 +143,43 @@ void trap_handle(arch::TrapFrame* tf, const TrapInfo& info)
int ret = vmm_handle_page_fault(curr->mm, info.val,
vm_fault_cause_from_trap(info.cause));
if (ret == -EFAULT) {
- task_kill_current(SIGSEGV);
+ kprintf("[trap] user SIGSEGV (MAPERR) epc=0x%llx addr=0x%llx\n",
+ info.epc, info.val);
+ force_sig_fault(SIGSEGV, 1 /* SEGV_MAPERR */, info.val);
} else if (ret == -ENOMEM) {
- task_kill_current(SIGKILL);
+ kprintf("[trap] user SIGKILL (OOM) epc=0x%llx addr=0x%llx\n",
+ info.epc, info.val);
+ force_sig_fault(SIGKILL, 0, info.val);
} else if (ret < 0) {
- task_kill_current(SIGSEGV);
+ kprintf("[trap] user SIGSEGV (ACCERR) epc=0x%llx addr=0x%llx\n",
+ info.epc, info.val);
+ force_sig_fault(SIGSEGV, 2 /* SEGV_ACCERR */, info.val);
}
break;
}
- kprintf("[trap] kernel page fault\n");
- kernel_panic("unhandled page fault");
+ /* Kernel page fault — try exception table fixup.
+ * This is the path used by copy_from_user/copy_to_user when
+ * accessing unmapped or faulting user addresses from S-mode. */
+ {
+ uint64 fixup = search_extable(info.epc);
+ if (fixup != 0) {
+ tf->epc = fixup;
+ break;
+ }
+ }
+ kprintf("[trap] kernel page fault: epc=0x%llx addr=0x%llx\n",
+ info.epc, info.val);
+ {
+ TaskControlBlock* curr = current_task();
+ if (curr) {
+ kprintf("[trap] pid=%d state=%d\n",
+ curr->pid, static_cast<int>(curr->state));
+ kprintf("[trap] tf->epc=%llx tf->ra=%llx tf->sp=%llx\n",
+ tf->epc, tf->ra, tf->sp);
+ }
+ }
+ kernel_panic("unhandled kernel page fault (no extable entry)");
break;
/* ---- System Call ---- */
@@ -155,14 +204,20 @@ void trap_handle(arch::TrapFrame* tf, const TrapInfo& info)
}
case TrapCause::IrqSoftware:
- /* IPI handling — Phase 2 stub */
+ /* S-mode software interrupt = IPI from another hart */
+ smp_handle_ipi();
break;
/* ---- Fatal exceptions ---- */
case TrapCause::IllegalInst:
- kprintf("[trap] illegal instruction\n");
- /* Phase 2: panic. Phase 3: task_kill(SIGILL) */
- kernel_panic("illegal instruction in user mode");
+ if (info.origin == TrapOrigin::FromUser) {
+ kprintf("[trap] user illegal instruction at epc=0x%llx stval=0x%llx\n",
+ info.epc, info.val);
+ force_sig_fault(SIGILL, 1 /* ILL_ILLOPC */, info.epc);
+ } else {
+ kprintf("[trap] illegal instruction at epc=0x%llx\n", info.epc);
+ kernel_panic("illegal instruction in kernel mode");
+ }
break;
case TrapCause::AccessFault:
@@ -187,8 +242,17 @@ void trap_handle(arch::TrapFrame* tf, const TrapInfo& info)
break;
}
- /* Check if rescheduling is needed before returning to user mode */
+ /* Process pending signals before returning to user mode */
if (info.origin == TrapOrigin::FromUser) {
+ do_signal(tf);
+ }
+
+ /* Preemption check on return from any interrupt.
+ * This is the kernel's equivalent of Linux's retint_kernel path:
+ * if a timer tick (or IPI) set need_resched, we must call schedule()
+ * before returning — otherwise the idle loop never yields to
+ * runnable tasks like PID 1. */
+ if (info.cls == TrapClass::Interrupt) {
sched_check_preempt();
}
}
diff --git a/na/kernel/uaccess.cc b/na/kernel/uaccess.cc
index 4a5b963..14c5fb7 100644
--- a/na/kernel/uaccess.cc
+++ b/na/kernel/uaccess.cc
@@ -15,25 +15,11 @@
#include <lume/task.h>
#include <lume/errno.h>
#include <lume/config.h>
-
-/* Kernel VA base — addresses at or above this are kernel space */
-inline constexpr uint64 kKernelVABase = 0xFFFFFFC000000000ULL;
+#include <lume/mm_layout.h>
extern "C" int __uaccess_memcpy(void* dst, const void* src, uint64 len);
extern "C" int64 __uaccess_strncpy(char* dst, const char* src, uint64 max_len);
-/* Validate that [addr, addr+len) is entirely in user space */
-static bool is_user_range(uint64 addr, uint64 len)
-{
- if (addr >= kKernelVABase)
- return false;
- if (addr + len < addr) /* overflow */
- return false;
- if (addr + len > kKernelVABase)
- return false;
- return true;
-}
-
int copy_from_user(void* kernel_dst, uint64 user_src, uint64 len)
{
if (len == 0) return 0;
@@ -56,9 +42,9 @@ int64 strncpy_from_user(char* kernel_dst, uint64 user_src, uint64 max_len)
/* We must also check that the string does not cross into kernel space.
This is safely handled by the assembly routine which will fault if it hits
- an unmapped page. However, to strictly respect kKernelVABase, we should
- restrict max_len to the distance to kKernelVABase. */
- uint64 max_allowed = kKernelVABase - user_src;
+ an unmapped page. However, to strictly respect kKernelVaBase, we should
+ restrict max_len to the distance to kKernelVaBase. */
+ uint64 max_allowed = kKernelVaBase - user_src;
if (max_len > max_allowed) {
max_len = max_allowed;
}
diff --git a/na/kernel/waitqueue.cc b/na/kernel/waitqueue.cc
index 9ee3f46..9689417 100644
--- a/na/kernel/waitqueue.cc
+++ b/na/kernel/waitqueue.cc
@@ -4,11 +4,18 @@
/*
* WaitQueue — blocking synchronization implementation
*
- * sleep(): current task → Sleeping, link into queue, schedule()
- * wake_one(): dequeue head task → Runnable, sched_enqueue()
+ * Two-phase wait protocol:
+ *
+ * prepare_to_wait(): task → Sleeping, link into queue
+ * [caller checks condition — if still false, calls schedule()]
+ * finish_wait(): remove from queue, restore Runnable
+ *
+ * Legacy sleep() wraps the two-phase pattern for simple callers.
+ *
+ * wake_one()/wake_all(): dequeue waiters → Runnable, sched_enqueue()
*
* The internal spinlock protects the wait list only.
- * schedule() is called AFTER releasing the spinlock to avoid
+ * schedule() is called AFTER releasing all spinlocks to avoid
* holding a lock across a context switch.
*/
@@ -18,40 +25,80 @@
#include <lume/kprintf.h>
#include <arch/cpu.h>
-void WaitQueue::sleep(Spinlock* external_lock)
-{
- TaskControlBlock* curr = current_task();
- if (!curr) {
- if (external_lock) external_lock->release();
- return;
- }
+/* ------------------------------------------------------------------ */
+/* Two-phase wait API */
+/* ------------------------------------------------------------------ */
- {
- LockGuard guard(lock_);
-
- /* Mark task as sleeping */
- curr->state = TaskState::Sleeping;
+void WaitQueue::prepare_to_wait(TaskControlBlock* task, TaskState new_state)
+{
+ LockGuard guard(lock_);
- /* Remove from RunQueue (sched_dequeue acquires its own lock) */
- /* We do NOT call sched_dequeue here because schedule() handles
- * the put_prev_task logic when state != Running. */
+ /* Set state BEFORE linking — this is the key to preventing lost wakeups.
+ * If wake_one fires between now and schedule(), it will set state back
+ * to Runnable, and schedule() will see Runnable and not block. */
+ task->state = new_state;
+ /* Only add to queue if not already linked (idempotent) */
+ if (task->run_link.next == &task->run_link) {
/* Link into wait queue tail */
- list_node* node = &curr->run_link;
+ list_node* node = &task->run_link;
node->prev = head_.prev;
node->next = &head_;
head_.prev->next = node;
head_.prev = node;
}
+}
+
+void WaitQueue::finish_wait(TaskControlBlock* task)
+{
+ LockGuard guard(lock_);
+
+ /* Remove from wait queue if still linked */
+ if (task->run_link.next != &task->run_link) {
+ list_node* node = &task->run_link;
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ node->init();
+ }
+
+ /* Ensure task is Runnable (it may have been woken by wake_one/wake_all
+ * already, or it may never have slept if the condition was true). */
+ if (task->state == TaskState::Sleeping) {
+ task->state = TaskState::Runnable;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Legacy convenience API */
+/* ------------------------------------------------------------------ */
+void WaitQueue::sleep(Spinlock* external_lock)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) {
+ if (external_lock) external_lock->release();
+ return;
+ }
+
+ /* Phase 1: add to queue, mark Sleeping */
+ prepare_to_wait(curr, TaskState::Sleeping);
+
+ /* Release external lock AFTER we are on the queue (critical ordering) */
if (external_lock) {
external_lock->release();
}
- /* Context switch away — will return here when woken */
+ /* Phase 2: context switch away — will return here when woken */
schedule();
+
+ /* Phase 3: clean up */
+ finish_wait(curr);
}
+/* ------------------------------------------------------------------ */
+/* Wakeup */
+/* ------------------------------------------------------------------ */
+
bool WaitQueue::wake_one()
{
LockGuard guard(lock_);
diff --git a/na/lib/decompress_lz4.cc b/na/lib/decompress_lz4.cc
deleted file mode 100644
index 6a93936..0000000
--- a/na/lib/decompress_lz4.cc
+++ /dev/null
@@ -1,77 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#include <lume/decompress.h>
-
-int lz4_decompress(const uint8* src, uint32 src_len, uint8* dst, uint32 dst_cap)
-{
- const uint8* ip = src;
- const uint8* iend = src + src_len;
- uint8* op = dst;
- uint8* oend = dst + dst_cap;
-
- while (ip < iend) {
- uint8 token = *ip++;
- uint32 lit_len = token >> 4;
-
- if (lit_len == 15) {
- uint8 s = 0;
- do {
- if (ip >= iend) {
- return -1;
- }
- s = *ip++;
- lit_len += s;
- } while (s == 255);
- }
-
- if (ip + lit_len > iend) {
- return -1;
- }
- if (op + lit_len > oend) {
- return -1;
- }
-
- for (uint32 i = 0; i < lit_len; ++i) {
- *op++ = *ip++;
- }
-
- if (ip >= iend) {
- break;
- }
- if (ip + 2 > iend) {
- return -1;
- }
-
- uint32 off = static_cast<uint32>(ip[0]) | (static_cast<uint32>(ip[1]) << 8);
- ip += 2;
- if (off == 0 || off > static_cast<uint32>(op - dst)) {
- return -1;
- }
-
- uint32 match_len = token & 0x0F;
- if (match_len == 15) {
- uint8 s = 0;
- do {
- if (ip >= iend) {
- return -1;
- }
- s = *ip++;
- match_len += s;
- } while (s == 255);
- }
- match_len += 4;
-
- if (op + match_len > oend) {
- return -1;
- }
-
- uint8* match = op - off;
- for (uint32 i = 0; i < match_len; ++i) {
- op[i] = match[i];
- }
- op += match_len;
- }
-
- return static_cast<int>(op - dst);
-}
diff --git a/na/lib/fdt.c b/na/lib/fdt.c
new file mode 100644
index 0000000..1b20383
--- /dev/null
+++ b/na/lib/fdt.c
@@ -0,0 +1,2 @@
+#include <lume/libfdt_env.h>
+#include "../scripts/dtc/libfdt/fdt.c"
diff --git a/na/lib/fdt_addresses.c b/na/lib/fdt_addresses.c
new file mode 100644
index 0000000..d58f896
--- /dev/null
+++ b/na/lib/fdt_addresses.c
@@ -0,0 +1,2 @@
+#include <lume/libfdt_env.h>
+#include "../scripts/dtc/libfdt/fdt_addresses.c"
diff --git a/na/lib/fdt_empty_tree.c b/na/lib/fdt_empty_tree.c
new file mode 100644
index 0000000..37f8ef8
--- /dev/null
+++ b/na/lib/fdt_empty_tree.c
@@ -0,0 +1,2 @@
+#include <lume/libfdt_env.h>
+#include "../scripts/dtc/libfdt/fdt_empty_tree.c"
diff --git a/na/lib/fdt_ro.c b/na/lib/fdt_ro.c
new file mode 100644
index 0000000..3599d49
--- /dev/null
+++ b/na/lib/fdt_ro.c
@@ -0,0 +1,2 @@
+#include <lume/libfdt_env.h>
+#include "../scripts/dtc/libfdt/fdt_ro.c"
diff --git a/na/lib/fdt_rw.c b/na/lib/fdt_rw.c
new file mode 100644
index 0000000..65d7a71
--- /dev/null
+++ b/na/lib/fdt_rw.c
@@ -0,0 +1,2 @@
+#include <lume/libfdt_env.h>
+#include "../scripts/dtc/libfdt/fdt_rw.c"
diff --git a/na/lib/fdt_strerror.c b/na/lib/fdt_strerror.c
new file mode 100644
index 0000000..dae35ea
--- /dev/null
+++ b/na/lib/fdt_strerror.c
@@ -0,0 +1,2 @@
+#include <lume/libfdt_env.h>
+#include "../scripts/dtc/libfdt/fdt_strerror.c"
diff --git a/na/lib/fdt_sw.c b/na/lib/fdt_sw.c
new file mode 100644
index 0000000..d04f220
--- /dev/null
+++ b/na/lib/fdt_sw.c
@@ -0,0 +1,2 @@
+#include <lume/libfdt_env.h>
+#include "../scripts/dtc/libfdt/fdt_sw.c"
diff --git a/na/lib/fdt_wip.c b/na/lib/fdt_wip.c
new file mode 100644
index 0000000..ebfb3c1
--- /dev/null
+++ b/na/lib/fdt_wip.c
@@ -0,0 +1,2 @@
+#include <lume/libfdt_env.h>
+#include "../scripts/dtc/libfdt/fdt_wip.c"
diff --git a/na/lib/klog.cc b/na/lib/klog.cc
index bf081de..86c67f8 100644
--- a/na/lib/klog.cc
+++ b/na/lib/klog.cc
@@ -166,9 +166,7 @@ static void ring_drop_oldest(uint32 needed)
static void console_emit(const char* s, int len)
{
- for (int i = 0; i < len; ++i) {
- early_putc(s[i]);
- }
+ console_write_all(s, len);
}
static int klog_format_line(char* out, int out_size,
@@ -329,12 +327,20 @@ static void klog_vex(KlogLevel level, KlogSubsys subsys,
ring_write_bytes(msg, hdr.len);
if (do_console) {
g_klog.console_tail = g_klog.head;
+ /* Emit to UART while holding the lock to prevent
+ * interleaved output from concurrent CPUs. */
+ char line[KLOG_LINE_MAX + 64];
+ int line_len = klog_format_line(line, sizeof(line),
+ hdr, msg, hdr.len);
+ if (line_len > 0) {
+ console_emit(line, line_len);
+ }
}
- }
-
- if (do_console) {
+ } else if (do_console) {
+ /* Pre-init or panic mode: emit without lock */
char line[KLOG_LINE_MAX + 64];
- int line_len = klog_format_line(line, sizeof(line), hdr, msg, hdr.len);
+ int line_len = klog_format_line(line, sizeof(line),
+ hdr, msg, hdr.len);
if (line_len > 0) {
console_emit(line, line_len);
}
diff --git a/na/lib/kobject.cc b/na/lib/kobject.cc
new file mode 100644
index 0000000..8e8329e
--- /dev/null
+++ b/na/lib/kobject.cc
@@ -0,0 +1,82 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * kobject — implementation.
+ *
+ * Lives in lib/ (mirroring Linux's lib/kobject.c) because kobject is a
+ * generic primitive, not tied to the driver model.
+ *
+ * Reference: docs/specs/storage_stack.md §4.2
+ */
+
+#include <lume/kobject.h>
+#include <lume/container_of.h>
+#include <lume/klog.h>
+
+void kobject_init(kobject* k, const kobj_type* type)
+{
+ if (k == nullptr) {
+ return;
+ }
+ k->name = nullptr;
+ k->parent = nullptr;
+ k->ktype = type;
+ k->state_in_sysfs = false;
+ k->entry.init();
+ k->children.init();
+ k->lock.init("kobject");
+ kref_init(&k->refcount);
+}
+
+int kobject_add(kobject* k, kobject* parent, const char* name)
+{
+ if (k == nullptr) {
+ return -1;
+ }
+ k->name = name;
+ if (parent != nullptr) {
+ kobject_get(parent);
+ k->parent = parent;
+ LockGuard g(parent->lock);
+ list_add_tail(&k->entry, &parent->children);
+ }
+ return 0;
+}
+
+void kobject_del(kobject* k)
+{
+ if (k == nullptr || k->parent == nullptr) {
+ return;
+ }
+ kobject* parent = k->parent;
+ {
+ LockGuard g(parent->lock);
+ list_del_init(&k->entry);
+ }
+ k->parent = nullptr;
+ kobject_put(parent);
+}
+
+void kobject_get(kobject* k)
+{
+ if (k != nullptr) {
+ kref_get(&k->refcount);
+ }
+}
+
+static void kobject_release(kref* r)
+{
+ kobject* k = container_of(r, kobject, refcount);
+ if (k->ktype != nullptr && k->ktype->release != nullptr) {
+ k->ktype->release(k);
+ }
+}
+
+void kobject_put(kobject* k)
+{
+ if (k == nullptr) {
+ return;
+ }
+ kref_put(&k->refcount, kobject_release);
+}
diff --git a/na/lib/kprintf.cc b/na/lib/kprintf.cc
index ddd2e59..a98de71 100644
--- a/na/lib/kprintf.cc
+++ b/na/lib/kprintf.cc
@@ -5,21 +5,44 @@
* kprintf.cc — Kernel formatted output
*
* Uses mpaland/printf's vsnprintf_ as the core formatting engine.
- * All kernel output ultimately goes through this module.
*
* Architecture:
* vsnprintf_() ← pure formatting, writes to buffer
* ↓
- * kprintf() ← format + emit to UART via early_putc
+ * kprintf() ← format + emit via console_write_all()
* kvprintf() ← va_list variant
* ksnprintf() ← format into caller's buffer
+ *
+ * Output path:
+ * Pre-registration: console_write_all() → early_putc() fallback
+ * Post-registration: console_write_all() → struct console chain
*/
#include <lume/kprintf.h>
#include <lume/console.h>
+#include <lume/atomic.h>
+#include <arch/cpu.h>
#include "printf.h"
/* ------------------------------------------------------------------ */
+/* Console spinlock (raw, no scheduler dependency — safe in panic) */
+/* ------------------------------------------------------------------ */
+
+static lume::atomic<uint32> g_console_lock{0};
+
+static inline void console_lock_acquire()
+{
+ while (g_console_lock.exchange(1, __ATOMIC_ACQUIRE) != 0) {
+ /* spin */
+ }
+}
+
+static inline void console_lock_release()
+{
+ g_console_lock.store(0, __ATOMIC_RELEASE);
+}
+
+/* ------------------------------------------------------------------ */
/* _putchar: required by mpaland/printf for printf_() / vprintf_() */
/* ------------------------------------------------------------------ */
@@ -40,10 +63,10 @@ extern "C" int kprintf(const char* fmt, ...)
int len = vsnprintf_(buf, sizeof(buf), fmt, va);
va_end(va);
- /* Emit to console */
- for (int i = 0; i < len && buf[i] != '\0'; ++i) {
- early_putc(buf[i]);
- }
+ /* Emit to console under lock */
+ console_lock_acquire();
+ console_write_all(buf, len);
+ console_lock_release();
return len;
}
@@ -52,9 +75,9 @@ extern "C" int kvprintf(const char* fmt, va_list va)
char buf[256];
int len = vsnprintf_(buf, sizeof(buf), fmt, va);
- for (int i = 0; i < len && buf[i] != '\0'; ++i) {
- early_putc(buf[i]);
- }
+ console_lock_acquire();
+ console_write_all(buf, len);
+ console_lock_release();
return len;
}
diff --git a/na/lib/module.mk b/na/lib/module.mk
index 2cfadf7..0b178f8 100644
--- a/na/lib/module.mk
+++ b/na/lib/module.mk
@@ -1,10 +1,10 @@
# lib/module.mk
SRCS_CC += lib/cxx_abi.cc
SRCS_CC += lib/rbtree.cc
+SRCS_CC += lib/kobject.cc
SRCS_CC += lib/kprintf.cc
SRCS_CC += lib/klog.cc
SRCS_CC += lib/random.cc
-SRCS_CC += lib/decompress_lz4.cc
SRCS_C += lib/printf.c
# ============================================================
@@ -39,3 +39,28 @@ MUSL_C_SRCS := $(filter-out $(MUSL_EXCLUDED_C),$(MUSL_ALL_C))
SRCS_C += $(MUSL_C_SRCS)
SRCS_S += $(filter %.S,$(MUSL_ASM_SRCS))
SRCS_s += $(filter %.s,$(MUSL_ASM_SRCS))
+
+# ============================================================
+# libfdt — kernel adapter shims around scripts/dtc/libfdt/.
+#
+# Each shim is two lines:
+# #include <linux/libfdt_env.h>
+# #include "../scripts/dtc/libfdt/fdt*.c"
+# matching Linux lib/fdt*.c verbatim. The compile recipe needs:
+# - <linux/libfdt_env.h> reachable via the default -Iinclude path
+# - -I scripts/dtc/libfdt so the upstream source's #include <libfdt.h>
+# and friends resolve to scripts/dtc/libfdt/libfdt.h
+# Overriding CFLAGS_LIBC (which the %.o: %.c rule uses) avoids dragging
+# in libc-shim headers — libfdt is freestanding-compatible and uses
+# only the string functions forward-declared in <linux/libfdt_env.h>.
+# ============================================================
+LIBFDT_FILES := fdt.c fdt_ro.c fdt_wip.c fdt_rw.c fdt_sw.c \
+ fdt_strerror.c fdt_empty_tree.c fdt_addresses.c
+SRCS_C += $(addprefix lib/,$(LIBFDT_FILES))
+
+# Per-target CFLAGS override: plain CFLAGS plus the upstream include dir.
+# Use `=` (recursive) not `:=` because $(CFLAGS) is defined in the top
+# Makefile AFTER this file is included — `:=` would expand it now (empty)
+# and lose -Iinclude, breaking <linux/libfdt_env.h>.
+$(addprefix $(BUILD)/lib/,$(LIBFDT_FILES:.c=.o)): CFLAGS_LIBC = \
+ $(CFLAGS) -I scripts/dtc/libfdt
diff --git a/na/mm/memblock.cc b/na/mm/memblock.cc
index 4cd0b16..aee0245 100644
--- a/na/mm/memblock.cc
+++ b/na/mm/memblock.cc
@@ -16,6 +16,7 @@
#include <lume/fdt.h>
#include <lume/klog.h>
#include <lume/kprintf.h>
+#include <lume/config.h>
#include <lume/addr.h>
extern "C" char _stext[], _kernel_end[];
@@ -24,7 +25,11 @@ extern "C" char _stext[], _kernel_end[];
/* Module state */
/* ------------------------------------------------------------------ */
-static struct {
+namespace {
+
+constexpr uint64 kMB = 1024ULL * 1024ULL;
+
+struct {
uint64 mem_base;
uint64 mem_size;
@@ -41,7 +46,7 @@ static struct {
/* ------------------------------------------------------------------ */
/* Insert a reserved region, keeping the array sorted by base. */
-static void insert_reserved(uint64 base, uint64 size)
+void insert_reserved(uint64 base, uint64 size)
{
if (g_mb.reserved_count >= kMemblockMaxRegions) {
kernel_panic("memblock: too many reserved regions");
@@ -57,7 +62,7 @@ static void insert_reserved(uint64 base, uint64 size)
}
/* Check if [base, base+size) overlaps any reserved region. */
-static bool overlaps_reserved(uint64 base, uint64 size)
+bool overlaps_reserved(uint64 base, uint64 size)
{
uint64 end = base + size;
for (int i = 0; i < g_mb.reserved_count; i++) {
@@ -71,7 +76,7 @@ static bool overlaps_reserved(uint64 base, uint64 size)
}
/* Find the end of the first reserved region that covers `addr`. */
-static uint64 reserved_end_covering(uint64 addr)
+uint64 reserved_end_covering(uint64 addr)
{
for (int i = 0; i < g_mb.reserved_count; i++) {
uint64 rb = g_mb.reserved[i].base;
@@ -83,13 +88,14 @@ static uint64 reserved_end_covering(uint64 addr)
return addr;
}
+} // namespace
+
/* ------------------------------------------------------------------ */
/* Public API */
/* ------------------------------------------------------------------ */
void memblock_init(uint64 fdt_paddr)
{
- (void)fdt_paddr;
/* 1. Get memory layout from FDT */
uint64 mem_base = 0;
@@ -104,21 +110,58 @@ void memblock_init(uint64 fdt_paddr)
g_mb.reserved_count = 0;
g_mb.active = true;
- /* 2. Reserve firmware region [mem_base, kernel_start).
- * OpenSBI occupies the first 2MB of RAM on RISC-V virt. */
- uint64 ks_pa = boot_va_to_pa(virt_addr(
- reinterpret_cast<uint64>(_stext))).raw;
- if (ks_pa > mem_base) {
- memblock_reserve(mem_base, ks_pa - mem_base);
+ /* 2. Reserve firmware region [mem_base, kPhysStart).
+ *
+ * Everything below the kernel's linked load address is firmware
+ * territory (OpenSBI), which is PMP-protected. */
+ {
+ uint64 fw_end = kPhysStart;
+ if (fw_end > mem_base) {
+ memblock_reserve(mem_base, fw_end - mem_base);
+ }
+ }
+
+ /* 2b. Reserve stale pre-relocation kernel image.
+ *
+ * After physical KASLR, the kernel is relocated from its original
+ * load address (kPhysStart) to a new PA. The old copy at
+ * [kPhysStart, kPhysStart + kernel_size) must not be allocated —
+ * it contains stale code/data that would corrupt page tables or
+ * other structures if reused. */
+ {
+ uint64 ks_pa = boot_va_to_pa(virt_addr(
+ reinterpret_cast<uint64>(_stext))).raw;
+ if (ks_pa != kPhysStart) {
+ /* Kernel was relocated — reserve the original location */
+ uint64 kernel_size = boot_va_to_pa(virt_addr(
+ reinterpret_cast<uint64>(_kernel_end))).raw - ks_pa;
+ memblock_reserve(kPhysStart, kernel_size);
+ }
}
/* 3. Reserve kernel image [_stext, _kernel_end) */
+ uint64 ks_pa = boot_va_to_pa(virt_addr(
+ reinterpret_cast<uint64>(_stext))).raw;
uint64 ke_pa = boot_va_to_pa(virt_addr(
reinterpret_cast<uint64>(_kernel_end))).raw;
memblock_reserve(ks_pa, ke_pa - ks_pa);
+ /* 4. Reserve FDT blob — it lives in RAM and must not be overwritten
+ * by vmemmap/page_map or buddy before fdt_unflatten() copies it. */
+ {
+ struct fdt_hdr { uint32 magic; uint32 totalsize; };
+ const auto* fh = reinterpret_cast<const fdt_hdr*>(fdt_paddr);
+ uint32 fdt_size = ((fh->totalsize & 0xFF) << 24) |
+ (((fh->totalsize >> 8) & 0xFF) << 16) |
+ (((fh->totalsize >> 16) & 0xFF) << 8) |
+ ((fh->totalsize >> 24) & 0xFF);
+ memblock_reserve(fdt_paddr, fdt_size);
+ kprintf("[memblock] FDT reserved [0x%llx, 0x%llx)\n",
+ fdt_paddr, fdt_paddr + fdt_size);
+ }
+
kprintf("[memblock] RAM [0x%llx, 0x%llx) = %llu MB\n",
- mem_base, mem_base + mem_size, mem_size / (1024 * 1024));
+ mem_base, mem_base + mem_size, mem_size / kMB);
kprintf("[memblock] kernel reserved [0x%llx, 0x%llx)\n", ks_pa, ke_pa);
}
@@ -196,7 +239,6 @@ void memblock_for_each_free(void (*cb)(uint64 base, uint64 size, void* ctx),
for (int i = 0; i < g_mb.reserved_count && pos < mem_end; i++) {
uint64 rb = g_mb.reserved[i].base;
if (rb < pos) {
- /* Overlapping/adjacent — skip */
uint64 re = g_mb.reserved[i].base + g_mb.reserved[i].size;
if (re > pos) {
pos = re;
@@ -204,13 +246,11 @@ void memblock_for_each_free(void (*cb)(uint64 base, uint64 size, void* ctx),
continue;
}
if (rb > pos) {
- /* Free gap [pos, rb) */
cb(pos, rb - pos, ctx);
}
pos = g_mb.reserved[i].base + g_mb.reserved[i].size;
}
- /* Trailing free region after last reserved */
if (pos < mem_end) {
cb(pos, mem_end - pos, ctx);
}
diff --git a/na/mm/module.mk b/na/mm/module.mk
index d497067..93cef80 100644
--- a/na/mm/module.mk
+++ b/na/mm/module.mk
@@ -3,3 +3,5 @@ SRCS_CC += mm/pmm.cc
SRCS_CC += mm/slab.cc
SRCS_CC += mm/vmm.cc
SRCS_CC += mm/vma.cc
+SRCS_CC += mm/page_cache.cc
+SRCS_CC += mm/tlb.cc
diff --git a/na/mm/page_cache.cc b/na/mm/page_cache.cc
new file mode 100644
index 0000000..25c6a60
--- /dev/null
+++ b/na/mm/page_cache.cc
@@ -0,0 +1,252 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * page_cache.cc — Minimal page cache implementation
+ *
+ * Open-addressing hash table indexed by (mapping, index) pairs.
+ * Fixed-size bucket array allocated at init time via slab.
+ *
+ * This is intentionally simple: no LRU, no writeback, no eviction.
+ * The interface in page_cache.h is designed so that upgrading to a
+ * radix tree + LRU scheme later only changes this file.
+ *
+ * Concurrency: protected by a single spinlock. Acceptable for
+ * initial bring-up; will be replaced by per-bucket or RCU-based
+ * locking when contention becomes measurable.
+ */
+
+#include <lume/page_cache.h>
+#include <lume/page.h>
+#include <lume/pmm.h>
+#include <lume/errno.h>
+#include <lume/new.h>
+#include <kernel/sync/spinlock.h>
+
+/* ------------------------------------------------------------------ */
+/* Hash table internals */
+/* ------------------------------------------------------------------ */
+
+struct CacheEntry {
+ void* mapping; // nullptr = empty slot
+ uint64 index;
+ Page* page;
+};
+
+namespace {
+
+constexpr uint64 kCacheBuckets = 1024;
+
+CacheEntry g_buckets[kCacheBuckets];
+Spinlock g_cache_lock;
+
+uint64 hash_key(void* mapping, uint64 index)
+{
+ uint64 m = reinterpret_cast<uint64>(mapping);
+ /* FNV-1a inspired mixing */
+ uint64 h = m ^ (index * 0x517cc1b727220a95ULL);
+ h ^= h >> 17;
+ h *= 0xbf58476d1ce4e5b9ULL;
+ h ^= h >> 31;
+ return h % kCacheBuckets;
+}
+
+/* Linear probe to find an entry or an empty slot. */
+CacheEntry* find_slot(void* mapping, uint64 index)
+{
+ uint64 start = hash_key(mapping, index);
+ for (uint64 i = 0; i < kCacheBuckets; i++) {
+ uint64 idx = (start + i) % kCacheBuckets;
+ CacheEntry* e = &g_buckets[idx];
+ if (e->mapping == nullptr) {
+ return e; // Empty slot
+ }
+ if (e->mapping == mapping && e->index == index) {
+ return e; // Found
+ }
+ }
+ return nullptr; // Table full
+}
+
+/* Find an existing entry (not an empty slot). */
+CacheEntry* find_entry(void* mapping, uint64 index)
+{
+ uint64 start = hash_key(mapping, index);
+ for (uint64 i = 0; i < kCacheBuckets; i++) {
+ uint64 idx = (start + i) % kCacheBuckets;
+ CacheEntry* e = &g_buckets[idx];
+ if (e->mapping == nullptr) {
+ return nullptr; // Empty = not found
+ }
+ if (e->mapping == mapping && e->index == index) {
+ return e;
+ }
+ }
+ return nullptr;
+}
+
+/*
+ * Remove an entry and fix up the linear-probe chain.
+ * Standard open-addressing deletion with backward shift.
+ */
+void remove_entry(CacheEntry* entry)
+{
+ uint64 removed = static_cast<uint64>(entry - g_buckets);
+ entry->mapping = nullptr;
+ entry->page = nullptr;
+
+ /* Backward-shift deletion to maintain probe chain integrity */
+ uint64 cur = (removed + 1) % kCacheBuckets;
+ while (g_buckets[cur].mapping != nullptr) {
+ uint64 ideal = hash_key(g_buckets[cur].mapping, g_buckets[cur].index);
+ /* Check if cur's ideal position is "before" removed in probe order */
+ bool needs_shift = false;
+ if (removed <= cur) {
+ needs_shift = (ideal <= removed || ideal > cur);
+ } else {
+ needs_shift = (ideal <= removed && ideal > cur);
+ }
+
+ if (needs_shift) {
+ g_buckets[removed] = g_buckets[cur];
+ g_buckets[cur].mapping = nullptr;
+ g_buckets[cur].page = nullptr;
+ removed = cur;
+ }
+ cur = (cur + 1) % kCacheBuckets;
+ }
+}
+
+} // namespace
+
+/* ------------------------------------------------------------------ */
+/* Public API */
+/* ------------------------------------------------------------------ */
+
+void page_cache_init()
+{
+ g_cache_lock.init("page_cache");
+ for (uint64 i = 0; i < kCacheBuckets; i++) {
+ g_buckets[i].mapping = nullptr;
+ g_buckets[i].index = 0;
+ g_buckets[i].page = nullptr;
+ }
+}
+
+Page* page_cache_get(void* mapping, uint64 index)
+{
+ LockGuard guard(g_cache_lock);
+ CacheEntry* e = find_entry(mapping, index);
+ if (!e) {
+ return nullptr;
+ }
+ page_incref(e->page);
+ return e->page;
+}
+
+int page_cache_insert(void* mapping, uint64 index, Page* page)
+{
+ if (!mapping || !page) {
+ return -EINVAL;
+ }
+
+ LockGuard guard(g_cache_lock);
+
+ /* Check for existing entry */
+ CacheEntry* existing = find_entry(mapping, index);
+ if (existing) {
+ return -EEXIST;
+ }
+
+ /* Find an empty slot */
+ CacheEntry* slot = find_slot(mapping, index);
+ if (!slot || slot->mapping != nullptr) {
+ return -ENOMEM; // Table full
+ }
+
+ slot->mapping = mapping;
+ slot->index = index;
+ slot->page = page;
+ page_incref(page); // Cache holds a reference
+
+ return 0;
+}
+
+void page_cache_put(Page* page)
+{
+ if (!page) {
+ return;
+ }
+ page_decref(page);
+}
+
+Page* page_cache_remove(void* mapping, uint64 index)
+{
+ LockGuard guard(g_cache_lock);
+ CacheEntry* e = find_entry(mapping, index);
+ if (!e) {
+ return nullptr;
+ }
+
+ Page* page = e->page;
+ remove_entry(e);
+ page_decref(page); // Release cache's reference
+ return page;
+}
+
+void page_cache_invalidate(void* mapping)
+{
+ if (!mapping) {
+ return;
+ }
+
+ LockGuard guard(g_cache_lock);
+
+ /* Scan all buckets — acceptable for current table size */
+ for (uint64 i = 0; i < kCacheBuckets; i++) {
+ if (g_buckets[i].mapping == mapping) {
+ Page* page = g_buckets[i].page;
+ remove_entry(&g_buckets[i]);
+ page_decref(page);
+ /* After removal, re-check this index since backward shift
+ * may have moved an entry into this slot. */
+ i--;
+ }
+ }
+}
+
+Page* page_cache_get_or_alloc(void* mapping, uint64 index)
+{
+ /* Fast path: already cached */
+ Page* page = page_cache_get(mapping, index);
+ if (page) {
+ return page;
+ }
+
+ /* Slow path: allocate and insert */
+ page = pmm_alloc_page();
+ if (!page) {
+ return nullptr;
+ }
+
+ int ret = page_cache_insert(mapping, index, page);
+ if (ret == -EEXIST) {
+ /* Race: another thread inserted between our get and insert.
+ * Free our allocation and return the existing page. */
+ pmm_free_page(page);
+ return page_cache_get(mapping, index);
+ }
+ if (ret < 0) {
+ pmm_free_page(page);
+ return nullptr;
+ }
+
+ /* Caller gets a ref (insert already added cache's ref,
+ * we need one more for the caller). */
+ /* Actually, insert did incref for cache. The page already has
+ * refcount=1 from alloc + 1 from insert = 2.
+ * We return it to caller who must put() when done.
+ * So we incref once more for the caller's reference. */
+ page_incref(page);
+ return page;
+}
diff --git a/na/mm/pmm.cc b/na/mm/pmm.cc
index a5a0b0e..b7cf611 100644
--- a/na/mm/pmm.cc
+++ b/na/mm/pmm.cc
@@ -43,16 +43,25 @@ struct PerCpuCache {
/* Module-private state */
/* ------------------------------------------------------------------ */
-static Spinlock g_pmm_lock;
-static list_node g_free_areas[kMaxOrder]; // Buddy free lists [0..10]
-static PerCpuCache g_pcp[kMaxCpus];
+namespace {
-static Page *g_page_map = nullptr; // Flat array of Page descriptors
-static uint64 g_page_map_pa = 0; // Physical base of page_map storage
-static uint64 g_page_map_bytes = 0; // Byte size of page_map storage
-static uint64 g_mem_base = 0; // Physical memory base (from FDT)
-static uint64 g_mem_size = 0; // Total physical memory size
-static uint64 g_num_pages = 0; // Total managed pages
+/* PCP Cache tuning parameters */
+constexpr uint32 kPcpBatchDivisor = 1024;
+constexpr uint32 kPcpMinBatchSize = 16;
+constexpr uint64 kMB = 1024ULL * 1024ULL;
+
+Spinlock g_pmm_lock;
+list_node g_free_areas[kMaxOrder]; // Buddy free lists [0..10]
+PerCpuCache g_pcp[kMaxCpus];
+
+Page *g_page_map = nullptr; // Flat array of Page descriptors
+uint64 g_page_map_pa = 0; // Physical base of page_map storage
+uint64 g_page_map_bytes = 0; // Byte size of page_map storage
+uint64 g_mem_base = 0; // Physical memory base (from FDT)
+uint64 g_mem_size = 0; // Total physical memory size
+uint64 g_num_pages = 0; // Total managed pages
+
+} // namespace
uint32 g_pcp_high_watermark = 0;
uint32 g_pcp_batch_size = 0;
@@ -62,63 +71,68 @@ uint32 g_pcp_batch_size = 0;
/* ------------------------------------------------------------------ */
/*
- * Physical Page Number: index into the page_map array.
- * PFN = (pa - g_mem_base) / kPageSize
+ * PFN (Page Frame Number) — fundamental conversion unit.
+ * When sparse vmemmap is implemented, only these two functions change.
+ * All other PA↔Page conversions go through them.
*/
-static inline uint64 pa_to_page_index(uint64 pa) {
- if (pa < g_mem_base || pa >= g_mem_base + g_num_pages * kPageSize)
- kernel_panic("pa_to_page: physical address outside managed range");
- return (pa - g_mem_base) / kPageSize;
-}
-
-static inline uint64 page_index_to_pa(uint64 idx) {
- if (idx >= g_num_pages)
- kernel_panic("page_to_pa: page index outside managed range");
- return g_mem_base + idx * kPageSize;
-}
-
-PhysAddr page_to_pa(const Page *page) {
+uint64 page_to_pfn(const Page *page) {
uintptr base = reinterpret_cast<uintptr>(g_page_map);
uintptr addr = reinterpret_cast<uintptr>(page);
uintptr end = base + g_num_pages * sizeof(Page);
- if (addr < base || addr >= end)
- kernel_panic("page_to_pa: page outside page_map bounds");
+ if (addr < base || addr >= end) {
+ kernel_panic("page_to_pfn: page outside page_map bounds");
+ }
uint64 idx = (addr - base) / sizeof(Page);
- return phys_addr(page_index_to_pa(idx));
+ return (g_mem_base / kPageSize) + idx;
+}
+
+Page *pfn_to_page(uint64 pfn) {
+ uint64 base_pfn = g_mem_base / kPageSize;
+ if (pfn < base_pfn || pfn >= base_pfn + g_num_pages) {
+ kernel_panic("pfn_to_page: PFN outside managed range");
+ }
+ return &g_page_map[pfn - base_pfn];
+}
+
+PhysAddr page_to_pa(const Page *page) {
+ return pfn_to_pa(Pfn{page_to_pfn(page)});
}
Page *pa_to_page(PhysAddr pa) {
- uint64 idx = pa_to_page_index(pa.raw);
- return &g_page_map[idx];
+ return pfn_to_page(pa_to_pfn(pa).raw);
}
VirtAddr page_to_va(const Page *page) {
return pa_to_va(page_to_pa(page));
}
+
/* ------------------------------------------------------------------ */
/* Buddy system internals (caller must hold g_pmm_lock) */
/* ------------------------------------------------------------------ */
+namespace {
+
/*
* Find a page's buddy at a given order.
* Buddy PFN = PFN ^ (1 << order).
*/
-static inline Page *buddy_of(Page *page, uint8 order) {
+inline Page *buddy_of(Page *page, uint8 order) {
uint64 pfn = static_cast<uint64>(page - g_page_map);
uint64 buddy_pfn = pfn ^ (1ULL << order);
- if (buddy_pfn >= g_num_pages)
+ if (buddy_pfn >= g_num_pages) {
return nullptr;
+ }
return &g_page_map[buddy_pfn];
}
/*
* Insert a free block into the buddy free list at the given order.
*/
-static void buddy_list_add(Page *page, uint8 order) {
- page->state = PageState::Free;
+void buddy_list_add(Page *page, uint8 order) {
+ page->type = PageType::Free;
page->order = order;
list_node *head = &g_free_areas[order];
list_node *node = &page->free.free_link;
@@ -131,7 +145,7 @@ static void buddy_list_add(Page *page, uint8 order) {
/*
* Remove a block from whatever buddy free list it is on.
*/
-static void buddy_list_del(Page *page) {
+void buddy_list_del(Page *page) {
list_node *node = &page->free.free_link;
node->prev->next = node->next;
node->next->prev = node->prev;
@@ -144,11 +158,12 @@ static void buddy_list_del(Page *page) {
* Splits higher-order blocks if no exact match is available.
* Returns nullptr if no memory available.
*/
-static Page *buddy_alloc(uint8 order) {
+Page *buddy_alloc(uint8 order) {
for (int cur = order; cur < kMaxOrder; cur++) {
list_node *head = &g_free_areas[cur];
- if (head->next == head)
+ if (head->next == head) {
continue; // Empty at this order
+ }
// Pop the first block from this order's free list
list_node *node = head->next;
@@ -166,9 +181,13 @@ static Page *buddy_alloc(uint8 order) {
buddy_list_add(split, static_cast<uint8>(cur));
}
- page->state = PageState::Allocated;
+ page->type = PageType::Allocated;
page->order = order;
page->refcount.store(1, __ATOMIC_RELAXED);
+ page->mapcount.store(0, __ATOMIC_RELAXED);
+ page->flags.store(0, __ATOMIC_RELAXED);
+ page->mapping = nullptr;
+ page->index = 0;
return page;
}
return nullptr;
@@ -178,16 +197,18 @@ static Page *buddy_alloc(uint8 order) {
* Return a block of 2^order pages to the buddy system.
* Coalesces with its buddy recursively up to kMaxOrder-1.
*/
-static void buddy_free(Page *page, uint8 order) {
+void buddy_free(Page *page, uint8 order) {
uint64 pfn = static_cast<uint64>(page - g_page_map);
while (order < kMaxOrder - 1) {
Page *buddy = buddy_of(page, order);
- if (!buddy)
+ if (!buddy) {
break;
+ }
// Buddy must be free AND at the same order to coalesce
- if (buddy->state != PageState::Free || buddy->order != order)
+ if (buddy->type != PageType::Free || buddy->order != order) {
break;
+ }
// Remove buddy from its free list and merge
buddy_list_del(buddy);
@@ -212,14 +233,15 @@ static void buddy_free(Page *page, uint8 order) {
* Refill the PCP from the global buddy system.
* Acquires g_pmm_lock internally.
*/
-static void refill_pcp(PerCpuCache *pcp) {
+void refill_pcp(PerCpuCache *pcp) {
LockGuard guard(g_pmm_lock);
for (uint32 i = 0; i < g_pcp_batch_size; i++) {
Page *page = buddy_alloc(0);
- if (!page)
+ if (!page) {
break;
+ }
// Put onto PCP list (reuse the free_link for PCP chain)
- page->state = PageState::PcpCached;
+ page->type = PageType::PcpCached;
list_node *node = &page->free.free_link;
node->next = pcp->free_list.next;
node->prev = &pcp->free_list;
@@ -233,13 +255,14 @@ static void refill_pcp(PerCpuCache *pcp) {
* Drain half the PCP back to the global buddy system.
* Acquires g_pmm_lock internally.
*/
-static void drain_pcp(PerCpuCache *pcp) {
+void drain_pcp(PerCpuCache *pcp) {
LockGuard guard(g_pmm_lock);
int to_drain = pcp->count / 2;
for (int i = 0; i < to_drain; i++) {
list_node *head = &pcp->free_list;
- if (head->next == head)
+ if (head->next == head) {
break;
+ }
list_node *node = head->next;
// Unlink from PCP
node->prev->next = node->next;
@@ -253,6 +276,8 @@ static void drain_pcp(PerCpuCache *pcp) {
}
}
+} // namespace
+
/* ------------------------------------------------------------------ */
/* Public API: single-page (fast path via PCP) */
/* ------------------------------------------------------------------ */
@@ -269,8 +294,9 @@ Page *pmm_alloc_page() {
uint64 cpu = arch::cpu::id();
PerCpuCache *pcp = &g_pcp[cpu];
- if (pcp->count == 0)
+ if (pcp->count == 0) {
refill_pcp(pcp);
+ }
Page *page = nullptr;
if (pcp->count > 0) {
@@ -289,19 +315,25 @@ Page *pmm_alloc_page() {
kernel_panic("pmm_alloc_page: PCP returned out-of-bounds page");
}
- page->state = PageState::Allocated;
+ page->type = PageType::Allocated;
page->order = 0;
page->refcount.store(1, __ATOMIC_RELAXED);
+ page->mapcount.store(0, __ATOMIC_RELAXED);
+ page->flags.store(0, __ATOMIC_RELAXED);
+ page->mapping = nullptr;
+ page->index = 0;
}
- if (was_on)
+ if (was_on) {
arch::cpu::intr_on();
+ }
return page;
}
void pmm_free_page(Page *page) {
- if (!page)
+ if (!page) {
kernel_panic("pmm_free_page: null page");
+ }
bool was_on = arch::cpu::intr_enabled();
arch::cpu::intr_off();
@@ -310,7 +342,7 @@ void pmm_free_page(Page *page) {
PerCpuCache *pcp = &g_pcp[cpu];
// Push onto PCP
- page->state = PageState::PcpCached;
+ page->type = PageType::PcpCached;
page->order = 0;
list_node *node = &page->free.free_link;
node->next = pcp->free_list.next;
@@ -320,11 +352,13 @@ void pmm_free_page(Page *page) {
pcp->count++;
// Drain if over high watermark
- if (pcp->count >= g_pcp_high_watermark)
+ if (pcp->count >= g_pcp_high_watermark) {
drain_pcp(pcp);
+ }
- if (was_on)
+ if (was_on) {
arch::cpu::intr_on();
+ }
}
/* ------------------------------------------------------------------ */
@@ -332,17 +366,20 @@ void pmm_free_page(Page *page) {
/* ------------------------------------------------------------------ */
Page *pmm_alloc_pages(uint8 order) {
- if (order >= kMaxOrder)
+ if (order >= kMaxOrder) {
return nullptr;
+ }
LockGuard guard(g_pmm_lock);
return buddy_alloc(order);
}
void pmm_free_pages(Page *page, uint8 order) {
- if (!page)
+ if (!page) {
kernel_panic("pmm_free_pages: null page");
- if (order >= kMaxOrder)
+ }
+ if (order >= kMaxOrder) {
kernel_panic("pmm_free_pages: order out of range");
+ }
LockGuard guard(g_pmm_lock);
buddy_free(page, order);
}
@@ -358,10 +395,11 @@ void page_decref(Page *page) {
if (old == 1) {
// Refcount dropped to 0 — return to pool based on originally allocated
// order
- if (page->order == 0)
+ if (page->order == 0) {
pmm_free_page(page);
- else
+ } else {
pmm_free_pages(page, page->order);
+ }
}
}
@@ -373,11 +411,14 @@ void page_decref(Page *page) {
* Helper: zero out a range of bytes. We cannot use memset here
* because lib/string may not be linked yet.
*/
-static void zero_range(void *start, uint64 bytes) {
+namespace {
+void zero_range(void *start, uint64 bytes) {
uint8 *p = static_cast<uint8 *>(start);
- for (uint64 i = 0; i < bytes; i++)
+ for (uint64 i = 0; i < bytes; i++) {
p[i] = 0;
+ }
}
+} // namespace
void pmm_init() {
/* 1. Get memory layout from memblock (already scanned FDT) */
@@ -390,9 +431,9 @@ void pmm_init() {
}
/* 2. PCP parameters */
- uint32 calc_batch = static_cast<uint32>(g_num_pages / (kMaxCpus * 1024));
- g_pcp_batch_size = (calc_batch > 16) ? calc_batch : 16;
- g_pcp_high_watermark = g_pcp_batch_size * 2;
+ uint32 calc_batch = static_cast<uint32>(g_num_pages / (kMaxCpus * kPcpBatchDivisor));
+ g_pcp_batch_size = (calc_batch > kPcpMinBatchSize) ? calc_batch : kPcpMinBatchSize;
+ g_pcp_high_watermark = g_pcp_batch_size * 4;
/* 3. page_map was allocated by vmm_init at vmemmap VA.
* No relocation needed — page_map starts life at the right VA. */
@@ -407,8 +448,13 @@ void pmm_init() {
for (uint64 i = 0; i < g_num_pages; i++) {
Page *page = &g_page_map[i];
page->refcount.store(0, __ATOMIC_RELAXED);
- page->state = PageState::Allocated;
+ page->mapcount.store(0, __ATOMIC_RELAXED);
+ page->flags.store(0, __ATOMIC_RELAXED);
+ page->type = PageType::Allocated;
page->order = 0;
+ page->mapping = nullptr;
+ page->index = 0;
+ page->lru.init();
page->free.free_link.init();
}
@@ -438,9 +484,7 @@ void pmm_init() {
uint64 start_pfn = (base - memblock_get_mem_base()) / kPageSize;
uint64 end_pfn = start_pfn + size / kPageSize;
- /* Access page_map via the extern g_page_map (already at vmemmap VA) */
- extern Page *g_page_map;
- extern list_node g_free_areas[];
+ /* Access page_map via the globals directly (they are in anon namespace now) */
uint64 pfn = start_pfn;
while (pfn < end_pfn) {
@@ -454,7 +498,7 @@ void pmm_init() {
}
Page *page = &g_page_map[pfn];
- page->state = PageState::Free;
+ page->type = PageType::Free;
page->order = order;
page->refcount.store(0, __ATOMIC_RELAXED);
page->free.free_link.init();
@@ -474,7 +518,7 @@ void pmm_init() {
&ctx);
kprintf("[pmm] buddy: %llu free pages (%llu MB)\n", ctx.total_pages,
- (ctx.total_pages * kPageSize) / (1024 * 1024));
+ (ctx.total_pages * kPageSize) / kMB);
/* 8. Retire memblock — all future allocations go through PMM */
memblock_retire();
diff --git a/na/mm/slab.cc b/na/mm/slab.cc
index e9b06cc..43d8c4a 100644
--- a/na/mm/slab.cc
+++ b/na/mm/slab.cc
@@ -79,7 +79,7 @@ Page* KmemCache::new_slab()
if (!page)
return nullptr;
- page->state = PageState::Slab;
+ page->type = PageType::Slab;
page->slab.cache = this;
page->slab.obj_count = 0;
@@ -197,7 +197,7 @@ void KmemCache::free(void* obj)
uint64 page_pa = obj_pa & ~(kPageSize - 1);
Page* page = pa_to_page(phys_addr(page_pa));
- if (page->state != PageState::Slab || page->slab.cache != this)
+ if (page->type != PageType::Slab || page->slab.cache != this)
kernel_panic("slab free: object does not belong to this cache");
bool was_on = arch::cpu::intr_enabled();
@@ -274,12 +274,12 @@ void kfree(void* ptr)
uint64 page_pa = pa & ~(kPageSize - 1);
Page* page = pa_to_page(phys_addr(page_pa));
- if (page->state == PageState::Slab) {
+ if (page->type == PageType::Slab) {
KmemCache* cache = page->slab.cache;
if (!cache)
kernel_panic("kfree: slab page has null cache");
cache->free(ptr);
- } else if (page->state == PageState::Allocated) {
+ } else if (page->type == PageType::Allocated) {
/* Large allocation — return directly to PMM */
pmm_free_pages(page, page->order);
} else {
diff --git a/na/mm/tlb.cc b/na/mm/tlb.cc
new file mode 100644
index 0000000..b7a8ba7
--- /dev/null
+++ b/na/mm/tlb.cc
@@ -0,0 +1,90 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * tlb.cc — TLB shootdown implementation
+ *
+ * Uses RISC-V SBI Remote Fence extension to perform cross-hart
+ * TLB invalidation. SBI firmware handles the IPI delivery and
+ * synchronization — the call blocks until all target harts have
+ * completed their local sfence.vma.
+ *
+ * When a VmSpace is provided, we use its cpu_mask to target only
+ * the CPUs that have this address space loaded in satp. This avoids
+ * unnecessary IPI storms on unrelated cores.
+ *
+ * Reference: docs/specs/linux_abi_mm_plan.md §SMP §2.1
+ */
+
+#include <lume/tlb.h>
+#include <lume/vmm.h>
+#include <lume/config.h>
+#include <arch/mmu.h>
+#include <arch/tlb.h>
+#include <arch/cpu.h>
+
+/*
+ * If a shootdown range exceeds this many pages, do a full flush
+ * instead of per-page invalidation.
+ */
+static constexpr uint64 kShootdownPageThreshold = 32;
+
+/*
+ * Build a cpu mask for all online CPUs (fallback when mm is null).
+ */
+namespace {
+uint64 all_cpus_mask()
+{
+ uint64 mask = 0;
+ for (uint32 i = 0; i < kMaxCpus; i++) {
+ mask |= (1ULL << i);
+ }
+ return mask;
+}
+
+/*
+ * Get the target cpu mask: use mm->cpu_mask when available,
+ * fall back to all_cpus_mask for kernel address space (mm == nullptr).
+ */
+uint64 target_cpu_mask(VmSpace* mm)
+{
+ if (mm) {
+ uint64 mask = mm->cpu_mask.load(__ATOMIC_RELAXED);
+ /* Always include our own CPU — we may still have stale TLB */
+ mask |= (1ULL << arch::cpu::id());
+ return mask;
+ }
+ return all_cpus_mask();
+}
+} // namespace
+
+void tlb_shootdown_range(VmSpace* mm, uint64 start, uint64 end)
+{
+ if (start >= end) {
+ return;
+ }
+
+ uint64 num_pages = (end - start) / kPageSize;
+ uint64 cpu_mask = target_cpu_mask(mm);
+
+ if (num_pages > kShootdownPageThreshold) {
+ /* Full flush on target CPUs via arch backend */
+ arch::tlb::flush_remote(cpu_mask, 0, ~0ULL);
+ return;
+ }
+
+ /* Range flush on target CPUs via arch backend */
+ arch::tlb::flush_remote(cpu_mask, start, end - start);
+}
+
+void tlb_shootdown_page(VmSpace* mm, uint64 va)
+{
+ uint64 cpu_mask = target_cpu_mask(mm);
+ arch::tlb::flush_remote(cpu_mask, va, kPageSize);
+}
+
+void tlb_shootdown_all()
+{
+ uint64 cpu_mask = all_cpus_mask();
+ arch::tlb::flush_remote(cpu_mask, 0, ~0ULL);
+}
diff --git a/na/mm/vma.cc b/na/mm/vma.cc
index 10db65d..da89e08 100644
--- a/na/mm/vma.cc
+++ b/na/mm/vma.cc
@@ -22,7 +22,8 @@
#include <lume/new.h>
#include <lume/klog.h>
#include <arch/pmap.h>
-#include <arch/mmu.h>
+#include <lume/tlb.h>
+#include <lume/mm_layout.h>
/* ------------------------------------------------------------------ */
/* Rbtree callbacks for VMA interval tree */
@@ -65,8 +66,7 @@ static bool range_overlaps(uint64 a_start, uint64 a_end,
return a_start < b_end && b_start < a_end;
}
-static constexpr uint64 kUserVaBase = kPageSize; // Avoid mapping the null page
-static constexpr uint64 kUserVaLimit = 0xFFFFFFC000000000ULL;
+
static bool vma_overlaps_locked(VmSpace* space, uint64 start, uint64 end)
{
@@ -188,11 +188,22 @@ VmSpace* VmSpace::create() {
space->vma_tree = RB_ROOT;
space->lock.init("VmSpace");
+ space->refcount.store(1, __ATOMIC_RELAXED);
space->brk_start = 0;
space->brk_end = 0;
return space;
}
+void VmSpace::incref() {
+ refcount.fetch_add(1, __ATOMIC_RELAXED);
+}
+
+void VmSpace::put() {
+ if (refcount.fetch_sub(1, __ATOMIC_ACQ_REL) == 1) {
+ destroy();
+ }
+}
+
void VmSpace::destroy() {
/* Drain VMA tree under lock, then release BEFORE delete this */
lock.acquire();
@@ -202,11 +213,13 @@ void VmSpace::destroy() {
VmAreaStruct* vma = reinterpret_cast<VmAreaStruct*>(
reinterpret_cast<char*>(node) - __builtin_offsetof(VmAreaStruct, rb));
- /* Decref all mapped physical pages */
+ /* Decref and dec mapcount for all mapped physical pages */
for (uint64 addr = vma->start; addr < vma->end; addr += kPageSize) {
uint64 pa;
if (pmap::lookup(root, addr, &pa)) {
- page_decref(pa_to_page(phys_addr(pa)));
+ Page* pg = pa_to_page(phys_addr(pa));
+ page_dec_mapcount(pg);
+ page_decref(pg);
}
}
@@ -285,8 +298,10 @@ int vmm_unmap_user(VmSpace* space, uint64 va, uint64 len) {
for (uint64 addr = unmap_start; addr < unmap_end; addr += kPageSize) {
uint64 pa;
if (pmap::lookup(space->root_pa, addr, &pa)) {
- page_decref(pa_to_page(phys_addr(pa)));
pmap::unmap(space->root_pa, addr);
+ Page* pg = pa_to_page(phys_addr(pa));
+ page_dec_mapcount(pg);
+ page_decref(pg);
}
}
}
@@ -362,7 +377,9 @@ int vmm_protect_user(VmSpace* space, uint64 va, uint64 len, uint64 perm)
}
if (perm == 0) {
- page_decref(pa_to_page(phys_addr(pa)));
+ Page* pg = pa_to_page(phys_addr(pa));
+ page_dec_mapcount(pg);
+ page_decref(pg);
pmap::unmap(space->root_pa, addr);
tlb_flush_needed = true;
continue;
@@ -385,7 +402,7 @@ int vmm_protect_user(VmSpace* space, uint64 va, uint64 len, uint64 perm)
}
if (tlb_flush_needed) {
- arch::mmu::flush_tlb_all();
+ tlb_shootdown_range(space, va, end);
}
return 0;
}
@@ -399,7 +416,11 @@ uint64 mm_find_free_va(VmSpace* space, uint64 length, uint64 align)
LockGuard guard(space->lock);
- uint64 cursor = align_up_any(kUserVaBase, align);
+ /* Linux: arch_get_unmapped_area() starts from mm->mmap_base.
+ * RISC-V: TASK_UNMAPPED_BASE = PAGE_ALIGN(TASK_SIZE / 3).
+ * Fall back to kUserVaBase if mmap_base not set (kernel VmSpaces). */
+ uint64 base = space->mmap_base ? space->mmap_base : kUserVaBase;
+ uint64 cursor = align_up_any(base, align);
struct rb_node* node = rb_first(&space->vma_tree);
while (node) {
VmAreaStruct* vma = node_to_vma(node);
@@ -494,22 +515,44 @@ int vmm_handle_page_fault(VmSpace* space, uint64 fault_addr, VmFaultCause cause)
return -ENOMEM;
}
- /* Decrement refcount of old page */
- page_decref(pa_to_page(phys_addr(paddr)));
+ /* New page gets mapcount = 1 (we just mapped it) */
+ page_inc_mapcount(page);
+
+ /* Decrement mapcount and refcount of old page */
+ Page* old_page = pa_to_page(phys_addr(paddr));
+ page_dec_mapcount(old_page);
+ page_decref(old_page);
/* Flush TLB to remove old read-only cached translation */
- asm volatile("sfence.vma %0" : : "r"(aligned_addr) : "memory");
+ tlb_shootdown_page(space, aligned_addr);
return 0;
}
+ /* DEBUG: page already mapped but still faulting — dump PTE */
+ {
+ uint64 cur_satp;
+ asm volatile("csrr %0, satp" : "=r"(cur_satp));
+ printk(KERN_ERR "fault: va=%llx pa=%llx cause=%d pte=%llx satp=%llx root=%llx\n",
+ aligned_addr, paddr, static_cast<int>(cause),
+ pmap::lookup_pte(space->root_pa, aligned_addr),
+ cur_satp, space->root_pa);
+ }
return 0; // Already mapped, maybe read fault on existing page
}
- /* Allocate physical page */
+ /* Allocate physical page and zero it.
+ * Linux: do_anonymous_page() uses alloc_zeroed_user_highpage_movable().
+ * Anonymous pages MUST be zeroed — userspace expects MAP_ANONYMOUS
+ * pages to contain all zeros (POSIX requirement). */
Page* page = pmm_alloc_page();
if (!page) return -ENOMEM;
uint64 pa = page_to_pa(page).raw;
+ /* Zero the page before mapping it to userspace */
+ /* Zero the page before mapping it to userspace */
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ for (uint64 i = 0; i < kPageSize; i++) kva[i] = 0;
+
/* Translate MI permission flags to hardware PTE bits (MD layer) */
uint64 pte_perm = pmap::vm_perm_to_pte(vma->perm);
@@ -518,7 +561,9 @@ int vmm_handle_page_fault(VmSpace* space, uint64 fault_addr, VmFaultCause cause)
return -ENOMEM;
}
- /* Initial mapping, refcount is already 1 from pmm_alloc_page */
+ /* Initial mapping: refcount is already 1 from pmm_alloc_page.
+ * Set mapcount = 1 to track this PTE mapping. */
+ page_inc_mapcount(page);
return 0;
}
@@ -561,7 +606,7 @@ VmSpace* vmm_clone(VmSpace* parent) {
if (child_vma->perm & VM_WRITE) {
pmap::make_cow(parent->root_pa, va);
/* Flush TLB for parent since we reduced permissions */
- asm volatile("sfence.vma %0" : : "r"(va) : "memory");
+ tlb_shootdown_page(parent, va);
}
/* Map in child as COW or read-only */
@@ -580,8 +625,10 @@ VmSpace* vmm_clone(VmSpace* parent) {
return nullptr;
}
- /* Increment physical page refcount */
- page_incref(pa_to_page(phys_addr(pa)));
+ /* Increment physical page refcount and mapcount */
+ Page* shared_page = pa_to_page(phys_addr(pa));
+ page_incref(shared_page);
+ page_inc_mapcount(shared_page);
}
}
diff --git a/na/mm/vmm.cc b/na/mm/vmm.cc
index 0aa2fb0..51617e3 100644
--- a/na/mm/vmm.cc
+++ b/na/mm/vmm.cc
@@ -18,6 +18,7 @@
#include <arch/config.h>
#include <arch/mmu.h>
#include <arch/pmap.h>
+#include <lume/tlb.h>
#include <lume/addr.h>
#include <lume/console.h>
#include <lume/fdt.h>
@@ -28,6 +29,7 @@
#include <lume/pmm.h>
#include <lume/types.h>
#include <lume/vmm.h>
+#include <lume/config.h>
/* Linker-exported section boundaries (virtual addresses) */
extern "C" char _stext[], _etext[];
@@ -36,25 +38,23 @@ extern "C" char _sdata[], _edata[];
extern "C" char _sbss[], _ebss[];
extern "C" char _kernel_end[];
-/* Global kernel page table root PA */
-static uint64 g_kernel_pgtbl = 0;
+/* Global kernel page table root PA — external linkage for task_exit */
+uint64 g_kernel_pgtbl = 0;
-/* VMM exports for PMM: page_map location after vmemmap mapping. */
-static uint64 g_vmemmap_page_map_pa = 0;
-static uint64 g_vmemmap_page_map_size = 0;
-static uint64 g_vmemmap_num_pages = 0;
+namespace {
-uint64 vmm_get_page_map_pa() { return g_vmemmap_page_map_pa; }
-uint64 vmm_get_page_map_size() { return g_vmemmap_page_map_size; }
-uint64 vmm_get_num_pages() { return g_vmemmap_num_pages; }
+/* VMM exports for PMM: page_map location after vmemmap mapping. */
+uint64 g_vmemmap_page_map_pa = 0;
+uint64 g_vmemmap_page_map_size = 0;
+uint64 g_vmemmap_num_pages = 0;
/* Convert a linker symbol to PA (kernel is executing in higher-half VA). */
-static inline uint64 sym_to_pa(const void *sym) {
+inline uint64 sym_to_pa(const void *sym) {
return boot_va_to_pa(virt_addr(reinterpret_cast<uint64>(sym))).raw;
}
/* Map a PA range into the kernel page table via the direct map. */
-static void map_range(uint64 root, uint64 pa_start, uint64 pa_end,
+void map_range(uint64 root, uint64 pa_start, uint64 pa_end,
uint64 perm) {
for (uint64 pa = pa_start; pa < pa_end; pa += kPageSize) {
uint64 va = pa_to_va(phys_addr(pa)).raw;
@@ -66,7 +66,7 @@ static void map_range(uint64 root, uint64 pa_start, uint64 pa_end,
}
/* Map a PA range into the kernel page table at the fixed linked VA. */
-static void map_range_linked(uint64 root, uint64 pa_start, uint64 pa_end,
+void map_range_linked(uint64 root, uint64 pa_start, uint64 pa_end,
uint64 perm) {
for (uint64 pa = pa_start; pa < pa_end; pa += kPageSize) {
uint64 va = boot_pa_to_va(phys_addr(pa)).raw;
@@ -78,7 +78,7 @@ static void map_range_linked(uint64 root, uint64 pa_start, uint64 pa_end,
}
/* Map a PA range to an explicit VA base, using 2MB pages where possible. */
-static void map_range_at_huge(uint64 root, uint64 va_base, uint64 pa_start,
+void map_range_at_huge(uint64 root, uint64 va_base, uint64 pa_start,
uint64 pa_end, uint64 perm) {
constexpr uint64 k2MB = 2ULL * 1024 * 1024;
uint64 pa = pa_start;
@@ -109,6 +109,12 @@ static void map_range_at_huge(uint64 root, uint64 va_base, uint64 pa_start,
}
}
+} // namespace
+
+uint64 vmm_get_page_map_pa() { return g_vmemmap_page_map_pa; }
+uint64 vmm_get_page_map_size() { return g_vmemmap_page_map_size; }
+uint64 vmm_get_num_pages() { return g_vmemmap_num_pages; }
+
void vmm_init() {
/* 1. Allocate new root page table (from memblock) */
uint64 root = pmap::create();
@@ -137,17 +143,36 @@ void vmm_init() {
*/
map_range_linked(root, text_start, text_end, pmap::PTE_R | pmap::PTE_X);
- /* 4. Map .rodata (R) at fixed linked VA */
- map_range_linked(root, ro_start, ro_end, pmap::PTE_R);
+ /* 4. Map .rodata (R) at fixed linked VA.
+ * We map [_srodata, _sdata) so that the .lume.reloc section (which
+ * sits in its own padded output section between _erodata and _sdata)
+ * is also covered. Relocator only reads it during early boot, but
+ * mapping it keeps post-vmm accesses to __lume_reloc_* symbols sane. */
+ map_range_linked(root, ro_start, page_align_down(sym_to_pa(_sdata)),
+ pmap::PTE_R);
/* 5. Map .data + .bss (RW) at fixed linked VA */
map_range_linked(root, data_start, bss_end, pmap::PTE_R | pmap::PTE_W);
- /* 6. Map remaining physical memory after kernel (RW) */
- uint64 after_kernel = page_align_up(sym_to_pa(_kernel_end));
- if (after_kernel < mem_end) {
- map_range(root, after_kernel, mem_end, pmap::PTE_R | pmap::PTE_W);
+ /* 6. Map usable physical RAM into the KASLR-slid direct map.
+ *
+ * The direct map provides pa_to_va() / va_to_pa() for the physical
+ * address space. We map from kPhysStart (the kernel's linked load
+ * address), skipping [mem_base, kPhysStart) because that region is
+ * PMP-protected firmware (OpenSBI) — S-mode cannot access it.
+ *
+ * Linux does the same on RISC-V: the linear map covers only the
+ * usable RAM range, not firmware-reserved regions.
+ *
+ * Uses 2MB huge pages for efficiency (64 entries for 128MB vs 32K).
+ */
+ uint64 dm_start = kPhysStart;
+ if (dm_start < mem_base) {
+ dm_start = mem_base;
}
+ uint64 dm_va_base = pa_to_va(phys_addr(dm_start)).raw;
+ map_range_at_huge(root, dm_va_base, dm_start, mem_end,
+ pmap::PTE_R | pmap::PTE_W);
/* 7. Allocate page_map from memblock (top of RAM) and map at VMEMMAP.
*
@@ -206,5 +231,5 @@ void vmm_map_kernel_mmio(uint64 pa, uint64 size) {
map_range(g_kernel_pgtbl, aligned_pa, aligned_pa + aligned_size,
pmap::PTE_R | pmap::PTE_W | pmap::PTE_G);
- arch::mmu::flush_tlb_all();
+ tlb_shootdown_all();
}
diff --git a/na/scripts/Kbuild.include b/na/scripts/Kbuild.include
new file mode 100644
index 0000000..76466bd
--- /dev/null
+++ b/na/scripts/Kbuild.include
@@ -0,0 +1,110 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2026 Richard Qin
+#
+# scripts/Kbuild.include — shared Make helpers for LumeOS.
+#
+# Mirrors a small, useful subset of Linux's scripts/Kbuild.include.
+# Provides:
+#
+# - Q / KBUILD_VERBOSE : `make V=1` shows full commands,
+# default V=0 prints a one-line summary
+# - $(call cmd,<name>) : echo a quiet summary, then run cmd_<name>
+# - $(call if_changed,<name>) : currently a thin alias for $(cmd) —
+# no .cmd-file hashing yet (Linux's full
+# if_changed uses fixdep to detect both
+# recipe-text and header changes; LumeOS
+# relies on `-MMD -MP` for headers and
+# on `make clean` for flag changes).
+# - $(call kbuild-objs,...) : translate Kbuild-style `obj-y := foo.o`
+# entries into SRCS_C / SRCS_CC.
+#
+# Naming convention adopted from Linux: each command has both a
+# `quiet_cmd_<name>` (single-line summary) and a `cmd_<name>` (full
+# command line). $(quiet) selects which prefix to display:
+# V=0 -> quiet := quiet_ -> echoes " CC foo.c"
+# V=1 -> quiet := -> echoes the full $(CC) $(CFLAGS) ...
+# ----------------------------------------------------------------------
+
+# --- Verbosity ---------------------------------------------------------
+# Override at the command line: `make V=1`. Default V=0 = quiet.
+KBUILD_VERBOSE ?= $(V)
+KBUILD_VERBOSE ?= 0
+
+ifeq ($(KBUILD_VERBOSE),1)
+ Q :=
+ quiet :=
+else
+ Q := @
+ quiet := quiet_
+endif
+
+# --- echo-cmd / cmd / if_changed ---------------------------------------
+#
+# echo-cmd prints the recipe's one-line summary (when quiet) or its full
+# command line (when verbose). Implementation uses $($(quiet)cmd_<name>)
+# so the right form is picked up automatically.
+#
+# cmd is the workhorse: print summary, then execute the real command.
+echo-cmd = $(if $($(quiet)cmd_$(1)),echo ' $($(quiet)cmd_$(1))';)
+
+cmd = @$(echo-cmd) $(cmd_$(1))
+
+# if_changed currently degenerates to `cmd` — full Linux semantics
+# require tracking the command-line via per-target .<file>.cmd files
+# and comparing on every rebuild, which needs fixdep. Until LumeOS
+# grows that infrastructure, `make clean` after editing CFLAGS suffices.
+if_changed = $(cmd)
+
+# --- Standard quiet_cmd_* / cmd_* templates ---------------------------
+#
+# Subdir Makefiles and the top-level Makefile can reference these by
+# name via $(call cmd,<name>). Add new pairs here as new recipe shapes
+# show up (e.g., cmd_lz / cmd_gz / cmd_zstd for compressed Image targets).
+
+ quiet_cmd_cc_o_c = CC $<
+ cmd_cc_o_c = $(CC) $(CFLAGS_LIBC) -c $< -o $@
+
+ quiet_cmd_cxx_o_cc = CXX $<
+ cmd_cxx_o_cc = $(CXX) $(CXXFLAGS) -c $< -o $@
+
+ quiet_cmd_as_o_S = AS $<
+ cmd_as_o_S = $(CC) $(ASFLAGS) -c $< -o $@
+
+ quiet_cmd_cc_s_c = CC -S $<
+ cmd_cc_s_c = $(CC) $(CFLAGS_LIBC) -S $< -o $@
+
+ quiet_cmd_objcopy_bin = OBJCOPY $@
+ cmd_objcopy_bin = $(OBJCOPY) -O binary $< $@
+
+# Objcopy with custom flags via per-target OBJCOPYFLAGS variable —
+# used by the .pi.o post-processing rule (--prefix-symbols=__pi_ etc.).
+ quiet_cmd_objcopy = OBJCOPY $@
+ cmd_objcopy = $(OBJCOPY) $(OBJCOPYFLAGS) $< $@
+
+ quiet_cmd_ld_kernel = LD $@
+ cmd_ld_kernel = $(LD) $(LDFLAGS) -T $(LINKER_SCRIPT) -o $@ $^
+
+# --- kbuild-objs: obj-y -> SRCS_C / SRCS_CC ---------------------------
+#
+# Usage from a subdir Makefile (e.g. arch/riscv/kernel/pi/Makefile):
+#
+# obj-dir := arch/riscv/kernel/pi
+# obj-y := fdt_early.pi.o cmdline_early.pi.o archrandom_early.pi.o
+# $(eval $(call kbuild-objs,$(obj-dir),$(obj-y:.pi.o=.o)))
+#
+# For each <name>.o entry, looks up <dir>/<name>.cc first, then
+# <dir>/<name>.c, and appends to SRCS_CC / SRCS_C respectively.
+# Pure .pi.o entries should be passed through `:.pi.o=.o` first by the
+# caller so the .pi suffix doesn't confuse the source-file lookup.
+
+define _kbuild-find-src
+$(if $(wildcard $(1)/$(basename $(2)).cc),\
+ $(eval SRCS_CC += $(1)/$(basename $(2)).cc),\
+ $(if $(wildcard $(1)/$(basename $(2)).c),\
+ $(eval SRCS_C += $(1)/$(basename $(2)).c),\
+ $(error kbuild-objs: no $(1)/$(basename $(2)).{c,cc} for obj $(2))))
+endef
+
+define kbuild-objs
+$(foreach _obj,$(2),$(call _kbuild-find-src,$(1),$(_obj)))
+endef
diff --git a/na/scripts/Kconfig.mk b/na/scripts/Kconfig.mk
index c979e76..2bdc934 100644
--- a/na/scripts/Kconfig.mk
+++ b/na/scripts/Kconfig.mk
@@ -60,6 +60,18 @@ CONFIG_DEBUG_LOCKS ?= y
CONFIG_SELFTEST ?= y
CONFIG_KLOG ?= y
+# --- Initramfs ------------------------------------------------------
+# Source for the initramfs image. Accepted forms (auto-detected by
+# scripts/gen_initramfs.sh):
+# - a listing file (gen_init_cpio format) → built into a cpio
+# - a directory → packed recursively
+# - a *.cpio file → used as-is
+# Override on the command line or pin in .config:
+# make all CONFIG_INITRAMFS_SOURCE=/path/to/rootfs
+CONFIG_INITRAMFS_SOURCE ?= usr/default_cpio_list
+CONFIG_INITRAMFS_ROOT_UID ?= 0
+CONFIG_INITRAMFS_ROOT_GID ?= 0
+
# ------------------------------------------------------------------
# §3 Derived constants (computed from CONFIG_ symbols)
#
diff --git a/na/scripts/dtc/libfdt/fdt.c b/na/scripts/dtc/libfdt/fdt.c
new file mode 100644
index 0000000..56d4dcb
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt.c
@@ -0,0 +1,347 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ */
+#include "libfdt_env.h"
+
+#include <fdt.h>
+#include <libfdt.h>
+
+#include "libfdt_internal.h"
+
+/*
+ * Minimal sanity check for a read-only tree. fdt_ro_probe_() checks
+ * that the given buffer contains what appears to be a flattened
+ * device tree with sane information in its header.
+ */
+int32_t fdt_ro_probe_(const void *fdt)
+{
+ uint32_t totalsize = fdt_totalsize(fdt);
+
+ if (can_assume(VALID_DTB))
+ return totalsize;
+
+ /* The device tree must be at an 8-byte aligned address */
+ if ((uintptr_t)fdt & 7)
+ return -FDT_ERR_ALIGNMENT;
+
+ if (fdt_magic(fdt) == FDT_MAGIC) {
+ /* Complete tree */
+ if (!can_assume(LATEST)) {
+ if (fdt_version(fdt) < FDT_FIRST_SUPPORTED_VERSION)
+ return -FDT_ERR_BADVERSION;
+ if (fdt_last_comp_version(fdt) >
+ FDT_LAST_SUPPORTED_VERSION)
+ return -FDT_ERR_BADVERSION;
+ }
+ } else if (fdt_magic(fdt) == FDT_SW_MAGIC) {
+ /* Unfinished sequential-write blob */
+ if (!can_assume(VALID_INPUT) && fdt_size_dt_struct(fdt) == 0)
+ return -FDT_ERR_BADSTATE;
+ } else {
+ return -FDT_ERR_BADMAGIC;
+ }
+
+ if (totalsize < INT32_MAX)
+ return totalsize;
+ else
+ return -FDT_ERR_TRUNCATED;
+}
+
+static int check_off_(uint32_t hdrsize, uint32_t totalsize, uint32_t off)
+{
+ return (off >= hdrsize) && (off <= totalsize);
+}
+
+static int check_block_(uint32_t hdrsize, uint32_t totalsize,
+ uint32_t base, uint32_t size)
+{
+ if (!check_off_(hdrsize, totalsize, base))
+ return 0; /* block start out of bounds */
+ if ((base + size) < base)
+ return 0; /* overflow */
+ if (!check_off_(hdrsize, totalsize, base + size))
+ return 0; /* block end out of bounds */
+ return 1;
+}
+
+size_t fdt_header_size_(uint32_t version)
+{
+ if (version <= 1)
+ return FDT_V1_SIZE;
+ else if (version <= 2)
+ return FDT_V2_SIZE;
+ else if (version <= 3)
+ return FDT_V3_SIZE;
+ else if (version <= 16)
+ return FDT_V16_SIZE;
+ else
+ return FDT_V17_SIZE;
+}
+
+size_t fdt_header_size(const void *fdt)
+{
+ return can_assume(LATEST) ? FDT_V17_SIZE :
+ fdt_header_size_(fdt_version(fdt));
+}
+
+int fdt_check_header(const void *fdt)
+{
+ size_t hdrsize;
+
+ /* The device tree must be at an 8-byte aligned address */
+ if ((uintptr_t)fdt & 7)
+ return -FDT_ERR_ALIGNMENT;
+
+ if (fdt_magic(fdt) != FDT_MAGIC)
+ return -FDT_ERR_BADMAGIC;
+ if (!can_assume(LATEST)) {
+ if ((fdt_version(fdt) < FDT_FIRST_SUPPORTED_VERSION)
+ || (fdt_last_comp_version(fdt) >
+ FDT_LAST_SUPPORTED_VERSION))
+ return -FDT_ERR_BADVERSION;
+ if (fdt_version(fdt) < fdt_last_comp_version(fdt))
+ return -FDT_ERR_BADVERSION;
+ }
+ hdrsize = fdt_header_size(fdt);
+ if (!can_assume(VALID_DTB)) {
+ if ((fdt_totalsize(fdt) < hdrsize)
+ || (fdt_totalsize(fdt) > INT_MAX))
+ return -FDT_ERR_TRUNCATED;
+
+ /* memrsv block must be 8 byte aligned */
+ if (fdt_off_mem_rsvmap(fdt) % sizeof(uint64_t))
+ return -FDT_ERR_ALIGNMENT;
+
+ /* Structure block must be 4 byte aligned */
+ if (fdt_off_dt_struct(fdt) % FDT_TAGSIZE)
+ return -FDT_ERR_ALIGNMENT;
+
+ /* Bounds check memrsv block */
+ if (!check_off_(hdrsize, fdt_totalsize(fdt),
+ fdt_off_mem_rsvmap(fdt)))
+ return -FDT_ERR_TRUNCATED;
+
+ /* Bounds check structure block */
+ if (!can_assume(LATEST) && fdt_version(fdt) < 17) {
+ if (!check_off_(hdrsize, fdt_totalsize(fdt),
+ fdt_off_dt_struct(fdt)))
+ return -FDT_ERR_TRUNCATED;
+ } else {
+ if (!check_block_(hdrsize, fdt_totalsize(fdt),
+ fdt_off_dt_struct(fdt),
+ fdt_size_dt_struct(fdt)))
+ return -FDT_ERR_TRUNCATED;
+ }
+
+ /* Bounds check strings block */
+ if (!check_block_(hdrsize, fdt_totalsize(fdt),
+ fdt_off_dt_strings(fdt),
+ fdt_size_dt_strings(fdt)))
+ return -FDT_ERR_TRUNCATED;
+ }
+
+ return 0;
+}
+
+const void *fdt_offset_ptr(const void *fdt, int offset, unsigned int len)
+{
+ unsigned int uoffset = offset;
+ unsigned int absoffset = offset + fdt_off_dt_struct(fdt);
+
+ if (offset < 0)
+ return NULL;
+
+ if (!can_assume(VALID_INPUT))
+ if ((absoffset < uoffset)
+ || ((absoffset + len) < absoffset)
+ || (absoffset + len) > fdt_totalsize(fdt))
+ return NULL;
+
+ if (can_assume(LATEST) || fdt_version(fdt) >= 0x11)
+ if (((uoffset + len) < uoffset)
+ || ((offset + len) > fdt_size_dt_struct(fdt)))
+ return NULL;
+
+ return fdt_offset_ptr_(fdt, offset);
+}
+
+uint32_t fdt_next_tag(const void *fdt, int startoffset, int *nextoffset)
+{
+ const fdt32_t *tagp, *lenp;
+ uint32_t tag, len, sum;
+ int offset = startoffset;
+ const char *p;
+
+ *nextoffset = -FDT_ERR_TRUNCATED;
+ tagp = fdt_offset_ptr(fdt, offset, FDT_TAGSIZE);
+ if (!can_assume(VALID_DTB) && !tagp)
+ return FDT_END; /* premature end */
+ tag = fdt32_to_cpu(*tagp);
+ offset += FDT_TAGSIZE;
+
+ *nextoffset = -FDT_ERR_BADSTRUCTURE;
+ switch (tag) {
+ case FDT_BEGIN_NODE:
+ /* skip name */
+ do {
+ p = fdt_offset_ptr(fdt, offset++, 1);
+ } while (p && (*p != '\0'));
+ if (!can_assume(VALID_DTB) && !p)
+ return FDT_END; /* premature end */
+ break;
+
+ case FDT_PROP:
+ lenp = fdt_offset_ptr(fdt, offset, sizeof(*lenp));
+ if (!can_assume(VALID_DTB) && !lenp)
+ return FDT_END; /* premature end */
+
+ len = fdt32_to_cpu(*lenp);
+ sum = len + offset;
+ if (!can_assume(VALID_DTB) &&
+ (INT_MAX <= sum || sum < (uint32_t) offset))
+ return FDT_END; /* premature end */
+
+ /* skip-name offset, length and value */
+ offset += sizeof(struct fdt_property) - FDT_TAGSIZE + len;
+
+ if (!can_assume(LATEST) &&
+ fdt_version(fdt) < 0x10 && len >= 8 &&
+ ((offset - len) % 8) != 0)
+ offset += 4;
+ break;
+
+ case FDT_END:
+ case FDT_END_NODE:
+ case FDT_NOP:
+ break;
+
+ default:
+ return FDT_END;
+ }
+
+ if (!fdt_offset_ptr(fdt, startoffset, offset - startoffset))
+ return FDT_END; /* premature end */
+
+ *nextoffset = FDT_TAGALIGN(offset);
+ return tag;
+}
+
+int fdt_check_node_offset_(const void *fdt, int offset)
+{
+ if (!can_assume(VALID_INPUT)
+ && ((offset < 0) || (offset % FDT_TAGSIZE)))
+ return -FDT_ERR_BADOFFSET;
+
+ if (fdt_next_tag(fdt, offset, &offset) != FDT_BEGIN_NODE)
+ return -FDT_ERR_BADOFFSET;
+
+ return offset;
+}
+
+int fdt_check_prop_offset_(const void *fdt, int offset)
+{
+ if (!can_assume(VALID_INPUT)
+ && ((offset < 0) || (offset % FDT_TAGSIZE)))
+ return -FDT_ERR_BADOFFSET;
+
+ if (fdt_next_tag(fdt, offset, &offset) != FDT_PROP)
+ return -FDT_ERR_BADOFFSET;
+
+ return offset;
+}
+
+int fdt_next_node(const void *fdt, int offset, int *depth)
+{
+ int nextoffset = 0;
+ uint32_t tag;
+
+ if (offset >= 0)
+ if ((nextoffset = fdt_check_node_offset_(fdt, offset)) < 0)
+ return nextoffset;
+
+ do {
+ offset = nextoffset;
+ tag = fdt_next_tag(fdt, offset, &nextoffset);
+
+ switch (tag) {
+ case FDT_PROP:
+ case FDT_NOP:
+ break;
+
+ case FDT_BEGIN_NODE:
+ if (depth)
+ (*depth)++;
+ break;
+
+ case FDT_END_NODE:
+ if (depth && ((--(*depth)) < 0))
+ return nextoffset;
+ break;
+
+ case FDT_END:
+ if ((nextoffset >= 0)
+ || ((nextoffset == -FDT_ERR_TRUNCATED) && !depth))
+ return -FDT_ERR_NOTFOUND;
+ else
+ return nextoffset;
+ }
+ } while (tag != FDT_BEGIN_NODE);
+
+ return offset;
+}
+
+int fdt_first_subnode(const void *fdt, int offset)
+{
+ int depth = 0;
+
+ offset = fdt_next_node(fdt, offset, &depth);
+ if (offset < 0 || depth != 1)
+ return -FDT_ERR_NOTFOUND;
+
+ return offset;
+}
+
+int fdt_next_subnode(const void *fdt, int offset)
+{
+ int depth = 1;
+
+ /*
+ * With respect to the parent, the depth of the next subnode will be
+ * the same as the last.
+ */
+ do {
+ offset = fdt_next_node(fdt, offset, &depth);
+ if (offset < 0 || depth < 1)
+ return -FDT_ERR_NOTFOUND;
+ } while (depth > 1);
+
+ return offset;
+}
+
+const char *fdt_find_string_len_(const char *strtab, int tabsize, const char *s,
+ int slen)
+{
+ const char *last = strtab + tabsize - (slen + 1);
+ const char *p;
+
+ for (p = strtab; p <= last; p++)
+ if (memcmp(p, s, slen) == 0 && p[slen] == '\0')
+ return p;
+ return NULL;
+}
+
+int fdt_move(const void *fdt, void *buf, int bufsize)
+{
+ if (!can_assume(VALID_INPUT) && bufsize < 0)
+ return -FDT_ERR_NOSPACE;
+
+ FDT_RO_PROBE(fdt);
+
+ if (fdt_totalsize(fdt) > (unsigned int)bufsize)
+ return -FDT_ERR_NOSPACE;
+
+ memmove(buf, fdt, fdt_totalsize(fdt));
+ return 0;
+}
diff --git a/na/scripts/dtc/libfdt/fdt.h b/na/scripts/dtc/libfdt/fdt.h
new file mode 100644
index 0000000..a07abfc
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause) */
+#ifndef FDT_H
+#define FDT_H
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ * Copyright 2012 Kim Phillips, Freescale Semiconductor.
+ */
+
+#ifndef __ASSEMBLER__
+
+struct fdt_header {
+ fdt32_t magic; /* magic word FDT_MAGIC */
+ fdt32_t totalsize; /* total size of DT block */
+ fdt32_t off_dt_struct; /* offset to structure */
+ fdt32_t off_dt_strings; /* offset to strings */
+ fdt32_t off_mem_rsvmap; /* offset to memory reserve map */
+ fdt32_t version; /* format version */
+ fdt32_t last_comp_version; /* last compatible version */
+
+ /* version 2 fields below */
+ fdt32_t boot_cpuid_phys; /* Which physical CPU id we're
+ booting on */
+ /* version 3 fields below */
+ fdt32_t size_dt_strings; /* size of the strings block */
+
+ /* version 17 fields below */
+ fdt32_t size_dt_struct; /* size of the structure block */
+};
+
+struct fdt_reserve_entry {
+ fdt64_t address;
+ fdt64_t size;
+};
+
+struct fdt_node_header {
+ fdt32_t tag;
+ char name[];
+};
+
+struct fdt_property {
+ fdt32_t tag;
+ fdt32_t len;
+ fdt32_t nameoff;
+ char data[];
+};
+
+#endif /* !__ASSEMBLER__ */
+
+#define FDT_MAGIC 0xd00dfeed /* 4: version, 4: total size */
+#define FDT_TAGSIZE sizeof(fdt32_t)
+
+#define FDT_BEGIN_NODE 0x1 /* Start node: full name */
+#define FDT_END_NODE 0x2 /* End node */
+#define FDT_PROP 0x3 /* Property: name off,
+ size, content */
+#define FDT_NOP 0x4 /* nop */
+#define FDT_END 0x9
+
+#define FDT_V1_SIZE (7*sizeof(fdt32_t))
+#define FDT_V2_SIZE (FDT_V1_SIZE + sizeof(fdt32_t))
+#define FDT_V3_SIZE (FDT_V2_SIZE + sizeof(fdt32_t))
+#define FDT_V16_SIZE FDT_V3_SIZE
+#define FDT_V17_SIZE (FDT_V16_SIZE + sizeof(fdt32_t))
+
+#endif /* FDT_H */
diff --git a/na/scripts/dtc/libfdt/fdt_addresses.c b/na/scripts/dtc/libfdt/fdt_addresses.c
new file mode 100644
index 0000000..c40ba09
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt_addresses.c
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2014 David Gibson <david@gibson.dropbear.id.au>
+ * Copyright (C) 2018 embedded brains GmbH
+ */
+#include "libfdt_env.h"
+
+#include <fdt.h>
+#include <libfdt.h>
+
+#include "libfdt_internal.h"
+
+static int fdt_cells(const void *fdt, int nodeoffset, const char *name)
+{
+ const fdt32_t *c;
+ uint32_t val;
+ int len;
+
+ c = fdt_getprop(fdt, nodeoffset, name, &len);
+ if (!c)
+ return len;
+
+ if (len != sizeof(*c))
+ return -FDT_ERR_BADNCELLS;
+
+ val = fdt32_to_cpu(*c);
+ if (val > FDT_MAX_NCELLS)
+ return -FDT_ERR_BADNCELLS;
+
+ return (int)val;
+}
+
+int fdt_address_cells(const void *fdt, int nodeoffset)
+{
+ int val;
+
+ val = fdt_cells(fdt, nodeoffset, "#address-cells");
+ if (val == 0)
+ return -FDT_ERR_BADNCELLS;
+ if (val == -FDT_ERR_NOTFOUND)
+ return 2;
+ return val;
+}
+
+int fdt_size_cells(const void *fdt, int nodeoffset)
+{
+ int val;
+
+ val = fdt_cells(fdt, nodeoffset, "#size-cells");
+ if (val == -FDT_ERR_NOTFOUND)
+ return 1;
+ return val;
+}
+
+/* This function assumes that [address|size]_cells is 1 or 2 */
+int fdt_appendprop_addrrange(void *fdt, int parent, int nodeoffset,
+ const char *name, uint64_t addr, uint64_t size)
+{
+ int addr_cells, size_cells, ret;
+ uint8_t data[sizeof(fdt64_t) * 2], *prop;
+
+ ret = fdt_address_cells(fdt, parent);
+ if (ret < 0)
+ return ret;
+ addr_cells = ret;
+
+ ret = fdt_size_cells(fdt, parent);
+ if (ret < 0)
+ return ret;
+ size_cells = ret;
+
+ /* check validity of address */
+ prop = data;
+ if (addr_cells == 1) {
+ if ((addr > UINT32_MAX) || (((uint64_t) UINT32_MAX + 1 - addr) < size))
+ return -FDT_ERR_BADVALUE;
+
+ fdt32_st(prop, (uint32_t)addr);
+ } else if (addr_cells == 2) {
+ fdt64_st(prop, addr);
+ } else {
+ return -FDT_ERR_BADNCELLS;
+ }
+
+ /* check validity of size */
+ prop += addr_cells * sizeof(fdt32_t);
+ if (size_cells == 1) {
+ if (size > UINT32_MAX)
+ return -FDT_ERR_BADVALUE;
+
+ fdt32_st(prop, (uint32_t)size);
+ } else if (size_cells == 2) {
+ fdt64_st(prop, size);
+ } else {
+ return -FDT_ERR_BADNCELLS;
+ }
+
+ return fdt_appendprop(fdt, nodeoffset, name, data,
+ (addr_cells + size_cells) * sizeof(fdt32_t));
+}
diff --git a/na/scripts/dtc/libfdt/fdt_empty_tree.c b/na/scripts/dtc/libfdt/fdt_empty_tree.c
new file mode 100644
index 0000000..49d54d4
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt_empty_tree.c
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2012 David Gibson, IBM Corporation.
+ */
+#include "libfdt_env.h"
+
+#include <fdt.h>
+#include <libfdt.h>
+
+#include "libfdt_internal.h"
+
+int fdt_create_empty_tree(void *buf, int bufsize)
+{
+ int err;
+
+ err = fdt_create(buf, bufsize);
+ if (err)
+ return err;
+
+ err = fdt_finish_reservemap(buf);
+ if (err)
+ return err;
+
+ err = fdt_begin_node(buf, "");
+ if (err)
+ return err;
+
+ err = fdt_end_node(buf);
+ if (err)
+ return err;
+
+ err = fdt_finish(buf);
+ if (err)
+ return err;
+
+ return fdt_open_into(buf, buf, bufsize);
+}
diff --git a/na/scripts/dtc/libfdt/fdt_overlay.c b/na/scripts/dtc/libfdt/fdt_overlay.c
new file mode 100644
index 0000000..51a3859
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt_overlay.c
@@ -0,0 +1,1101 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2016 Free Electrons
+ * Copyright (C) 2016 NextThing Co.
+ */
+#include "libfdt_env.h"
+
+#include <fdt.h>
+#include <libfdt.h>
+
+#include "libfdt_internal.h"
+
+/**
+ * overlay_get_target_phandle - retrieves the target phandle of a fragment
+ * @fdto: pointer to the device tree overlay blob
+ * @fragment: node offset of the fragment in the overlay
+ *
+ * overlay_get_target_phandle() retrieves the target phandle of an
+ * overlay fragment when that fragment uses a phandle (target
+ * property) instead of a path (target-path property).
+ *
+ * returns:
+ * the phandle pointed by the target property
+ * 0, if the phandle was not found
+ * -1, if the phandle was malformed
+ */
+static uint32_t overlay_get_target_phandle(const void *fdto, int fragment)
+{
+ const fdt32_t *val;
+ int len;
+
+ val = fdt_getprop(fdto, fragment, "target", &len);
+ if (!val)
+ return 0;
+
+ if ((len != sizeof(*val)) || (fdt32_to_cpu(*val) == (uint32_t)-1))
+ return (uint32_t)-1;
+
+ return fdt32_to_cpu(*val);
+}
+
+int fdt_overlay_target_offset(const void *fdt, const void *fdto,
+ int fragment_offset, char const **pathp)
+{
+ uint32_t phandle;
+ const char *path = NULL;
+ int path_len = 0, ret;
+
+ /* Try first to do a phandle based lookup */
+ phandle = overlay_get_target_phandle(fdto, fragment_offset);
+ if (phandle == (uint32_t)-1)
+ return -FDT_ERR_BADPHANDLE;
+
+ /* no phandle, try path */
+ if (!phandle) {
+ /* And then a path based lookup */
+ path = fdt_getprop(fdto, fragment_offset, "target-path", &path_len);
+ if (path)
+ ret = fdt_path_offset(fdt, path);
+ else
+ ret = path_len;
+ } else
+ ret = fdt_node_offset_by_phandle(fdt, phandle);
+
+ /*
+ * If we haven't found either a target or a
+ * target-path property in a node that contains a
+ * __overlay__ subnode (we wouldn't be called
+ * otherwise), consider it a improperly written
+ * overlay
+ */
+ if (ret < 0 && path_len == -FDT_ERR_NOTFOUND)
+ ret = -FDT_ERR_BADOVERLAY;
+
+ /* return on error */
+ if (ret < 0)
+ return ret;
+
+ /* return pointer to path (if available) */
+ if (pathp)
+ *pathp = path ? path : NULL;
+
+ return ret;
+}
+
+/**
+ * overlay_phandle_add_offset - Increases a phandle by an offset
+ * @fdt: Base device tree blob
+ * @node: Device tree overlay blob
+ * @name: Name of the property to modify (phandle or linux,phandle)
+ * @delta: offset to apply
+ *
+ * overlay_phandle_add_offset() increments a node phandle by a given
+ * offset.
+ *
+ * returns:
+ * 0 on success.
+ * Negative error code on error
+ */
+static int overlay_phandle_add_offset(void *fdt, int node,
+ const char *name, uint32_t delta)
+{
+ fdt32_t *valp, val;
+ int len;
+
+ valp = fdt_getprop_w(fdt, node, name, &len);
+ if (!valp)
+ return len;
+
+ if (len != sizeof(val))
+ return -FDT_ERR_BADPHANDLE;
+
+ val = fdt32_ld(valp);
+ if (val + delta < val || val + delta == (uint32_t)-1)
+ return -FDT_ERR_NOPHANDLES;
+
+ fdt32_st(valp, val + delta);
+ return 0;
+}
+
+/**
+ * overlay_adjust_node_phandles - Offsets the phandles of a node
+ * @fdto: Device tree overlay blob
+ * @node: Offset of the node we want to adjust
+ * @delta: Offset to shift the phandles of
+ *
+ * overlay_adjust_node_phandles() adds a constant to all the phandles
+ * of a given node. This is mainly use as part of the overlay
+ * application process, when we want to update all the overlay
+ * phandles to not conflict with the overlays of the base device tree.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_adjust_node_phandles(void *fdto, int node,
+ uint32_t delta)
+{
+ int child;
+ int ret;
+
+ ret = overlay_phandle_add_offset(fdto, node, "phandle", delta);
+ if (ret && ret != -FDT_ERR_NOTFOUND)
+ return ret;
+
+ ret = overlay_phandle_add_offset(fdto, node, "linux,phandle", delta);
+ if (ret && ret != -FDT_ERR_NOTFOUND)
+ return ret;
+
+ fdt_for_each_subnode(child, fdto, node) {
+ ret = overlay_adjust_node_phandles(fdto, child, delta);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * overlay_adjust_local_phandles - Adjust the phandles of a whole overlay
+ * @fdto: Device tree overlay blob
+ * @delta: Offset to shift the phandles of
+ *
+ * overlay_adjust_local_phandles() adds a constant to all the
+ * phandles of an overlay. This is mainly use as part of the overlay
+ * application process, when we want to update all the overlay
+ * phandles to not conflict with the overlays of the base device tree.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_adjust_local_phandles(void *fdto, uint32_t delta)
+{
+ /*
+ * Start adjusting the phandles from the overlay root
+ */
+ return overlay_adjust_node_phandles(fdto, 0, delta);
+}
+
+/**
+ * overlay_update_local_node_references - Adjust the overlay references
+ * @fdto: Device tree overlay blob
+ * @tree_node: Node offset of the node to operate on
+ * @fixup_node: Node offset of the matching local fixups node
+ * @delta: Offset to shift the phandles of
+ *
+ * overlay_update_local_nodes_references() update the phandles
+ * pointing to a node within the device tree overlay by adding a
+ * constant delta.
+ *
+ * This is mainly used as part of a device tree application process,
+ * where you want the device tree overlays phandles to not conflict
+ * with the ones from the base device tree before merging them.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_update_local_node_references(void *fdto,
+ int tree_node,
+ int fixup_node,
+ uint32_t delta)
+{
+ int fixup_prop;
+ int fixup_child;
+ int ret;
+
+ fdt_for_each_property_offset(fixup_prop, fdto, fixup_node) {
+ const fdt32_t *fixup_val;
+ const char *name;
+ char *tree_val;
+ int fixup_len;
+ int tree_len;
+ int i;
+
+ fixup_val = fdt_getprop_by_offset(fdto, fixup_prop,
+ &name, &fixup_len);
+ if (!fixup_val)
+ return fixup_len;
+
+ if (fixup_len % sizeof(uint32_t))
+ return -FDT_ERR_BADOVERLAY;
+ fixup_len /= sizeof(uint32_t);
+
+ tree_val = fdt_getprop_w(fdto, tree_node, name, &tree_len);
+ if (!tree_val) {
+ if (tree_len == -FDT_ERR_NOTFOUND)
+ return -FDT_ERR_BADOVERLAY;
+
+ return tree_len;
+ }
+
+ for (i = 0; i < fixup_len; i++) {
+ fdt32_t *refp;
+
+ refp = (fdt32_t *)(tree_val + fdt32_ld_(fixup_val + i));
+
+ /*
+ * phandles to fixup can be unaligned, so use
+ * fdt32_{ld,st}() to read/write them.
+ */
+ fdt32_st(refp, fdt32_ld(refp) + delta);
+ }
+ }
+
+ fdt_for_each_subnode(fixup_child, fdto, fixup_node) {
+ const char *fixup_child_name = fdt_get_name(fdto, fixup_child,
+ NULL);
+ int tree_child;
+
+ tree_child = fdt_subnode_offset(fdto, tree_node,
+ fixup_child_name);
+ if (tree_child == -FDT_ERR_NOTFOUND)
+ return -FDT_ERR_BADOVERLAY;
+ if (tree_child < 0)
+ return tree_child;
+
+ ret = overlay_update_local_node_references(fdto,
+ tree_child,
+ fixup_child,
+ delta);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * overlay_update_local_references - Adjust the overlay references
+ * @fdto: Device tree overlay blob
+ * @delta: Offset to shift the phandles of
+ *
+ * overlay_update_local_references() update all the phandles pointing
+ * to a node within the device tree overlay by adding a constant
+ * delta to not conflict with the base overlay.
+ *
+ * This is mainly used as part of a device tree application process,
+ * where you want the device tree overlays phandles to not conflict
+ * with the ones from the base device tree before merging them.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_update_local_references(void *fdto, uint32_t delta)
+{
+ int fixups;
+
+ fixups = fdt_path_offset(fdto, "/__local_fixups__");
+ if (fixups < 0) {
+ /* There's no local phandles to adjust, bail out */
+ if (fixups == -FDT_ERR_NOTFOUND)
+ return 0;
+
+ return fixups;
+ }
+
+ /*
+ * Update our local references from the root of the tree
+ */
+ return overlay_update_local_node_references(fdto, 0, fixups,
+ delta);
+}
+
+/**
+ * overlay_fixup_one_phandle - Set an overlay phandle to the base one
+ * @fdto: Device tree overlay blob
+ * @symbols_off: Node offset of the symbols node in the base device tree
+ * @path: Path to a node holding a phandle in the overlay
+ * @path_len: number of path characters to consider
+ * @name: Name of the property holding the phandle reference in the overlay
+ * @name_len: number of name characters to consider
+ * @poffset: Offset within the overlay property where the phandle is stored
+ * @phandle: Phandle referencing the node
+ *
+ * overlay_fixup_one_phandle() resolves an overlay phandle pointing to
+ * a node in the base device tree.
+ *
+ * This is part of the device tree overlay application process, when
+ * you want all the phandles in the overlay to point to the actual
+ * base dt nodes.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_fixup_one_phandle(void *fdto, int symbols_off,
+ const char *path, uint32_t path_len,
+ const char *name, uint32_t name_len,
+ int poffset, uint32_t phandle)
+{
+ fdt32_t phandle_prop;
+ int fixup_off;
+
+ if (symbols_off < 0)
+ return symbols_off;
+
+ fixup_off = fdt_path_offset_namelen(fdto, path, path_len);
+ if (fixup_off == -FDT_ERR_NOTFOUND)
+ return -FDT_ERR_BADOVERLAY;
+ if (fixup_off < 0)
+ return fixup_off;
+
+ phandle_prop = cpu_to_fdt32(phandle);
+ return fdt_setprop_inplace_namelen_partial(fdto, fixup_off,
+ name, name_len, poffset,
+ &phandle_prop,
+ sizeof(phandle_prop));
+}
+
+/**
+ * overlay_fixup_phandle - Set an overlay phandle to the base one
+ * @fdt: Base Device Tree blob
+ * @fdto: Device tree overlay blob
+ * @symbols_off: Node offset of the symbols node in the base device tree
+ * @property: Property offset in the overlay holding the list of fixups
+ *
+ * overlay_fixup_phandle() resolves all the overlay phandles pointed
+ * to in a __fixups__ property, and updates them to match the phandles
+ * in use in the base device tree.
+ *
+ * This is part of the device tree overlay application process, when
+ * you want all the phandles in the overlay to point to the actual
+ * base dt nodes.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_fixup_phandle(void *fdt, void *fdto, int symbols_off,
+ int property)
+{
+ const char *value;
+ const char *label;
+ int len;
+ const char *symbol_path;
+ int prop_len;
+ int symbol_off;
+ uint32_t phandle;
+
+ value = fdt_getprop_by_offset(fdto, property,
+ &label, &len);
+ if (!value) {
+ if (len == -FDT_ERR_NOTFOUND)
+ return -FDT_ERR_INTERNAL;
+
+ return len;
+ }
+
+ symbol_path = fdt_getprop(fdt, symbols_off, label, &prop_len);
+ if (!symbol_path)
+ return prop_len;
+
+ symbol_off = fdt_path_offset(fdt, symbol_path);
+ if (symbol_off < 0)
+ return symbol_off;
+
+ phandle = fdt_get_phandle(fdt, symbol_off);
+ if (!phandle)
+ return -FDT_ERR_NOTFOUND;
+
+ do {
+ const char *path, *name, *fixup_end;
+ const char *fixup_str = value;
+ uint32_t path_len, name_len;
+ uint32_t fixup_len;
+ const char *sep;
+ char *endptr;
+ int poffset, ret;
+
+ fixup_end = memchr(value, '\0', len);
+ if (!fixup_end)
+ return -FDT_ERR_BADOVERLAY;
+ fixup_len = fixup_end - fixup_str;
+
+ len -= fixup_len + 1;
+ value += fixup_len + 1;
+
+ path = fixup_str;
+ sep = memchr(fixup_str, ':', fixup_len);
+ if (!sep || *sep != ':')
+ return -FDT_ERR_BADOVERLAY;
+
+ path_len = sep - path;
+ if (path_len == (fixup_len - 1))
+ return -FDT_ERR_BADOVERLAY;
+
+ fixup_len -= path_len + 1;
+ name = sep + 1;
+ sep = memchr(name, ':', fixup_len);
+ if (!sep || *sep != ':')
+ return -FDT_ERR_BADOVERLAY;
+
+ name_len = sep - name;
+ if (!name_len)
+ return -FDT_ERR_BADOVERLAY;
+
+ poffset = strtoul(sep + 1, &endptr, 10);
+ if ((*endptr != '\0') || (endptr <= (sep + 1)))
+ return -FDT_ERR_BADOVERLAY;
+
+ ret = overlay_fixup_one_phandle(fdto, symbols_off,
+ path, path_len, name, name_len,
+ poffset, phandle);
+ if (ret)
+ return ret;
+ } while (len > 0);
+
+ return 0;
+}
+
+/**
+ * overlay_fixup_phandles - Resolve the overlay phandles to the base
+ * device tree
+ * @fdt: Base Device Tree blob
+ * @fdto: Device tree overlay blob
+ *
+ * overlay_fixup_phandles() resolves all the overlay phandles pointing
+ * to nodes in the base device tree.
+ *
+ * This is one of the steps of the device tree overlay application
+ * process, when you want all the phandles in the overlay to point to
+ * the actual base dt nodes.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_fixup_phandles(void *fdt, void *fdto)
+{
+ int fixups_off, symbols_off;
+ int property;
+
+ /* We can have overlays without any fixups */
+ fixups_off = fdt_path_offset(fdto, "/__fixups__");
+ if (fixups_off == -FDT_ERR_NOTFOUND)
+ return 0; /* nothing to do */
+ if (fixups_off < 0)
+ return fixups_off;
+
+ /* And base DTs without symbols */
+ symbols_off = fdt_path_offset(fdt, "/__symbols__");
+ if ((symbols_off < 0 && (symbols_off != -FDT_ERR_NOTFOUND)))
+ return symbols_off;
+
+ fdt_for_each_property_offset(property, fdto, fixups_off) {
+ int ret;
+
+ ret = overlay_fixup_phandle(fdt, fdto, symbols_off, property);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * overlay_adjust_local_conflicting_phandle: Changes a phandle value
+ * @fdto: Device tree overlay
+ * @node: The node the phandle is set for
+ * @fdt_phandle: The new value for the phandle
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_adjust_local_conflicting_phandle(void *fdto, int node,
+ uint32_t fdt_phandle)
+{
+ const fdt32_t *php;
+ int len, ret;
+
+ php = fdt_getprop(fdto, node, "phandle", &len);
+ if (php && len == sizeof(*php)) {
+ ret = fdt_setprop_inplace_u32(fdto, node, "phandle", fdt_phandle);
+ if (ret)
+ return ret;
+ }
+
+ php = fdt_getprop(fdto, node, "linux,phandle", &len);
+ if (php && len == sizeof(*php)) {
+ ret = fdt_setprop_inplace_u32(fdto, node, "linux,phandle", fdt_phandle);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * overlay_update_node_conflicting_references - Recursively replace phandle values
+ * @fdto: Device tree overlay blob
+ * @tree_node: Node to recurse into
+ * @fixup_node: Node offset of the matching local fixups node
+ * @fdt_phandle: Value to replace phandles with
+ * @fdto_phandle: Value to be replaced
+ *
+ * Replaces all phandles with value @fdto_phandle by @fdt_phandle.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_update_node_conflicting_references(void *fdto, int tree_node,
+ int fixup_node,
+ uint32_t fdt_phandle,
+ uint32_t fdto_phandle)
+{
+ int fixup_prop;
+ int fixup_child;
+ int ret;
+
+ fdt_for_each_property_offset(fixup_prop, fdto, fixup_node) {
+ const fdt32_t *fixup_val;
+ const char *name;
+ char *tree_val;
+ int fixup_len;
+ int tree_len;
+ int i;
+
+ fixup_val = fdt_getprop_by_offset(fdto, fixup_prop,
+ &name, &fixup_len);
+ if (!fixup_val)
+ return fixup_len;
+
+ if (fixup_len % sizeof(uint32_t))
+ return -FDT_ERR_BADOVERLAY;
+ fixup_len /= sizeof(uint32_t);
+
+ tree_val = fdt_getprop_w(fdto, tree_node, name, &tree_len);
+ if (!tree_val) {
+ if (tree_len == -FDT_ERR_NOTFOUND)
+ return -FDT_ERR_BADOVERLAY;
+
+ return tree_len;
+ }
+
+ for (i = 0; i < fixup_len; i++) {
+ fdt32_t *refp;
+ uint32_t valp;
+
+ refp = (fdt32_t *)(tree_val + fdt32_ld_(fixup_val + i));
+ valp = fdt32_ld(refp);
+
+ if (valp == fdto_phandle)
+ fdt32_st(refp, fdt_phandle);
+ }
+ }
+
+ fdt_for_each_subnode(fixup_child, fdto, fixup_node) {
+ const char *fixup_child_name = fdt_get_name(fdto, fixup_child, NULL);
+ int tree_child;
+
+ tree_child = fdt_subnode_offset(fdto, tree_node, fixup_child_name);
+
+ if (tree_child == -FDT_ERR_NOTFOUND)
+ return -FDT_ERR_BADOVERLAY;
+ if (tree_child < 0)
+ return tree_child;
+
+ ret = overlay_update_node_conflicting_references(fdto, tree_child,
+ fixup_child,
+ fdt_phandle,
+ fdto_phandle);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * overlay_update_local_conflicting_references - Recursively replace phandle values
+ * @fdto: Device tree overlay blob
+ * @fdt_phandle: Value to replace phandles with
+ * @fdto_phandle: Value to be replaced
+ *
+ * Replaces all phandles with value @fdto_phandle by @fdt_phandle.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_update_local_conflicting_references(void *fdto,
+ uint32_t fdt_phandle,
+ uint32_t fdto_phandle)
+{
+ int fixups;
+
+ fixups = fdt_path_offset(fdto, "/__local_fixups__");
+ if (fixups == -FDT_ERR_NOTFOUND)
+ return 0;
+ if (fixups < 0)
+ return fixups;
+
+ return overlay_update_node_conflicting_references(fdto, 0, fixups,
+ fdt_phandle,
+ fdto_phandle);
+}
+
+/**
+ * overlay_prevent_phandle_overwrite_node - Helper function for overlay_prevent_phandle_overwrite
+ * @fdt: Base Device tree blob
+ * @fdtnode: Node in fdt that is checked for an overwrite
+ * @fdto: Device tree overlay blob
+ * @fdtonode: Node in fdto matching @fdtnode
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_prevent_phandle_overwrite_node(void *fdt, int fdtnode,
+ void *fdto, int fdtonode)
+{
+ uint32_t fdt_phandle, fdto_phandle;
+ int fdtochild;
+
+ fdt_phandle = fdt_get_phandle(fdt, fdtnode);
+ fdto_phandle = fdt_get_phandle(fdto, fdtonode);
+
+ if (fdt_phandle && fdto_phandle) {
+ int ret;
+
+ ret = overlay_adjust_local_conflicting_phandle(fdto, fdtonode,
+ fdt_phandle);
+ if (ret)
+ return ret;
+
+ ret = overlay_update_local_conflicting_references(fdto,
+ fdt_phandle,
+ fdto_phandle);
+ if (ret)
+ return ret;
+ }
+
+ fdt_for_each_subnode(fdtochild, fdto, fdtonode) {
+ const char *name = fdt_get_name(fdto, fdtochild, NULL);
+ int fdtchild;
+ int ret;
+
+ fdtchild = fdt_subnode_offset(fdt, fdtnode, name);
+ if (fdtchild == -FDT_ERR_NOTFOUND)
+ /*
+ * no further overwrites possible here as this node is
+ * new
+ */
+ continue;
+
+ ret = overlay_prevent_phandle_overwrite_node(fdt, fdtchild,
+ fdto, fdtochild);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * overlay_prevent_phandle_overwrite - Fixes overlay phandles to not overwrite base phandles
+ * @fdt: Base Device Tree blob
+ * @fdto: Device tree overlay blob
+ *
+ * Checks recursively if applying fdto overwrites phandle values in the base
+ * dtb. When such a phandle is found, the fdto is changed to use the fdt's
+ * phandle value to not break references in the base.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_prevent_phandle_overwrite(void *fdt, void *fdto)
+{
+ int fragment;
+
+ fdt_for_each_subnode(fragment, fdto, 0) {
+ int overlay;
+ int target;
+ int ret;
+
+ overlay = fdt_subnode_offset(fdto, fragment, "__overlay__");
+ if (overlay == -FDT_ERR_NOTFOUND)
+ continue;
+
+ if (overlay < 0)
+ return overlay;
+
+ target = fdt_overlay_target_offset(fdt, fdto, fragment, NULL);
+ if (target == -FDT_ERR_NOTFOUND)
+ /*
+ * The subtree doesn't exist in the base, so nothing
+ * will be overwritten.
+ */
+ continue;
+ else if (target < 0)
+ return target;
+
+ ret = overlay_prevent_phandle_overwrite_node(fdt, target,
+ fdto, overlay);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * overlay_apply_node - Merges a node into the base device tree
+ * @fdt: Base Device Tree blob
+ * @target: Node offset in the base device tree to apply the fragment to
+ * @fdto: Device tree overlay blob
+ * @node: Node offset in the overlay holding the changes to merge
+ *
+ * overlay_apply_node() merges a node into a target base device tree
+ * node pointed.
+ *
+ * This is part of the final step in the device tree overlay
+ * application process, when all the phandles have been adjusted and
+ * resolved and you just have to merge overlay into the base device
+ * tree.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_apply_node(void *fdt, int target,
+ void *fdto, int node)
+{
+ int property;
+ int subnode;
+
+ fdt_for_each_property_offset(property, fdto, node) {
+ const char *name;
+ const void *prop;
+ int prop_len;
+ int ret;
+
+ prop = fdt_getprop_by_offset(fdto, property, &name,
+ &prop_len);
+ if (prop_len == -FDT_ERR_NOTFOUND)
+ return -FDT_ERR_INTERNAL;
+ if (prop_len < 0)
+ return prop_len;
+
+ ret = fdt_setprop(fdt, target, name, prop, prop_len);
+ if (ret)
+ return ret;
+ }
+
+ fdt_for_each_subnode(subnode, fdto, node) {
+ const char *name = fdt_get_name(fdto, subnode, NULL);
+ int nnode;
+ int ret;
+
+ nnode = fdt_add_subnode(fdt, target, name);
+ if (nnode == -FDT_ERR_EXISTS) {
+ nnode = fdt_subnode_offset(fdt, target, name);
+ if (nnode == -FDT_ERR_NOTFOUND)
+ return -FDT_ERR_INTERNAL;
+ }
+
+ if (nnode < 0)
+ return nnode;
+
+ ret = overlay_apply_node(fdt, nnode, fdto, subnode);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * overlay_merge - Merge an overlay into its base device tree
+ * @fdt: Base Device Tree blob
+ * @fdto: Device tree overlay blob
+ *
+ * overlay_merge() merges an overlay into its base device tree.
+ *
+ * This is the next to last step in the device tree overlay application
+ * process, when all the phandles have been adjusted and resolved and
+ * you just have to merge overlay into the base device tree.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_merge(void *fdt, void *fdto)
+{
+ int fragment;
+
+ fdt_for_each_subnode(fragment, fdto, 0) {
+ int overlay;
+ int target;
+ int ret;
+
+ /*
+ * Each fragments will have an __overlay__ node. If
+ * they don't, it's not supposed to be merged
+ */
+ overlay = fdt_subnode_offset(fdto, fragment, "__overlay__");
+ if (overlay == -FDT_ERR_NOTFOUND)
+ continue;
+
+ if (overlay < 0)
+ return overlay;
+
+ target = fdt_overlay_target_offset(fdt, fdto, fragment, NULL);
+ if (target < 0)
+ return target;
+
+ ret = overlay_apply_node(fdt, target, fdto, overlay);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int get_path_len(const void *fdt, int nodeoffset)
+{
+ int len = 0, namelen;
+ const char *name;
+
+ FDT_RO_PROBE(fdt);
+
+ for (;;) {
+ name = fdt_get_name(fdt, nodeoffset, &namelen);
+ if (!name)
+ return namelen;
+
+ /* root? we're done */
+ if (namelen == 0)
+ break;
+
+ nodeoffset = fdt_parent_offset(fdt, nodeoffset);
+ if (nodeoffset < 0)
+ return nodeoffset;
+ len += namelen + 1;
+ }
+
+ /* in case of root pretend it's "/" */
+ if (len == 0)
+ len++;
+ return len;
+}
+
+/**
+ * overlay_symbol_update - Update the symbols of base tree after a merge
+ * @fdt: Base Device Tree blob
+ * @fdto: Device tree overlay blob
+ *
+ * overlay_symbol_update() updates the symbols of the base tree with the
+ * symbols of the applied overlay
+ *
+ * This is the last step in the device tree overlay application
+ * process, allowing the reference of overlay symbols by subsequent
+ * overlay operations.
+ *
+ * returns:
+ * 0 on success
+ * Negative error code on failure
+ */
+static int overlay_symbol_update(void *fdt, void *fdto)
+{
+ int root_sym, ov_sym, prop, path_len, fragment, target;
+ int len, frag_name_len, ret, rel_path_len;
+ const char *s, *e;
+ const char *path;
+ const char *name;
+ const char *frag_name;
+ const char *rel_path;
+ const char *target_path;
+ char *buf;
+ void *p;
+
+ ov_sym = fdt_subnode_offset(fdto, 0, "__symbols__");
+
+ /* if no overlay symbols exist no problem */
+ if (ov_sym < 0)
+ return 0;
+
+ root_sym = fdt_subnode_offset(fdt, 0, "__symbols__");
+
+ /* it no root symbols exist we should create them */
+ if (root_sym == -FDT_ERR_NOTFOUND)
+ root_sym = fdt_add_subnode(fdt, 0, "__symbols__");
+
+ /* any error is fatal now */
+ if (root_sym < 0)
+ return root_sym;
+
+ /* iterate over each overlay symbol */
+ fdt_for_each_property_offset(prop, fdto, ov_sym) {
+ path = fdt_getprop_by_offset(fdto, prop, &name, &path_len);
+ if (!path)
+ return path_len;
+
+ /* verify it's a string property (terminated by a single \0) */
+ if (path_len < 1 || memchr(path, '\0', path_len) != &path[path_len - 1])
+ return -FDT_ERR_BADVALUE;
+
+ /* keep end marker to avoid strlen() */
+ e = path + path_len;
+
+ if (*path != '/')
+ return -FDT_ERR_BADVALUE;
+
+ /* get fragment name first */
+ s = strchr(path + 1, '/');
+ if (!s) {
+ /* Symbol refers to something that won't end
+ * up in the target tree */
+ continue;
+ }
+
+ frag_name = path + 1;
+ frag_name_len = s - path - 1;
+
+ /* verify format; safe since "s" lies in \0 terminated prop */
+ len = sizeof("/__overlay__/") - 1;
+ if ((e - s) > len && (memcmp(s, "/__overlay__/", len) == 0)) {
+ /* /<fragment-name>/__overlay__/<relative-subnode-path> */
+ rel_path = s + len;
+ rel_path_len = e - rel_path - 1;
+ } else if ((e - s) == len
+ && (memcmp(s, "/__overlay__", len - 1) == 0)) {
+ /* /<fragment-name>/__overlay__ */
+ rel_path = "";
+ rel_path_len = 0;
+ } else {
+ /* Symbol refers to something that won't end
+ * up in the target tree */
+ continue;
+ }
+
+ /* find the fragment index in which the symbol lies */
+ ret = fdt_subnode_offset_namelen(fdto, 0, frag_name,
+ frag_name_len);
+ /* not found? */
+ if (ret < 0)
+ return -FDT_ERR_BADOVERLAY;
+ fragment = ret;
+
+ /* an __overlay__ subnode must exist */
+ ret = fdt_subnode_offset(fdto, fragment, "__overlay__");
+ if (ret < 0)
+ return -FDT_ERR_BADOVERLAY;
+
+ /* get the target of the fragment */
+ ret = fdt_overlay_target_offset(fdt, fdto, fragment, &target_path);
+ if (ret < 0)
+ return ret;
+ target = ret;
+
+ /* if we have a target path use */
+ if (!target_path) {
+ ret = get_path_len(fdt, target);
+ if (ret < 0)
+ return ret;
+ len = ret;
+ } else {
+ len = strlen(target_path);
+ }
+
+ ret = fdt_setprop_placeholder(fdt, root_sym, name,
+ len + (len > 1) + rel_path_len + 1, &p);
+ if (ret < 0)
+ return ret;
+
+ if (!target_path) {
+ /* again in case setprop_placeholder changed it */
+ ret = fdt_overlay_target_offset(fdt, fdto, fragment, &target_path);
+ if (ret < 0)
+ return ret;
+ target = ret;
+ }
+
+ buf = p;
+ if (len > 1) { /* target is not root */
+ if (!target_path) {
+ ret = fdt_get_path(fdt, target, buf, len + 1);
+ if (ret < 0)
+ return ret;
+ } else
+ memcpy(buf, target_path, len + 1);
+
+ } else
+ len--;
+
+ buf[len] = '/';
+ memcpy(buf + len + 1, rel_path, rel_path_len);
+ buf[len + 1 + rel_path_len] = '\0';
+ }
+
+ return 0;
+}
+
+int fdt_overlay_apply(void *fdt, void *fdto)
+{
+ uint32_t delta;
+ int ret;
+
+ FDT_RO_PROBE(fdt);
+ FDT_RO_PROBE(fdto);
+
+ ret = fdt_find_max_phandle(fdt, &delta);
+ if (ret)
+ goto err;
+
+ /* Increase all phandles in the fdto by delta */
+ ret = overlay_adjust_local_phandles(fdto, delta);
+ if (ret)
+ goto err;
+
+ /* Adapt the phandle values in fdto to the above increase */
+ ret = overlay_update_local_references(fdto, delta);
+ if (ret)
+ goto err;
+
+ /* Update fdto's phandles using symbols from fdt */
+ ret = overlay_fixup_phandles(fdt, fdto);
+ if (ret)
+ goto err;
+
+ /* Don't overwrite phandles in fdt */
+ ret = overlay_prevent_phandle_overwrite(fdt, fdto);
+ if (ret)
+ goto err;
+
+ ret = overlay_merge(fdt, fdto);
+ if (ret)
+ goto err;
+
+ ret = overlay_symbol_update(fdt, fdto);
+ if (ret)
+ goto err;
+
+ /*
+ * The overlay has been damaged, erase its magic.
+ */
+ fdt_set_magic(fdto, ~0);
+
+ return 0;
+
+err:
+ /*
+ * The overlay might have been damaged, erase its magic.
+ */
+ fdt_set_magic(fdto, ~0);
+
+ /*
+ * The base device tree might have been damaged, erase its
+ * magic.
+ */
+ fdt_set_magic(fdt, ~0);
+
+ return ret;
+}
diff --git a/na/scripts/dtc/libfdt/fdt_ro.c b/na/scripts/dtc/libfdt/fdt_ro.c
new file mode 100644
index 0000000..63494fb
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt_ro.c
@@ -0,0 +1,888 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ */
+#include "libfdt_env.h"
+
+#include <fdt.h>
+#include <libfdt.h>
+
+#include "libfdt_internal.h"
+
+static int fdt_nodename_eq_(const void *fdt, int offset,
+ const char *s, int len)
+{
+ int olen;
+ const char *p = fdt_get_name(fdt, offset, &olen);
+
+ if (!p || olen < len)
+ /* short match */
+ return 0;
+
+ if (memcmp(p, s, len) != 0)
+ return 0;
+
+ if (p[len] == '\0')
+ return 1;
+ else if (!memchr(s, '@', len) && (p[len] == '@'))
+ return 1;
+ else
+ return 0;
+}
+
+const char *fdt_get_string(const void *fdt, int stroffset, int *lenp)
+{
+ int32_t totalsize;
+ uint32_t absoffset;
+ size_t len;
+ int err;
+ const char *s, *n;
+
+ if (can_assume(VALID_INPUT)) {
+ s = (const char *)fdt + fdt_off_dt_strings(fdt) + stroffset;
+
+ if (lenp)
+ *lenp = strlen(s);
+ return s;
+ }
+ totalsize = fdt_ro_probe_(fdt);
+ err = totalsize;
+ if (totalsize < 0)
+ goto fail;
+
+ err = -FDT_ERR_BADOFFSET;
+ absoffset = stroffset + fdt_off_dt_strings(fdt);
+ if (absoffset >= (unsigned)totalsize)
+ goto fail;
+ len = totalsize - absoffset;
+
+ if (fdt_magic(fdt) == FDT_MAGIC) {
+ if (stroffset < 0)
+ goto fail;
+ if (can_assume(LATEST) || fdt_version(fdt) >= 17) {
+ if ((unsigned)stroffset >= fdt_size_dt_strings(fdt))
+ goto fail;
+ if ((fdt_size_dt_strings(fdt) - stroffset) < len)
+ len = fdt_size_dt_strings(fdt) - stroffset;
+ }
+ } else if (fdt_magic(fdt) == FDT_SW_MAGIC) {
+ unsigned int sw_stroffset = -stroffset;
+
+ if ((stroffset >= 0) ||
+ (sw_stroffset > fdt_size_dt_strings(fdt)))
+ goto fail;
+ if (sw_stroffset < len)
+ len = sw_stroffset;
+ } else {
+ err = -FDT_ERR_INTERNAL;
+ goto fail;
+ }
+
+ s = (const char *)fdt + absoffset;
+ n = memchr(s, '\0', len);
+ if (!n) {
+ /* missing terminating NULL */
+ err = -FDT_ERR_TRUNCATED;
+ goto fail;
+ }
+
+ if (lenp)
+ *lenp = n - s;
+ return s;
+
+fail:
+ if (lenp)
+ *lenp = err;
+ return NULL;
+}
+
+const char *fdt_string(const void *fdt, int stroffset)
+{
+ return fdt_get_string(fdt, stroffset, NULL);
+}
+
+static int fdt_string_eq_(const void *fdt, int stroffset,
+ const char *s, int len)
+{
+ int slen;
+ const char *p = fdt_get_string(fdt, stroffset, &slen);
+
+ return p && (slen == len) && (memcmp(p, s, len) == 0);
+}
+
+int fdt_find_max_phandle(const void *fdt, uint32_t *phandle)
+{
+ uint32_t max = 0;
+ int offset = -1;
+
+ while (true) {
+ uint32_t value;
+
+ offset = fdt_next_node(fdt, offset, NULL);
+ if (offset < 0) {
+ if (offset == -FDT_ERR_NOTFOUND)
+ break;
+
+ return offset;
+ }
+
+ value = fdt_get_phandle(fdt, offset);
+
+ if (value > max)
+ max = value;
+ }
+
+ if (phandle)
+ *phandle = max;
+
+ return 0;
+}
+
+int fdt_generate_phandle(const void *fdt, uint32_t *phandle)
+{
+ uint32_t max;
+ int err;
+
+ err = fdt_find_max_phandle(fdt, &max);
+ if (err < 0)
+ return err;
+
+ if (max == FDT_MAX_PHANDLE)
+ return -FDT_ERR_NOPHANDLES;
+
+ if (phandle)
+ *phandle = max + 1;
+
+ return 0;
+}
+
+static const struct fdt_reserve_entry *fdt_mem_rsv(const void *fdt, int n)
+{
+ unsigned int offset = n * sizeof(struct fdt_reserve_entry);
+ unsigned int absoffset = fdt_off_mem_rsvmap(fdt) + offset;
+
+ if (!can_assume(VALID_INPUT)) {
+ if (absoffset < fdt_off_mem_rsvmap(fdt))
+ return NULL;
+ if (absoffset > fdt_totalsize(fdt) -
+ sizeof(struct fdt_reserve_entry))
+ return NULL;
+ }
+ return fdt_mem_rsv_(fdt, n);
+}
+
+int fdt_get_mem_rsv(const void *fdt, int n, uint64_t *address, uint64_t *size)
+{
+ const struct fdt_reserve_entry *re;
+
+ FDT_RO_PROBE(fdt);
+ re = fdt_mem_rsv(fdt, n);
+ if (!can_assume(VALID_INPUT) && !re)
+ return -FDT_ERR_BADOFFSET;
+
+ *address = fdt64_ld_(&re->address);
+ *size = fdt64_ld_(&re->size);
+ return 0;
+}
+
+int fdt_num_mem_rsv(const void *fdt)
+{
+ int i;
+ const struct fdt_reserve_entry *re;
+
+ for (i = 0; (re = fdt_mem_rsv(fdt, i)) != NULL; i++) {
+ if (fdt64_ld_(&re->size) == 0)
+ return i;
+ }
+ return -FDT_ERR_TRUNCATED;
+}
+
+static int nextprop_(const void *fdt, int offset)
+{
+ uint32_t tag;
+ int nextoffset;
+
+ do {
+ tag = fdt_next_tag(fdt, offset, &nextoffset);
+
+ switch (tag) {
+ case FDT_END:
+ if (nextoffset >= 0)
+ return -FDT_ERR_BADSTRUCTURE;
+ else
+ return nextoffset;
+
+ case FDT_PROP:
+ return offset;
+ }
+ offset = nextoffset;
+ } while (tag == FDT_NOP);
+
+ return -FDT_ERR_NOTFOUND;
+}
+
+int fdt_subnode_offset_namelen(const void *fdt, int offset,
+ const char *name, int namelen)
+{
+ int depth;
+
+ FDT_RO_PROBE(fdt);
+
+ for (depth = 0;
+ (offset >= 0) && (depth >= 0);
+ offset = fdt_next_node(fdt, offset, &depth))
+ if ((depth == 1)
+ && fdt_nodename_eq_(fdt, offset, name, namelen))
+ return offset;
+
+ if (depth < 0)
+ return -FDT_ERR_NOTFOUND;
+ return offset; /* error */
+}
+
+int fdt_subnode_offset(const void *fdt, int parentoffset,
+ const char *name)
+{
+ return fdt_subnode_offset_namelen(fdt, parentoffset, name, strlen(name));
+}
+
+int fdt_path_offset_namelen(const void *fdt, const char *path, int namelen)
+{
+ const char *end = path + namelen;
+ const char *p = path;
+ int offset = 0;
+
+ FDT_RO_PROBE(fdt);
+
+ if (!can_assume(VALID_INPUT) && namelen <= 0)
+ return -FDT_ERR_BADPATH;
+
+ /* see if we have an alias */
+ if (*path != '/') {
+ const char *q = memchr(path, '/', end - p);
+
+ if (!q)
+ q = end;
+
+ p = fdt_get_alias_namelen(fdt, p, q - p);
+ if (!p)
+ return -FDT_ERR_BADPATH;
+ offset = fdt_path_offset(fdt, p);
+
+ p = q;
+ }
+
+ while (p < end) {
+ const char *q;
+
+ while (*p == '/') {
+ p++;
+ if (p == end)
+ return offset;
+ }
+ q = memchr(p, '/', end - p);
+ if (! q)
+ q = end;
+
+ offset = fdt_subnode_offset_namelen(fdt, offset, p, q-p);
+ if (offset < 0)
+ return offset;
+
+ p = q;
+ }
+
+ return offset;
+}
+
+int fdt_path_offset(const void *fdt, const char *path)
+{
+ return fdt_path_offset_namelen(fdt, path, strlen(path));
+}
+
+const char *fdt_get_name(const void *fdt, int nodeoffset, int *len)
+{
+ const struct fdt_node_header *nh = fdt_offset_ptr_(fdt, nodeoffset);
+ const char *nameptr;
+ int err;
+
+ if (!can_assume(VALID_DTB) && (((err = fdt_ro_probe_(fdt)) < 0)
+ || ((err = fdt_check_node_offset_(fdt, nodeoffset)) < 0)))
+ goto fail;
+
+ nameptr = nh->name;
+
+ if (!can_assume(LATEST) && fdt_version(fdt) < 0x10) {
+ /*
+ * For old FDT versions, match the naming conventions of V16:
+ * give only the leaf name (after all /). The actual tree
+ * contents are loosely checked.
+ */
+ const char *leaf;
+ leaf = strrchr(nameptr, '/');
+ if (leaf == NULL) {
+ err = -FDT_ERR_BADSTRUCTURE;
+ goto fail;
+ }
+ nameptr = leaf+1;
+ }
+
+ if (len)
+ *len = strlen(nameptr);
+
+ return nameptr;
+
+ fail:
+ if (len)
+ *len = err;
+ return NULL;
+}
+
+int fdt_first_property_offset(const void *fdt, int nodeoffset)
+{
+ int offset;
+
+ if ((offset = fdt_check_node_offset_(fdt, nodeoffset)) < 0)
+ return offset;
+
+ return nextprop_(fdt, offset);
+}
+
+int fdt_next_property_offset(const void *fdt, int offset)
+{
+ if ((offset = fdt_check_prop_offset_(fdt, offset)) < 0)
+ return offset;
+
+ return nextprop_(fdt, offset);
+}
+
+static const struct fdt_property *fdt_get_property_by_offset_(const void *fdt,
+ int offset,
+ int *lenp)
+{
+ int err;
+ const struct fdt_property *prop;
+
+ if (!can_assume(VALID_INPUT) &&
+ (err = fdt_check_prop_offset_(fdt, offset)) < 0) {
+ if (lenp)
+ *lenp = err;
+ return NULL;
+ }
+
+ prop = fdt_offset_ptr_(fdt, offset);
+
+ if (lenp)
+ *lenp = fdt32_ld_(&prop->len);
+
+ return prop;
+}
+
+const struct fdt_property *fdt_get_property_by_offset(const void *fdt,
+ int offset,
+ int *lenp)
+{
+ /* Prior to version 16, properties may need realignment
+ * and this API does not work. fdt_getprop_*() will, however. */
+
+ if (!can_assume(LATEST) && fdt_version(fdt) < 0x10) {
+ if (lenp)
+ *lenp = -FDT_ERR_BADVERSION;
+ return NULL;
+ }
+
+ return fdt_get_property_by_offset_(fdt, offset, lenp);
+}
+
+static const struct fdt_property *fdt_get_property_namelen_(const void *fdt,
+ int offset,
+ const char *name,
+ int namelen,
+ int *lenp,
+ int *poffset)
+{
+ for (offset = fdt_first_property_offset(fdt, offset);
+ (offset >= 0);
+ (offset = fdt_next_property_offset(fdt, offset))) {
+ const struct fdt_property *prop;
+
+ prop = fdt_get_property_by_offset_(fdt, offset, lenp);
+ if (!can_assume(LIBFDT_FLAWLESS) && !prop) {
+ offset = -FDT_ERR_INTERNAL;
+ break;
+ }
+ if (fdt_string_eq_(fdt, fdt32_ld_(&prop->nameoff),
+ name, namelen)) {
+ if (poffset)
+ *poffset = offset;
+ return prop;
+ }
+ }
+
+ if (lenp)
+ *lenp = offset;
+ return NULL;
+}
+
+
+const struct fdt_property *fdt_get_property_namelen(const void *fdt,
+ int offset,
+ const char *name,
+ int namelen, int *lenp)
+{
+ /* Prior to version 16, properties may need realignment
+ * and this API does not work. fdt_getprop_*() will, however. */
+ if (!can_assume(LATEST) && fdt_version(fdt) < 0x10) {
+ if (lenp)
+ *lenp = -FDT_ERR_BADVERSION;
+ return NULL;
+ }
+
+ return fdt_get_property_namelen_(fdt, offset, name, namelen, lenp,
+ NULL);
+}
+
+
+const struct fdt_property *fdt_get_property(const void *fdt,
+ int nodeoffset,
+ const char *name, int *lenp)
+{
+ return fdt_get_property_namelen(fdt, nodeoffset, name,
+ strlen(name), lenp);
+}
+
+const void *fdt_getprop_namelen(const void *fdt, int nodeoffset,
+ const char *name, int namelen, int *lenp)
+{
+ int poffset;
+ const struct fdt_property *prop;
+
+ prop = fdt_get_property_namelen_(fdt, nodeoffset, name, namelen, lenp,
+ &poffset);
+ if (!prop)
+ return NULL;
+
+ /* Handle realignment */
+ if (!can_assume(LATEST) && fdt_version(fdt) < 0x10 &&
+ (poffset + sizeof(*prop)) % 8 && fdt32_ld_(&prop->len) >= 8)
+ return prop->data + 4;
+ return prop->data;
+}
+
+const void *fdt_getprop_by_offset(const void *fdt, int offset,
+ const char **namep, int *lenp)
+{
+ const struct fdt_property *prop;
+
+ prop = fdt_get_property_by_offset_(fdt, offset, lenp);
+ if (!prop)
+ return NULL;
+ if (namep) {
+ const char *name;
+ int namelen;
+
+ if (!can_assume(VALID_INPUT)) {
+ name = fdt_get_string(fdt, fdt32_ld_(&prop->nameoff),
+ &namelen);
+ *namep = name;
+ if (!name) {
+ if (lenp)
+ *lenp = namelen;
+ return NULL;
+ }
+ } else {
+ *namep = fdt_string(fdt, fdt32_ld_(&prop->nameoff));
+ }
+ }
+
+ /* Handle realignment */
+ if (!can_assume(LATEST) && fdt_version(fdt) < 0x10 &&
+ (offset + sizeof(*prop)) % 8 && fdt32_ld_(&prop->len) >= 8)
+ return prop->data + 4;
+ return prop->data;
+}
+
+const void *fdt_getprop(const void *fdt, int nodeoffset,
+ const char *name, int *lenp)
+{
+ return fdt_getprop_namelen(fdt, nodeoffset, name, strlen(name), lenp);
+}
+
+uint32_t fdt_get_phandle(const void *fdt, int nodeoffset)
+{
+ const fdt32_t *php;
+ int len;
+
+ /* FIXME: This is a bit sub-optimal, since we potentially scan
+ * over all the properties twice. */
+ php = fdt_getprop(fdt, nodeoffset, "phandle", &len);
+ if (!php || (len != sizeof(*php))) {
+ php = fdt_getprop(fdt, nodeoffset, "linux,phandle", &len);
+ if (!php || (len != sizeof(*php)))
+ return 0;
+ }
+
+ return fdt32_ld_(php);
+}
+
+static const void *fdt_path_getprop_namelen(const void *fdt, const char *path,
+ const char *propname, int propnamelen,
+ int *lenp)
+{
+ int offset = fdt_path_offset(fdt, path);
+
+ if (offset < 0)
+ return NULL;
+
+ return fdt_getprop_namelen(fdt, offset, propname, propnamelen, lenp);
+}
+
+const char *fdt_get_alias_namelen(const void *fdt,
+ const char *name, int namelen)
+{
+ int len;
+ const char *alias;
+
+ alias = fdt_path_getprop_namelen(fdt, "/aliases", name, namelen, &len);
+
+ if (!can_assume(VALID_DTB) &&
+ !(alias && len > 0 && alias[len - 1] == '\0' && *alias == '/'))
+ return NULL;
+
+ return alias;
+}
+
+const char *fdt_get_alias(const void *fdt, const char *name)
+{
+ return fdt_get_alias_namelen(fdt, name, strlen(name));
+}
+
+const char *fdt_get_symbol_namelen(const void *fdt,
+ const char *name, int namelen)
+{
+ return fdt_path_getprop_namelen(fdt, "/__symbols__", name, namelen, NULL);
+}
+
+const char *fdt_get_symbol(const void *fdt, const char *name)
+{
+ return fdt_get_symbol_namelen(fdt, name, strlen(name));
+}
+
+int fdt_get_path(const void *fdt, int nodeoffset, char *buf, int buflen)
+{
+ int pdepth = 0, p = 0;
+ int offset, depth, namelen;
+ const char *name;
+
+ FDT_RO_PROBE(fdt);
+
+ if (buflen < 2)
+ return -FDT_ERR_NOSPACE;
+
+ for (offset = 0, depth = 0;
+ (offset >= 0) && (offset <= nodeoffset);
+ offset = fdt_next_node(fdt, offset, &depth)) {
+ while (pdepth > depth) {
+ do {
+ p--;
+ } while (buf[p-1] != '/');
+ pdepth--;
+ }
+
+ if (pdepth >= depth) {
+ name = fdt_get_name(fdt, offset, &namelen);
+ if (!name)
+ return namelen;
+ if ((p + namelen + 1) <= buflen) {
+ memcpy(buf + p, name, namelen);
+ p += namelen;
+ buf[p++] = '/';
+ pdepth++;
+ }
+ }
+
+ if (offset == nodeoffset) {
+ if (pdepth < (depth + 1))
+ return -FDT_ERR_NOSPACE;
+
+ if (p > 1) /* special case so that root path is "/", not "" */
+ p--;
+ buf[p] = '\0';
+ return 0;
+ }
+ }
+
+ if ((offset == -FDT_ERR_NOTFOUND) || (offset >= 0))
+ return -FDT_ERR_BADOFFSET;
+ else if (offset == -FDT_ERR_BADOFFSET)
+ return -FDT_ERR_BADSTRUCTURE;
+
+ return offset; /* error from fdt_next_node() */
+}
+
+int fdt_supernode_atdepth_offset(const void *fdt, int nodeoffset,
+ int supernodedepth, int *nodedepth)
+{
+ int offset, depth;
+ int supernodeoffset = -FDT_ERR_INTERNAL;
+
+ FDT_RO_PROBE(fdt);
+
+ if (supernodedepth < 0)
+ return -FDT_ERR_NOTFOUND;
+
+ for (offset = 0, depth = 0;
+ (offset >= 0) && (offset <= nodeoffset);
+ offset = fdt_next_node(fdt, offset, &depth)) {
+ if (depth == supernodedepth)
+ supernodeoffset = offset;
+
+ if (offset == nodeoffset) {
+ if (nodedepth)
+ *nodedepth = depth;
+
+ if (supernodedepth > depth)
+ return -FDT_ERR_NOTFOUND;
+ else
+ return supernodeoffset;
+ }
+ }
+
+ if (!can_assume(VALID_INPUT)) {
+ if ((offset == -FDT_ERR_NOTFOUND) || (offset >= 0))
+ return -FDT_ERR_BADOFFSET;
+ else if (offset == -FDT_ERR_BADOFFSET)
+ return -FDT_ERR_BADSTRUCTURE;
+ }
+
+ return offset; /* error from fdt_next_node() */
+}
+
+int fdt_node_depth(const void *fdt, int nodeoffset)
+{
+ int nodedepth;
+ int err;
+
+ err = fdt_supernode_atdepth_offset(fdt, nodeoffset, 0, &nodedepth);
+ if (err)
+ return (can_assume(LIBFDT_FLAWLESS) || err < 0) ? err :
+ -FDT_ERR_INTERNAL;
+ return nodedepth;
+}
+
+int fdt_parent_offset(const void *fdt, int nodeoffset)
+{
+ int nodedepth = fdt_node_depth(fdt, nodeoffset);
+
+ if (nodedepth < 0)
+ return nodedepth;
+ return fdt_supernode_atdepth_offset(fdt, nodeoffset,
+ nodedepth - 1, NULL);
+}
+
+int fdt_node_offset_by_prop_value(const void *fdt, int startoffset,
+ const char *propname,
+ const void *propval, int proplen)
+{
+ int offset;
+ const void *val;
+ int len;
+
+ FDT_RO_PROBE(fdt);
+
+ /* FIXME: The algorithm here is pretty horrible: we scan each
+ * property of a node in fdt_getprop(), then if that didn't
+ * find what we want, we scan over them again making our way
+ * to the next node. Still it's the easiest to implement
+ * approach; performance can come later. */
+ for (offset = fdt_next_node(fdt, startoffset, NULL);
+ offset >= 0;
+ offset = fdt_next_node(fdt, offset, NULL)) {
+ val = fdt_getprop(fdt, offset, propname, &len);
+ if (val && (len == proplen)
+ && (memcmp(val, propval, len) == 0))
+ return offset;
+ }
+
+ return offset; /* error from fdt_next_node() */
+}
+
+int fdt_node_offset_by_phandle(const void *fdt, uint32_t phandle)
+{
+ int offset;
+
+ if ((phandle == 0) || (phandle == ~0U))
+ return -FDT_ERR_BADPHANDLE;
+
+ FDT_RO_PROBE(fdt);
+
+ /* FIXME: The algorithm here is pretty horrible: we
+ * potentially scan each property of a node in
+ * fdt_get_phandle(), then if that didn't find what
+ * we want, we scan over them again making our way to the next
+ * node. Still it's the easiest to implement approach;
+ * performance can come later. */
+ for (offset = fdt_next_node(fdt, -1, NULL);
+ offset >= 0;
+ offset = fdt_next_node(fdt, offset, NULL)) {
+ if (fdt_get_phandle(fdt, offset) == phandle)
+ return offset;
+ }
+
+ return offset; /* error from fdt_next_node() */
+}
+
+int fdt_stringlist_contains(const char *strlist, int listlen, const char *str)
+{
+ int len = strlen(str);
+ const char *p;
+
+ while (listlen >= len) {
+ if (memcmp(str, strlist, len+1) == 0)
+ return 1;
+ p = memchr(strlist, '\0', listlen);
+ if (!p)
+ return 0; /* malformed strlist.. */
+ listlen -= (p-strlist) + 1;
+ strlist = p + 1;
+ }
+ return 0;
+}
+
+int fdt_stringlist_count(const void *fdt, int nodeoffset, const char *property)
+{
+ const char *list, *end;
+ int length, count = 0;
+
+ list = fdt_getprop(fdt, nodeoffset, property, &length);
+ if (!list)
+ return length;
+
+ end = list + length;
+
+ while (list < end) {
+ length = strnlen(list, end - list) + 1;
+
+ /* Abort if the last string isn't properly NUL-terminated. */
+ if (list + length > end)
+ return -FDT_ERR_BADVALUE;
+
+ list += length;
+ count++;
+ }
+
+ return count;
+}
+
+int fdt_stringlist_search(const void *fdt, int nodeoffset, const char *property,
+ const char *string)
+{
+ int length, len, idx = 0;
+ const char *list, *end;
+
+ list = fdt_getprop(fdt, nodeoffset, property, &length);
+ if (!list)
+ return length;
+
+ len = strlen(string) + 1;
+ end = list + length;
+
+ while (list < end) {
+ length = strnlen(list, end - list) + 1;
+
+ /* Abort if the last string isn't properly NUL-terminated. */
+ if (list + length > end)
+ return -FDT_ERR_BADVALUE;
+
+ if (length == len && memcmp(list, string, length) == 0)
+ return idx;
+
+ list += length;
+ idx++;
+ }
+
+ return -FDT_ERR_NOTFOUND;
+}
+
+const char *fdt_stringlist_get(const void *fdt, int nodeoffset,
+ const char *property, int idx,
+ int *lenp)
+{
+ const char *list, *end;
+ int length;
+
+ list = fdt_getprop(fdt, nodeoffset, property, &length);
+ if (!list) {
+ if (lenp)
+ *lenp = length;
+
+ return NULL;
+ }
+
+ end = list + length;
+
+ while (list < end) {
+ length = strnlen(list, end - list) + 1;
+
+ /* Abort if the last string isn't properly NUL-terminated. */
+ if (list + length > end) {
+ if (lenp)
+ *lenp = -FDT_ERR_BADVALUE;
+
+ return NULL;
+ }
+
+ if (idx == 0) {
+ if (lenp)
+ *lenp = length - 1;
+
+ return list;
+ }
+
+ list += length;
+ idx--;
+ }
+
+ if (lenp)
+ *lenp = -FDT_ERR_NOTFOUND;
+
+ return NULL;
+}
+
+int fdt_node_check_compatible(const void *fdt, int nodeoffset,
+ const char *compatible)
+{
+ const void *prop;
+ int len;
+
+ prop = fdt_getprop(fdt, nodeoffset, "compatible", &len);
+ if (!prop)
+ return len;
+
+ return !fdt_stringlist_contains(prop, len, compatible);
+}
+
+int fdt_node_offset_by_compatible(const void *fdt, int startoffset,
+ const char *compatible)
+{
+ int offset, err;
+
+ FDT_RO_PROBE(fdt);
+
+ /* FIXME: The algorithm here is pretty horrible: we scan each
+ * property of a node in fdt_node_check_compatible(), then if
+ * that didn't find what we want, we scan over them again
+ * making our way to the next node. Still it's the easiest to
+ * implement approach; performance can come later. */
+ for (offset = fdt_next_node(fdt, startoffset, NULL);
+ offset >= 0;
+ offset = fdt_next_node(fdt, offset, NULL)) {
+ err = fdt_node_check_compatible(fdt, offset, compatible);
+ if ((err < 0) && (err != -FDT_ERR_NOTFOUND))
+ return err;
+ else if (err == 0)
+ return offset;
+ }
+
+ return offset; /* error from fdt_next_node() */
+}
diff --git a/na/scripts/dtc/libfdt/fdt_rw.c b/na/scripts/dtc/libfdt/fdt_rw.c
new file mode 100644
index 0000000..90ea14e
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt_rw.c
@@ -0,0 +1,512 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ */
+#include "libfdt_env.h"
+
+#include <fdt.h>
+#include <libfdt.h>
+
+#include "libfdt_internal.h"
+
+static int fdt_blocks_misordered_(const void *fdt,
+ int mem_rsv_size, int struct_size)
+{
+ return (fdt_off_mem_rsvmap(fdt) < FDT_ALIGN(sizeof(struct fdt_header), 8))
+ || (fdt_off_dt_struct(fdt) <
+ (fdt_off_mem_rsvmap(fdt) + mem_rsv_size))
+ || (fdt_off_dt_strings(fdt) <
+ (fdt_off_dt_struct(fdt) + struct_size))
+ || (fdt_totalsize(fdt) <
+ (fdt_off_dt_strings(fdt) + fdt_size_dt_strings(fdt)));
+}
+
+static void fdt_downgrade_version(void *fdt)
+{
+ if (!can_assume(LATEST) && fdt_version(fdt) > FDT_LAST_SUPPORTED_VERSION)
+ fdt_set_version(fdt, FDT_LAST_SUPPORTED_VERSION);
+}
+
+static int fdt_rw_probe_(void *fdt)
+{
+ if (can_assume(VALID_DTB))
+ return 0;
+ FDT_RO_PROBE(fdt);
+
+ if (!can_assume(LATEST) && fdt_version(fdt) < 17)
+ return -FDT_ERR_BADVERSION;
+ if (fdt_blocks_misordered_(fdt, sizeof(struct fdt_reserve_entry),
+ fdt_size_dt_struct(fdt)))
+ return -FDT_ERR_BADLAYOUT;
+
+ fdt_downgrade_version(fdt);
+ return 0;
+}
+
+#define FDT_RW_PROBE(fdt) \
+ { \
+ int err_; \
+ if ((err_ = fdt_rw_probe_(fdt)) != 0) \
+ return err_; \
+ }
+
+static inline unsigned int fdt_data_size_(void *fdt)
+{
+ return fdt_off_dt_strings(fdt) + fdt_size_dt_strings(fdt);
+}
+
+static int fdt_splice_(void *fdt, void *splicepoint, int oldlen, int newlen)
+{
+ char *p = splicepoint;
+ unsigned int dsize = fdt_data_size_(fdt);
+ size_t soff = p - (char *)fdt;
+
+ if ((oldlen < 0) || (soff + oldlen < soff) || (soff + oldlen > dsize))
+ return -FDT_ERR_BADOFFSET;
+ if ((p < (char *)fdt) || (dsize + newlen < (unsigned)oldlen))
+ return -FDT_ERR_BADOFFSET;
+ if (dsize - oldlen + newlen > fdt_totalsize(fdt))
+ return -FDT_ERR_NOSPACE;
+ memmove(p + newlen, p + oldlen, ((char *)fdt + dsize) - (p + oldlen));
+ return 0;
+}
+
+static int fdt_splice_mem_rsv_(void *fdt, struct fdt_reserve_entry *p,
+ int oldn, int newn)
+{
+ int delta = (newn - oldn) * sizeof(*p);
+ int err;
+ err = fdt_splice_(fdt, p, oldn * sizeof(*p), newn * sizeof(*p));
+ if (err)
+ return err;
+ fdt_set_off_dt_struct(fdt, fdt_off_dt_struct(fdt) + delta);
+ fdt_set_off_dt_strings(fdt, fdt_off_dt_strings(fdt) + delta);
+ return 0;
+}
+
+static int fdt_splice_struct_(void *fdt, void *p,
+ int oldlen, int newlen)
+{
+ int delta = newlen - oldlen;
+ int err;
+
+ if ((err = fdt_splice_(fdt, p, oldlen, newlen)))
+ return err;
+
+ fdt_set_size_dt_struct(fdt, fdt_size_dt_struct(fdt) + delta);
+ fdt_set_off_dt_strings(fdt, fdt_off_dt_strings(fdt) + delta);
+ return 0;
+}
+
+/* Must only be used to roll back in case of error */
+static void fdt_del_last_string_(void *fdt, const char *s)
+{
+ int newlen = strlen(s) + 1;
+
+ fdt_set_size_dt_strings(fdt, fdt_size_dt_strings(fdt) - newlen);
+}
+
+static int fdt_splice_string_(void *fdt, int newlen)
+{
+ void *p = (char *)fdt
+ + fdt_off_dt_strings(fdt) + fdt_size_dt_strings(fdt);
+ int err;
+
+ if ((err = fdt_splice_(fdt, p, 0, newlen)))
+ return err;
+
+ fdt_set_size_dt_strings(fdt, fdt_size_dt_strings(fdt) + newlen);
+ return 0;
+}
+
+/**
+ * fdt_find_add_string_() - Find or allocate a string
+ *
+ * @fdt: pointer to the device tree to check/adjust
+ * @s: string to find/add
+ * @allocated: Set to 0 if the string was found, 1 if not found and so
+ * allocated. Ignored if can_assume(NO_ROLLBACK)
+ * @return offset of string in the string table (whether found or added)
+ */
+static int fdt_find_add_string_(void *fdt, const char *s, int slen,
+ int *allocated)
+{
+ char *strtab = (char *)fdt + fdt_off_dt_strings(fdt);
+ const char *p;
+ char *new;
+ int err;
+
+ if (!can_assume(NO_ROLLBACK))
+ *allocated = 0;
+
+ p = fdt_find_string_len_(strtab, fdt_size_dt_strings(fdt), s, slen);
+ if (p)
+ /* found it */
+ return (p - strtab);
+
+ new = strtab + fdt_size_dt_strings(fdt);
+ err = fdt_splice_string_(fdt, slen + 1);
+ if (err)
+ return err;
+
+ if (!can_assume(NO_ROLLBACK))
+ *allocated = 1;
+
+ memcpy(new, s, slen);
+ new[slen] = '\0';
+
+ return (new - strtab);
+}
+
+int fdt_add_mem_rsv(void *fdt, uint64_t address, uint64_t size)
+{
+ struct fdt_reserve_entry *re;
+ int err;
+
+ FDT_RW_PROBE(fdt);
+
+ re = fdt_mem_rsv_w_(fdt, fdt_num_mem_rsv(fdt));
+ err = fdt_splice_mem_rsv_(fdt, re, 0, 1);
+ if (err)
+ return err;
+
+ re->address = cpu_to_fdt64(address);
+ re->size = cpu_to_fdt64(size);
+ return 0;
+}
+
+int fdt_del_mem_rsv(void *fdt, int n)
+{
+ struct fdt_reserve_entry *re = fdt_mem_rsv_w_(fdt, n);
+
+ FDT_RW_PROBE(fdt);
+
+ if (n >= fdt_num_mem_rsv(fdt))
+ return -FDT_ERR_NOTFOUND;
+
+ return fdt_splice_mem_rsv_(fdt, re, 1, 0);
+}
+
+static int fdt_resize_property_(void *fdt, int nodeoffset,
+ const char *name, int namelen,
+ int len, struct fdt_property **prop)
+{
+ int oldlen;
+ int err;
+
+ *prop = fdt_get_property_namelen_w(fdt, nodeoffset, name, namelen,
+ &oldlen);
+ if (!*prop)
+ return oldlen;
+
+ if ((err = fdt_splice_struct_(fdt, (*prop)->data, FDT_TAGALIGN(oldlen),
+ FDT_TAGALIGN(len))))
+ return err;
+
+ (*prop)->len = cpu_to_fdt32(len);
+ return 0;
+}
+
+static int fdt_add_property_(void *fdt, int nodeoffset, const char *name,
+ int namelen, int len, struct fdt_property **prop)
+{
+ int proplen;
+ int nextoffset;
+ int namestroff;
+ int err;
+ int allocated;
+
+ if ((nextoffset = fdt_check_node_offset_(fdt, nodeoffset)) < 0)
+ return nextoffset;
+
+ namestroff = fdt_find_add_string_(fdt, name, namelen, &allocated);
+ if (namestroff < 0)
+ return namestroff;
+
+ *prop = fdt_offset_ptr_w_(fdt, nextoffset);
+ proplen = sizeof(**prop) + FDT_TAGALIGN(len);
+
+ err = fdt_splice_struct_(fdt, *prop, 0, proplen);
+ if (err) {
+ /* Delete the string if we failed to add it */
+ if (!can_assume(NO_ROLLBACK) && allocated)
+ fdt_del_last_string_(fdt, name);
+ return err;
+ }
+
+ (*prop)->tag = cpu_to_fdt32(FDT_PROP);
+ (*prop)->nameoff = cpu_to_fdt32(namestroff);
+ (*prop)->len = cpu_to_fdt32(len);
+ return 0;
+}
+
+int fdt_set_name(void *fdt, int nodeoffset, const char *name)
+{
+ char *namep;
+ int oldlen, newlen;
+ int err;
+
+ FDT_RW_PROBE(fdt);
+
+ namep = (char *)(uintptr_t)fdt_get_name(fdt, nodeoffset, &oldlen);
+ if (!namep)
+ return oldlen;
+
+ newlen = strlen(name);
+
+ err = fdt_splice_struct_(fdt, namep, FDT_TAGALIGN(oldlen+1),
+ FDT_TAGALIGN(newlen+1));
+ if (err)
+ return err;
+
+ memcpy(namep, name, newlen+1);
+ return 0;
+}
+
+int fdt_setprop_placeholder_namelen(void *fdt, int nodeoffset, const char *name,
+ int namelen, int len, void **prop_data)
+{
+ struct fdt_property *prop;
+ int err;
+
+ FDT_RW_PROBE(fdt);
+
+ err = fdt_resize_property_(fdt, nodeoffset, name, namelen, len, &prop);
+ if (err == -FDT_ERR_NOTFOUND)
+ err = fdt_add_property_(fdt, nodeoffset, name, namelen, len,
+ &prop);
+ if (err)
+ return err;
+
+ *prop_data = prop->data;
+ return 0;
+}
+
+int fdt_setprop_namelen(void *fdt, int nodeoffset, const char *name,
+ int namelen, const void *val, int len)
+{
+ void *prop_data;
+ int err;
+
+ err = fdt_setprop_placeholder_namelen(fdt, nodeoffset, name, namelen,
+ len, &prop_data);
+ if (err)
+ return err;
+
+ if (len)
+ memcpy(prop_data, val, len);
+ return 0;
+}
+
+int fdt_appendprop(void *fdt, int nodeoffset, const char *name,
+ const void *val, int len)
+{
+ struct fdt_property *prop;
+ int err, oldlen, newlen;
+
+ FDT_RW_PROBE(fdt);
+
+ prop = fdt_get_property_w(fdt, nodeoffset, name, &oldlen);
+ if (prop) {
+ newlen = len + oldlen;
+ err = fdt_splice_struct_(fdt, prop->data,
+ FDT_TAGALIGN(oldlen),
+ FDT_TAGALIGN(newlen));
+ if (err)
+ return err;
+ prop->len = cpu_to_fdt32(newlen);
+ memcpy(prop->data + oldlen, val, len);
+ } else {
+ err = fdt_add_property_(fdt, nodeoffset, name, strlen(name),
+ len, &prop);
+ if (err)
+ return err;
+ memcpy(prop->data, val, len);
+ }
+ return 0;
+}
+
+int fdt_delprop(void *fdt, int nodeoffset, const char *name)
+{
+ struct fdt_property *prop;
+ int len, proplen;
+
+ FDT_RW_PROBE(fdt);
+
+ prop = fdt_get_property_w(fdt, nodeoffset, name, &len);
+ if (!prop)
+ return len;
+
+ proplen = sizeof(*prop) + FDT_TAGALIGN(len);
+ return fdt_splice_struct_(fdt, prop, proplen, 0);
+}
+
+int fdt_add_subnode_namelen(void *fdt, int parentoffset,
+ const char *name, int namelen)
+{
+ struct fdt_node_header *nh;
+ int offset, nextoffset;
+ int nodelen;
+ int err;
+ uint32_t tag;
+ fdt32_t *endtag;
+
+ FDT_RW_PROBE(fdt);
+
+ offset = fdt_subnode_offset_namelen(fdt, parentoffset, name, namelen);
+ if (offset >= 0)
+ return -FDT_ERR_EXISTS;
+ else if (offset != -FDT_ERR_NOTFOUND)
+ return offset;
+
+ /* Try to place the new node after the parent's properties */
+ tag = fdt_next_tag(fdt, parentoffset, &nextoffset);
+ /* the fdt_subnode_offset_namelen() should ensure this never hits */
+ if (!can_assume(LIBFDT_FLAWLESS) && (tag != FDT_BEGIN_NODE))
+ return -FDT_ERR_INTERNAL;
+ do {
+ offset = nextoffset;
+ tag = fdt_next_tag(fdt, offset, &nextoffset);
+ } while ((tag == FDT_PROP) || (tag == FDT_NOP));
+
+ nh = fdt_offset_ptr_w_(fdt, offset);
+ nodelen = sizeof(*nh) + FDT_TAGALIGN(namelen+1) + FDT_TAGSIZE;
+
+ err = fdt_splice_struct_(fdt, nh, 0, nodelen);
+ if (err)
+ return err;
+
+ nh->tag = cpu_to_fdt32(FDT_BEGIN_NODE);
+ memset(nh->name, 0, FDT_TAGALIGN(namelen+1));
+ memcpy(nh->name, name, namelen);
+ endtag = (fdt32_t *)((char *)nh + nodelen - FDT_TAGSIZE);
+ *endtag = cpu_to_fdt32(FDT_END_NODE);
+
+ return offset;
+}
+
+int fdt_add_subnode(void *fdt, int parentoffset, const char *name)
+{
+ return fdt_add_subnode_namelen(fdt, parentoffset, name, strlen(name));
+}
+
+int fdt_del_node(void *fdt, int nodeoffset)
+{
+ int endoffset;
+
+ FDT_RW_PROBE(fdt);
+
+ endoffset = fdt_node_end_offset_(fdt, nodeoffset);
+ if (endoffset < 0)
+ return endoffset;
+
+ return fdt_splice_struct_(fdt, fdt_offset_ptr_w_(fdt, nodeoffset),
+ endoffset - nodeoffset, 0);
+}
+
+static void fdt_packblocks_(const char *old, char *new,
+ int mem_rsv_size,
+ int struct_size,
+ int strings_size)
+{
+ int mem_rsv_off, struct_off, strings_off;
+
+ mem_rsv_off = FDT_ALIGN(sizeof(struct fdt_header), 8);
+ struct_off = mem_rsv_off + mem_rsv_size;
+ strings_off = struct_off + struct_size;
+
+ memmove(new + mem_rsv_off, old + fdt_off_mem_rsvmap(old), mem_rsv_size);
+ fdt_set_off_mem_rsvmap(new, mem_rsv_off);
+
+ memmove(new + struct_off, old + fdt_off_dt_struct(old), struct_size);
+ fdt_set_off_dt_struct(new, struct_off);
+ fdt_set_size_dt_struct(new, struct_size);
+
+ memmove(new + strings_off, old + fdt_off_dt_strings(old), strings_size);
+ fdt_set_off_dt_strings(new, strings_off);
+ fdt_set_size_dt_strings(new, fdt_size_dt_strings(old));
+}
+
+int fdt_open_into(const void *fdt, void *buf, int bufsize)
+{
+ int err;
+ int mem_rsv_size, struct_size;
+ int newsize;
+ const char *fdtstart = fdt;
+ const char *fdtend = fdtstart + fdt_totalsize(fdt);
+ char *tmp;
+
+ FDT_RO_PROBE(fdt);
+
+ mem_rsv_size = (fdt_num_mem_rsv(fdt)+1)
+ * sizeof(struct fdt_reserve_entry);
+
+ if (can_assume(LATEST) || fdt_version(fdt) >= 17) {
+ struct_size = fdt_size_dt_struct(fdt);
+ } else if (fdt_version(fdt) == 16) {
+ struct_size = 0;
+ while (fdt_next_tag(fdt, struct_size, &struct_size) != FDT_END)
+ ;
+ if (struct_size < 0)
+ return struct_size;
+ } else {
+ return -FDT_ERR_BADVERSION;
+ }
+
+ if (can_assume(LIBFDT_ORDER) ||
+ !fdt_blocks_misordered_(fdt, mem_rsv_size, struct_size)) {
+ /* no further work necessary */
+ err = fdt_move(fdt, buf, bufsize);
+ if (err)
+ return err;
+ fdt_set_version(buf, 17);
+ fdt_set_size_dt_struct(buf, struct_size);
+ fdt_set_totalsize(buf, bufsize);
+ return 0;
+ }
+
+ /* Need to reorder */
+ newsize = FDT_ALIGN(sizeof(struct fdt_header), 8) + mem_rsv_size
+ + struct_size + fdt_size_dt_strings(fdt);
+
+ if (bufsize < newsize)
+ return -FDT_ERR_NOSPACE;
+
+ /* First attempt to build converted tree at beginning of buffer */
+ tmp = buf;
+ /* But if that overlaps with the old tree... */
+ if (((tmp + newsize) > fdtstart) && (tmp < fdtend)) {
+ /* Try right after the old tree instead */
+ tmp = (char *)(uintptr_t)fdtend;
+ if ((tmp + newsize) > ((char *)buf + bufsize))
+ return -FDT_ERR_NOSPACE;
+ }
+
+ fdt_packblocks_(fdt, tmp, mem_rsv_size, struct_size,
+ fdt_size_dt_strings(fdt));
+ memmove(buf, tmp, newsize);
+
+ fdt_set_magic(buf, FDT_MAGIC);
+ fdt_set_totalsize(buf, bufsize);
+ fdt_set_version(buf, 17);
+ fdt_set_last_comp_version(buf, 16);
+ fdt_set_boot_cpuid_phys(buf, fdt_boot_cpuid_phys(fdt));
+
+ return 0;
+}
+
+int fdt_pack(void *fdt)
+{
+ int mem_rsv_size;
+
+ FDT_RW_PROBE(fdt);
+
+ mem_rsv_size = (fdt_num_mem_rsv(fdt)+1)
+ * sizeof(struct fdt_reserve_entry);
+ fdt_packblocks_(fdt, fdt, mem_rsv_size, fdt_size_dt_struct(fdt),
+ fdt_size_dt_strings(fdt));
+ fdt_set_totalsize(fdt, fdt_data_size_(fdt));
+
+ return 0;
+}
diff --git a/na/scripts/dtc/libfdt/fdt_strerror.c b/na/scripts/dtc/libfdt/fdt_strerror.c
new file mode 100644
index 0000000..d852b77
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt_strerror.c
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#include "libfdt_env.h"
+
+#include <fdt.h>
+#include <libfdt.h>
+
+#include "libfdt_internal.h"
+
+struct fdt_errtabent {
+ const char *str;
+};
+
+#define FDT_ERRTABENT(val) \
+ [(val)] = { .str = #val, }
+
+static struct fdt_errtabent fdt_errtable[] = {
+ FDT_ERRTABENT(FDT_ERR_NOTFOUND),
+ FDT_ERRTABENT(FDT_ERR_EXISTS),
+ FDT_ERRTABENT(FDT_ERR_NOSPACE),
+
+ FDT_ERRTABENT(FDT_ERR_BADOFFSET),
+ FDT_ERRTABENT(FDT_ERR_BADPATH),
+ FDT_ERRTABENT(FDT_ERR_BADPHANDLE),
+ FDT_ERRTABENT(FDT_ERR_BADSTATE),
+
+ FDT_ERRTABENT(FDT_ERR_TRUNCATED),
+ FDT_ERRTABENT(FDT_ERR_BADMAGIC),
+ FDT_ERRTABENT(FDT_ERR_BADVERSION),
+ FDT_ERRTABENT(FDT_ERR_BADSTRUCTURE),
+ FDT_ERRTABENT(FDT_ERR_BADLAYOUT),
+ FDT_ERRTABENT(FDT_ERR_INTERNAL),
+ FDT_ERRTABENT(FDT_ERR_BADNCELLS),
+ FDT_ERRTABENT(FDT_ERR_BADVALUE),
+ FDT_ERRTABENT(FDT_ERR_BADOVERLAY),
+ FDT_ERRTABENT(FDT_ERR_NOPHANDLES),
+ FDT_ERRTABENT(FDT_ERR_BADFLAGS),
+ FDT_ERRTABENT(FDT_ERR_ALIGNMENT),
+};
+#define FDT_ERRTABSIZE ((int)(sizeof(fdt_errtable) / sizeof(fdt_errtable[0])))
+
+const char *fdt_strerror(int errval)
+{
+ if (errval > 0)
+ return "<valid offset/length>";
+ else if (errval == 0)
+ return "<no error>";
+ else if (-errval < FDT_ERRTABSIZE) {
+ const char *s = fdt_errtable[-errval].str;
+
+ if (s)
+ return s;
+ }
+
+ return "<unknown error>";
+}
diff --git a/na/scripts/dtc/libfdt/fdt_sw.c b/na/scripts/dtc/libfdt/fdt_sw.c
new file mode 100644
index 0000000..4c569ee
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt_sw.c
@@ -0,0 +1,384 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ */
+#include "libfdt_env.h"
+
+#include <fdt.h>
+#include <libfdt.h>
+
+#include "libfdt_internal.h"
+
+static int fdt_sw_probe_(void *fdt)
+{
+ if (!can_assume(VALID_INPUT)) {
+ if (fdt_magic(fdt) == FDT_MAGIC)
+ return -FDT_ERR_BADSTATE;
+ else if (fdt_magic(fdt) != FDT_SW_MAGIC)
+ return -FDT_ERR_BADMAGIC;
+ }
+
+ return 0;
+}
+
+#define FDT_SW_PROBE(fdt) \
+ { \
+ int err; \
+ if ((err = fdt_sw_probe_(fdt)) != 0) \
+ return err; \
+ }
+
+/* 'memrsv' state: Initial state after fdt_create()
+ *
+ * Allowed functions:
+ * fdt_add_reservemap_entry()
+ * fdt_finish_reservemap() [moves to 'struct' state]
+ */
+static int fdt_sw_probe_memrsv_(void *fdt)
+{
+ int err = fdt_sw_probe_(fdt);
+ if (err)
+ return err;
+
+ if (!can_assume(VALID_INPUT) && fdt_off_dt_strings(fdt) != 0)
+ return -FDT_ERR_BADSTATE;
+ return 0;
+}
+
+#define FDT_SW_PROBE_MEMRSV(fdt) \
+ { \
+ int err; \
+ if ((err = fdt_sw_probe_memrsv_(fdt)) != 0) \
+ return err; \
+ }
+
+/* 'struct' state: Enter this state after fdt_finish_reservemap()
+ *
+ * Allowed functions:
+ * fdt_begin_node()
+ * fdt_end_node()
+ * fdt_property*()
+ * fdt_finish() [moves to 'complete' state]
+ */
+static int fdt_sw_probe_struct_(void *fdt)
+{
+ int err = fdt_sw_probe_(fdt);
+ if (err)
+ return err;
+
+ if (!can_assume(VALID_INPUT) &&
+ fdt_off_dt_strings(fdt) != fdt_totalsize(fdt))
+ return -FDT_ERR_BADSTATE;
+ return 0;
+}
+
+#define FDT_SW_PROBE_STRUCT(fdt) \
+ { \
+ int err; \
+ if ((err = fdt_sw_probe_struct_(fdt)) != 0) \
+ return err; \
+ }
+
+static inline uint32_t sw_flags(void *fdt)
+{
+ /* assert: (fdt_magic(fdt) == FDT_SW_MAGIC) */
+ return fdt_last_comp_version(fdt);
+}
+
+/* 'complete' state: Enter this state after fdt_finish()
+ *
+ * Allowed functions: none
+ */
+
+static void *fdt_grab_space_(void *fdt, size_t len)
+{
+ unsigned int offset = fdt_size_dt_struct(fdt);
+ unsigned int spaceleft;
+
+ spaceleft = fdt_totalsize(fdt) - fdt_off_dt_struct(fdt)
+ - fdt_size_dt_strings(fdt);
+
+ if ((offset + len < offset) || (offset + len > spaceleft))
+ return NULL;
+
+ fdt_set_size_dt_struct(fdt, offset + len);
+ return fdt_offset_ptr_w_(fdt, offset);
+}
+
+int fdt_create_with_flags(void *buf, int bufsize, uint32_t flags)
+{
+ const int hdrsize = FDT_ALIGN(sizeof(struct fdt_header),
+ sizeof(struct fdt_reserve_entry));
+ void *fdt = buf;
+
+ if (bufsize < hdrsize)
+ return -FDT_ERR_NOSPACE;
+
+ if (flags & ~FDT_CREATE_FLAGS_ALL)
+ return -FDT_ERR_BADFLAGS;
+
+ memset(buf, 0, bufsize);
+
+ /*
+ * magic and last_comp_version keep intermediate state during the fdt
+ * creation process, which is replaced with the proper FDT format by
+ * fdt_finish().
+ *
+ * flags should be accessed with sw_flags().
+ */
+ fdt_set_magic(fdt, FDT_SW_MAGIC);
+ fdt_set_version(fdt, FDT_LAST_SUPPORTED_VERSION);
+ fdt_set_last_comp_version(fdt, flags);
+
+ fdt_set_totalsize(fdt, bufsize);
+
+ fdt_set_off_mem_rsvmap(fdt, hdrsize);
+ fdt_set_off_dt_struct(fdt, fdt_off_mem_rsvmap(fdt));
+ fdt_set_off_dt_strings(fdt, 0);
+
+ return 0;
+}
+
+int fdt_create(void *buf, int bufsize)
+{
+ return fdt_create_with_flags(buf, bufsize, 0);
+}
+
+int fdt_resize(void *fdt, void *buf, int bufsize)
+{
+ size_t headsize, tailsize;
+ char *oldtail, *newtail;
+
+ FDT_SW_PROBE(fdt);
+
+ if (bufsize < 0)
+ return -FDT_ERR_NOSPACE;
+
+ headsize = fdt_off_dt_struct(fdt) + fdt_size_dt_struct(fdt);
+ tailsize = fdt_size_dt_strings(fdt);
+
+ if (!can_assume(VALID_DTB) &&
+ headsize + tailsize > fdt_totalsize(fdt))
+ return -FDT_ERR_INTERNAL;
+
+ if ((headsize + tailsize) > (unsigned)bufsize)
+ return -FDT_ERR_NOSPACE;
+
+ oldtail = (char *)fdt + fdt_totalsize(fdt) - tailsize;
+ newtail = (char *)buf + bufsize - tailsize;
+
+ /* Two cases to avoid clobbering data if the old and new
+ * buffers partially overlap */
+ if (buf <= fdt) {
+ memmove(buf, fdt, headsize);
+ memmove(newtail, oldtail, tailsize);
+ } else {
+ memmove(newtail, oldtail, tailsize);
+ memmove(buf, fdt, headsize);
+ }
+
+ fdt_set_totalsize(buf, bufsize);
+ if (fdt_off_dt_strings(buf))
+ fdt_set_off_dt_strings(buf, bufsize);
+
+ return 0;
+}
+
+int fdt_add_reservemap_entry(void *fdt, uint64_t addr, uint64_t size)
+{
+ struct fdt_reserve_entry *re;
+ int offset;
+
+ FDT_SW_PROBE_MEMRSV(fdt);
+
+ offset = fdt_off_dt_struct(fdt);
+ if ((offset + sizeof(*re)) > fdt_totalsize(fdt))
+ return -FDT_ERR_NOSPACE;
+
+ re = (struct fdt_reserve_entry *)((char *)fdt + offset);
+ re->address = cpu_to_fdt64(addr);
+ re->size = cpu_to_fdt64(size);
+
+ fdt_set_off_dt_struct(fdt, offset + sizeof(*re));
+
+ return 0;
+}
+
+int fdt_finish_reservemap(void *fdt)
+{
+ int err = fdt_add_reservemap_entry(fdt, 0, 0);
+
+ if (err)
+ return err;
+
+ fdt_set_off_dt_strings(fdt, fdt_totalsize(fdt));
+ return 0;
+}
+
+int fdt_begin_node(void *fdt, const char *name)
+{
+ struct fdt_node_header *nh;
+ int namelen;
+
+ FDT_SW_PROBE_STRUCT(fdt);
+
+ namelen = strlen(name) + 1;
+ nh = fdt_grab_space_(fdt, sizeof(*nh) + FDT_TAGALIGN(namelen));
+ if (! nh)
+ return -FDT_ERR_NOSPACE;
+
+ nh->tag = cpu_to_fdt32(FDT_BEGIN_NODE);
+ memcpy(nh->name, name, namelen);
+ return 0;
+}
+
+int fdt_end_node(void *fdt)
+{
+ fdt32_t *en;
+
+ FDT_SW_PROBE_STRUCT(fdt);
+
+ en = fdt_grab_space_(fdt, FDT_TAGSIZE);
+ if (! en)
+ return -FDT_ERR_NOSPACE;
+
+ *en = cpu_to_fdt32(FDT_END_NODE);
+ return 0;
+}
+
+static int fdt_add_string_(void *fdt, const char *s)
+{
+ char *strtab = (char *)fdt + fdt_totalsize(fdt);
+ unsigned int strtabsize = fdt_size_dt_strings(fdt);
+ unsigned int len = strlen(s) + 1;
+ unsigned int struct_top, offset;
+
+ offset = strtabsize + len;
+ struct_top = fdt_off_dt_struct(fdt) + fdt_size_dt_struct(fdt);
+ if (fdt_totalsize(fdt) - offset < struct_top)
+ return 0; /* no more room :( */
+
+ memcpy(strtab - offset, s, len);
+ fdt_set_size_dt_strings(fdt, strtabsize + len);
+ return -offset;
+}
+
+/* Must only be used to roll back in case of error */
+static void fdt_del_last_string_(void *fdt, const char *s)
+{
+ int strtabsize = fdt_size_dt_strings(fdt);
+ int len = strlen(s) + 1;
+
+ fdt_set_size_dt_strings(fdt, strtabsize - len);
+}
+
+static int fdt_find_add_string_(void *fdt, const char *s, int *allocated)
+{
+ char *strtab = (char *)fdt + fdt_totalsize(fdt);
+ int strtabsize = fdt_size_dt_strings(fdt);
+ const char *p;
+
+ *allocated = 0;
+
+ p = fdt_find_string_(strtab - strtabsize, strtabsize, s);
+ if (p)
+ return p - strtab;
+
+ *allocated = 1;
+
+ return fdt_add_string_(fdt, s);
+}
+
+int fdt_property_placeholder(void *fdt, const char *name, int len, void **valp)
+{
+ struct fdt_property *prop;
+ int nameoff;
+ int allocated;
+
+ FDT_SW_PROBE_STRUCT(fdt);
+
+ /* String de-duplication can be slow, _NO_NAME_DEDUP skips it */
+ if (sw_flags(fdt) & FDT_CREATE_FLAG_NO_NAME_DEDUP) {
+ allocated = 1;
+ nameoff = fdt_add_string_(fdt, name);
+ } else {
+ nameoff = fdt_find_add_string_(fdt, name, &allocated);
+ }
+ if (nameoff == 0)
+ return -FDT_ERR_NOSPACE;
+
+ prop = fdt_grab_space_(fdt, sizeof(*prop) + FDT_TAGALIGN(len));
+ if (! prop) {
+ if (allocated)
+ fdt_del_last_string_(fdt, name);
+ return -FDT_ERR_NOSPACE;
+ }
+
+ prop->tag = cpu_to_fdt32(FDT_PROP);
+ prop->nameoff = cpu_to_fdt32(nameoff);
+ prop->len = cpu_to_fdt32(len);
+ *valp = prop->data;
+ return 0;
+}
+
+int fdt_property(void *fdt, const char *name, const void *val, int len)
+{
+ void *ptr;
+ int ret;
+
+ ret = fdt_property_placeholder(fdt, name, len, &ptr);
+ if (ret)
+ return ret;
+ memcpy(ptr, val, len);
+ return 0;
+}
+
+int fdt_finish(void *fdt)
+{
+ char *p = (char *)fdt;
+ fdt32_t *end;
+ int oldstroffset, newstroffset;
+ uint32_t tag;
+ int offset, nextoffset;
+
+ FDT_SW_PROBE_STRUCT(fdt);
+
+ /* Add terminator */
+ end = fdt_grab_space_(fdt, sizeof(*end));
+ if (! end)
+ return -FDT_ERR_NOSPACE;
+ *end = cpu_to_fdt32(FDT_END);
+
+ /* Relocate the string table */
+ oldstroffset = fdt_totalsize(fdt) - fdt_size_dt_strings(fdt);
+ newstroffset = fdt_off_dt_struct(fdt) + fdt_size_dt_struct(fdt);
+ memmove(p + newstroffset, p + oldstroffset, fdt_size_dt_strings(fdt));
+ fdt_set_off_dt_strings(fdt, newstroffset);
+
+ /* Walk the structure, correcting string offsets */
+ offset = 0;
+ while ((tag = fdt_next_tag(fdt, offset, &nextoffset)) != FDT_END) {
+ if (tag == FDT_PROP) {
+ struct fdt_property *prop =
+ fdt_offset_ptr_w_(fdt, offset);
+ int nameoff;
+
+ nameoff = fdt32_to_cpu(prop->nameoff);
+ nameoff += fdt_size_dt_strings(fdt);
+ prop->nameoff = cpu_to_fdt32(nameoff);
+ }
+ offset = nextoffset;
+ }
+ if (nextoffset < 0)
+ return nextoffset;
+
+ /* Finally, adjust the header */
+ fdt_set_totalsize(fdt, newstroffset + fdt_size_dt_strings(fdt));
+
+ /* And fix up fields that were keeping intermediate state. */
+ fdt_set_last_comp_version(fdt, FDT_LAST_COMPATIBLE_VERSION);
+ fdt_set_magic(fdt, FDT_MAGIC);
+
+ return 0;
+}
diff --git a/na/scripts/dtc/libfdt/fdt_wip.c b/na/scripts/dtc/libfdt/fdt_wip.c
new file mode 100644
index 0000000..c2d7566
--- /dev/null
+++ b/na/scripts/dtc/libfdt/fdt_wip.c
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause)
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ */
+#include "libfdt_env.h"
+
+#include <fdt.h>
+#include <libfdt.h>
+
+#include "libfdt_internal.h"
+
+int fdt_setprop_inplace_namelen_partial(void *fdt, int nodeoffset,
+ const char *name, int namelen,
+ uint32_t idx, const void *val,
+ int len)
+{
+ void *propval;
+ int proplen;
+
+ propval = fdt_getprop_namelen_w(fdt, nodeoffset, name, namelen,
+ &proplen);
+ if (!propval)
+ return proplen;
+
+ if ((unsigned)proplen < (len + idx))
+ return -FDT_ERR_NOSPACE;
+
+ memcpy((char *)propval + idx, val, len);
+ return 0;
+}
+
+int fdt_setprop_inplace(void *fdt, int nodeoffset, const char *name,
+ const void *val, int len)
+{
+ const void *propval;
+ int proplen;
+
+ propval = fdt_getprop(fdt, nodeoffset, name, &proplen);
+ if (!propval)
+ return proplen;
+
+ if (proplen != len)
+ return -FDT_ERR_NOSPACE;
+
+ return fdt_setprop_inplace_namelen_partial(fdt, nodeoffset, name,
+ strlen(name), 0,
+ val, len);
+}
+
+static void fdt_nop_region_(void *start, int len)
+{
+ fdt32_t *p;
+
+ for (p = start; (char *)p < ((char *)start + len); p++)
+ *p = cpu_to_fdt32(FDT_NOP);
+}
+
+int fdt_nop_property(void *fdt, int nodeoffset, const char *name)
+{
+ struct fdt_property *prop;
+ int len;
+
+ prop = fdt_get_property_w(fdt, nodeoffset, name, &len);
+ if (!prop)
+ return len;
+
+ fdt_nop_region_(prop, len + sizeof(*prop));
+
+ return 0;
+}
+
+int fdt_node_end_offset_(void *fdt, int offset)
+{
+ int depth = 0;
+
+ while ((offset >= 0) && (depth >= 0))
+ offset = fdt_next_node(fdt, offset, &depth);
+
+ return offset;
+}
+
+int fdt_nop_node(void *fdt, int nodeoffset)
+{
+ int endoffset;
+
+ endoffset = fdt_node_end_offset_(fdt, nodeoffset);
+ if (endoffset < 0)
+ return endoffset;
+
+ fdt_nop_region_(fdt_offset_ptr_w(fdt, nodeoffset, 0),
+ endoffset - nodeoffset);
+ return 0;
+}
diff --git a/na/scripts/dtc/libfdt/libfdt.h b/na/scripts/dtc/libfdt/libfdt.h
new file mode 100644
index 0000000..81aaf7c
--- /dev/null
+++ b/na/scripts/dtc/libfdt/libfdt.h
@@ -0,0 +1,2540 @@
+/* SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause) */
+#ifndef LIBFDT_H
+#define LIBFDT_H
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ */
+
+#include "libfdt_env.h"
+#include "fdt.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define FDT_FIRST_SUPPORTED_VERSION 0x02
+#define FDT_LAST_COMPATIBLE_VERSION 0x10
+#define FDT_LAST_SUPPORTED_VERSION 0x11
+
+/* Error codes: informative error codes */
+#define FDT_ERR_NOTFOUND 1
+ /* FDT_ERR_NOTFOUND: The requested node or property does not exist */
+#define FDT_ERR_EXISTS 2
+ /* FDT_ERR_EXISTS: Attempted to create a node or property which
+ * already exists */
+#define FDT_ERR_NOSPACE 3
+ /* FDT_ERR_NOSPACE: Operation needed to expand the device
+ * tree, but its buffer did not have sufficient space to
+ * contain the expanded tree. Use fdt_open_into() to move the
+ * device tree to a buffer with more space. */
+
+/* Error codes: codes for bad parameters */
+#define FDT_ERR_BADOFFSET 4
+ /* FDT_ERR_BADOFFSET: Function was passed a structure block
+ * offset which is out-of-bounds, or which points to an
+ * unsuitable part of the structure for the operation. */
+#define FDT_ERR_BADPATH 5
+ /* FDT_ERR_BADPATH: Function was passed a badly formatted path
+ * (e.g. missing a leading / for a function which requires an
+ * absolute path) */
+#define FDT_ERR_BADPHANDLE 6
+ /* FDT_ERR_BADPHANDLE: Function was passed an invalid phandle.
+ * This can be caused either by an invalid phandle property
+ * length, or the phandle value was either 0 or -1, which are
+ * not permitted. */
+#define FDT_ERR_BADSTATE 7
+ /* FDT_ERR_BADSTATE: Function was passed an incomplete device
+ * tree created by the sequential-write functions, which is
+ * not sufficiently complete for the requested operation. */
+
+/* Error codes: codes for bad device tree blobs */
+#define FDT_ERR_TRUNCATED 8
+ /* FDT_ERR_TRUNCATED: FDT or a sub-block is improperly
+ * terminated (overflows, goes outside allowed bounds, or
+ * isn't properly terminated). */
+#define FDT_ERR_BADMAGIC 9
+ /* FDT_ERR_BADMAGIC: Given "device tree" appears not to be a
+ * device tree at all - it is missing the flattened device
+ * tree magic number. */
+#define FDT_ERR_BADVERSION 10
+ /* FDT_ERR_BADVERSION: Given device tree has a version which
+ * can't be handled by the requested operation. For
+ * read-write functions, this may mean that fdt_open_into() is
+ * required to convert the tree to the expected version. */
+#define FDT_ERR_BADSTRUCTURE 11
+ /* FDT_ERR_BADSTRUCTURE: Given device tree has a corrupt
+ * structure block or other serious error (e.g. misnested
+ * nodes, or subnodes preceding properties). */
+#define FDT_ERR_BADLAYOUT 12
+ /* FDT_ERR_BADLAYOUT: For read-write functions, the given
+ * device tree has it's sub-blocks in an order that the
+ * function can't handle (memory reserve map, then structure,
+ * then strings). Use fdt_open_into() to reorganize the tree
+ * into a form suitable for the read-write operations. */
+
+/* "Can't happen" error indicating a bug in libfdt */
+#define FDT_ERR_INTERNAL 13
+ /* FDT_ERR_INTERNAL: libfdt has failed an internal assertion.
+ * Should never be returned, if it is, it indicates a bug in
+ * libfdt itself. */
+
+/* Errors in device tree content */
+#define FDT_ERR_BADNCELLS 14
+ /* FDT_ERR_BADNCELLS: Device tree has a #address-cells, #size-cells
+ * or similar property with a bad format or value */
+
+#define FDT_ERR_BADVALUE 15
+ /* FDT_ERR_BADVALUE: Device tree has a property with an unexpected
+ * value. For example: a property expected to contain a string list
+ * is not NUL-terminated within the length of its value. */
+
+#define FDT_ERR_BADOVERLAY 16
+ /* FDT_ERR_BADOVERLAY: The device tree overlay, while
+ * correctly structured, cannot be applied due to some
+ * unexpected or missing value, property or node. */
+
+#define FDT_ERR_NOPHANDLES 17
+ /* FDT_ERR_NOPHANDLES: The device tree doesn't have any
+ * phandle available anymore without causing an overflow */
+
+#define FDT_ERR_BADFLAGS 18
+ /* FDT_ERR_BADFLAGS: The function was passed a flags field that
+ * contains invalid flags or an invalid combination of flags. */
+
+#define FDT_ERR_ALIGNMENT 19
+ /* FDT_ERR_ALIGNMENT: The device tree base address is not 8-byte
+ * aligned. */
+
+#define FDT_ERR_MAX 19
+
+/* constants */
+#define FDT_MAX_PHANDLE 0xfffffffe
+ /* Valid values for phandles range from 1 to 2^32-2. */
+
+/**********************************************************************/
+/* Low-level functions (you probably don't need these) */
+/**********************************************************************/
+
+/**
+ * fdt_offset_ptr - safely get a byte range within the device tree blob
+ * @fdt: Pointer to the device tree blob
+ * @offset: Offset within the blob to the desired byte range
+ * @checklen: Required length of the byte range
+ *
+ * fdt_offset_ptr() returns a pointer to the byte range of length @checklen at
+ * the given @offset within the device tree blob, after verifying that the byte
+ * range fits entirely within the blob and does not overflow.
+ *
+ * returns:
+ * pointer to the byte range, on success
+ * NULL, if the requested range does not fit within the blob
+ */
+#ifndef SWIG /* This function is not useful in Python */
+const void *fdt_offset_ptr(const void *fdt, int offset, unsigned int checklen);
+#endif
+static inline void *fdt_offset_ptr_w(void *fdt, int offset, int checklen)
+{
+ return (void *)(uintptr_t)fdt_offset_ptr(fdt, offset, checklen);
+}
+
+/**
+ * fdt_next_tag - get next tag in the device tree
+ * @fdt: Pointer to the device tree blob
+ * @offset: Offset within the blob to start searching
+ * @nextoffset: Pointer to variable to store the offset of the next tag
+ *
+ * fdt_next_tag() returns the tag type of the next tag in the device tree
+ * blob starting from the given @offset. If @nextoffset is non-NULL, it will
+ * be set to the offset immediately following the tag.
+ *
+ * returns:
+ * the tag type (FDT_BEGIN_NODE, FDT_END_NODE, FDT_PROP, FDT_NOP, FDT_END),
+ * FDT_END, if offset is out of bounds
+ */
+uint32_t fdt_next_tag(const void *fdt, int offset, int *nextoffset);
+
+/*
+ * External helpers to access words from a device tree blob. They're built
+ * to work even with unaligned pointers on platforms (such as ARMv5) that don't
+ * like unaligned loads and stores.
+ */
+static inline uint16_t fdt16_ld(const fdt16_t *p)
+{
+ const uint8_t *bp = (const uint8_t *)p;
+
+ return ((uint16_t)bp[0] << 8) | bp[1];
+}
+
+static inline uint32_t fdt32_ld(const fdt32_t *p)
+{
+ const uint8_t *bp = (const uint8_t *)p;
+
+ return ((uint32_t)bp[0] << 24)
+ | ((uint32_t)bp[1] << 16)
+ | ((uint32_t)bp[2] << 8)
+ | bp[3];
+}
+
+static inline void fdt32_st(void *property, uint32_t value)
+{
+ uint8_t *bp = (uint8_t *)property;
+
+ bp[0] = value >> 24;
+ bp[1] = (value >> 16) & 0xff;
+ bp[2] = (value >> 8) & 0xff;
+ bp[3] = value & 0xff;
+}
+
+static inline uint64_t fdt64_ld(const fdt64_t *p)
+{
+ const uint8_t *bp = (const uint8_t *)p;
+
+ return ((uint64_t)bp[0] << 56)
+ | ((uint64_t)bp[1] << 48)
+ | ((uint64_t)bp[2] << 40)
+ | ((uint64_t)bp[3] << 32)
+ | ((uint64_t)bp[4] << 24)
+ | ((uint64_t)bp[5] << 16)
+ | ((uint64_t)bp[6] << 8)
+ | bp[7];
+}
+
+static inline void fdt64_st(void *property, uint64_t value)
+{
+ uint8_t *bp = (uint8_t *)property;
+
+ bp[0] = value >> 56;
+ bp[1] = (value >> 48) & 0xff;
+ bp[2] = (value >> 40) & 0xff;
+ bp[3] = (value >> 32) & 0xff;
+ bp[4] = (value >> 24) & 0xff;
+ bp[5] = (value >> 16) & 0xff;
+ bp[6] = (value >> 8) & 0xff;
+ bp[7] = value & 0xff;
+}
+
+/**********************************************************************/
+/* Traversal functions */
+/**********************************************************************/
+
+int fdt_next_node(const void *fdt, int offset, int *depth);
+
+/**
+ * fdt_first_subnode() - get offset of first direct subnode
+ * @fdt: FDT blob
+ * @offset: Offset of node to check
+ *
+ * Return: offset of first subnode, or -FDT_ERR_NOTFOUND if there is none
+ */
+int fdt_first_subnode(const void *fdt, int offset);
+
+/**
+ * fdt_next_subnode() - get offset of next direct subnode
+ * @fdt: FDT blob
+ * @offset: Offset of previous subnode
+ *
+ * After first calling fdt_first_subnode(), call this function repeatedly to
+ * get direct subnodes of a parent node.
+ *
+ * Return: offset of next subnode, or -FDT_ERR_NOTFOUND if there are no more
+ * subnodes
+ */
+int fdt_next_subnode(const void *fdt, int offset);
+
+/**
+ * fdt_for_each_subnode - iterate over all subnodes of a parent
+ *
+ * @node: child node (int, lvalue)
+ * @fdt: FDT blob (const void *)
+ * @parent: parent node (int)
+ *
+ * This is actually a wrapper around a for loop and would be used like so:
+ *
+ * fdt_for_each_subnode(node, fdt, parent) {
+ * Use node
+ * ...
+ * }
+ *
+ * if ((node < 0) && (node != -FDT_ERR_NOTFOUND)) {
+ * Error handling
+ * }
+ *
+ * Note that this is implemented as a macro and @node is used as
+ * iterator in the loop. The parent variable be constant or even a
+ * literal.
+ */
+#define fdt_for_each_subnode(node, fdt, parent) \
+ for (node = fdt_first_subnode(fdt, parent); \
+ node >= 0; \
+ node = fdt_next_subnode(fdt, node))
+
+/**********************************************************************/
+/* General functions */
+/**********************************************************************/
+#define fdt_get_header(fdt, field) \
+ (fdt32_ld(&((const struct fdt_header *)(fdt))->field))
+#define fdt_magic(fdt) (fdt_get_header(fdt, magic))
+#define fdt_totalsize(fdt) (fdt_get_header(fdt, totalsize))
+#define fdt_off_dt_struct(fdt) (fdt_get_header(fdt, off_dt_struct))
+#define fdt_off_dt_strings(fdt) (fdt_get_header(fdt, off_dt_strings))
+#define fdt_off_mem_rsvmap(fdt) (fdt_get_header(fdt, off_mem_rsvmap))
+#define fdt_version(fdt) (fdt_get_header(fdt, version))
+#define fdt_last_comp_version(fdt) (fdt_get_header(fdt, last_comp_version))
+#define fdt_boot_cpuid_phys(fdt) (fdt_get_header(fdt, boot_cpuid_phys))
+#define fdt_size_dt_strings(fdt) (fdt_get_header(fdt, size_dt_strings))
+#define fdt_size_dt_struct(fdt) (fdt_get_header(fdt, size_dt_struct))
+
+#define fdt_set_hdr_(name) \
+ static inline void fdt_set_##name(void *fdt, uint32_t val) \
+ { \
+ struct fdt_header *fdth = (struct fdt_header *)fdt; \
+ fdth->name = cpu_to_fdt32(val); \
+ }
+fdt_set_hdr_(magic)
+fdt_set_hdr_(totalsize)
+fdt_set_hdr_(off_dt_struct)
+fdt_set_hdr_(off_dt_strings)
+fdt_set_hdr_(off_mem_rsvmap)
+fdt_set_hdr_(version)
+fdt_set_hdr_(last_comp_version)
+fdt_set_hdr_(boot_cpuid_phys)
+fdt_set_hdr_(size_dt_strings)
+fdt_set_hdr_(size_dt_struct)
+#undef fdt_set_hdr_
+
+/**
+ * fdt_header_size - return the size of the tree's header
+ * @fdt: pointer to a flattened device tree
+ *
+ * Return: size of DTB header in bytes
+ */
+size_t fdt_header_size(const void *fdt);
+
+/**
+ * fdt_header_size_ - internal function to get header size from a version number
+ * @version: device tree version number
+ *
+ * Return: size of DTB header in bytes
+ */
+size_t fdt_header_size_(uint32_t version);
+
+/**
+ * fdt_check_header - sanity check a device tree header
+ * @fdt: pointer to data which might be a flattened device tree
+ *
+ * fdt_check_header() checks that the given buffer contains what
+ * appears to be a flattened device tree, and that the header contains
+ * valid information (to the extent that can be determined from the
+ * header alone).
+ *
+ * returns:
+ * 0, if the buffer appears to contain a valid device tree
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_TRUNCATED, standard meanings, as above
+ */
+int fdt_check_header(const void *fdt);
+
+/**
+ * fdt_move - move a device tree around in memory
+ * @fdt: pointer to the device tree to move
+ * @buf: pointer to memory where the device is to be moved
+ * @bufsize: size of the memory space at buf
+ *
+ * fdt_move() relocates, if possible, the device tree blob located at
+ * fdt to the buffer at buf of size bufsize. The buffer may overlap
+ * with the existing device tree blob at fdt. Therefore,
+ * fdt_move(fdt, fdt, fdt_totalsize(fdt))
+ * should always succeed.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, bufsize is insufficient to contain the device tree
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE, standard meanings
+ */
+int fdt_move(const void *fdt, void *buf, int bufsize);
+
+/**********************************************************************/
+/* Read-only functions */
+/**********************************************************************/
+
+/**
+ * fdt_check_full - check device tree validity
+ * @fdt: pointer to the device tree blob
+ * @bufsize: size of the buffer containing the device tree
+ *
+ * fdt_check_full() checks that the given buffer contains a valid
+ * flattened device tree and that the tree structure is internally
+ * consistent. This is a more thorough check than fdt_check_header().
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_check_full(const void *fdt, size_t bufsize);
+
+/**
+ * fdt_get_string - retrieve a string from the strings block of a device tree
+ * @fdt: pointer to the device tree blob
+ * @stroffset: offset of the string within the strings block (native endian)
+ * @lenp: optional pointer to return the string's length
+ *
+ * fdt_get_string() retrieves a pointer to a single string from the
+ * strings block of the device tree blob at fdt, and optionally also
+ * returns the string's length in *lenp.
+ *
+ * returns:
+ * a pointer to the string, on success
+ * NULL, if stroffset is out of bounds, or doesn't point to a valid string
+ */
+const char *fdt_get_string(const void *fdt, int stroffset, int *lenp);
+
+/**
+ * fdt_string - retrieve a string from the strings block of a device tree
+ * @fdt: pointer to the device tree blob
+ * @stroffset: offset of the string within the strings block (native endian)
+ *
+ * fdt_string() retrieves a pointer to a single string from the
+ * strings block of the device tree blob at fdt.
+ *
+ * returns:
+ * a pointer to the string, on success
+ * NULL, if stroffset is out of bounds, or doesn't point to a valid string
+ */
+const char *fdt_string(const void *fdt, int stroffset);
+
+/**
+ * fdt_find_max_phandle - find and return the highest phandle in a tree
+ * @fdt: pointer to the device tree blob
+ * @phandle: return location for the highest phandle value found in the tree
+ *
+ * fdt_find_max_phandle() finds the highest phandle value in the given device
+ * tree. The value returned in @phandle is only valid if the function returns
+ * success.
+ *
+ * returns:
+ * 0 on success or a negative error code on failure
+ */
+int fdt_find_max_phandle(const void *fdt, uint32_t *phandle);
+
+/**
+ * fdt_get_max_phandle - retrieves the highest phandle in a tree
+ * @fdt: pointer to the device tree blob
+ *
+ * fdt_get_max_phandle retrieves the highest phandle in the given
+ * device tree. This will ignore badly formatted phandles, or phandles
+ * with a value of 0 or -1.
+ *
+ * This function is deprecated in favour of fdt_find_max_phandle().
+ *
+ * returns:
+ * the highest phandle on success
+ * 0, if no phandle was found in the device tree
+ * -1, if an error occurred
+ */
+static inline uint32_t fdt_get_max_phandle(const void *fdt)
+{
+ uint32_t phandle;
+ int err;
+
+ err = fdt_find_max_phandle(fdt, &phandle);
+ if (err < 0)
+ return (uint32_t)-1;
+
+ return phandle;
+}
+
+/**
+ * fdt_generate_phandle - return a new, unused phandle for a device tree blob
+ * @fdt: pointer to the device tree blob
+ * @phandle: return location for the new phandle
+ *
+ * Walks the device tree blob and looks for the highest phandle value. On
+ * success, the new, unused phandle value (one higher than the previously
+ * highest phandle value in the device tree blob) will be returned in the
+ * @phandle parameter.
+ *
+ * Return: 0 on success or a negative error-code on failure
+ */
+int fdt_generate_phandle(const void *fdt, uint32_t *phandle);
+
+/**
+ * fdt_num_mem_rsv - retrieve the number of memory reserve map entries
+ * @fdt: pointer to the device tree blob
+ *
+ * Returns the number of entries in the device tree blob's memory
+ * reservation map. This does not include the terminating 0,0 entry
+ * or any other (0,0) entries reserved for expansion.
+ *
+ * returns:
+ * the number of entries
+ */
+int fdt_num_mem_rsv(const void *fdt);
+
+/**
+ * fdt_get_mem_rsv - retrieve one memory reserve map entry
+ * @fdt: pointer to the device tree blob
+ * @n: index of reserve map entry
+ * @address: pointer to 64-bit variable to hold the start address
+ * @size: pointer to 64-bit variable to hold the size of the entry
+ *
+ * On success, @address and @size will contain the address and size of
+ * the n-th reserve map entry from the device tree blob, in
+ * native-endian format.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE, standard meanings
+ */
+int fdt_get_mem_rsv(const void *fdt, int n, uint64_t *address, uint64_t *size);
+
+/**
+ * fdt_subnode_offset_namelen - find a subnode based on substring
+ * @fdt: pointer to the device tree blob
+ * @parentoffset: structure block offset of a node
+ * @name: name of the subnode to locate
+ * @namelen: number of characters of name to consider
+ *
+ * Identical to fdt_subnode_offset(), but only examine the first
+ * namelen characters of name for matching the subnode name. This is
+ * useful for finding subnodes based on a portion of a larger string,
+ * such as a full path.
+ *
+ * Return: offset of the subnode or -FDT_ERR_NOTFOUND if name not found.
+ */
+#ifndef SWIG /* Not available in Python */
+int fdt_subnode_offset_namelen(const void *fdt, int parentoffset,
+ const char *name, int namelen);
+#endif
+/**
+ * fdt_subnode_offset - find a subnode of a given node
+ * @fdt: pointer to the device tree blob
+ * @parentoffset: structure block offset of a node
+ * @name: name of the subnode to locate
+ *
+ * fdt_subnode_offset() finds a subnode of the node at structure block
+ * offset parentoffset with the given name. name may include a unit
+ * address, in which case fdt_subnode_offset() will find the subnode
+ * with that unit address, or the unit address may be omitted, in
+ * which case fdt_subnode_offset() will find an arbitrary subnode
+ * whose name excluding unit address matches the given name.
+ *
+ * returns:
+ * structure block offset of the requested subnode (>=0), on success
+ * -FDT_ERR_NOTFOUND, if the requested subnode does not exist
+ * -FDT_ERR_BADOFFSET, if parentoffset did not point to an FDT_BEGIN_NODE
+ * tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings.
+ */
+int fdt_subnode_offset(const void *fdt, int parentoffset, const char *name);
+
+/**
+ * fdt_path_offset_namelen - find a tree node by its full path
+ * @fdt: pointer to the device tree blob
+ * @path: full path of the node to locate
+ * @namelen: number of characters of path to consider
+ *
+ * Identical to fdt_path_offset(), but only consider the first namelen
+ * characters of path as the path name.
+ *
+ * Return: offset of the node or negative libfdt error value otherwise
+ */
+#ifndef SWIG /* Not available in Python */
+int fdt_path_offset_namelen(const void *fdt, const char *path, int namelen);
+#endif
+
+/**
+ * fdt_path_offset - find a tree node by its full path
+ * @fdt: pointer to the device tree blob
+ * @path: full path of the node to locate
+ *
+ * fdt_path_offset() finds a node of a given path in the device tree.
+ * Each path component may omit the unit address portion, but the
+ * results of this are undefined if any such path component is
+ * ambiguous (that is if there are multiple nodes at the relevant
+ * level matching the given component, differentiated only by unit
+ * address).
+ *
+ * If the path is not absolute (i.e. does not begin with '/'), the
+ * first component is treated as an alias. That is, the property by
+ * that name is looked up in the /aliases node, and the value of that
+ * property used in place of that first component.
+ *
+ * For example, for this small fragment
+ *
+ * / {
+ * aliases {
+ * i2c2 = &foo; // RHS compiles to "/soc@0/i2c@30a40000/eeprom@52"
+ * };
+ * soc@0 {
+ * foo: i2c@30a40000 {
+ * bar: eeprom@52 {
+ * };
+ * };
+ * };
+ * };
+ *
+ * these would be equivalent:
+ *
+ * /soc@0/i2c@30a40000/eeprom@52
+ * i2c2/eeprom@52
+ *
+ * returns:
+ * structure block offset of the node with the requested path (>=0), on
+ * success
+ * -FDT_ERR_BADPATH, given path does not begin with '/' and the first
+ * component is not a valid alias
+ * -FDT_ERR_NOTFOUND, if the requested node does not exist
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings.
+ */
+int fdt_path_offset(const void *fdt, const char *path);
+
+/**
+ * fdt_get_name - retrieve the name of a given node
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: structure block offset of the starting node
+ * @lenp: pointer to an integer variable (will be overwritten) or NULL
+ *
+ * fdt_get_name() retrieves the name (including unit address) of the
+ * device tree node at structure block offset nodeoffset. If lenp is
+ * non-NULL, the length of this name is also returned, in the integer
+ * pointed to by lenp.
+ *
+ * returns:
+ * pointer to the node's name, on success
+ * If lenp is non-NULL, *lenp contains the length of that name
+ * (>=0)
+ * NULL, on error
+ * if lenp is non-NULL *lenp contains an error code (<0):
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE
+ * tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE, standard meanings
+ */
+const char *fdt_get_name(const void *fdt, int nodeoffset, int *lenp);
+
+/**
+ * fdt_first_property_offset - find the offset of a node's first property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: structure block offset of a node
+ *
+ * fdt_first_property_offset() finds the first property of the node at
+ * the given structure block offset.
+ *
+ * returns:
+ * structure block offset of the property (>=0), on success
+ * -FDT_ERR_NOTFOUND, if the requested node has no properties
+ * -FDT_ERR_BADOFFSET, if nodeoffset did not point to an FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings.
+ */
+int fdt_first_property_offset(const void *fdt, int nodeoffset);
+
+/**
+ * fdt_next_property_offset - step through a node's properties
+ * @fdt: pointer to the device tree blob
+ * @offset: structure block offset of a property
+ *
+ * fdt_next_property_offset() finds the property immediately after the
+ * one at the given structure block offset. This will be a property
+ * of the same node as the given property.
+ *
+ * returns:
+ * structure block offset of the next property (>=0), on success
+ * -FDT_ERR_NOTFOUND, if the given property is the last in its node
+ * -FDT_ERR_BADOFFSET, if nodeoffset did not point to an FDT_PROP tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings.
+ */
+int fdt_next_property_offset(const void *fdt, int offset);
+
+/**
+ * fdt_for_each_property_offset - iterate over all properties of a node
+ *
+ * @property: property offset (int, lvalue)
+ * @fdt: FDT blob (const void *)
+ * @node: node offset (int)
+ *
+ * This is actually a wrapper around a for loop and would be used like so:
+ *
+ * fdt_for_each_property_offset(property, fdt, node) {
+ * Use property
+ * ...
+ * }
+ *
+ * if ((property < 0) && (property != -FDT_ERR_NOTFOUND)) {
+ * Error handling
+ * }
+ *
+ * Note that this is implemented as a macro and property is used as
+ * iterator in the loop. The node variable can be constant or even a
+ * literal.
+ */
+#define fdt_for_each_property_offset(property, fdt, node) \
+ for (property = fdt_first_property_offset(fdt, node); \
+ property >= 0; \
+ property = fdt_next_property_offset(fdt, property))
+
+/**
+ * fdt_get_property_by_offset - retrieve the property at a given offset
+ * @fdt: pointer to the device tree blob
+ * @offset: offset of the property to retrieve
+ * @lenp: pointer to an integer variable (will be overwritten) or NULL
+ *
+ * fdt_get_property_by_offset() retrieves a pointer to the
+ * fdt_property structure within the device tree blob at the given
+ * offset. If lenp is non-NULL, the length of the property value is
+ * also returned, in the integer pointed to by lenp.
+ *
+ * Note that this code only works on device tree versions >= 16. fdt_getprop()
+ * works on all versions.
+ *
+ * returns:
+ * pointer to the structure representing the property
+ * if lenp is non-NULL, *lenp contains the length of the property
+ * value (>=0)
+ * NULL, on error
+ * if lenp is non-NULL, *lenp contains an error code (<0):
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_PROP tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+const struct fdt_property *fdt_get_property_by_offset(const void *fdt,
+ int offset,
+ int *lenp);
+static inline struct fdt_property *fdt_get_property_by_offset_w(void *fdt,
+ int offset,
+ int *lenp)
+{
+ return (struct fdt_property *)(uintptr_t)
+ fdt_get_property_by_offset(fdt, offset, lenp);
+}
+
+/**
+ * fdt_get_property_namelen - find a property based on substring
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to find
+ * @name: name of the property to find
+ * @namelen: number of characters of name to consider
+ * @lenp: pointer to an integer variable (will be overwritten) or NULL
+ *
+ * Identical to fdt_get_property(), but only examine the first namelen
+ * characters of name for matching the property name.
+ *
+ * Return: pointer to the structure representing the property, or NULL
+ * if not found
+ */
+#ifndef SWIG /* Not available in Python */
+const struct fdt_property *fdt_get_property_namelen(const void *fdt,
+ int nodeoffset,
+ const char *name,
+ int namelen, int *lenp);
+static inline struct fdt_property *
+fdt_get_property_namelen_w(void *fdt, int nodeoffset, const char *name,
+ int namelen, int *lenp)
+{
+ return (struct fdt_property *)(uintptr_t)fdt_get_property_namelen(
+ fdt, nodeoffset, name, namelen, lenp);
+}
+#endif
+
+/**
+ * fdt_get_property - find a given property in a given node
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to find
+ * @name: name of the property to find
+ * @lenp: pointer to an integer variable (will be overwritten) or NULL
+ *
+ * fdt_get_property() retrieves a pointer to the fdt_property
+ * structure within the device tree blob corresponding to the property
+ * named 'name' of the node at offset nodeoffset. If lenp is
+ * non-NULL, the length of the property value is also returned, in the
+ * integer pointed to by lenp.
+ *
+ * returns:
+ * pointer to the structure representing the property
+ * if lenp is non-NULL, *lenp contains the length of the property
+ * value (>=0)
+ * NULL, on error
+ * if lenp is non-NULL, *lenp contains an error code (<0):
+ * -FDT_ERR_NOTFOUND, node does not have named property
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE
+ * tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+const struct fdt_property *fdt_get_property(const void *fdt, int nodeoffset,
+ const char *name, int *lenp);
+static inline struct fdt_property *fdt_get_property_w(void *fdt, int nodeoffset,
+ const char *name,
+ int *lenp)
+{
+ return (struct fdt_property *)(uintptr_t)
+ fdt_get_property(fdt, nodeoffset, name, lenp);
+}
+
+/**
+ * fdt_getprop_by_offset - retrieve the value of a property at a given offset
+ * @fdt: pointer to the device tree blob
+ * @offset: offset of the property to read
+ * @namep: pointer to a string variable (will be overwritten) or NULL
+ * @lenp: pointer to an integer variable (will be overwritten) or NULL
+ *
+ * fdt_getprop_by_offset() retrieves a pointer to the value of the
+ * property at structure block offset 'offset' (this will be a pointer
+ * to within the device blob itself, not a copy of the value). If
+ * lenp is non-NULL, the length of the property value is also
+ * returned, in the integer pointed to by lenp. If namep is non-NULL,
+ * the property's name will also be returned in the char * pointed to
+ * by namep (this will be a pointer to within the device tree's string
+ * block, not a new copy of the name).
+ *
+ * returns:
+ * pointer to the property's value
+ * if lenp is non-NULL, *lenp contains the length of the property
+ * value (>=0)
+ * if namep is non-NULL *namep contains a pointer to the property
+ * name.
+ * NULL, on error
+ * if lenp is non-NULL, *lenp contains an error code (<0):
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_PROP tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+#ifndef SWIG /* This function is not useful in Python */
+const void *fdt_getprop_by_offset(const void *fdt, int offset,
+ const char **namep, int *lenp);
+#endif
+
+/**
+ * fdt_getprop_namelen - get property value based on substring
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to find
+ * @name: name of the property to find
+ * @namelen: number of characters of name to consider
+ * @lenp: pointer to an integer variable (will be overwritten) or NULL
+ *
+ * Identical to fdt_getprop(), but only examine the first namelen
+ * characters of name for matching the property name.
+ *
+ * Return: pointer to the property's value or NULL on error
+ */
+#ifndef SWIG /* Not available in Python */
+const void *fdt_getprop_namelen(const void *fdt, int nodeoffset,
+ const char *name, int namelen, int *lenp);
+static inline void *fdt_getprop_namelen_w(void *fdt, int nodeoffset,
+ const char *name, int namelen,
+ int *lenp)
+{
+ return (void *)(uintptr_t)fdt_getprop_namelen(fdt, nodeoffset, name,
+ namelen, lenp);
+}
+#endif
+
+/**
+ * fdt_getprop - retrieve the value of a given property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to find
+ * @name: name of the property to find
+ * @lenp: pointer to an integer variable (will be overwritten) or NULL
+ *
+ * fdt_getprop() retrieves a pointer to the value of the property
+ * named @name of the node at offset @nodeoffset (this will be a
+ * pointer to within the device blob itself, not a copy of the value).
+ * If @lenp is non-NULL, the length of the property value is also
+ * returned, in the integer pointed to by @lenp.
+ *
+ * returns:
+ * pointer to the property's value
+ * if lenp is non-NULL, *lenp contains the length of the property
+ * value (>=0)
+ * NULL, on error
+ * if lenp is non-NULL, *lenp contains an error code (<0):
+ * -FDT_ERR_NOTFOUND, node does not have named property
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE
+ * tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+const void *fdt_getprop(const void *fdt, int nodeoffset,
+ const char *name, int *lenp);
+static inline void *fdt_getprop_w(void *fdt, int nodeoffset,
+ const char *name, int *lenp)
+{
+ return (void *)(uintptr_t)fdt_getprop(fdt, nodeoffset, name, lenp);
+}
+
+/**
+ * fdt_get_phandle - retrieve the phandle of a given node
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: structure block offset of the node
+ *
+ * fdt_get_phandle() retrieves the phandle of the device tree node at
+ * structure block offset nodeoffset.
+ *
+ * returns:
+ * the phandle of the node at nodeoffset, on success (!= 0, != -1)
+ * 0, if the node has no phandle, or another error occurs
+ */
+uint32_t fdt_get_phandle(const void *fdt, int nodeoffset);
+
+/**
+ * fdt_get_alias_namelen - get alias based on substring
+ * @fdt: pointer to the device tree blob
+ * @name: name of the alias to look up
+ * @namelen: number of characters of name to consider
+ *
+ * Identical to fdt_get_alias(), but only examine the first @namelen
+ * characters of @name for matching the alias name.
+ *
+ * Return: a pointer to the expansion of the alias named @name, if it exists,
+ * NULL otherwise
+ */
+#ifndef SWIG /* Not available in Python */
+const char *fdt_get_alias_namelen(const void *fdt,
+ const char *name, int namelen);
+#endif
+
+/**
+ * fdt_get_alias - retrieve the path referenced by a given alias
+ * @fdt: pointer to the device tree blob
+ * @name: name of the alias to look up
+ *
+ * fdt_get_alias() retrieves the value of a given alias. That is, the
+ * value of the property named @name in the node /aliases.
+ *
+ * returns:
+ * a pointer to the expansion of the alias named 'name', if it exists
+ * NULL, if the given alias or the /aliases node does not exist
+ */
+const char *fdt_get_alias(const void *fdt, const char *name);
+
+/**
+ * fdt_get_symbol_namelen - get symbol based on substring
+ * @fdt: pointer to the device tree blob
+ * @name: name of the symbol to look up
+ * @namelen: number of characters of name to consider
+ *
+ * Identical to fdt_get_symbol(), but only examine the first @namelen
+ * characters of @name for matching the symbol name.
+ *
+ * Return: a pointer to the expansion of the symbol named @name, if it exists,
+ * NULL otherwise
+ */
+#ifndef SWIG /* Not available in Python */
+const char *fdt_get_symbol_namelen(const void *fdt,
+ const char *name, int namelen);
+#endif
+
+/**
+ * fdt_get_symbol - retrieve the path referenced by a given symbol
+ * @fdt: pointer to the device tree blob
+ * @name: name of the symbol to look up
+ *
+ * fdt_get_symbol() retrieves the value of a given symbol. That is,
+ * the value of the property named @name in the node
+ * /__symbols__. Such a node exists only for a device tree blob that
+ * has been compiled with the -@ dtc option. Each property corresponds
+ * to a label appearing in the device tree source, with the name of
+ * the property being the label and the value being the full path of
+ * the node it is attached to.
+ *
+ * returns:
+ * a pointer to the expansion of the symbol named 'name', if it exists
+ * NULL, if the given symbol or the /__symbols__ node does not exist
+ */
+const char *fdt_get_symbol(const void *fdt, const char *name);
+
+/**
+ * fdt_get_path - determine the full path of a node
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose path to find
+ * @buf: character buffer to contain the returned path (will be overwritten)
+ * @buflen: size of the character buffer at buf
+ *
+ * fdt_get_path() computes the full path of the node at offset
+ * nodeoffset, and records that path in the buffer at buf.
+ *
+ * NOTE: This function is expensive, as it must scan the device tree
+ * structure from the start to nodeoffset.
+ *
+ * returns:
+ * 0, on success
+ * buf contains the absolute path of the node at
+ * nodeoffset, as a NUL-terminated string.
+ * -FDT_ERR_BADOFFSET, nodeoffset does not refer to a BEGIN_NODE tag
+ * -FDT_ERR_NOSPACE, the path of the given node is longer than (bufsize-1)
+ * characters and will not fit in the given buffer.
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE, standard meanings
+ */
+int fdt_get_path(const void *fdt, int nodeoffset, char *buf, int buflen);
+
+/**
+ * fdt_supernode_atdepth_offset - find a specific ancestor of a node
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose parent to find
+ * @supernodedepth: depth of the ancestor to find
+ * @nodedepth: pointer to an integer variable (will be overwritten) or NULL
+ *
+ * fdt_supernode_atdepth_offset() finds an ancestor of the given node
+ * at a specific depth from the root (where the root itself has depth
+ * 0, its immediate subnodes depth 1 and so forth). So
+ * fdt_supernode_atdepth_offset(fdt, nodeoffset, 0, NULL);
+ * will always return 0, the offset of the root node. If the node at
+ * nodeoffset has depth D, then:
+ * fdt_supernode_atdepth_offset(fdt, nodeoffset, D, NULL);
+ * will return nodeoffset itself.
+ *
+ * NOTE: This function is expensive, as it must scan the device tree
+ * structure from the start to nodeoffset.
+ *
+ * returns:
+ * structure block offset of the node at node offset's ancestor
+ * of depth supernodedepth (>=0), on success
+ * -FDT_ERR_BADOFFSET, nodeoffset does not refer to a BEGIN_NODE tag
+ * -FDT_ERR_NOTFOUND, supernodedepth was greater than the depth of
+ * nodeoffset
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE, standard meanings
+ */
+int fdt_supernode_atdepth_offset(const void *fdt, int nodeoffset,
+ int supernodedepth, int *nodedepth);
+
+/**
+ * fdt_node_depth - find the depth of a given node
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose parent to find
+ *
+ * fdt_node_depth() finds the depth of a given node. The root node
+ * has depth 0, its immediate subnodes depth 1 and so forth.
+ *
+ * NOTE: This function is expensive, as it must scan the device tree
+ * structure from the start to nodeoffset.
+ *
+ * returns:
+ * depth of the node at nodeoffset (>=0), on success
+ * -FDT_ERR_BADOFFSET, nodeoffset does not refer to a BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE, standard meanings
+ */
+int fdt_node_depth(const void *fdt, int nodeoffset);
+
+/**
+ * fdt_parent_offset - find the parent of a given node
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose parent to find
+ *
+ * fdt_parent_offset() locates the parent node of a given node (that
+ * is, it finds the offset of the node which contains the node at
+ * nodeoffset as a subnode).
+ *
+ * NOTE: This function is expensive, as it must scan the device tree
+ * structure from the start to nodeoffset, *twice*.
+ *
+ * returns:
+ * structure block offset of the parent of the node at nodeoffset
+ * (>=0), on success
+ * -FDT_ERR_BADOFFSET, nodeoffset does not refer to a BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE, standard meanings
+ */
+int fdt_parent_offset(const void *fdt, int nodeoffset);
+
+/**
+ * fdt_node_offset_by_prop_value - find nodes with a given property value
+ * @fdt: pointer to the device tree blob
+ * @startoffset: only find nodes after this offset
+ * @propname: property name to check
+ * @propval: property value to search for
+ * @proplen: length of the value in propval
+ *
+ * fdt_node_offset_by_prop_value() returns the offset of the first
+ * node after startoffset, which has a property named propname whose
+ * value is of length proplen and has value equal to propval; or if
+ * startoffset is -1, the very first such node in the tree.
+ *
+ * To iterate through all nodes matching the criterion, the following
+ * idiom can be used:
+ * offset = fdt_node_offset_by_prop_value(fdt, -1, propname,
+ * propval, proplen);
+ * while (offset != -FDT_ERR_NOTFOUND) {
+ * // other code here
+ * offset = fdt_node_offset_by_prop_value(fdt, offset, propname,
+ * propval, proplen);
+ * }
+ *
+ * Note the -1 in the first call to the function, if 0 is used here
+ * instead, the function will never locate the root node, even if it
+ * matches the criterion.
+ *
+ * returns:
+ * structure block offset of the located node (>= 0, >startoffset),
+ * on success
+ * -FDT_ERR_NOTFOUND, no node matching the criterion exists in the
+ * tree after startoffset
+ * -FDT_ERR_BADOFFSET, nodeoffset does not refer to a BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE, standard meanings
+ */
+int fdt_node_offset_by_prop_value(const void *fdt, int startoffset,
+ const char *propname,
+ const void *propval, int proplen);
+
+/**
+ * fdt_node_offset_by_phandle - find the node with a given phandle
+ * @fdt: pointer to the device tree blob
+ * @phandle: phandle value
+ *
+ * fdt_node_offset_by_phandle() returns the offset of the node
+ * which has the given phandle value. If there is more than one node
+ * in the tree with the given phandle (an invalid tree), results are
+ * undefined.
+ *
+ * returns:
+ * structure block offset of the located node (>= 0), on success
+ * -FDT_ERR_NOTFOUND, no node with that phandle exists
+ * -FDT_ERR_BADPHANDLE, given phandle value was invalid (0 or -1)
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE, standard meanings
+ */
+int fdt_node_offset_by_phandle(const void *fdt, uint32_t phandle);
+
+/**
+ * fdt_node_check_compatible - check a node's compatible property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of a tree node
+ * @compatible: string to match against
+ *
+ * fdt_node_check_compatible() returns 0 if the given node contains a
+ * @compatible property with the given string as one of its elements,
+ * it returns non-zero otherwise, or on error.
+ *
+ * returns:
+ * 0, if the node has a 'compatible' property listing the given string
+ * 1, if the node has a 'compatible' property, but it does not list
+ * the given string
+ * -FDT_ERR_NOTFOUND, if the given node has no 'compatible' property
+ * -FDT_ERR_BADOFFSET, if nodeoffset does not refer to a BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE, standard meanings
+ */
+int fdt_node_check_compatible(const void *fdt, int nodeoffset,
+ const char *compatible);
+
+/**
+ * fdt_node_offset_by_compatible - find nodes with a given 'compatible' value
+ * @fdt: pointer to the device tree blob
+ * @startoffset: only find nodes after this offset
+ * @compatible: 'compatible' string to match against
+ *
+ * fdt_node_offset_by_compatible() returns the offset of the first
+ * node after startoffset, which has a 'compatible' property which
+ * lists the given compatible string; or if startoffset is -1, the
+ * very first such node in the tree.
+ *
+ * To iterate through all nodes matching the criterion, the following
+ * idiom can be used:
+ * offset = fdt_node_offset_by_compatible(fdt, -1, compatible);
+ * while (offset != -FDT_ERR_NOTFOUND) {
+ * // other code here
+ * offset = fdt_node_offset_by_compatible(fdt, offset, compatible);
+ * }
+ *
+ * Note the -1 in the first call to the function, if 0 is used here
+ * instead, the function will never locate the root node, even if it
+ * matches the criterion.
+ *
+ * returns:
+ * structure block offset of the located node (>= 0, >startoffset),
+ * on success
+ * -FDT_ERR_NOTFOUND, no node matching the criterion exists in the
+ * tree after startoffset
+ * -FDT_ERR_BADOFFSET, nodeoffset does not refer to a BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE, standard meanings
+ */
+int fdt_node_offset_by_compatible(const void *fdt, int startoffset,
+ const char *compatible);
+
+/**
+ * fdt_stringlist_contains - check a string list property for a string
+ * @strlist: Property containing a list of strings to check
+ * @listlen: Length of property
+ * @str: String to search for
+ *
+ * This is a utility function provided for convenience. The list contains
+ * one or more strings, each terminated by \0, as is found in a device tree
+ * "compatible" property.
+ *
+ * Return: 1 if the string is found in the list, 0 not found, or invalid list
+ */
+int fdt_stringlist_contains(const char *strlist, int listlen, const char *str);
+
+/**
+ * fdt_stringlist_count - count the number of strings in a string list
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of a tree node
+ * @property: name of the property containing the string list
+ *
+ * Return:
+ * the number of strings in the given property
+ * -FDT_ERR_BADVALUE if the property value is not NUL-terminated
+ * -FDT_ERR_NOTFOUND if the property does not exist
+ */
+int fdt_stringlist_count(const void *fdt, int nodeoffset, const char *property);
+
+/**
+ * fdt_stringlist_search - find a string in a string list and return its index
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of a tree node
+ * @property: name of the property containing the string list
+ * @string: string to look up in the string list
+ *
+ * Note that it is possible for this function to succeed on property values
+ * that are not NUL-terminated. That's because the function will stop after
+ * finding the first occurrence of @string. This can for example happen with
+ * small-valued cell properties, such as #address-cells, when searching for
+ * the empty string.
+ *
+ * return:
+ * the index of the string in the list of strings
+ * -FDT_ERR_BADVALUE if the property value is not NUL-terminated
+ * -FDT_ERR_NOTFOUND if the property does not exist or does not contain
+ * the given string
+ */
+int fdt_stringlist_search(const void *fdt, int nodeoffset, const char *property,
+ const char *string);
+
+/**
+ * fdt_stringlist_get() - obtain the string at a given index in a string list
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of a tree node
+ * @property: name of the property containing the string list
+ * @index: index of the string to return
+ * @lenp: return location for the string length or an error code on failure
+ *
+ * Note that this will successfully extract strings from properties with
+ * non-NUL-terminated values. For example on small-valued cell properties
+ * this function will return the empty string.
+ *
+ * If non-NULL, the length of the string (on success) or a negative error-code
+ * (on failure) will be stored in the integer pointer to by lenp.
+ *
+ * Return:
+ * A pointer to the string at the given index in the string list or NULL on
+ * failure. On success the length of the string will be stored in the memory
+ * location pointed to by the lenp parameter, if non-NULL. On failure one of
+ * the following negative error codes will be returned in the lenp parameter
+ * (if non-NULL):
+ * -FDT_ERR_BADVALUE if the property value is not NUL-terminated
+ * -FDT_ERR_NOTFOUND if the property does not exist
+ */
+const char *fdt_stringlist_get(const void *fdt, int nodeoffset,
+ const char *property, int index,
+ int *lenp);
+
+/**********************************************************************/
+/* Read-only functions (addressing related) */
+/**********************************************************************/
+
+/**
+ * FDT_MAX_NCELLS - maximum value for #address-cells and #size-cells
+ *
+ * This is the maximum value for #address-cells, #size-cells and
+ * similar properties that will be processed by libfdt. IEE1275
+ * requires that OF implementations handle values up to 4.
+ * Implementations may support larger values, but in practice higher
+ * values aren't used.
+ */
+#define FDT_MAX_NCELLS 4
+
+/**
+ * fdt_address_cells - retrieve address size for a bus represented in the tree
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node to find the address size for
+ *
+ * When the node has a valid #address-cells property, returns its value.
+ *
+ * returns:
+ * 0 <= n < FDT_MAX_NCELLS, on success
+ * 2, if the node has no #address-cells property
+ * -FDT_ERR_BADNCELLS, if the node has a badly formatted or invalid
+ * #address-cells property
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_address_cells(const void *fdt, int nodeoffset);
+
+/**
+ * fdt_size_cells - retrieve address range size for a bus represented in the
+ * tree
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node to find the address range size for
+ *
+ * When the node has a valid #size-cells property, returns its value.
+ *
+ * returns:
+ * 0 <= n < FDT_MAX_NCELLS, on success
+ * 1, if the node has no #size-cells property
+ * -FDT_ERR_BADNCELLS, if the node has a badly formatted or invalid
+ * #size-cells property
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_size_cells(const void *fdt, int nodeoffset);
+
+
+/**********************************************************************/
+/* Write-in-place functions */
+/**********************************************************************/
+
+/**
+ * fdt_setprop_inplace_namelen_partial - change a property's value,
+ * but not its size
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @namelen: number of characters of name to consider
+ * @idx: index of the property to change in the array
+ * @val: pointer to data to replace the property value with
+ * @len: length of the property value
+ *
+ * Identical to fdt_setprop_inplace(), but modifies the given property
+ * starting from the given index, and using only the first characters
+ * of the name. It is useful when you want to manipulate only one value of
+ * an array and you have a string that doesn't end with \0.
+ *
+ * Return: 0 on success, negative libfdt error value otherwise
+ */
+#ifndef SWIG /* Not available in Python */
+int fdt_setprop_inplace_namelen_partial(void *fdt, int nodeoffset,
+ const char *name, int namelen,
+ uint32_t idx, const void *val,
+ int len);
+#endif
+
+/**
+ * fdt_setprop_inplace - change a property's value, but not its size
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: pointer to data to replace the property value with
+ * @len: length of the property value
+ *
+ * fdt_setprop_inplace() replaces the value of a given property with
+ * the data in val, of length len. This function cannot change the
+ * size of a property, and so will only work if len is equal to the
+ * current length of the property.
+ *
+ * This function will alter only the bytes in the blob which contain
+ * the given property value, and will not alter or move any other part
+ * of the tree.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, if len is not equal to the property's current length
+ * -FDT_ERR_NOTFOUND, node does not have the named property
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+#ifndef SWIG /* Not available in Python */
+int fdt_setprop_inplace(void *fdt, int nodeoffset, const char *name,
+ const void *val, int len);
+#endif
+
+/**
+ * fdt_setprop_inplace_u32 - change the value of a 32-bit integer property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: 32-bit integer value to replace the property with
+ *
+ * fdt_setprop_inplace_u32() replaces the value of a given property
+ * with the 32-bit integer value in val, converting val to big-endian
+ * if necessary. This function cannot change the size of a property,
+ * and so will only work if the property already exists and has length
+ * 4.
+ *
+ * This function will alter only the bytes in the blob which contain
+ * the given property value, and will not alter or move any other part
+ * of the tree.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, if the property's length is not equal to 4
+ * -FDT_ERR_NOTFOUND, node does not have the named property
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+static inline int fdt_setprop_inplace_u32(void *fdt, int nodeoffset,
+ const char *name, uint32_t val)
+{
+ fdt32_t tmp = cpu_to_fdt32(val);
+ return fdt_setprop_inplace(fdt, nodeoffset, name, &tmp, sizeof(tmp));
+}
+
+/**
+ * fdt_setprop_inplace_u64 - change the value of a 64-bit integer property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: 64-bit integer value to replace the property with
+ *
+ * fdt_setprop_inplace_u64() replaces the value of a given property
+ * with the 64-bit integer value in val, converting val to big-endian
+ * if necessary. This function cannot change the size of a property,
+ * and so will only work if the property already exists and has length
+ * 8.
+ *
+ * This function will alter only the bytes in the blob which contain
+ * the given property value, and will not alter or move any other part
+ * of the tree.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, if the property's length is not equal to 8
+ * -FDT_ERR_NOTFOUND, node does not have the named property
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+static inline int fdt_setprop_inplace_u64(void *fdt, int nodeoffset,
+ const char *name, uint64_t val)
+{
+ fdt64_t tmp = cpu_to_fdt64(val);
+ return fdt_setprop_inplace(fdt, nodeoffset, name, &tmp, sizeof(tmp));
+}
+
+/**
+ * fdt_setprop_inplace_cell - change the value of a single-cell property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node containing the property
+ * @name: name of the property to change the value of
+ * @val: new value of the 32-bit cell
+ *
+ * This is an alternative name for fdt_setprop_inplace_u32()
+ * Return: 0 on success, negative libfdt error number otherwise.
+ */
+static inline int fdt_setprop_inplace_cell(void *fdt, int nodeoffset,
+ const char *name, uint32_t val)
+{
+ return fdt_setprop_inplace_u32(fdt, nodeoffset, name, val);
+}
+
+/**
+ * fdt_nop_property - replace a property with nop tags
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to nop
+ * @name: name of the property to nop
+ *
+ * fdt_nop_property() will replace a given property's representation
+ * in the blob with FDT_NOP tags, effectively removing it from the
+ * tree.
+ *
+ * This function will alter only the bytes in the blob which contain
+ * the property, and will not alter or move any other part of the
+ * tree.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOTFOUND, node does not have the named property
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_nop_property(void *fdt, int nodeoffset, const char *name);
+
+/**
+ * fdt_nop_node - replace a node (subtree) with nop tags
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node to nop
+ *
+ * fdt_nop_node() will replace a given node's representation in the
+ * blob, including all its subnodes, if any, with FDT_NOP tags,
+ * effectively removing it from the tree.
+ *
+ * This function will alter only the bytes in the blob which contain
+ * the node and its properties and subnodes, and will not alter or
+ * move any other part of the tree.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_nop_node(void *fdt, int nodeoffset);
+
+/**********************************************************************/
+/* Sequential write functions */
+/**********************************************************************/
+
+/* fdt_create_with_flags flags */
+#define FDT_CREATE_FLAG_NO_NAME_DEDUP 0x1
+ /* FDT_CREATE_FLAG_NO_NAME_DEDUP: Do not try to de-duplicate property
+ * names in the fdt. This can result in faster creation times, but
+ * a larger fdt. */
+
+#define FDT_CREATE_FLAGS_ALL (FDT_CREATE_FLAG_NO_NAME_DEDUP)
+
+/**
+ * fdt_create_with_flags - begin creation of a new fdt
+ * @buf: pointer to memory allocated where fdt will be created
+ * @bufsize: size of the memory space at fdt
+ * @flags: a valid combination of FDT_CREATE_FLAG_ flags, or 0.
+ *
+ * fdt_create_with_flags() begins the process of creating a new fdt with
+ * the sequential write interface.
+ *
+ * fdt creation process must end with fdt_finish() to produce a valid fdt.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, bufsize is insufficient for a minimal fdt
+ * -FDT_ERR_BADFLAGS, flags is not valid
+ */
+int fdt_create_with_flags(void *buf, int bufsize, uint32_t flags);
+
+/**
+ * fdt_create - begin creation of a new fdt
+ * @buf: pointer to memory allocated where fdt will be created
+ * @bufsize: size of the memory space at fdt
+ *
+ * fdt_create() is equivalent to fdt_create_with_flags() with flags=0.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, bufsize is insufficient for a minimal fdt
+ */
+int fdt_create(void *buf, int bufsize);
+
+/**
+ * fdt_resize - move and resize a device tree in sequential write state
+ * @fdt: Pointer to the device tree to resize
+ * @buf: Buffer where resized tree should be placed
+ * @bufsize: Size of the buffer at @buf
+ *
+ * fdt_resize() moves the device tree blob from @fdt to @buf and
+ * resizes it to fit in the new buffer size.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, if @bufsize is too small
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE, standard meanings
+ */
+int fdt_resize(void *fdt, void *buf, int bufsize);
+
+/**
+ * fdt_add_reservemap_entry - add an entry to the memory reserve map
+ * @fdt: Pointer to the device tree blob
+ * @addr: Start address of the reserve map entry
+ * @size: Size of the reserved region
+ *
+ * fdt_add_reservemap_entry() adds a memory reserve map entry to the
+ * device tree blob during the sequential write process. This function
+ * can only be called after fdt_create() and before fdt_finish_reservemap().
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, if there is insufficient space in the blob
+ * -FDT_ERR_BADSTATE, if not in the correct sequential write state
+ */
+int fdt_add_reservemap_entry(void *fdt, uint64_t addr, uint64_t size);
+
+/**
+ * fdt_finish_reservemap - complete the memory reserve map
+ * @fdt: Pointer to the device tree blob
+ *
+ * fdt_finish_reservemap() completes the memory reserve map section
+ * of the device tree blob during sequential write. After calling this
+ * function, no more reserve map entries can be added and the blob
+ * moves to the structure creation phase.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADSTATE, if not in the correct sequential write state
+ */
+int fdt_finish_reservemap(void *fdt);
+
+/**
+ * fdt_begin_node - start creation of a new node
+ * @fdt: Pointer to the device tree blob
+ * @name: Name of the node to create
+ *
+ * fdt_begin_node() starts the creation of a new node with the given
+ * @name during sequential write. After calling this function, properties
+ * can be added with fdt_property() and subnodes can be created with
+ * additional fdt_begin_node() calls. The node must be completed with
+ * fdt_end_node().
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, if there is insufficient space in the blob
+ * -FDT_ERR_BADSTATE, if not in the correct sequential write state
+ */
+int fdt_begin_node(void *fdt, const char *name);
+
+/**
+ * fdt_property - add a property to the current node
+ * @fdt: Pointer to the device tree blob
+ * @name: Name of the property to add
+ * @val: Pointer to the property value
+ * @len: Length of the property value in bytes
+ *
+ * fdt_property() adds a property with the given @name and value to
+ * the current node during sequential write. This function can only
+ * be called between fdt_begin_node() and fdt_end_node().
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, if there is insufficient space in the blob
+ * -FDT_ERR_BADSTATE, if not currently within a node
+ */
+int fdt_property(void *fdt, const char *name, const void *val, int len);
+static inline int fdt_property_u32(void *fdt, const char *name, uint32_t val)
+{
+ fdt32_t tmp = cpu_to_fdt32(val);
+ return fdt_property(fdt, name, &tmp, sizeof(tmp));
+}
+static inline int fdt_property_u64(void *fdt, const char *name, uint64_t val)
+{
+ fdt64_t tmp = cpu_to_fdt64(val);
+ return fdt_property(fdt, name, &tmp, sizeof(tmp));
+}
+
+#ifndef SWIG /* Not available in Python */
+static inline int fdt_property_cell(void *fdt, const char *name, uint32_t val)
+{
+ return fdt_property_u32(fdt, name, val);
+}
+#endif
+
+/**
+ * fdt_property_placeholder - add a new property and return a ptr to its value
+ *
+ * @fdt: pointer to the device tree blob
+ * @name: name of property to add
+ * @len: length of property value in bytes
+ * @valp: returns a pointer to where the value should be placed
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_NOSPACE, standard meanings
+ */
+int fdt_property_placeholder(void *fdt, const char *name, int len, void **valp);
+
+#define fdt_property_string(fdt, name, str) \
+ fdt_property(fdt, name, str, strlen(str)+1)
+
+/**
+ * fdt_end_node - complete the current node
+ * @fdt: Pointer to the device tree blob
+ *
+ * fdt_end_node() completes the current node during sequential write. This
+ * function must be called to close each node started with
+ * fdt_begin_node(). After calling this function, no more properties or subnodes
+ * can be added to the node.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADSTATE, if not currently within a node
+ */
+int fdt_end_node(void *fdt);
+
+/**
+ * fdt_finish - complete device tree creation
+ * @fdt: Pointer to the device tree blob
+ *
+ * fdt_finish() completes the device tree creation process started with
+ * fdt_create(). This function finalizes the device tree blob and makes it ready
+ * for use. After calling this function, the blob is complete and can be used
+ * with libfdt read-only and read-write functions, but not with sequential write
+ * functions.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADSTATE, if the sequential write process is incomplete
+ */
+int fdt_finish(void *fdt);
+
+/**********************************************************************/
+/* Read-write functions */
+/**********************************************************************/
+
+/**
+ * fdt_create_empty_tree - create an empty device tree
+ * @buf: Buffer where the empty tree should be created
+ * @bufsize: Size of the buffer at @buf
+ *
+ * fdt_create_empty_tree() creates a minimal empty device tree blob
+ * in the given buffer. The tree contains only a root node with no
+ * properties or subnodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, if @bufsize is too small for even an empty tree
+ */
+int fdt_create_empty_tree(void *buf, int bufsize);
+
+/**
+ * fdt_open_into - move a device tree into a new buffer and make editable
+ * @fdt: Pointer to the device tree to move
+ * @buf: Buffer where the editable tree should be placed
+ * @bufsize: Size of the buffer at @buf
+ *
+ * fdt_open_into() moves and reorganizes the device tree blob from @fdt
+ * into @buf, converting it to a format suitable for read-write operations.
+ * The new buffer should allow space for modifications.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, if @bufsize is too small
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_open_into(const void *fdt, void *buf, int bufsize);
+
+/**
+ * fdt_pack - pack a device tree blob
+ * @fdt: Pointer to the device tree blob
+ *
+ * fdt_pack() reorganizes the device tree blob to eliminate any free space
+ * and pack it into the minimum possible size. This is useful after making
+ * modifications that might have left gaps in the blob.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT, standard meanings
+ */
+int fdt_pack(void *fdt);
+
+/**
+ * fdt_add_mem_rsv - add one memory reserve map entry
+ * @fdt: pointer to the device tree blob
+ * @address: 64-bit start address of the reserve map entry
+ * @size: 64-bit size of the reserved region
+ *
+ * Adds a reserve map entry to the given blob reserving a region at
+ * address address of length size.
+ *
+ * This function will insert data into the reserve map and will
+ * therefore change the indexes of some entries in the table.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new reservation entry
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_add_mem_rsv(void *fdt, uint64_t address, uint64_t size);
+
+/**
+ * fdt_del_mem_rsv - remove a memory reserve map entry
+ * @fdt: pointer to the device tree blob
+ * @n: entry to remove
+ *
+ * fdt_del_mem_rsv() removes the n-th memory reserve map entry from
+ * the blob.
+ *
+ * This function will delete data from the reservation table and will
+ * therefore change the indexes of some entries in the table.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOTFOUND, there is no entry of the given index (i.e. there
+ * are less than n+1 reserve map entries)
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_del_mem_rsv(void *fdt, int n);
+
+/**
+ * fdt_set_name - change the name of a given node
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: structure block offset of a node
+ * @name: name to give the node
+ *
+ * fdt_set_name() replaces the name (including unit address, if any)
+ * of the given node with the given string. NOTE: this function can't
+ * efficiently check if the new name is unique amongst the given
+ * node's siblings; results are undefined if this function is invoked
+ * with a name equal to one of the given node's siblings.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob
+ * to contain the new name
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE, standard meanings
+ */
+int fdt_set_name(void *fdt, int nodeoffset, const char *name);
+
+/**
+ * fdt_setprop_namelen - create or change a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @namelen: length of the name
+ * @val: pointer to data to set the property value to
+ * @len: length of the property value
+ *
+ * fdt_setprop_namelen() sets the value of the named property in the given
+ * node to the given value and length, creating the property if it
+ * does not already exist.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_setprop_namelen(void *fdt, int nodeoffset, const char *name,
+ int namelen, const void *val, int len);
+
+/**
+ * fdt_setprop - create or change a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: pointer to data to set the property value to
+ * @len: length of the property value
+ *
+ * fdt_setprop() sets the value of the named property in the given
+ * node to the given value and length, creating the property if it
+ * does not already exist.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+static inline int fdt_setprop(void *fdt, int nodeoffset, const char *name,
+ const void *val, int len)
+{
+ return fdt_setprop_namelen(fdt, nodeoffset, name, strlen(name), val,
+ len);
+}
+
+/**
+ * fdt_setprop_placeholder_namelen - allocate space for a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @namelen: length of the name
+ * @len: length of the property value
+ * @prop_data: return pointer to property data
+ *
+ * fdt_setprop_placeholder_namelen() allocates the named property in the given node.
+ * If the property exists it is resized. In either case a pointer to the
+ * property data is returned.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_setprop_placeholder_namelen(void *fdt, int nodeoffset, const char *name,
+ int namelen, int len, void **prop_data);
+
+/**
+ * fdt_setprop_placeholder - allocate space for a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @len: length of the property value
+ * @prop_data: return pointer to property data
+ *
+ * fdt_setprop_placeholder() allocates the named property in the given node.
+ * If the property exists it is resized. In either case a pointer to the
+ * property data is returned.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+static inline int fdt_setprop_placeholder(void *fdt, int nodeoffset,
+ const char *name, int len,
+ void **prop_data)
+{
+ return fdt_setprop_placeholder_namelen(fdt, nodeoffset, name,
+ strlen(name), len, prop_data);
+}
+
+/**
+ * fdt_setprop_u32 - set a property to a 32-bit integer
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: 32-bit integer value for the property (native endian)
+ *
+ * fdt_setprop_u32() sets the value of the named property in the given
+ * node to the given 32-bit integer value (converting to big-endian if
+ * necessary), or creates a new property with that value if it does
+ * not already exist.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+static inline int fdt_setprop_u32(void *fdt, int nodeoffset, const char *name,
+ uint32_t val)
+{
+ fdt32_t tmp = cpu_to_fdt32(val);
+ return fdt_setprop(fdt, nodeoffset, name, &tmp, sizeof(tmp));
+}
+
+/**
+ * fdt_setprop_u64 - set a property to a 64-bit integer
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: 64-bit integer value for the property (native endian)
+ *
+ * fdt_setprop_u64() sets the value of the named property in the given
+ * node to the given 64-bit integer value (converting to big-endian if
+ * necessary), or creates a new property with that value if it does
+ * not already exist.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+static inline int fdt_setprop_u64(void *fdt, int nodeoffset, const char *name,
+ uint64_t val)
+{
+ fdt64_t tmp = cpu_to_fdt64(val);
+ return fdt_setprop(fdt, nodeoffset, name, &tmp, sizeof(tmp));
+}
+
+/**
+ * fdt_setprop_cell - set a property to a single cell value
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: 32-bit integer value for the property (native endian)
+ *
+ * This is an alternative name for fdt_setprop_u32()
+ *
+ * Return: 0 on success, negative libfdt error value otherwise.
+ */
+static inline int fdt_setprop_cell(void *fdt, int nodeoffset, const char *name,
+ uint32_t val)
+{
+ return fdt_setprop_u32(fdt, nodeoffset, name, val);
+}
+
+/**
+ * fdt_setprop_string - set a property to a string value
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @str: string value for the property
+ *
+ * fdt_setprop_string() sets the value of the named property in the
+ * given node to the given string value (using the length of the
+ * string to determine the new length of the property), or creates a
+ * new property with that value if it does not already exist.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+#define fdt_setprop_string(fdt, nodeoffset, name, str) \
+ fdt_setprop((fdt), (nodeoffset), (name), (str), strlen(str)+1)
+
+/**
+ * fdt_setprop_namelen_string - set a property to a string value
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @namelen: number of characters of name to consider
+ * @str: string value for the property
+ *
+ * fdt_setprop_namelen_string() sets the value of the named property in the
+ * given node to the given string value (using the length of the
+ * string to determine the new length of the property), or creates a
+ * new property with that value if it does not already exist.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+#define fdt_setprop_namelen_string(fdt, nodeoffset, name, namelen, str) \
+ fdt_setprop_namelen((fdt), (nodeoffset), (name), (namelen), (str), \
+ strlen(str) + 1)
+
+/**
+ * fdt_setprop_empty - set a property to an empty value
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ *
+ * fdt_setprop_empty() sets the value of the named property in the
+ * given node to an empty (zero length) value, or creates a new empty
+ * property if it does not already exist.
+ *
+ * This function may insert or delete data from the blob, and will
+ * therefore change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+#define fdt_setprop_empty(fdt, nodeoffset, name) \
+ fdt_setprop((fdt), (nodeoffset), (name), NULL, 0)
+
+/**
+ * fdt_appendprop - append to or create a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to append to
+ * @val: pointer to data to append to the property value
+ * @len: length of the data to append to the property value
+ *
+ * fdt_appendprop() appends the value to the named property in the
+ * given node, creating the property if it does not already exist.
+ *
+ * This function may insert data into the blob, and will therefore
+ * change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_appendprop(void *fdt, int nodeoffset, const char *name,
+ const void *val, int len);
+
+/**
+ * fdt_appendprop_u32 - append a 32-bit integer value to a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: 32-bit integer value to append to the property (native endian)
+ *
+ * fdt_appendprop_u32() appends the given 32-bit integer value
+ * (converting to big-endian if necessary) to the value of the named
+ * property in the given node, or creates a new property with that
+ * value if it does not already exist.
+ *
+ * This function may insert data into the blob, and will therefore
+ * change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+static inline int fdt_appendprop_u32(void *fdt, int nodeoffset,
+ const char *name, uint32_t val)
+{
+ fdt32_t tmp = cpu_to_fdt32(val);
+ return fdt_appendprop(fdt, nodeoffset, name, &tmp, sizeof(tmp));
+}
+
+/**
+ * fdt_appendprop_u64 - append a 64-bit integer value to a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: 64-bit integer value to append to the property (native endian)
+ *
+ * fdt_appendprop_u64() appends the given 64-bit integer value
+ * (converting to big-endian if necessary) to the value of the named
+ * property in the given node, or creates a new property with that
+ * value if it does not already exist.
+ *
+ * This function may insert data into the blob, and will therefore
+ * change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+static inline int fdt_appendprop_u64(void *fdt, int nodeoffset,
+ const char *name, uint64_t val)
+{
+ fdt64_t tmp = cpu_to_fdt64(val);
+ return fdt_appendprop(fdt, nodeoffset, name, &tmp, sizeof(tmp));
+}
+
+/**
+ * fdt_appendprop_cell - append a single cell value to a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @val: 32-bit integer value to append to the property (native endian)
+ *
+ * This is an alternative name for fdt_appendprop_u32()
+ *
+ * Return: 0 on success, negative libfdt error value otherwise.
+ */
+static inline int fdt_appendprop_cell(void *fdt, int nodeoffset,
+ const char *name, uint32_t val)
+{
+ return fdt_appendprop_u32(fdt, nodeoffset, name, val);
+}
+
+/**
+ * fdt_appendprop_string - append a string to a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to change
+ * @name: name of the property to change
+ * @str: string value to append to the property
+ *
+ * fdt_appendprop_string() appends the given string to the value of
+ * the named property in the given node, or creates a new property
+ * with that value if it does not already exist.
+ *
+ * This function may insert data into the blob, and will therefore
+ * change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain the new property value
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+#define fdt_appendprop_string(fdt, nodeoffset, name, str) \
+ fdt_appendprop((fdt), (nodeoffset), (name), (str), strlen(str)+1)
+
+/**
+ * fdt_appendprop_addrrange - append a address range property
+ * @fdt: pointer to the device tree blob
+ * @parent: offset of the parent node
+ * @nodeoffset: offset of the node to add a property at
+ * @name: name of property
+ * @addr: start address of a given range
+ * @size: size of a given range
+ *
+ * fdt_appendprop_addrrange() appends an address range value (start
+ * address and size) to the value of the named property in the given
+ * node, or creates a new property with that value if it does not
+ * already exist.
+ *
+ * Cell sizes are determined by parent's #address-cells and #size-cells.
+ *
+ * This function may insert data into the blob, and will therefore
+ * change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADNCELLS, if the node has a badly formatted or invalid
+ * #address-cells property
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADVALUE, addr or size doesn't fit to respective cells size
+ * -FDT_ERR_NOSPACE, there is insufficient free space in the blob to
+ * contain a new property
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_appendprop_addrrange(void *fdt, int parent, int nodeoffset,
+ const char *name, uint64_t addr, uint64_t size);
+
+/**
+ * fdt_delprop - delete a property
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node whose property to nop
+ * @name: name of the property to nop
+ *
+ * fdt_delprop() will delete the given property.
+ *
+ * This function will delete data from the blob, and will therefore
+ * change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOTFOUND, node does not have the named property
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_delprop(void *fdt, int nodeoffset, const char *name);
+
+/**
+ * fdt_add_subnode_namelen - creates a new node based on substring
+ * @fdt: pointer to the device tree blob
+ * @parentoffset: structure block offset of a node
+ * @name: name of the subnode to create
+ * @namelen: number of characters of name to consider
+ *
+ * Identical to fdt_add_subnode(), but use only the first @namelen
+ * characters of @name as the name of the new node. This is useful for
+ * creating subnodes based on a portion of a larger string, such as a
+ * full path.
+ *
+ * Return: structure block offset of the created subnode (>=0),
+ * negative libfdt error value otherwise
+ */
+#ifndef SWIG /* Not available in Python */
+int fdt_add_subnode_namelen(void *fdt, int parentoffset,
+ const char *name, int namelen);
+#endif
+
+/**
+ * fdt_add_subnode - creates a new node
+ * @fdt: pointer to the device tree blob
+ * @parentoffset: structure block offset of a node
+ * @name: name of the subnode to locate
+ *
+ * fdt_add_subnode() creates a new node as a subnode of the node at
+ * structure block offset parentoffset, with the given name (which
+ * should include the unit address, if any).
+ *
+ * This function will insert data into the blob, and will therefore
+ * change the offsets of some existing nodes.
+ *
+ * returns:
+ * structure block offset of the created subnode (>=0), on success
+ * -FDT_ERR_NOTFOUND, if the requested subnode does not exist
+ * -FDT_ERR_BADOFFSET, if parentoffset did not point to an FDT_BEGIN_NODE
+ * tag
+ * -FDT_ERR_EXISTS, if the node at parentoffset already has a subnode of
+ * the given name
+ * -FDT_ERR_NOSPACE, if there is insufficient free space in the
+ * blob to contain the new node
+ * -FDT_ERR_NOSPACE
+ * -FDT_ERR_BADLAYOUT
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings.
+ */
+int fdt_add_subnode(void *fdt, int parentoffset, const char *name);
+
+/**
+ * fdt_del_node - delete a node (subtree)
+ * @fdt: pointer to the device tree blob
+ * @nodeoffset: offset of the node to nop
+ *
+ * fdt_del_node() will remove the given node, including all its
+ * subnodes if any, from the blob.
+ *
+ * This function will delete data from the blob, and will therefore
+ * change the offsets of some existing nodes.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_BADOFFSET, nodeoffset did not point to FDT_BEGIN_NODE tag
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_del_node(void *fdt, int nodeoffset);
+
+/**
+ * fdt_overlay_apply - Applies a DT overlay on a base DT
+ * @fdt: pointer to the base device tree blob
+ * @fdto: pointer to the device tree overlay blob
+ *
+ * fdt_overlay_apply() will apply the given device tree overlay on the
+ * given base device tree.
+ *
+ * Expect the base device tree to be modified, even if the function
+ * returns an error.
+ *
+ * returns:
+ * 0, on success
+ * -FDT_ERR_NOSPACE, there's not enough space in the base device tree
+ * -FDT_ERR_NOTFOUND, the overlay points to some nonexistent nodes or
+ * properties in the base DT
+ * -FDT_ERR_BADPHANDLE,
+ * -FDT_ERR_BADOVERLAY,
+ * -FDT_ERR_NOPHANDLES,
+ * -FDT_ERR_INTERNAL,
+ * -FDT_ERR_BADLAYOUT,
+ * -FDT_ERR_BADMAGIC,
+ * -FDT_ERR_BADOFFSET,
+ * -FDT_ERR_BADPATH,
+ * -FDT_ERR_BADVERSION,
+ * -FDT_ERR_BADSTRUCTURE,
+ * -FDT_ERR_BADSTATE,
+ * -FDT_ERR_TRUNCATED, standard meanings
+ */
+int fdt_overlay_apply(void *fdt, void *fdto);
+
+/**
+ * fdt_overlay_target_offset - retrieves the offset of a fragment's target
+ * @fdt: Base device tree blob
+ * @fdto: Device tree overlay blob
+ * @fragment_offset: node offset of the fragment in the overlay
+ * @pathp: pointer which receives the path of the target (or NULL)
+ *
+ * fdt_overlay_target_offset() retrieves the target offset in the base
+ * device tree of a fragment, no matter how the actual targeting is
+ * done (through a phandle or a path)
+ *
+ * returns:
+ * the targeted node offset in the base device tree
+ * Negative error code on error
+ */
+int fdt_overlay_target_offset(const void *fdt, const void *fdto,
+ int fragment_offset, char const **pathp);
+
+/**********************************************************************/
+/* Debugging / informational functions */
+/**********************************************************************/
+
+/**
+ * fdt_strerror - return string description of error code
+ * @errval: Error code returned by a libfdt function
+ *
+ * fdt_strerror() returns a string description of the error code passed
+ * in @errval.
+ *
+ * returns:
+ * pointer to a string describing the error code
+ */
+const char *fdt_strerror(int errval);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* LIBFDT_H */
diff --git a/na/scripts/dtc/libfdt/libfdt_env.h b/na/scripts/dtc/libfdt/libfdt_env.h
new file mode 100644
index 0000000..5580b48
--- /dev/null
+++ b/na/scripts/dtc/libfdt/libfdt_env.h
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause) */
+#ifndef LIBFDT_ENV_H
+#define LIBFDT_ENV_H
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ * Copyright 2012 Kim Phillips, Freescale Semiconductor.
+ */
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+
+#ifdef __CHECKER__
+#define FDT_FORCE __attribute__((force))
+#define FDT_BITWISE __attribute__((bitwise))
+#else
+#define FDT_FORCE
+#define FDT_BITWISE
+#endif
+
+typedef uint16_t FDT_BITWISE fdt16_t;
+typedef uint32_t FDT_BITWISE fdt32_t;
+typedef uint64_t FDT_BITWISE fdt64_t;
+
+#define EXTRACT_BYTE(x, n) ((unsigned long long)((uint8_t *)&x)[n])
+#define CPU_TO_FDT16(x) ((EXTRACT_BYTE(x, 0) << 8) | EXTRACT_BYTE(x, 1))
+#define CPU_TO_FDT32(x) ((EXTRACT_BYTE(x, 0) << 24) | (EXTRACT_BYTE(x, 1) << 16) | \
+ (EXTRACT_BYTE(x, 2) << 8) | EXTRACT_BYTE(x, 3))
+#define CPU_TO_FDT64(x) ((EXTRACT_BYTE(x, 0) << 56) | (EXTRACT_BYTE(x, 1) << 48) | \
+ (EXTRACT_BYTE(x, 2) << 40) | (EXTRACT_BYTE(x, 3) << 32) | \
+ (EXTRACT_BYTE(x, 4) << 24) | (EXTRACT_BYTE(x, 5) << 16) | \
+ (EXTRACT_BYTE(x, 6) << 8) | EXTRACT_BYTE(x, 7))
+
+static inline uint16_t fdt16_to_cpu(fdt16_t x)
+{
+ return (FDT_FORCE uint16_t)CPU_TO_FDT16(x);
+}
+static inline fdt16_t cpu_to_fdt16(uint16_t x)
+{
+ return (FDT_FORCE fdt16_t)CPU_TO_FDT16(x);
+}
+
+static inline uint32_t fdt32_to_cpu(fdt32_t x)
+{
+ return (FDT_FORCE uint32_t)CPU_TO_FDT32(x);
+}
+static inline fdt32_t cpu_to_fdt32(uint32_t x)
+{
+ return (FDT_FORCE fdt32_t)CPU_TO_FDT32(x);
+}
+
+static inline uint64_t fdt64_to_cpu(fdt64_t x)
+{
+ return (FDT_FORCE uint64_t)CPU_TO_FDT64(x);
+}
+static inline fdt64_t cpu_to_fdt64(uint64_t x)
+{
+ return (FDT_FORCE fdt64_t)CPU_TO_FDT64(x);
+}
+#undef CPU_TO_FDT64
+#undef CPU_TO_FDT32
+#undef CPU_TO_FDT16
+#undef EXTRACT_BYTE
+
+#endif /* LIBFDT_ENV_H */
diff --git a/na/scripts/dtc/libfdt/libfdt_internal.h b/na/scripts/dtc/libfdt/libfdt_internal.h
new file mode 100644
index 0000000..0e103ca
--- /dev/null
+++ b/na/scripts/dtc/libfdt/libfdt_internal.h
@@ -0,0 +1,202 @@
+/* SPDX-License-Identifier: (GPL-2.0-or-later OR BSD-2-Clause) */
+#ifndef LIBFDT_INTERNAL_H
+#define LIBFDT_INTERNAL_H
+/*
+ * libfdt - Flat Device Tree manipulation
+ * Copyright (C) 2006 David Gibson, IBM Corporation.
+ */
+#include <fdt.h>
+
+#define FDT_ALIGN(x, a) (((x) + (a) - 1) & ~((a) - 1))
+#define FDT_TAGALIGN(x) (FDT_ALIGN((x), FDT_TAGSIZE))
+
+int32_t fdt_ro_probe_(const void *fdt);
+#define FDT_RO_PROBE(fdt) \
+ { \
+ if (!can_assume(VALID_DTB)) { \
+ int32_t totalsize_; \
+ if ((totalsize_ = fdt_ro_probe_(fdt)) < 0) \
+ return totalsize_; \
+ } \
+ }
+
+int fdt_check_node_offset_(const void *fdt, int offset);
+int fdt_check_prop_offset_(const void *fdt, int offset);
+
+const char *fdt_find_string_len_(const char *strtab, int tabsize, const char *s,
+ int s_len);
+static inline const char *fdt_find_string_(const char *strtab, int tabsize,
+ const char *s)
+{
+ return fdt_find_string_len_(strtab, tabsize, s, strlen(s));
+}
+
+int fdt_node_end_offset_(void *fdt, int nodeoffset);
+
+static inline const void *fdt_offset_ptr_(const void *fdt, int offset)
+{
+ return (const char *)fdt + fdt_off_dt_struct(fdt) + offset;
+}
+
+static inline void *fdt_offset_ptr_w_(void *fdt, int offset)
+{
+ return (void *)(uintptr_t)fdt_offset_ptr_(fdt, offset);
+}
+
+static inline const struct fdt_reserve_entry *fdt_mem_rsv_(const void *fdt, int n)
+{
+ const struct fdt_reserve_entry *rsv_table =
+ (const struct fdt_reserve_entry *)
+ ((const char *)fdt + fdt_off_mem_rsvmap(fdt));
+
+ return rsv_table + n;
+}
+static inline struct fdt_reserve_entry *fdt_mem_rsv_w_(void *fdt, int n)
+{
+ return (void *)(uintptr_t)fdt_mem_rsv_(fdt, n);
+}
+
+/*
+ * Internal helpers to access structural elements of the device tree
+ * blob (rather than for example reading integers from within property
+ * values). We assume that we are either given a naturally aligned
+ * address for the platform or if we are not, we are on a platform
+ * where unaligned memory reads will be handled in a graceful manner.
+ * If not the external helpers fdtXX_ld() from libfdt.h can be used
+ * instead.
+ */
+static inline uint32_t fdt32_ld_(const fdt32_t *p)
+{
+ return fdt32_to_cpu(*p);
+}
+
+static inline uint64_t fdt64_ld_(const fdt64_t *p)
+{
+ return fdt64_to_cpu(*p);
+}
+
+#define FDT_SW_MAGIC (~FDT_MAGIC)
+
+/**********************************************************************/
+/* Checking controls */
+/**********************************************************************/
+
+#ifndef FDT_ASSUME_MASK
+#define FDT_ASSUME_MASK 0
+#endif
+
+/*
+ * Defines assumptions which can be enabled. Each of these can be enabled
+ * individually. For maximum safety, don't enable any assumptions!
+ *
+ * For minimal code size and no safety, use ASSUME_PERFECT at your own risk.
+ * You should have another method of validating the device tree, such as a
+ * signature or hash check before using libfdt.
+ *
+ * For situations where security is not a concern it may be safe to enable
+ * ASSUME_PERFECT.
+ */
+enum {
+ /*
+ * This does essentially no checks. Only the latest device-tree
+ * version is correctly handled. Inconsistencies or errors in the device
+ * tree may cause undefined behaviour or crashes. Invalid parameters
+ * passed to libfdt may do the same.
+ *
+ * If an error occurs when modifying the tree it may leave the tree in
+ * an intermediate (but valid) state. As an example, adding a property
+ * where there is insufficient space may result in the property name
+ * being added to the string table even though the property itself is
+ * not added to the struct section.
+ *
+ * Only use this if you have a fully validated device tree with
+ * the latest supported version and wish to minimise code size.
+ */
+ ASSUME_PERFECT = 0xff,
+
+ /*
+ * This assumes that the device tree is sane. i.e. header metadata
+ * and basic hierarchy are correct.
+ *
+ * With this assumption enabled, normal device trees produced by libfdt
+ * and the compiler should be handled safely. Malicious device trees and
+ * complete garbage may cause libfdt to behave badly or crash. Truncated
+ * device trees (e.g. those only partially loaded) can also cause
+ * problems.
+ *
+ * Note: Only checks that relate exclusively to the device tree itself
+ * (not the parameters passed to libfdt) are disabled by this
+ * assumption. This includes checking headers, tags and the like.
+ */
+ ASSUME_VALID_DTB = 1 << 0,
+
+ /*
+ * This builds on ASSUME_VALID_DTB and further assumes that libfdt
+ * functions are called with valid parameters, i.e. not trigger
+ * FDT_ERR_BADOFFSET or offsets that are out of bounds. It disables any
+ * extensive checking of parameters and the device tree, making various
+ * assumptions about correctness.
+ *
+ * It doesn't make sense to enable this assumption unless
+ * ASSUME_VALID_DTB is also enabled.
+ */
+ ASSUME_VALID_INPUT = 1 << 1,
+
+ /*
+ * This disables checks for device-tree version and removes all code
+ * which handles older versions.
+ *
+ * Only enable this if you know you have a device tree with the latest
+ * version.
+ */
+ ASSUME_LATEST = 1 << 2,
+
+ /*
+ * This assumes that it is OK for a failed addition to the device tree,
+ * due to lack of space or some other problem, to skip any rollback
+ * steps (such as dropping the property name from the string table).
+ * This is safe to enable in most circumstances, even though it may
+ * leave the tree in a sub-optimal state.
+ */
+ ASSUME_NO_ROLLBACK = 1 << 3,
+
+ /*
+ * This assumes that the device tree components appear in a 'convenient'
+ * order, i.e. the memory reservation block first, then the structure
+ * block and finally the string block.
+ *
+ * This order is not specified by the device-tree specification,
+ * but is expected by libfdt. The device-tree compiler always created
+ * device trees with this order.
+ *
+ * This assumption disables a check in fdt_open_into() and removes the
+ * ability to fix the problem there. This is safe if you know that the
+ * device tree is correctly ordered. See fdt_blocks_misordered_().
+ */
+ ASSUME_LIBFDT_ORDER = 1 << 4,
+
+ /*
+ * This assumes that libfdt itself does not have any internal bugs. It
+ * drops certain checks that should never be needed unless libfdt has an
+ * undiscovered bug.
+ *
+ * This can generally be considered safe to enable.
+ */
+ ASSUME_LIBFDT_FLAWLESS = 1 << 5,
+};
+
+/**
+ * can_assume_() - check if a particular assumption is enabled
+ *
+ * @mask: Mask to check (ASSUME_...)
+ * @return true if that assumption is enabled, else false
+ */
+static inline bool can_assume_(int mask)
+{
+ return FDT_ASSUME_MASK & mask;
+}
+
+/** helper macros for checking assumptions */
+#define can_assume(_assume) can_assume_(ASSUME_ ## _assume)
+
+#endif /* LIBFDT_INTERNAL_H */
diff --git a/na/scripts/extract-asm-offsets.sed b/na/scripts/extract-asm-offsets.sed
new file mode 100644
index 0000000..c97ec2c
--- /dev/null
+++ b/na/scripts/extract-asm-offsets.sed
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Extract `->SYM val COMMENT` markers emitted by inline asm in
+# asm-offsets.c into proper `#define SYM val /* COMMENT */` lines.
+# 1:1 with Linux scripts/Makefile.lib `sed-offsets`.
+#
+# Input : assembly source produced by `gcc -S asm-offsets.c` —
+# inline asm emits lines like
+# .ascii "->KERNEL_MAP_VIRT_ADDR 0 offsetof(...)"
+# Output : header fragment for include/generated/asm-offsets.h
+#
+# Step 1 unwraps the `.ascii "..."` directive so the actual payload
+# (starting with `->`) is exposed. Steps 2-4 then rewrite into a
+# proper #define.
+s:^[[:space:]]*\.ascii[[:space:]]*"\(.*\)".*:\1:
+/^->/{s:->#\(.*\):/* \1 */:;
+ s:^->\([^ ]*\) [\$#]*\([^ ]*\) \(.*\):#define \1 \2 /* \3 */:;
+ s:->::;
+ p;}
diff --git a/na/scripts/gen_initramfs.sh b/na/scripts/gen_initramfs.sh
new file mode 100755
index 0000000..7eba2fd
--- /dev/null
+++ b/na/scripts/gen_initramfs.sh
@@ -0,0 +1,250 @@
+#!/bin/sh
+# Copyright (C) Martin Schlemmer <azarah@nosferatu.za.org>
+# Copyright (C) 2006 Sam Ravnborg <sam@ravnborg.org>
+#
+# Released under the terms of the GNU GPL
+#
+# Generate a cpio packed initramfs. It uses gen_init_cpio to generate
+# the cpio archive.
+# This script assumes that gen_init_cpio is located in usr/ directory
+
+# error out on errors
+set -e
+
+usage() {
+cat << EOF
+Usage:
+$0 [-o <file>] [-l <dep_list>] [-u <uid>] [-g <gid>] {-d | <cpio_source>} ...
+ -o <file> Create initramfs file named <file> by using gen_init_cpio
+ -l <dep_list> Create dependency list named <dep_list>
+ -u <uid> User ID to map to user ID 0 (root).
+ <uid> is only meaningful if <cpio_source> is a
+ directory. "squash" forces all files to uid 0.
+ -g <gid> Group ID to map to group ID 0 (root).
+ <gid> is only meaningful if <cpio_source> is a
+ directory. "squash" forces all files to gid 0.
+ -d <date> Use date for all file mtime values
+ <cpio_source> File list or directory for cpio archive.
+ If <cpio_source> is a .cpio file it will be used
+ as direct input to initramfs.
+
+All options except -o and -l may be repeated and are interpreted
+sequentially and immediately. -u and -g states are preserved across
+<cpio_source> options so an explicit "-u 0 -g 0" is required
+to reset the root/group mapping.
+EOF
+}
+
+# awk style field access
+# $1 - field number; rest is argument string
+field() {
+ shift $1 ; echo $1
+}
+
+filetype() {
+ local argv1="$1"
+
+ # symlink test must come before file test
+ if [ -L "${argv1}" ]; then
+ echo "slink"
+ elif [ -f "${argv1}" ]; then
+ echo "file"
+ elif [ -d "${argv1}" ]; then
+ echo "dir"
+ elif [ -b "${argv1}" -o -c "${argv1}" ]; then
+ echo "nod"
+ elif [ -p "${argv1}" ]; then
+ echo "pipe"
+ elif [ -S "${argv1}" ]; then
+ echo "sock"
+ else
+ echo "invalid"
+ fi
+ return 0
+}
+
+print_mtime() {
+ local my_mtime="0"
+
+ if [ -e "$1" ]; then
+ my_mtime=$(find "$1" -printf "%T@\n" | sort -r | head -n 1)
+ fi
+
+ echo "# Last modified: ${my_mtime}" >> $cpio_list
+ echo "" >> $cpio_list
+}
+
+list_parse() {
+ if [ -z "$dep_list" -o -L "$1" ]; then
+ return
+ fi
+ echo "$1" | sed 's/:/\\:/g; s/$/ \\/' >> $dep_list
+}
+
+# for each file print a line in following format
+# <filetype> <name> <path to file> <octal mode> <uid> <gid>
+# for links, devices etc the format differs. See gen_init_cpio for details
+parse() {
+ local location="$1"
+ local name="/${location#${srcdir}}"
+ # change '//' into '/'
+ name=$(echo "$name" | sed -e 's://*:/:g')
+ local mode="$2"
+ local uid="$3"
+ local gid="$4"
+ local ftype=$(filetype "${location}")
+ # remap uid/gid to 0 if necessary
+ [ "$root_uid" = "squash" ] && uid=0 || [ "$uid" -eq "$root_uid" ] && uid=0
+ [ "$root_gid" = "squash" ] && gid=0 || [ "$gid" -eq "$root_gid" ] && gid=0
+ local str="${mode} ${uid} ${gid}"
+
+ [ "${ftype}" = "invalid" ] && return 0
+ [ "${location}" = "${srcdir}" ] && return 0
+
+ case "${ftype}" in
+ "file")
+ str="${ftype} ${name} ${location} ${str}"
+ ;;
+ "nod")
+ local dev="`LC_ALL=C ls -l "${location}"`"
+ local maj=`field 5 ${dev}`
+ local min=`field 6 ${dev}`
+ maj=${maj%,}
+
+ [ -b "${location}" ] && dev="b" || dev="c"
+
+ str="${ftype} ${name} ${str} ${dev} ${maj} ${min}"
+ ;;
+ "slink")
+ local target=`readlink "${location}"`
+ str="${ftype} ${name} ${target} ${str}"
+ ;;
+ *)
+ str="${ftype} ${name} ${str}"
+ ;;
+ esac
+
+ echo "${str}" >> $cpio_list
+
+ return 0
+}
+
+unknown_option() {
+ printf "ERROR: unknown option \"$arg\"\n" >&2
+ printf "If the filename validly begins with '-', " >&2
+ printf "then it must be prefixed\n" >&2
+ printf "by './' so that it won't be interpreted as an option." >&2
+ printf "\n" >&2
+ usage >&2
+ exit 1
+}
+
+header() {
+ printf "\n#####################\n# $1\n" >> $cpio_list
+}
+
+# process one directory (incl sub-directories)
+dir_filelist() {
+ header "$1"
+
+ srcdir=$(echo "$1" | sed -e 's://*:/:g')
+ dirlist=$(find "${srcdir}" -printf "%p %m %U %G\n" | LC_ALL=C sort)
+
+ # If $dirlist is only one line, then the directory is empty
+ if [ "$(echo "${dirlist}" | wc -l)" -gt 1 ]; then
+ print_mtime "$1"
+
+ echo "${dirlist}" | \
+ while read x; do
+ list_parse $x
+ parse $x
+ done
+ fi
+}
+
+input_file() {
+ source="$1"
+ if [ -f "$1" ]; then
+ # If a regular file is specified, assume it is in
+ # gen_init_cpio format
+ header "$1"
+ print_mtime "$1" >> $cpio_list
+ cat "$1" >> $cpio_list
+ if [ -n "$dep_list" ]; then
+ echo "$1 \\" >> $dep_list
+ cat "$1" | while read type dir file perm ; do
+ if [ "$type" = "file" ]; then
+ echo "$file \\" >> $dep_list
+ fi
+ done
+ fi
+ elif [ -d "$1" ]; then
+ # If a directory is specified then add all files in it to fs
+ dir_filelist "$1"
+ else
+ echo " ${prog}: Cannot open '$1'" >&2
+ exit 1
+ fi
+}
+
+prog=$0
+root_uid=0
+root_gid=0
+dep_list=
+timestamp=
+cpio_list=$(mktemp ${TMPDIR:-/tmp}/cpiolist.XXXXXX)
+# gen_init_cpio writes to stdout by default
+output=""
+
+trap "rm -f $cpio_list" EXIT
+
+while [ $# -gt 0 ]; do
+ arg="$1"
+ shift
+ case "$arg" in
+ "-l") # files included in initramfs - used by kbuild
+ dep_list="$1"
+ echo "deps_initramfs := \\" > $dep_list
+ shift
+ ;;
+ "-o") # generate cpio image named $1
+ output="-o $1"
+ shift
+ ;;
+ "-u") # map $1 to uid=0 (root)
+ root_uid="$1"
+ [ "$root_uid" = "-1" ] && root_uid=$(id -u || echo 0)
+ shift
+ ;;
+ "-g") # map $1 to gid=0 (root)
+ root_gid="$1"
+ [ "$root_gid" = "-1" ] && root_gid=$(id -g || echo 0)
+ shift
+ ;;
+ "-d") # date for file mtimes
+ timestamp="$(date -d"$1" +%s || :)"
+ if test -n "$timestamp"; then
+ timestamp="-t $timestamp"
+ fi
+ shift
+ ;;
+ "-h")
+ usage
+ exit 0
+ ;;
+ *)
+ case "$arg" in
+ "-"*)
+ unknown_option
+ ;;
+ *) # input file/dir - process it
+ input_file "$arg"
+ ;;
+ esac
+ ;;
+ esac
+done
+
+# If output_file is set we will generate cpio archive
+# we are careful to delete tmp files
+usr/gen_init_cpio $output $timestamp $cpio_list
diff --git a/na/scripts/gen_reloc.py b/na/scripts/gen_reloc.py
deleted file mode 100644
index 6709e17..0000000
--- a/na/scripts/gen_reloc.py
+++ /dev/null
@@ -1,224 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# Copyright (C) 2026 Richard Qin
-
-import argparse
-import struct
-import sys
-
-ELF_MAGIC = b"\x7fELF"
-ELFCLASS64 = 2
-ELFDATA2LSB = 1
-EM_RISCV = 243
-SHT_SYMTAB = 2
-SHT_RELA = 4
-PT_LOAD = 1
-
-R_RISCV_64 = 2
-R_RISCV_RELATIVE = 3
-
-RELOC_MAGIC = 0x4C554D45524C4F43 # "LUMERLOC"
-RELOC_VERSION = 1
-RELOC_ARCH_RISCV = 1
-
-
-def read_struct(fmt, data, offset):
- size = struct.calcsize(fmt)
- return struct.unpack_from(fmt, data, offset), size
-
-
-def parse_elf_header(data):
- if data[:4] != ELF_MAGIC:
- raise ValueError("not an ELF file")
- ei_class = data[4]
- ei_data = data[5]
- if ei_class != ELFCLASS64 or ei_data != ELFDATA2LSB:
- raise ValueError("unsupported ELF class/data")
- fmt = "<16sHHIQQQIHHHHHH"
- (e_ident, e_type, e_machine, e_version, e_entry, e_phoff, e_shoff,
- e_flags, e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum,
- e_shstrndx), _ = read_struct(fmt, data, 0)
- return {
- "e_machine": e_machine,
- "e_phoff": e_phoff,
- "e_phentsize": e_phentsize,
- "e_phnum": e_phnum,
- "e_shoff": e_shoff,
- "e_shentsize": e_shentsize,
- "e_shnum": e_shnum,
- "e_shstrndx": e_shstrndx,
- }
-
-
-def parse_program_headers(data, ehdr):
- phdrs = []
- fmt = "<IIQQQQQQ"
- for i in range(ehdr["e_phnum"]):
- off = ehdr["e_phoff"] + i * ehdr["e_phentsize"]
- (p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz,
- p_align), _ = read_struct(fmt, data, off)
- phdrs.append({
- "p_type": p_type,
- "p_offset": p_offset,
- "p_vaddr": p_vaddr,
- "p_filesz": p_filesz,
- "p_memsz": p_memsz,
- })
- return phdrs
-
-
-def parse_section_headers(data, ehdr):
- shdrs = []
- fmt = "<IIQQQQIIQQ"
- for i in range(ehdr["e_shnum"]):
- off = ehdr["e_shoff"] + i * ehdr["e_shentsize"]
- (sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size,
- sh_link, sh_info, sh_addralign, sh_entsize), _ = read_struct(fmt, data, off)
- shdrs.append({
- "sh_name": sh_name,
- "sh_type": sh_type,
- "sh_addr": sh_addr,
- "sh_offset": sh_offset,
- "sh_size": sh_size,
- "sh_link": sh_link,
- "sh_entsize": sh_entsize,
- })
- return shdrs
-
-
-def get_strtab(data, shdrs, idx):
- sh = shdrs[idx]
- return data[sh["sh_offset"]:sh["sh_offset"] + sh["sh_size"]]
-
-
-def get_section_name(strtab, sh_name):
- end = strtab.find(b"\x00", sh_name)
- if end < 0:
- return ""
- return strtab[sh_name:end].decode("ascii", errors="ignore")
-
-
-def find_symbol(data, shdrs, name):
- for i, sh in enumerate(shdrs):
- if sh["sh_type"] != SHT_SYMTAB:
- continue
- strtab = get_strtab(data, shdrs, sh["sh_link"])
- entsize = sh["sh_entsize"] or 24
- count = sh["sh_size"] // entsize
- for j in range(count):
- off = sh["sh_offset"] + j * entsize
- (st_name, st_info, st_other, st_shndx, st_value, st_size), _ = read_struct(
- "<IBBHQQ", data, off
- )
- sym_name = get_section_name(strtab, st_name)
- if sym_name == name:
- return st_value
- return None
-
-
-def read_symtab(data, shdrs, idx):
- sh = shdrs[idx]
- entsize = sh["sh_entsize"] or 24
- count = sh["sh_size"] // entsize
- syms = []
- for j in range(count):
- off = sh["sh_offset"] + j * entsize
- (st_name, st_info, st_other, st_shndx, st_value, st_size), _ = read_struct(
- "<IBBHQQ", data, off
- )
- syms.append((st_value, st_size, st_info, st_shndx))
- return syms
-
-
-def vaddr_in_load(vaddr, phdrs, size):
- for ph in phdrs:
- if ph["p_type"] != PT_LOAD:
- continue
- start = ph["p_vaddr"]
- end = ph["p_vaddr"] + ph["p_memsz"]
- if vaddr >= start and vaddr + size <= end:
- return True
- return False
-
-
-def main():
- ap = argparse.ArgumentParser()
- ap.add_argument("--elf", required=True)
- ap.add_argument("--out", required=True)
- args = ap.parse_args()
-
- data = open(args.elf, "rb").read()
- ehdr = parse_elf_header(data)
- if ehdr["e_machine"] != EM_RISCV:
- raise ValueError("only EM_RISCV supported")
-
- phdrs = parse_program_headers(data, ehdr)
- shdrs = parse_section_headers(data, ehdr)
-
- link_base = find_symbol(data, shdrs, "_start")
- if link_base is None:
- raise ValueError("_start symbol not found")
-
- image_end = find_symbol(data, shdrs, "__lume_image_end")
- if image_end is None:
- raise ValueError("__lume_image_end symbol not found")
- image_size = image_end - link_base
-
- entries = []
- for sh in shdrs:
- if sh["sh_type"] != SHT_RELA:
- continue
- if sh["sh_link"] >= len(shdrs):
- continue
- symtab = read_symtab(data, shdrs, sh["sh_link"])
- entsize = sh["sh_entsize"] or 24
- count = sh["sh_size"] // entsize
- for i in range(count):
- off = sh["sh_offset"] + i * entsize
- (r_offset, r_info, r_addend), _ = read_struct("<QQq", data, off)
- r_type = r_info & 0xFFFFFFFF
- r_sym = r_info >> 32
- if r_type not in (R_RISCV_RELATIVE, R_RISCV_64):
- continue
- if r_offset < link_base or r_offset >= link_base + image_size:
- continue
- if not vaddr_in_load(r_offset, phdrs, 8):
- continue
- ent_offset = r_offset - link_base
- if r_type == R_RISCV_RELATIVE:
- ent_addend = r_addend
- ent_type = 1
- else:
- sym_val = 0
- if r_sym < len(symtab):
- sym_val = symtab[r_sym][0]
- ent_addend = sym_val + r_addend
- if ent_addend < link_base or ent_addend >= link_base + image_size:
- continue
- ent_type = 2
- entries.append((ent_offset, ent_addend, ent_type))
-
- header = struct.pack(
- "<QHHIII",
- RELOC_MAGIC,
- RELOC_VERSION,
- RELOC_ARCH_RISCV,
- 24,
- len(entries),
- 0,
- )
-
- with open(args.out, "wb") as f:
- f.write(header)
- for off, add, typ in entries:
- f.write(struct.pack("<QQII", off, add, typ, 0))
-
- return 0
-
-
-if __name__ == "__main__":
- try:
- sys.exit(main())
- except Exception as e:
- print("gen_reloc: " + str(e), file=sys.stderr)
- sys.exit(1)
diff --git a/na/scripts/pack_image.py b/na/scripts/pack_image.py
deleted file mode 100644
index dc5c6db..0000000
--- a/na/scripts/pack_image.py
+++ /dev/null
@@ -1,227 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# Copyright (C) 2026 Richard Qin
-
-import argparse
-import struct
-import sys
-
-MAGIC = 0x4C554D45434F4D50 # "LUMECOMP"
-COMP_NONE = 0
-COMP_LZ4 = 1
-
-HDR_FMT = "<QIHHIIQQQQQQII"
-HDR_SIZE = struct.calcsize(HDR_FMT)
-HDR_OFFSET = 64
-
-
-def crc32_update(crc: int, data: bytes) -> int:
- for b in data:
- crc ^= b
- for _ in range(8):
- mask = -(crc & 1) & 0xFFFFFFFF
- crc = ((crc >> 1) ^ (0xEDB88320 & mask)) & 0xFFFFFFFF
- return crc
-
-
-def crc32_ieee(data: bytes) -> int:
- crc = 0xFFFFFFFF
- crc = crc32_update(crc, data)
- return (crc ^ 0xFFFFFFFF) & 0xFFFFFFFF
-
-
-def lz4_compress(data: bytes) -> bytes:
- n = len(data)
- if n == 0:
- return b""
-
- min_match = 4
- hash_log = 16
- hash_size = 1 << hash_log
- max_offset = 65535
-
- def hash4(b0, b1, b2, b3):
- v = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
- return (v * 2654435761) & (hash_size - 1)
-
- htab = [-1] * hash_size
- out = bytearray()
- anchor = 0
- i = 0
-
- while i + min_match <= n:
- h = hash4(data[i], data[i + 1], data[i + 2], data[i + 3])
- ref = htab[h]
- htab[h] = i
-
- if ref >= 0 and i - ref <= max_offset and data[ref:ref + min_match] == data[i:i + min_match]:
- match_len = min_match
- while i + match_len < n and data[ref + match_len] == data[i + match_len]:
- match_len += 1
-
- lit_len = i - anchor
- token = (15 if lit_len >= 15 else lit_len) << 4
- match_field = match_len - min_match
- token |= 15 if match_field >= 15 else match_field
- out.append(token)
-
- if lit_len >= 15:
- l = lit_len - 15
- while l >= 255:
- out.append(255)
- l -= 255
- out.append(l)
-
- out.extend(data[anchor:i])
-
- offset = i - ref
- out.append(offset & 0xFF)
- out.append((offset >> 8) & 0xFF)
-
- if match_field >= 15:
- l = match_field - 15
- while l >= 255:
- out.append(255)
- l -= 255
- out.append(l)
-
- i += match_len
- anchor = i
- continue
-
- i += 1
-
- lit_len = n - anchor
- token = (15 if lit_len >= 15 else lit_len) << 4
- out.append(token)
- if lit_len >= 15:
- l = lit_len - 15
- while l >= 255:
- out.append(255)
- l -= 255
- out.append(l)
- out.extend(data[anchor:])
- return bytes(out)
-
-
-def get_header_offset(data: bytes) -> int:
- if len(data) < HDR_OFFSET + HDR_SIZE:
- return -1
- if data[HDR_OFFSET:HDR_OFFSET + 8] != struct.pack("<Q", MAGIC):
- return -1
- return HDR_OFFSET
-
-
-def read_struct(fmt, data, offset):
- size = struct.calcsize(fmt)
- return struct.unpack_from(fmt, data, offset), size
-
-
-def parse_elf_symbols(path: str):
- data = open(path, "rb").read()
- if data[:4] != b"\x7fELF":
- return {}
- if data[4] != 2 or data[5] != 1:
- return {}
- (e_ident, e_type, e_machine, e_version, e_entry, e_phoff, e_shoff,
- e_flags, e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum,
- e_shstrndx), _ = read_struct("<16sHHIQQQIHHHHHH", data, 0)
-
- symbols = {}
- for i in range(e_shnum):
- off = e_shoff + i * e_shentsize
- (sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size,
- sh_link, sh_info, sh_addralign, sh_entsize), _ = read_struct(
- "<IIQQQQIIQQ", data, off)
- if sh_type != 2:
- continue
- if sh_link >= e_shnum:
- continue
- str_off = e_shoff + sh_link * e_shentsize
- (sname, stype, sflags, saddr, soff, ssize, slink, sinfo,
- salign, sentsz), _ = read_struct("<IIQQQQIIQQ", data, str_off)
- strtab = data[soff:soff + ssize]
- entsize = sh_entsize or 24
- count = sh_size // entsize
- for j in range(count):
- ent_off = sh_offset + j * entsize
- (st_name, st_info, st_other, st_shndx, st_value, st_size), _ = read_struct(
- "<IBBHQQ", data, ent_off)
- if st_name == 0:
- continue
- end = strtab.find(b"\x00", st_name)
- if end < 0:
- continue
- name = strtab[st_name:end].decode("ascii", errors="ignore")
- if name:
- symbols[name] = st_value
- break
- return symbols
-
-
-def main():
- ap = argparse.ArgumentParser()
- ap.add_argument("--elf")
- ap.add_argument("--input", required=True)
- ap.add_argument("--output", required=True)
- ap.add_argument("--compress", choices=["none", "lz4"], default="none")
- args = ap.parse_args()
-
- raw = open(args.input, "rb").read()
- hdr_off = get_header_offset(raw)
- if hdr_off < 0:
- raise ValueError("compressed header magic not found")
-
- header = list(struct.unpack_from(HDR_FMT, raw, hdr_off))
- if header[0] != MAGIC:
- raise ValueError("bad header magic")
- if header[2] < HDR_SIZE:
- raise ValueError("header_size too small")
-
- if args.compress == "lz4":
- payload = lz4_compress(raw)
- comp = COMP_LZ4
- else:
- payload = b""
- comp = COMP_NONE
-
- uncompressed_size = len(raw)
- compressed_size = len(payload)
- payload_offset = (len(raw) + 7) & ~7 if payload else 0
- if payload and args.elf:
- syms = parse_elf_symbols(args.elf)
- link_base = syms.get("_start")
- bss_end = syms.get("_ebss")
- if link_base is not None and bss_end is not None and bss_end >= link_base:
- payload_offset = (bss_end - link_base + 7) & ~7
-
- header[4] = comp
- header[6] = uncompressed_size
- header[7] = compressed_size
- header[8] = 0 # load_phys_base auto
- header[11] = payload_offset
- header[12] = 0
- header[13] = crc32_ieee(payload) if payload else 0
-
- header_bytes = struct.pack(HDR_FMT, *header)
- header[12] = crc32_ieee(header_bytes)
-
- out = bytearray(raw)
- struct.pack_into(HDR_FMT, out, hdr_off, *header)
- if payload:
- if payload_offset > len(out):
- out.extend(b"\x00" * (payload_offset - len(out)))
- out.extend(payload)
-
- with open(args.output, "wb") as f:
- f.write(out)
-
- return 0
-
-
-if __name__ == "__main__":
- try:
- sys.exit(main())
- except Exception as e:
- print("pack_image: " + str(e), file=sys.stderr)
- sys.exit(1)
diff --git a/na/test/module.mk b/na/test/module.mk
index da6c065..47a693c 100644
--- a/na/test/module.mk
+++ b/na/test/module.mk
@@ -3,6 +3,8 @@ SRCS_CC += test/selftest.cc
SRCS_CC += test/test_pmm.cc
SRCS_CC += test/test_slab.cc
SRCS_CC += test/test_spinlock.cc
+SRCS_CC += test/test_mutex.cc
+SRCS_CC += test/test_driver_model.cc
SRCS_CC += test/test_rbtree.cc
SRCS_CC += test/test_vmm.cc
SRCS_CC += test/test_vma.cc
@@ -12,7 +14,17 @@ SRCS_CC += test/test_trap.cc
SRCS_CC += test/test_sched.cc
SRCS_CC += test/test_klog.cc
SRCS_CC += test/test_random.cc
-SRCS_CC += test/test_reloc.cc
SRCS_CC += test/test_waitqueue.cc
SRCS_CC += test/test_uaccess.cc
SRCS_CC += test/test_sys_mm.cc
+SRCS_CC += test/test_smp.cc
+SRCS_CC += test/test_dcache.cc
+SRCS_CC += test/test_namei.cc
+SRCS_CC += test/test_vfs_io.cc
+SRCS_CC += test/test_exec.cc
+SRCS_CC += test/test_pipe_devfs.cc
+SRCS_CC += test/test_sys_process.cc
+SRCS_CC += test/test_signal.cc
+SRCS_CC += test/test_pid.cc
+SRCS_CC += test/test_sys_fs_extra.cc
+SRCS_CC += test/test_page_cache.cc
diff --git a/na/test/selftest.cc b/na/test/selftest.cc
index 96962e2..e1bb2b4 100644
--- a/na/test/selftest.cc
+++ b/na/test/selftest.cc
@@ -14,6 +14,8 @@
void selftest_pmm();
void selftest_slab();
void selftest_spinlock();
+void selftest_mutex();
+void selftest_driver_model();
void selftest_rbtree();
void selftest_vmm();
void selftest_fdt();
@@ -23,10 +25,21 @@ void selftest_trap();
void selftest_sched();
void selftest_klog();
void selftest_random();
-void selftest_reloc();
void selftest_waitqueue();
void selftest_uaccess();
void selftest_sys_mm();
+void selftest_dcache();
+void selftest_namei();
+void selftest_vfs_io();
+void selftest_exec();
+void selftest_devfs();
+void selftest_pipe();
+void selftest_fcntl();
+void selftest_sys_process();
+void selftest_signal();
+void selftest_pid();
+void selftest_sys_fs_extra();
+void selftest_page_cache();
void selftest_run_all()
{
@@ -35,6 +48,8 @@ void selftest_run_all()
kprintf("========================================\n");
selftest_spinlock();
+ selftest_mutex();
+ selftest_driver_model();
selftest_pmm();
selftest_slab();
selftest_rbtree();
@@ -46,10 +61,21 @@ void selftest_run_all()
selftest_sched();
selftest_klog();
selftest_random();
- selftest_reloc();
selftest_waitqueue();
selftest_uaccess();
selftest_sys_mm();
+ selftest_dcache();
+ selftest_namei();
+ selftest_vfs_io();
+ selftest_devfs();
+ selftest_pipe();
+ selftest_fcntl();
+ selftest_sys_process();
+ selftest_signal();
+ selftest_pid();
+ selftest_sys_fs_extra();
+ selftest_page_cache();
+ selftest_exec();
kprintf("========================================\n");
kprintf(" All self-tests PASSED\n");
diff --git a/na/test/test_dcache.cc b/na/test/test_dcache.cc
new file mode 100644
index 0000000..9e0eec5
--- /dev/null
+++ b/na/test/test_dcache.cc
@@ -0,0 +1,232 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_dcache.cc — Self-tests for the dentry cache layer
+ */
+
+#include <lume/selftest.h>
+#include <lume/dcache.h>
+#include <lume/fs.h>
+#include <lume/mount.h>
+#include <lume/errno.h>
+#include <lume/slab.h>
+
+/* Root dentry/mount from VFS init */
+extern dentry* g_root_dentry;
+extern vfsmount* g_root_mnt;
+
+void selftest_dcache()
+{
+ /* ---- d_alloc: basic allocation under root ---- */
+ st_begin("dcache: d_alloc basic");
+ {
+ qstr name{};
+ name.name = "test_child";
+ name.len = 10;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+ ST_ASSERT_EQ(child->d_name.len, 10u);
+ ST_ASSERT(child->d_parent == g_root_dentry);
+ ST_ASSERT(child->d_inode == nullptr); /* not instantiated yet */
+ ST_ASSERT(child->d_sb == g_root_dentry->d_sb);
+
+ /* Name was copied into inline buffer */
+ bool match = true;
+ for (uint32 i = 0; i < name.len; i++) {
+ if (child->d_name.name[i] != name.name[i]) { match = false; break; }
+ }
+ ST_ASSERT(match);
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_lookup: find existing child ---- */
+ st_begin("dcache: d_lookup finds child");
+ {
+ qstr name{};
+ name.name = "lookup_me";
+ name.len = 9;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+
+ /* d_lookup should find it */
+ dentry* found = d_lookup(g_root_dentry, &name);
+ ST_ASSERT(found != nullptr);
+ ST_ASSERT(found == child);
+ dput(found); /* drop d_lookup ref */
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_lookup: miss returns nullptr ---- */
+ st_begin("dcache: d_lookup miss returns nullptr");
+ {
+ qstr name{};
+ name.name = "nonexistent_xyz";
+ name.len = 15;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* found = d_lookup(g_root_dentry, &name);
+ ST_ASSERT(found == nullptr);
+ }
+ st_pass();
+
+ /* ---- d_instantiate: bind inode to dentry ---- */
+ st_begin("dcache: d_instantiate binds inode");
+ {
+ qstr name{};
+ name.name = "has_inode";
+ name.len = 9;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+ ST_ASSERT(child->d_inode == nullptr);
+
+ /* Create a dummy inode */
+ inode* ip = new_inode(g_root_dentry->d_sb);
+ ST_ASSERT(ip != nullptr);
+ ip->i_mode = S_IFREG | 0644;
+
+ d_instantiate(child, ip);
+ ST_ASSERT(child->d_inode == ip);
+ ST_ASSERT(child->d_flags & DCACHE_REGULAR_TYPE);
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- dget/dput: refcount ---- */
+ st_begin("dcache: dget/dput refcount");
+ {
+ qstr name{};
+ name.name = "reftest";
+ name.len = 7;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+ /* d_alloc sets refcount = 1 */
+ int rc1 = child->d_lockref.load();
+ ST_ASSERT(rc1 >= 1);
+
+ dget(child);
+ int rc2 = child->d_lockref.load();
+ ST_ASSERT_EQ(rc2, rc1 + 1);
+
+ dput(child);
+ int rc3 = child->d_lockref.load();
+ ST_ASSERT_EQ(rc3, rc1);
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_drop: removes from parent children list ---- */
+ st_begin("dcache: d_drop removes from parent");
+ {
+ qstr name{};
+ name.name = "dropme";
+ name.len = 6;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+
+ /* Should be findable */
+ dentry* f1 = d_lookup(g_root_dentry, &name);
+ ST_ASSERT(f1 != nullptr);
+ dput(f1);
+
+ /* Drop it */
+ d_drop(child);
+
+ /* Should no longer be findable */
+ dentry* f2 = d_lookup(g_root_dentry, &name);
+ ST_ASSERT(f2 == nullptr);
+
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_path: root returns "/" ---- */
+ st_begin("dcache: d_path root returns /");
+ {
+ path p{};
+ p.de = g_root_dentry;
+ p.mnt = g_root_mnt;
+
+ char buf[64];
+ char* result = d_path(&p, buf, sizeof(buf));
+ ST_ASSERT(result != nullptr);
+ ST_ASSERT(result[0] == '/');
+ ST_ASSERT(result[1] == '\0');
+ }
+ st_pass();
+
+ /* ---- d_path: child path ---- */
+ st_begin("dcache: d_path child returns /name");
+ {
+ qstr name{};
+ name.name = "pathtest";
+ name.len = 8;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+
+ path p{};
+ p.de = child;
+ p.mnt = g_root_mnt;
+
+ char buf[64];
+ char* result = d_path(&p, buf, sizeof(buf));
+ ST_ASSERT(result != nullptr);
+
+ /* Should be "/pathtest" */
+ const char* expected = "/pathtest";
+ bool eq = true;
+ for (int i = 0; expected[i]; i++) {
+ if (result[i] != expected[i]) { eq = false; break; }
+ }
+ ST_ASSERT(eq);
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_alloc_anon: root-like dentry ---- */
+ st_begin("dcache: d_alloc_anon");
+ {
+ dentry* anon = d_alloc_anon(g_root_dentry->d_sb);
+ ST_ASSERT(anon != nullptr);
+ ST_ASSERT(anon->d_parent == anon); /* root: parent is self */
+ ST_ASSERT(anon->d_sb == g_root_dentry->d_sb);
+ dput(anon);
+ }
+ st_pass();
+
+ /* ---- full_name_hash: deterministic ---- */
+ st_begin("dcache: full_name_hash deterministic");
+ {
+ uint32 h1 = full_name_hash("hello", 5);
+ uint32 h2 = full_name_hash("hello", 5);
+ ST_ASSERT_EQ(h1, h2);
+
+ uint32 h3 = full_name_hash("world", 5);
+ ST_ASSERT_NE(h1, h3);
+ }
+ st_pass();
+}
diff --git a/na/test/test_driver_model.cc b/na/test/test_driver_model.cc
new file mode 100644
index 0000000..8f105b3
--- /dev/null
+++ b/na/test/test_driver_model.cc
@@ -0,0 +1,209 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_driver_model — exercises kref, kobject, and the bus/device/driver
+ * core. Uses a dummy bus + dummy driver + dummy device so the test is
+ * self-contained and doesn't depend on FDT/platform_device.
+ */
+
+#include <lume/selftest.h>
+#include <lume/kref.h>
+#include <lume/kobject.h>
+#include <lume/device.h>
+
+/* ------------------------------------------------------------------ */
+/* Test 1: kref get/put + release fires exactly once */
+/* ------------------------------------------------------------------ */
+
+namespace {
+
+struct krefable {
+ kref ref;
+ int release_count;
+};
+
+void krefable_release(kref* r)
+{
+ auto* k = container_of(r, krefable, ref);
+ k->release_count++;
+}
+
+void test_kref()
+{
+ st_begin("kref: init/get/put");
+
+ krefable k = {};
+ kref_init(&k.ref);
+
+ ST_ASSERT_EQ(kref_read(&k.ref), 1);
+
+ kref_get(&k.ref);
+ kref_get(&k.ref);
+ ST_ASSERT_EQ(kref_read(&k.ref), 3);
+
+ ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 0);
+ ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 0);
+ ST_ASSERT_EQ(k.release_count, 0);
+
+ ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 1);
+ ST_ASSERT_EQ(k.release_count, 1);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 2: kobject parent/child + release on final put */
+/* ------------------------------------------------------------------ */
+
+int g_child_released = 0;
+int g_parent_released = 0;
+
+void child_release(kobject*) { g_child_released++; }
+void parent_release(kobject*) { g_parent_released++; }
+
+const kobj_type child_ktype = { .release = child_release };
+const kobj_type parent_ktype = { .release = parent_release };
+
+void test_kobject_hierarchy()
+{
+ st_begin("kobject: parent/child + ref counting");
+
+ g_child_released = 0;
+ g_parent_released = 0;
+
+ kobject parent;
+ kobject_init(&parent, &parent_ktype);
+
+ kobject child;
+ kobject_init(&child, &child_ktype);
+ kobject_add(&child, &parent, "child");
+
+ /* Adding child took a ref on parent. */
+ ST_ASSERT_EQ(kref_read(&parent.refcount), 2);
+ ST_ASSERT(child.parent == &parent);
+
+ /* Drop child's "added" reference — release should NOT fire because
+ * we (the test) still hold the original kobject_init ref. */
+ kobject_del(&child);
+ ST_ASSERT(child.parent == nullptr);
+ ST_ASSERT_EQ(kref_read(&parent.refcount), 1);
+ ST_ASSERT_EQ(g_child_released, 0);
+ ST_ASSERT_EQ(g_parent_released, 0);
+
+ /* Drop final refs. */
+ kobject_put(&child);
+ ST_ASSERT_EQ(g_child_released, 1);
+
+ kobject_put(&parent);
+ ST_ASSERT_EQ(g_parent_released, 1);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 3: bus / device / driver bind via match + probe */
+/* ------------------------------------------------------------------ */
+
+struct dummy_dev { device dev; int probed; };
+struct dummy_drv { device_driver drv; const char* want; };
+
+int g_probes = 0;
+
+int dummy_match(device* dev, device_driver* drv)
+{
+ auto* dd = container_of(drv, dummy_drv, drv);
+ return (dev->init_name != nullptr &&
+ dd->want != nullptr &&
+ __builtin_strcmp(dev->init_name, dd->want) == 0) ? 1 : 0;
+}
+
+int dummy_probe(device* dev)
+{
+ auto* dd = container_of(dev, dummy_dev, dev);
+ dd->probed++;
+ g_probes++;
+ return 0;
+}
+
+bus_type dummy_bus = {
+ .name = "test_dummy",
+ .match = dummy_match,
+ .probe = nullptr, /* fall through to drv->probe */
+ .devices = {},
+ .drivers = {},
+ .lock = {},
+ .registered = false,
+};
+
+void test_bus_bind()
+{
+ st_begin("driver model: bus / driver / device bind");
+
+ g_probes = 0;
+ ST_ASSERT_EQ(bus_register(&dummy_bus), 0);
+
+ /* Register driver before any matching device exists. */
+ dummy_drv drv_a = {};
+ drv_a.drv.name = "drv_a";
+ drv_a.drv.bus = &dummy_bus;
+ drv_a.drv.probe = dummy_probe;
+ drv_a.want = "compat_a";
+ ST_ASSERT_EQ(driver_register(&drv_a.drv), 0);
+
+ /* Register a device that should bind to drv_a immediately. */
+ dummy_dev dev_a = {};
+ dev_a.dev.bus = &dummy_bus;
+ dev_a.dev.init_name = "compat_a";
+ ST_ASSERT_EQ(device_register(&dev_a.dev), 0);
+ ST_ASSERT_EQ(dev_a.probed, 1);
+ ST_ASSERT(dev_a.dev.driver == &drv_a.drv);
+
+ /* Register a device first, then a driver — should also bind. */
+ dummy_dev dev_b = {};
+ dev_b.dev.bus = &dummy_bus;
+ dev_b.dev.init_name = "compat_b";
+ ST_ASSERT_EQ(device_register(&dev_b.dev), 0);
+ ST_ASSERT_EQ(dev_b.probed, 0);
+
+ dummy_drv drv_b = {};
+ drv_b.drv.name = "drv_b";
+ drv_b.drv.bus = &dummy_bus;
+ drv_b.drv.probe = dummy_probe;
+ drv_b.want = "compat_b";
+ ST_ASSERT_EQ(driver_register(&drv_b.drv), 0);
+ ST_ASSERT_EQ(dev_b.probed, 1);
+ ST_ASSERT(dev_b.dev.driver == &drv_b.drv);
+
+ /* Non-matching device sits unbound. */
+ dummy_dev dev_c = {};
+ dev_c.dev.bus = &dummy_bus;
+ dev_c.dev.init_name = "compat_unknown";
+ ST_ASSERT_EQ(device_register(&dev_c.dev), 0);
+ ST_ASSERT_EQ(dev_c.probed, 0);
+ ST_ASSERT(dev_c.dev.driver == nullptr);
+
+ ST_ASSERT_EQ(g_probes, 2);
+
+ /* Cleanup. */
+ device_unregister(&dev_a.dev);
+ device_unregister(&dev_b.dev);
+ device_unregister(&dev_c.dev);
+ driver_unregister(&drv_a.drv);
+ driver_unregister(&drv_b.drv);
+
+ st_pass();
+}
+
+} // namespace
+
+/* ------------------------------------------------------------------ */
+/* Entry */
+/* ------------------------------------------------------------------ */
+
+void selftest_driver_model()
+{
+ test_kref();
+ test_kobject_hierarchy();
+ test_bus_bind();
+}
diff --git a/na/test/test_exec.cc b/na/test/test_exec.cc
new file mode 100644
index 0000000..bbfa3b6
--- /dev/null
+++ b/na/test/test_exec.cc
@@ -0,0 +1,447 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_exec.cc — Self-tests for execve / ELF loader
+ *
+ * Tests the ELF validation, binary loading, and user stack construction
+ * by creating synthetic ELF binaries in RamFS and invoking do_execve
+ * in a controlled environment.
+ *
+ * We cannot test the actual user-space transition (that requires
+ * sret to U-mode), but we CAN test everything up to and including
+ * the trapframe setup.
+ */
+
+#include <lume/selftest.h>
+#include <lume/elf.h>
+#include <lume/task.h>
+#include <lume/vmm.h>
+#include <lume/pmm.h>
+#include <lume/addr.h>
+#include <lume/errno.h>
+#include <lume/fdtable.h>
+#include <lume/fs.h>
+#include <lume/fs_struct.h>
+#include <lume/dcache.h>
+#include <lume/namei.h>
+#include <lume/file.h>
+#include <lume/mm_layout.h>
+#include <arch/trapframe.h>
+#include <arch/pmap.h>
+
+/* Extern: do_execve (kernel/exec.cc) */
+int do_execve(const char* pathname, const char* const* argv,
+ const char* const* envp);
+
+/* Extern: VFS file creation syscalls */
+int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* ================================================================== */
+/* Helper: build a minimal valid RISC-V ELF64 in a buffer */
+/* ================================================================== */
+
+struct TestElf {
+ Elf64_Ehdr ehdr;
+ Elf64_Phdr phdr;
+ uint8 code[16]; /* Tiny "program" — just a few NOPs */
+};
+
+/*
+ * Build a minimal static ELF64 for RISC-V.
+ * Single PT_LOAD segment mapping the tiny code at 0x10000.
+ * Entry point = 0x10000 + offsetof(code).
+ */
+static void build_minimal_elf(TestElf* elf)
+{
+ /* Zero everything */
+ for (uint64 i = 0; i < sizeof(*elf); i++)
+ reinterpret_cast<uint8*>(elf)[i] = 0;
+
+ /* ELF header */
+ Elf64_Ehdr* e = &elf->ehdr;
+ e->e_ident[EI_MAG0] = ELFMAG0;
+ e->e_ident[EI_MAG1] = ELFMAG1;
+ e->e_ident[EI_MAG2] = ELFMAG2;
+ e->e_ident[EI_MAG3] = ELFMAG3;
+ e->e_ident[EI_CLASS] = ELFCLASS64;
+ e->e_ident[EI_DATA] = ELFDATA2LSB;
+ e->e_ident[EI_VERSION] = 1;
+ e->e_type = ET_EXEC;
+ e->e_machine = EM_RISCV;
+ e->e_version = 1;
+ e->e_phoff = sizeof(Elf64_Ehdr);
+ e->e_phentsize = sizeof(Elf64_Phdr);
+ e->e_phnum = 1;
+ e->e_ehsize = sizeof(Elf64_Ehdr);
+
+ /* Code section offset within the file */
+ uint64 code_offset = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr);
+
+ /* Entry point = vaddr of code */
+ uint64 load_vaddr = 0x10000;
+ e->e_entry = load_vaddr + code_offset;
+
+ /* Program header — single PT_LOAD covering entire file */
+ Elf64_Phdr* p = &elf->phdr;
+ p->p_type = PT_LOAD;
+ p->p_flags = PF_R | PF_X;
+ p->p_offset = 0;
+ p->p_vaddr = load_vaddr;
+ p->p_paddr = load_vaddr;
+ p->p_filesz = sizeof(TestElf);
+ p->p_memsz = sizeof(TestElf);
+ p->p_align = 0x1000;
+
+ /* Code: RISC-V "unimp" (all zeros = illegal instruction, but good enough for testing) */
+ /* We just need the loader to succeed; we won't actually execute this. */
+ elf->code[0] = 0x13; /* nop = addi x0, x0, 0 */
+ elf->code[4] = 0x13;
+ elf->code[8] = 0x73; /* ebreak */
+}
+
+/* ================================================================== */
+/* Helper: write a buffer to a file in RamFS */
+/* ================================================================== */
+
+static int create_test_file(const char* path, const void* data, uint64 size)
+{
+ /* Open with O_CREAT | O_WRONLY | O_TRUNC */
+ int64 fd = sys_openat(
+ static_cast<uint64>(-100), /* AT_FDCWD */
+ reinterpret_cast<uint64>(path),
+ O_CREAT | O_WRONLY | O_TRUNC,
+ 0755, 0, 0);
+ if (fd < 0) return static_cast<int>(fd);
+
+ int64 written = sys_write(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(data),
+ size, 0, 0, 0);
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+
+ if (written < 0) return static_cast<int>(written);
+ if (static_cast<uint64>(written) != size) return -EIO;
+ return 0;
+}
+
+/* ================================================================== */
+/* Helper: ensure current task has VFS context for file ops */
+/* ================================================================== */
+
+struct ExecTestCtx {
+ TaskControlBlock* curr;
+ VmSpace* old_mm;
+ arch::TrapFrame fake_tf;
+ arch::TrapFrame* old_tf;
+ uint64 saved_satp;
+};
+
+static void exec_test_setup(ExecTestCtx* ctx)
+{
+ ctx->curr = current_task();
+
+ /* Save kernel SATP so we can restore it in teardown */
+ asm volatile("csrr %0, satp" : "=r"(ctx->saved_satp));
+
+ /* Save and replace mm with a fresh one */
+ ctx->old_mm = ctx->curr->mm;
+ ctx->curr->mm = VmSpace::create();
+
+ /* Set up a fake trapframe so do_execve can modify it */
+ for (uint64 i = 0; i < sizeof(ctx->fake_tf); i++)
+ reinterpret_cast<uint8*>(&ctx->fake_tf)[i] = 0;
+ ctx->old_tf = ctx->curr->trapframe;
+ ctx->curr->trapframe = &ctx->fake_tf;
+}
+
+static void exec_test_teardown(ExecTestCtx* ctx)
+{
+ /* Restore trapframe */
+ ctx->curr->trapframe = ctx->old_tf;
+
+ /* Destroy the test mm (may have been replaced by do_execve) */
+ VmSpace* test_mm = ctx->curr->mm;
+ ctx->curr->mm = ctx->old_mm;
+ if (test_mm && test_mm != ctx->old_mm) {
+ /* If do_execve succeeded, it activated new_mm's page table.
+ * Switch back to the kernel page table before destroying. */
+ asm volatile("csrw satp, %0" :: "r"(ctx->saved_satp));
+ asm volatile("sfence.vma");
+ test_mm->put();
+ }
+}
+
+/* ================================================================== */
+/* Test cases */
+/* ================================================================== */
+
+void selftest_exec()
+{
+ /* ---- 1a. Non-existent path under missing directory ---- */
+ st_begin("exec: missing path returns error");
+ {
+ const char* argv[] = { "/no_such_dir/no_file", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/no_such_dir/no_file", argv, envp);
+ ST_ASSERT(ret < 0); /* -ENOENT from filename_lookup */
+ }
+ st_pass();
+
+ /* ---- 1b. Directory is not executable ---- */
+ st_begin("exec: directory returns -EACCES");
+ {
+ /* "/" exists and is a directory */
+ const char* argv[] = { "/dev", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/dev", argv, envp);
+ ST_ASSERT_EQ(ret, -EACCES);
+ }
+ st_pass();
+
+ /* ---- 2. Bad ELF magic ---- */
+ st_begin("exec: bad ELF magic returns -ENOEXEC");
+ {
+ uint8 garbage[128];
+ for (int i = 0; i < 128; i++) garbage[i] = 0xAA;
+ int err = create_test_file("/test_bad_magic", garbage, 128);
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_bad_magic", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_bad_magic", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 3. Wrong ELF class (32-bit) ---- */
+ st_begin("exec: 32-bit ELF returns -ENOEXEC");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ elf.ehdr.e_ident[EI_CLASS] = 1; /* ELFCLASS32 */
+ int err = create_test_file("/test_elf32", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_elf32", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_elf32", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 4. Wrong endianness (big-endian) ---- */
+ st_begin("exec: big-endian ELF returns -ENOEXEC");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ elf.ehdr.e_ident[EI_DATA] = 2; /* ELFDATA2MSB */
+ int err = create_test_file("/test_be", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_be", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_be", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 5. Wrong machine (x86_64 instead of RISC-V) ---- */
+ st_begin("exec: wrong machine type returns -ENOEXEC");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ elf.ehdr.e_machine = 62; /* EM_X86_64 */
+ int err = create_test_file("/test_x86", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_x86", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_x86", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 6. Truncated ELF (too small for header) ---- */
+ st_begin("exec: truncated ELF returns -ENOEXEC");
+ {
+ /* Valid magic but file is too short for full header */
+ uint8 tiny[8] = { 0x7f, 'E', 'L', 'F', 2, 1, 1, 0 };
+ int err = create_test_file("/test_tiny", tiny, sizeof(tiny));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_tiny", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_tiny", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 7. Zero program headers ---- */
+ st_begin("exec: zero phnum returns -ENOEXEC");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ elf.ehdr.e_phnum = 0;
+ int err = create_test_file("/test_nophdr", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_nophdr", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_nophdr", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 8. Valid ELF: successful load and trapframe setup ---- */
+ st_begin("exec: valid ELF loads and sets up trapframe");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ int err = create_test_file("/test_valid", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_valid", nullptr };
+ const char* envp[] = { "HOME=/", nullptr };
+ int ret = do_execve("/test_valid", argv, envp);
+ ST_ASSERT_EQ(ret, 0);
+
+ /* Verify trapframe was set up correctly */
+ arch::TrapFrame* tf = &ctx.fake_tf;
+
+ /* Entry point should match what we set in the ELF */
+ uint64 code_offset = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr);
+ uint64 expected_entry = 0x10000 + code_offset;
+ ST_ASSERT_EQ(tf->epc, expected_entry);
+
+ /* Stack pointer should be in user space, below the stack top */
+ ST_ASSERT(tf->sp > 0);
+ ST_ASSERT(tf->sp < kUserStackTop);
+ ST_ASSERT(tf->sp >= kUserStackBase);
+
+ /* sstatus: SPP=0 (U-mode), SPIE=1 (interrupts enabled on sret) */
+ uint64 spp_bit = (1ULL << 8);
+ uint64 spie_bit = (1ULL << 5);
+ ST_ASSERT_EQ(tf->sstatus & spp_bit, 0ULL); /* Not S-mode */
+ ST_ASSERT_EQ(tf->sstatus & spie_bit, spie_bit); /* SPIE set */
+
+ /* General registers should be zeroed */
+ ST_ASSERT_EQ(tf->ra, 0ULL);
+ ST_ASSERT_EQ(tf->gp, 0ULL);
+ ST_ASSERT_EQ(tf->tp, 0ULL);
+ for (int i = 0; i < 7; i++)
+ ST_ASSERT_EQ(tf->t[i], 0ULL);
+ for (int i = 0; i < 12; i++)
+ ST_ASSERT_EQ(tf->s[i], 0ULL);
+ for (int i = 0; i < 8; i++)
+ ST_ASSERT_EQ(tf->a[i], 0ULL);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 9. Verify user stack layout (argc/argv) ---- */
+ st_begin("exec: user stack has correct argc/argv layout");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ int err = create_test_file("/test_stack", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_stack", "arg1", nullptr };
+ const char* envp[] = { "A=B", nullptr };
+ int ret = do_execve("/test_stack", argv, envp);
+ ST_ASSERT_EQ(ret, 0);
+
+ /* The stack was set up in the new mm.
+ * Read argc from the top of the stack using pmap lookup. */
+ VmSpace* mm = ctx.curr->mm;
+ uint64 sp = ctx.fake_tf.sp;
+
+ /* Look up the physical page for sp */
+ uint64 pa = 0;
+ bool mapped = pmap::lookup(mm->root_pa, sp & ~(kPageSize - 1), &pa);
+ ST_ASSERT(mapped);
+
+ /* Read argc (first uint64 on stack) */
+ uint64 page_offset = sp & (kPageSize - 1);
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 argc = *reinterpret_cast<uint64*>(kva + page_offset);
+ ST_ASSERT_EQ(argc, 2ULL); /* "/test_stack" and "arg1" */
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 10. Multiple consecutive execve replaces mm correctly ---- */
+ st_begin("exec: consecutive execve replaces address space");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ int err = create_test_file("/test_reexec", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ /* First exec */
+ const char* argv[] = { "/test_reexec", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_reexec", argv, envp);
+ ST_ASSERT_EQ(ret, 0);
+
+ VmSpace* first_mm = ctx.curr->mm;
+ ST_ASSERT(first_mm != nullptr);
+
+ /* Second exec — should replace the first mm */
+ ret = do_execve("/test_reexec", argv, envp);
+ ST_ASSERT_EQ(ret, 0);
+
+ /* mm should be different (new address space) */
+ VmSpace* second_mm = ctx.curr->mm;
+ ST_ASSERT(second_mm != nullptr);
+ ST_ASSERT_NE(first_mm, second_mm);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+}
diff --git a/na/test/test_mutex.cc b/na/test/test_mutex.cc
new file mode 100644
index 0000000..e86c984
--- /dev/null
+++ b/na/test/test_mutex.cc
@@ -0,0 +1,183 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_mutex — self-tests for the Mutex sleeping lock.
+ *
+ * Coverage:
+ * - basic lock/unlock single-thread
+ * - try_lock when free and when held
+ * - mutual exclusion across kthreads (no lost increments)
+ * - FIFO wake order (A acquires, B/C queue; release lets B in then C)
+ */
+
+#include <lume/selftest.h>
+#include <lume/mutex.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+#include <lume/kprintf.h>
+
+extern TaskControlBlock* kthread_create(const char* name,
+ void (*entry)(void*), void* arg,
+ uint8 priority);
+extern void task_exit(int code);
+
+/* ------------------------------------------------------------------ */
+/* Test 1: basic lock/unlock */
+/* ------------------------------------------------------------------ */
+
+static void test_basic()
+{
+ st_begin("mutex: basic lock/unlock");
+
+ Mutex m;
+ m.init("test_basic");
+
+ ST_ASSERT(!m.is_locked());
+ m.lock();
+ ST_ASSERT(m.is_locked());
+ ST_ASSERT(m.is_owned_by_current());
+ m.unlock();
+ ST_ASSERT(!m.is_locked());
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 2: try_lock */
+/* ------------------------------------------------------------------ */
+
+static void test_trylock()
+{
+ st_begin("mutex: try_lock");
+
+ Mutex m;
+ m.init("test_trylock");
+
+ ST_ASSERT(m.try_lock() == true);
+ ST_ASSERT(m.is_locked());
+
+ /* A second try_lock from the same task: since owner == current,
+ * we still return false (try_lock is non-recursive). */
+ ST_ASSERT(m.try_lock() == false);
+
+ m.unlock();
+ ST_ASSERT(!m.is_locked());
+ ST_ASSERT(m.try_lock() == true);
+ m.unlock();
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 3: mutual exclusion across kthreads */
+/* ------------------------------------------------------------------ */
+
+static Mutex g_mut;
+static volatile int32 g_counter = 0;
+static volatile int32 g_done = 0;
+static constexpr int32 kIters = 200;
+
+static void mutex_worker(void*)
+{
+ for (int i = 0; i < kIters; i++) {
+ g_mut.lock();
+ int32 v = g_counter;
+ /* Yield inside the critical section to expose missing mutex.
+ * If the mutex were broken we'd see lost increments. */
+ sched_yield();
+ g_counter = v + 1;
+ g_mut.unlock();
+ }
+ __sync_fetch_and_add(&g_done, 1);
+ task_exit(0);
+}
+
+static void test_mutual_exclusion()
+{
+ st_begin("mutex: mutual exclusion (2 workers)");
+
+ g_mut.init("test_mutex_mx");
+ g_counter = 0;
+ g_done = 0;
+
+ kthread_create("mut_w1", mutex_worker, nullptr, 0);
+ kthread_create("mut_w2", mutex_worker, nullptr, 0);
+
+ /* Spin-yield until both workers finish. */
+ while (g_done < 2) {
+ sched_yield();
+ }
+
+ ST_ASSERT_EQ(g_counter, 2 * kIters);
+ ST_ASSERT(!g_mut.is_locked());
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 4: FIFO wake order */
+/* ------------------------------------------------------------------ */
+
+static Mutex g_fifo_mut;
+static volatile int32 g_fifo_seq = 0; // monotonically increases
+static volatile int32 g_fifo_order[3]; // order each worker observed
+static volatile int32 g_fifo_done = 0;
+
+static void fifo_worker(void* arg)
+{
+ int slot = static_cast<int>(reinterpret_cast<uint64>(arg));
+ g_fifo_mut.lock();
+ g_fifo_order[slot] = __sync_fetch_and_add(&g_fifo_seq, 1);
+ g_fifo_mut.unlock();
+ __sync_fetch_and_add(&g_fifo_done, 1);
+ task_exit(0);
+}
+
+static void test_fifo_order()
+{
+ st_begin("mutex: FIFO wake order (3 waiters)");
+
+ g_fifo_mut.init("test_mutex_fifo");
+ g_fifo_seq = 0;
+ g_fifo_done = 0;
+ g_fifo_order[0] = g_fifo_order[1] = g_fifo_order[2] = -1;
+
+ /* Main grabs the lock first so workers will queue up behind. */
+ g_fifo_mut.lock();
+
+ /* Spawn workers in deterministic order. After each create() yield
+ * once so the new thread reaches mutex.lock() and enqueues itself
+ * before we spawn the next one. */
+ kthread_create("fifo_A", fifo_worker, reinterpret_cast<void*>(0), 0);
+ sched_yield();
+ kthread_create("fifo_B", fifo_worker, reinterpret_cast<void*>(1), 0);
+ sched_yield();
+ kthread_create("fifo_C", fifo_worker, reinterpret_cast<void*>(2), 0);
+ sched_yield();
+
+ /* Release: A should fire next, then B, then C. */
+ g_fifo_mut.unlock();
+
+ while (g_fifo_done < 3) {
+ sched_yield();
+ }
+
+ ST_ASSERT_EQ(g_fifo_order[0], 0);
+ ST_ASSERT_EQ(g_fifo_order[1], 1);
+ ST_ASSERT_EQ(g_fifo_order[2], 2);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Entry */
+/* ------------------------------------------------------------------ */
+
+void selftest_mutex()
+{
+ test_basic();
+ test_trylock();
+ test_mutual_exclusion();
+ test_fifo_order();
+}
diff --git a/na/test/test_namei.cc b/na/test/test_namei.cc
new file mode 100644
index 0000000..8e4ecb0
--- /dev/null
+++ b/na/test/test_namei.cc
@@ -0,0 +1,173 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_namei.cc — Self-tests for pathname resolution (namei.cc)
+ */
+
+#include <lume/selftest.h>
+#include <lume/namei.h>
+#include <lume/dcache.h>
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/mount.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+
+extern dentry* g_root_dentry;
+extern vfsmount* g_root_mnt;
+
+/* sys_mkdirat from kernel/sys_fs.cc */
+int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64);
+
+void selftest_namei()
+{
+ /* ---- lookup root "/" ---- */
+ st_begin("namei: lookup root /");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de == g_root_dentry);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* ---- lookup /dev (exists from boot) ---- */
+ st_begin("namei: lookup /dev");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de != nullptr);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* ---- lookup nonexistent path returns -ENOENT ---- */
+ st_begin("namei: lookup nonexistent returns -ENOENT");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/this_does_not_exist", 0, &p);
+ ST_ASSERT_EQ(err, -ENOENT);
+ }
+ st_pass();
+
+ /* ---- lookup /dev/console (chardev) ---- */
+ st_begin("namei: lookup /dev/console");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/console", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ ST_ASSERT(S_ISCHR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* ---- lookup with LOOKUP_DIRECTORY on non-dir returns -ENOTDIR ---- */
+ st_begin("namei: LOOKUP_DIRECTORY on file returns -ENOTDIR");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/console", LOOKUP_DIRECTORY, &p);
+ ST_ASSERT_EQ(err, -ENOTDIR);
+ }
+ st_pass();
+
+ /* ---- lookup with LOOKUP_DIRECTORY on dir succeeds ---- */
+ st_begin("namei: LOOKUP_DIRECTORY on dir succeeds");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev", LOOKUP_DIRECTORY, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* ---- kern_path shortcut ---- */
+ st_begin("namei: kern_path works like filename_lookup");
+ {
+ path p{};
+ int err = kern_path("/dev", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ }
+ st_pass();
+
+ /* ---- dot path: /dev/. resolves to /dev ---- */
+ st_begin("namei: /dev/. resolves to /dev");
+ {
+ path p1{}, p2{};
+ int e1 = filename_lookup(AT_FDCWD, "/dev", 0, &p1);
+ int e2 = filename_lookup(AT_FDCWD, "/dev/.", 0, &p2);
+ ST_ASSERT_EQ(e1, 0);
+ ST_ASSERT_EQ(e2, 0);
+ ST_ASSERT(p1.de == p2.de);
+ }
+ st_pass();
+
+ /* ---- dotdot path: /dev/.. resolves to root ---- */
+ st_begin("namei: /dev/.. resolves to root");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/..", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de == g_root_dentry);
+ }
+ st_pass();
+
+ /* ---- lookup into non-dir returns -ENOTDIR ---- */
+ st_begin("namei: path through non-dir returns -ENOTDIR");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/console/child", 0, &p);
+ ST_ASSERT_EQ(err, -ENOTDIR);
+ }
+ st_pass();
+
+ /* ---- null/empty paths ---- */
+ st_begin("namei: null name returns -EINVAL");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, nullptr, 0, &p);
+ ST_ASSERT_EQ(err, -EINVAL);
+ }
+ st_pass();
+
+ /* ---- LOOKUP_PARENT: returns parent dentry ---- */
+ st_begin("namei: LOOKUP_PARENT returns parent");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/console", LOOKUP_PARENT, &p);
+ ST_ASSERT_EQ(err, 0);
+ /* Should return /dev, not /dev/console */
+ path dev_path{};
+ int e2 = filename_lookup(AT_FDCWD, "/dev", 0, &dev_path);
+ ST_ASSERT_EQ(e2, 0);
+ ST_ASSERT(p.de == dev_path.de);
+ }
+ st_pass();
+
+ /* ---- mkdir + lookup round-trip ---- */
+ st_begin("namei: mkdir then lookup succeeds");
+ {
+ /* Create /test_namei_dir via sys_mkdirat */
+ int64 ret = sys_mkdirat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>("/test_namei_dir"),
+ 0755, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+
+ /* Now lookup should find it */
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/test_namei_dir", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* NOTE: trailing slash "/dev/" not tested — Phase 1 namei
+ * doesn't handle empty trailing components. */
+}
diff --git a/na/test/test_page_cache.cc b/na/test/test_page_cache.cc
new file mode 100644
index 0000000..ee0e0e2
--- /dev/null
+++ b/na/test/test_page_cache.cc
@@ -0,0 +1,180 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_page_cache.cc — Self-tests for the page cache hash table
+ *
+ * Tests: insert, get, remove, duplicate insert, invalidate,
+ * get_or_alloc, null args.
+ */
+
+#include <lume/selftest.h>
+#include <lume/page_cache.h>
+#include <lume/page.h>
+#include <lume/pmm.h>
+#include <lume/errno.h>
+
+void selftest_page_cache()
+{
+ /* Use sentinel pointers as fake "mapping" identifiers.
+ * These are never dereferenced — only used as hash keys. */
+ int mapping_a_tag = 0;
+ int mapping_b_tag = 0;
+ void* mapping_a = &mapping_a_tag;
+ void* mapping_b = &mapping_b_tag;
+
+ /* ---- insert and get ---- */
+ st_begin("page_cache: insert then get returns same page");
+ {
+ Page* page = pmm_alloc_page();
+ ST_ASSERT(page != nullptr);
+
+ int ret = page_cache_insert(mapping_a, 0, page);
+ ST_ASSERT_EQ(ret, 0);
+
+ Page* got = page_cache_get(mapping_a, 0);
+ ST_ASSERT_EQ(got, page);
+
+ /* put the reference from get */
+ page_cache_put(got);
+
+ /* Clean up */
+ page_cache_remove(mapping_a, 0);
+ pmm_free_page(page);
+ }
+ st_pass();
+
+ /* ---- get nonexistent returns nullptr ---- */
+ st_begin("page_cache: get nonexistent returns nullptr");
+ {
+ Page* got = page_cache_get(mapping_a, 99999);
+ ST_ASSERT(got == nullptr);
+ }
+ st_pass();
+
+ /* ---- duplicate insert returns -EEXIST ---- */
+ st_begin("page_cache: duplicate insert returns -EEXIST");
+ {
+ Page* page = pmm_alloc_page();
+ ST_ASSERT(page != nullptr);
+
+ int ret1 = page_cache_insert(mapping_a, 1, page);
+ ST_ASSERT_EQ(ret1, 0);
+
+ Page* page2 = pmm_alloc_page();
+ ST_ASSERT(page2 != nullptr);
+
+ int ret2 = page_cache_insert(mapping_a, 1, page2);
+ ST_ASSERT_EQ(ret2, -EEXIST);
+
+ /* Clean up */
+ page_cache_remove(mapping_a, 1);
+ pmm_free_page(page);
+ pmm_free_page(page2);
+ }
+ st_pass();
+
+ /* ---- remove returns page ---- */
+ st_begin("page_cache: remove returns page and deletes entry");
+ {
+ Page* page = pmm_alloc_page();
+ ST_ASSERT(page != nullptr);
+
+ page_cache_insert(mapping_a, 2, page);
+
+ Page* removed = page_cache_remove(mapping_a, 2);
+ ST_ASSERT_EQ(removed, page);
+
+ /* Should be gone now */
+ Page* got = page_cache_get(mapping_a, 2);
+ ST_ASSERT(got == nullptr);
+
+ pmm_free_page(page);
+ }
+ st_pass();
+
+ /* ---- remove nonexistent returns nullptr ---- */
+ st_begin("page_cache: remove nonexistent returns nullptr");
+ {
+ Page* got = page_cache_remove(mapping_a, 77777);
+ ST_ASSERT(got == nullptr);
+ }
+ st_pass();
+
+ /* ---- different mappings are independent ---- */
+ st_begin("page_cache: different mappings are independent");
+ {
+ Page* pa = pmm_alloc_page();
+ Page* pb = pmm_alloc_page();
+ ST_ASSERT(pa != nullptr);
+ ST_ASSERT(pb != nullptr);
+
+ page_cache_insert(mapping_a, 5, pa);
+ page_cache_insert(mapping_b, 5, pb);
+
+ Page* got_a = page_cache_get(mapping_a, 5);
+ Page* got_b = page_cache_get(mapping_b, 5);
+ ST_ASSERT_EQ(got_a, pa);
+ ST_ASSERT_EQ(got_b, pb);
+ ST_ASSERT_NE(got_a, got_b);
+
+ page_cache_put(got_a);
+ page_cache_put(got_b);
+ page_cache_remove(mapping_a, 5);
+ page_cache_remove(mapping_b, 5);
+ pmm_free_page(pa);
+ pmm_free_page(pb);
+ }
+ st_pass();
+
+ /* ---- invalidate removes all entries for a mapping ---- */
+ st_begin("page_cache: invalidate removes all entries for mapping");
+ {
+ Page* pages[4];
+ for (int i = 0; i < 4; i++) {
+ pages[i] = pmm_alloc_page();
+ ST_ASSERT(pages[i] != nullptr);
+ page_cache_insert(mapping_a, 10 + i, pages[i]);
+ }
+
+ /* Also insert one for mapping_b */
+ Page* pb = pmm_alloc_page();
+ page_cache_insert(mapping_b, 10, pb);
+
+ /* Invalidate mapping_a */
+ page_cache_invalidate(mapping_a);
+
+ /* All mapping_a entries should be gone */
+ for (int i = 0; i < 4; i++) {
+ Page* got = page_cache_get(mapping_a, 10 + i);
+ ST_ASSERT(got == nullptr);
+ pmm_free_page(pages[i]);
+ }
+
+ /* mapping_b should still be there */
+ Page* got_b = page_cache_get(mapping_b, 10);
+ ST_ASSERT_EQ(got_b, pb);
+ page_cache_put(got_b);
+ page_cache_remove(mapping_b, 10);
+ pmm_free_page(pb);
+ }
+ st_pass();
+
+ /* ---- insert null mapping returns -EINVAL ---- */
+ st_begin("page_cache: insert null mapping returns -EINVAL");
+ {
+ Page* page = pmm_alloc_page();
+ int ret = page_cache_insert(nullptr, 0, page);
+ ST_ASSERT_EQ(ret, -EINVAL);
+ pmm_free_page(page);
+ }
+ st_pass();
+
+ /* ---- insert null page returns -EINVAL ---- */
+ st_begin("page_cache: insert null page returns -EINVAL");
+ {
+ int ret = page_cache_insert(mapping_a, 0, nullptr);
+ ST_ASSERT_EQ(ret, -EINVAL);
+ }
+ st_pass();
+}
diff --git a/na/test/test_pid.cc b/na/test/test_pid.cc
new file mode 100644
index 0000000..e79eb1e
--- /dev/null
+++ b/na/test/test_pid.cc
@@ -0,0 +1,106 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_pid.cc — Self-tests for PID allocator
+ *
+ * Tests: alloc, free, re-use, exhaustion guard, reserved PIDs.
+ */
+
+#include <lume/selftest.h>
+#include <lume/pid.h>
+
+void selftest_pid()
+{
+ /* Create a local PidAllocator for isolated testing */
+ PidAllocator pa;
+ pa.init();
+
+ /* ---- PID 0 is reserved (idle/swapper) ---- */
+ st_begin("pid: PID 0 is reserved");
+ {
+ /* First alloc should return 1 (PID 0 reserved for idle) */
+ int32 first = pa.alloc();
+ ST_ASSERT(first >= 1);
+ }
+ st_pass();
+
+ /* ---- Sequential allocation returns unique PIDs ---- */
+ st_begin("pid: sequential allocation returns unique PIDs");
+ {
+ PidAllocator pa2;
+ pa2.init();
+
+ int32 p1 = pa2.alloc();
+ int32 p2 = pa2.alloc();
+ int32 p3 = pa2.alloc();
+ ST_ASSERT(p1 >= 1);
+ ST_ASSERT(p2 >= 1);
+ ST_ASSERT(p3 >= 1);
+ ST_ASSERT_NE(p1, p2);
+ ST_ASSERT_NE(p2, p3);
+ ST_ASSERT_NE(p1, p3);
+ }
+ st_pass();
+
+ /* ---- Freed PID can be reused ---- */
+ st_begin("pid: freed PID can be reused");
+ {
+ PidAllocator pa3;
+ pa3.init();
+
+ /* Allocate all PIDs from 2..N */
+ constexpr int kAllocCount = 60;
+ int32 pids[kAllocCount];
+ for (int i = 0; i < kAllocCount; i++) {
+ pids[i] = pa3.alloc();
+ ST_ASSERT(pids[i] >= 1);
+ }
+
+ /* Free one in the middle */
+ int32 freed_pid = pids[kAllocCount / 2];
+ pa3.free(freed_pid);
+
+ /* Allocate again — should eventually get the freed PID */
+ bool found = false;
+ for (int i = 0; i < kAllocCount; i++) {
+ int32 p = pa3.alloc();
+ if (p == freed_pid) { found = true; break; }
+ pa3.free(p);
+ }
+ ST_ASSERT(found);
+ }
+ st_pass();
+
+ /* ---- free invalid PID is safe ---- */
+ st_begin("pid: free invalid PID is no-op");
+ {
+ PidAllocator pa4;
+ pa4.init();
+
+ /* These should not crash */
+ pa4.free(-1);
+ pa4.free(kMaxPid);
+ pa4.free(kMaxPid + 100);
+
+ /* Allocator should still work — PID 0 still reserved */
+ int32 p = pa4.alloc();
+ ST_ASSERT(p >= 1);
+ }
+ st_pass();
+
+ /* ---- Monotonic allocation before wrap ---- */
+ st_begin("pid: monotonic allocation");
+ {
+ PidAllocator pa5;
+ pa5.init();
+
+ int32 prev = pa5.alloc();
+ for (int i = 0; i < 10; i++) {
+ int32 next = pa5.alloc();
+ ST_ASSERT(next > prev);
+ prev = next;
+ }
+ }
+ st_pass();
+}
diff --git a/na/test/test_pipe_devfs.cc b/na/test/test_pipe_devfs.cc
new file mode 100644
index 0000000..ed375ed
--- /dev/null
+++ b/na/test/test_pipe_devfs.cc
@@ -0,0 +1,420 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_pipe_devfs.cc — Self-tests for pipe, /dev/null, /dev/zero, fcntl
+ */
+
+#include <lume/selftest.h>
+#include <lume/errno.h>
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/fs_types.h>
+#include <lume/task.h>
+#include <lume/fdtable.h>
+#include <lume/pipe.h>
+
+/* Syscall entry points */
+int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_pipe2(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_fcntl(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_fstat_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* ================================================================== */
+/* Helpers */
+/* ================================================================== */
+
+static int64 open_dev(const char* path, int flags)
+{
+ return sys_openat(
+ static_cast<uint64>(-100), /* AT_FDCWD */
+ reinterpret_cast<uint64>(path),
+ static_cast<uint64>(flags),
+ 0, 0, 0);
+}
+
+static int64 do_close(int64 fd)
+{
+ return sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+}
+
+static int64 do_read(int64 fd, void* buf, uint64 count)
+{
+ return sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), count, 0, 0, 0);
+}
+
+static int64 do_write(int64 fd, const void* buf, uint64 count)
+{
+ return sys_write(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), count, 0, 0, 0);
+}
+
+/* fcntl command constants — must match sys_fs.cc */
+inline constexpr int F_DUPFD = 0;
+inline constexpr int F_GETFD = 1;
+inline constexpr int F_SETFD = 2;
+inline constexpr int F_GETFL = 3;
+inline constexpr int F_SETFL = 4;
+inline constexpr int F_DUPFD_CLOEXEC = 1030;
+inline constexpr int FD_CLOEXEC = 1;
+
+/* ================================================================== */
+/* /dev/null tests */
+/* ================================================================== */
+
+void selftest_devfs()
+{
+ /* ---- /dev/null: read returns EOF ---- */
+ st_begin("devfs: /dev/null read returns 0 (EOF)");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ char buf[16];
+ int64 n = do_read(fd, buf, sizeof(buf));
+ ST_ASSERT_EQ(n, (int64)0);
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- /dev/null: write discards data ---- */
+ st_begin("devfs: /dev/null write discards data");
+ {
+ int64 fd = open_dev("/dev/null", O_WRONLY);
+ ST_ASSERT(fd >= 0);
+
+ const char* msg = "discarded";
+ int64 n = do_write(fd, msg, 9);
+ ST_ASSERT_EQ(n, (int64)9);
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- /dev/null: fstat reports S_IFCHR ---- */
+ st_begin("devfs: /dev/null fstat is S_IFCHR");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ kstat st{};
+ int64 ret = sys_fstat_wrapper(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&st), 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+ ST_ASSERT(S_ISCHR(st.st_mode));
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- /dev/zero: read returns zeros ---- */
+ st_begin("devfs: /dev/zero read returns zeros");
+ {
+ int64 fd = open_dev("/dev/zero", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ char buf[32];
+ /* Pre-fill with non-zero */
+ for (int i = 0; i < 32; i++) buf[i] = 0xFF;
+
+ int64 n = do_read(fd, buf, 32);
+ ST_ASSERT_EQ(n, (int64)32);
+
+ bool all_zero = true;
+ for (int i = 0; i < 32; i++) {
+ if (buf[i] != 0) { all_zero = false; break; }
+ }
+ ST_ASSERT(all_zero);
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- /dev/zero: write discards data ---- */
+ st_begin("devfs: /dev/zero write discards data");
+ {
+ int64 fd = open_dev("/dev/zero", O_WRONLY);
+ ST_ASSERT(fd >= 0);
+
+ char buf[16] = {1, 2, 3};
+ int64 n = do_write(fd, buf, 16);
+ ST_ASSERT_EQ(n, (int64)16);
+ do_close(fd);
+ }
+ st_pass();
+}
+
+/* ================================================================== */
+/* Pipe tests */
+/* ================================================================== */
+
+void selftest_pipe()
+{
+ /* ---- pipe2: basic creation ---- */
+ st_begin("pipe: pipe2 creates two valid fds");
+ {
+ int fds[2] = {-1, -1};
+ int64 ret = sys_pipe2(
+ reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+ ST_ASSERT(fds[0] >= 0);
+ ST_ASSERT(fds[1] >= 0);
+ ST_ASSERT_NE(fds[0], fds[1]);
+
+ do_close(fds[0]);
+ do_close(fds[1]);
+ }
+ st_pass();
+
+ /* ---- pipe: write then read ---- */
+ st_begin("pipe: write then read returns correct data");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ const char* msg = "pipe_test";
+ int64 w = do_write(fds[1], msg, 9);
+ ST_ASSERT_EQ(w, (int64)9);
+
+ char buf[16] = {};
+ int64 r = do_read(fds[0], buf, 16);
+ ST_ASSERT_EQ(r, (int64)9);
+
+ bool match = true;
+ for (int i = 0; i < 9; i++) {
+ if (buf[i] != msg[i]) { match = false; break; }
+ }
+ ST_ASSERT(match);
+
+ do_close(fds[0]);
+ do_close(fds[1]);
+ }
+ st_pass();
+
+ /* ---- pipe: read from closed write end returns EOF ---- */
+ st_begin("pipe: read after writer close returns EOF");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ /* Write some data, then close write end */
+ const char* msg = "eof";
+ do_write(fds[1], msg, 3);
+ do_close(fds[1]);
+
+ /* Read the data */
+ char buf[16];
+ int64 r1 = do_read(fds[0], buf, 16);
+ ST_ASSERT_EQ(r1, (int64)3);
+
+ /* Next read should return 0 (EOF) */
+ int64 r2 = do_read(fds[0], buf, 16);
+ ST_ASSERT_EQ(r2, (int64)0);
+
+ do_close(fds[0]);
+ }
+ st_pass();
+
+ /* ---- pipe: write to closed read end returns -EPIPE ---- */
+ st_begin("pipe: write after reader close returns -EPIPE");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ /* Close read end */
+ do_close(fds[0]);
+
+ /* Write should fail */
+ const char* msg = "broken";
+ int64 w = do_write(fds[1], msg, 6);
+ ST_ASSERT_EQ(w, (int64)-EPIPE);
+
+ do_close(fds[1]);
+ }
+ st_pass();
+
+ /* ---- pipe: multiple writes accumulate ---- */
+ st_begin("pipe: multiple writes accumulate in buffer");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ do_write(fds[1], "AAA", 3);
+ do_write(fds[1], "BBB", 3);
+
+ char buf[16] = {};
+ int64 r = do_read(fds[0], buf, 16);
+ ST_ASSERT_EQ(r, (int64)6);
+ ST_ASSERT_EQ(buf[0], 'A');
+ ST_ASSERT_EQ(buf[3], 'B');
+
+ do_close(fds[0]);
+ do_close(fds[1]);
+ }
+ st_pass();
+
+ /* ---- pipe: read partial ---- */
+ st_begin("pipe: partial read leaves remainder");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ do_write(fds[1], "ABCDEF", 6);
+
+ char buf[3] = {};
+ int64 r1 = do_read(fds[0], buf, 3);
+ ST_ASSERT_EQ(r1, (int64)3);
+ ST_ASSERT_EQ(buf[0], 'A');
+ ST_ASSERT_EQ(buf[2], 'C');
+
+ int64 r2 = do_read(fds[0], buf, 3);
+ ST_ASSERT_EQ(r2, (int64)3);
+ ST_ASSERT_EQ(buf[0], 'D');
+ ST_ASSERT_EQ(buf[2], 'F');
+
+ do_close(fds[0]);
+ do_close(fds[1]);
+ }
+ st_pass();
+}
+
+/* ================================================================== */
+/* fcntl tests */
+/* ================================================================== */
+
+void selftest_fcntl()
+{
+ /* ---- fcntl F_DUPFD ---- */
+ st_begin("fcntl: F_DUPFD duplicates fd");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 fd2 = sys_fcntl(static_cast<uint64>(fd), F_DUPFD, 0, 0, 0, 0);
+ ST_ASSERT(fd2 >= 0);
+ ST_ASSERT_NE(fd, fd2);
+
+ /* Both should be valid reads */
+ char buf[4];
+ int64 r = do_read(fd2, buf, 4);
+ ST_ASSERT_EQ(r, (int64)0); /* /dev/null → EOF */
+
+ do_close(fd);
+ do_close(fd2);
+ }
+ st_pass();
+
+ /* ---- fcntl F_DUPFD with min_fd ---- */
+ st_begin("fcntl: F_DUPFD respects min_fd");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 fd2 = sys_fcntl(static_cast<uint64>(fd), F_DUPFD, 100, 0, 0, 0);
+ ST_ASSERT(fd2 >= 100);
+
+ do_close(fd);
+ do_close(fd2);
+ }
+ st_pass();
+
+ /* ---- fcntl F_GETFD / F_SETFD ---- */
+ st_begin("fcntl: F_GETFD/F_SETFD cloexec");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ /* Initially no CLOEXEC */
+ int64 flags = sys_fcntl(static_cast<uint64>(fd), F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(flags, (int64)0);
+
+ /* Set CLOEXEC */
+ int64 ret = sys_fcntl(static_cast<uint64>(fd), F_SETFD, FD_CLOEXEC, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+
+ /* Verify it was set */
+ flags = sys_fcntl(static_cast<uint64>(fd), F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(flags, (int64)FD_CLOEXEC);
+
+ /* Clear it */
+ sys_fcntl(static_cast<uint64>(fd), F_SETFD, 0, 0, 0, 0);
+ flags = sys_fcntl(static_cast<uint64>(fd), F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(flags, (int64)0);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- fcntl F_GETFL ---- */
+ st_begin("fcntl: F_GETFL returns open flags");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 fl = sys_fcntl(static_cast<uint64>(fd), F_GETFL, 0, 0, 0, 0);
+ /* O_RDONLY == 0, so result should have no write bits */
+ ST_ASSERT_EQ(fl & O_WRONLY, (int64)0);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- fcntl F_SETFL O_NONBLOCK ---- */
+ st_begin("fcntl: F_SETFL toggles O_NONBLOCK");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ /* Set O_NONBLOCK */
+ sys_fcntl(static_cast<uint64>(fd), F_SETFL, O_NONBLOCK, 0, 0, 0);
+ int64 fl = sys_fcntl(static_cast<uint64>(fd), F_GETFL, 0, 0, 0, 0);
+ ST_ASSERT(fl & O_NONBLOCK);
+
+ /* Clear it */
+ sys_fcntl(static_cast<uint64>(fd), F_SETFL, 0, 0, 0, 0);
+ fl = sys_fcntl(static_cast<uint64>(fd), F_GETFL, 0, 0, 0, 0);
+ ST_ASSERT_EQ(fl & O_NONBLOCK, (int64)0);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- fcntl F_DUPFD_CLOEXEC ---- */
+ st_begin("fcntl: F_DUPFD_CLOEXEC sets cloexec");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 fd2 = sys_fcntl(static_cast<uint64>(fd), F_DUPFD_CLOEXEC, 0, 0, 0, 0);
+ ST_ASSERT(fd2 >= 0);
+
+ int64 flags = sys_fcntl(static_cast<uint64>(fd2), F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(flags, (int64)FD_CLOEXEC);
+
+ do_close(fd);
+ do_close(fd2);
+ }
+ st_pass();
+
+ /* ---- fcntl bad fd ---- */
+ st_begin("fcntl: bad fd returns -EBADF");
+ {
+ int64 ret = sys_fcntl(999, F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- fcntl unknown cmd ---- */
+ st_begin("fcntl: unknown cmd returns -EINVAL");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 ret = sys_fcntl(static_cast<uint64>(fd), 9999, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+ do_close(fd);
+ }
+ st_pass();
+}
diff --git a/na/test/test_pmm.cc b/na/test/test_pmm.cc
index 53b2b2a..3330866 100644
--- a/na/test/test_pmm.cc
+++ b/na/test/test_pmm.cc
@@ -25,7 +25,7 @@ void selftest_pmm()
{
Page* page = pmm_alloc_page();
ST_ASSERT(page != nullptr);
- ST_ASSERT(page->state == PageState::Allocated);
+ ST_ASSERT(page->type == PageType::Allocated);
ST_ASSERT(page->order == 0);
uint64 pa = page_to_pa(page).raw;
@@ -123,11 +123,11 @@ void selftest_pmm()
page_decref(page); // 2 -> 1, should NOT free
ST_ASSERT(page->refcount.load() == 1);
- ST_ASSERT(page->state == PageState::Allocated);
+ ST_ASSERT(page->type == PageType::Allocated);
page_decref(page); // 1 -> 0, should free to pool
// After decref to 0, page is returned to pool.
- // We can't safely check page->state because it might be reused.
+ // We can't safely check page->type because it might be reused.
}
st_pass();
st_begin("pmm: pmm_alloc_pages natural alignment (order-3)");
diff --git a/na/test/test_reloc.cc b/na/test/test_reloc.cc
deleted file mode 100644
index 54e7144..0000000
--- a/na/test/test_reloc.cc
+++ /dev/null
@@ -1,46 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-/*
- * Boot Self-Test: relocation engine
- */
-
-#include <lume/relocate.h>
-#include <lume/selftest.h>
-
-struct RelocBlob {
- RelocTableHeader hdr;
- RelocEntry entries[2];
-};
-
-void selftest_reloc()
-{
- st_begin("reloc: relative + abs64");
-
- alignas(8) uint8 buf[64] = {};
- uint64 base = reinterpret_cast<uint64>(buf);
- uint64 link_base = base - 0x1000;
-
- RelocBlob blob = {};
- blob.hdr.magic = kRelocTableMagic;
- blob.hdr.version = kRelocTableVersion;
- blob.hdr.arch = RELOC_ARCH_RISCV;
- blob.hdr.entry_size = sizeof(RelocEntry);
- blob.hdr.entry_count = 2;
-
- blob.entries[0].offset = 8;
- blob.entries[0].addend = 0x200;
- blob.entries[0].type = RELOC_RISCV_RELATIVE;
-
- blob.entries[1].offset = 16;
- blob.entries[1].addend = link_base + 0x1234;
- blob.entries[1].type = RELOC_RISCV_64;
-
- RelocStatus st = relocate_apply(base, sizeof(buf), link_base,
- &blob, sizeof(blob));
- ST_ASSERT_EQ(st, RELOC_OK);
- ST_ASSERT_EQ(*reinterpret_cast<uint64*>(base + 8), base + 0x200);
- ST_ASSERT_EQ(*reinterpret_cast<uint64*>(base + 16), base + 0x1234);
-
- st_pass();
-}
diff --git a/na/test/test_signal.cc b/na/test/test_signal.cc
new file mode 100644
index 0000000..360b0e6
--- /dev/null
+++ b/na/test/test_signal.cc
@@ -0,0 +1,193 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_signal.cc — Self-tests for signal infrastructure
+ *
+ * Tests the kernel-internal signal APIs directly (not via syscalls,
+ * since syscalls use copy_to_user which rejects kernel-space buffers).
+ */
+
+#include <lume/selftest.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+#include <lume/new.h>
+
+/* From signal.cc */
+extern int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task);
+
+/* Drain all queued sigqueue entries from a task's pending list */
+static void flush_pending(TaskControlBlock* task)
+{
+ task->pending.signal = sigset_t{};
+ list_node* head = &task->pending.list;
+ while (head->next != head) {
+ list_node* node = head->next;
+ head->next = node->next;
+ node->next->prev = head;
+ auto* q = list_entry<sigqueue, &sigqueue::list>(node);
+ delete q;
+ }
+}
+
+void selftest_signal()
+{
+ TaskControlBlock* curr = current_task();
+
+ /* Ensure sighand exists for this task */
+ if (!curr->sighand) {
+ curr->sighand = new sighand_struct;
+ curr->sighand->init();
+ }
+
+ /* Ensure pending list is initialized */
+ if (curr->pending.list.next == nullptr) {
+ curr->pending.list.next = &curr->pending.list;
+ curr->pending.list.prev = &curr->pending.list;
+ curr->pending.signal = sigset_t{};
+ }
+
+ /* ---- send_signal queues a signal ---- */
+ st_begin("signal: send_signal queues to pending");
+ {
+ curr->pending.signal = sigset_t{};
+
+ kernel_siginfo info{};
+ info.si_signo = SIGUSR1;
+ info.si_code = 0;
+
+ int ret = send_signal(SIGUSR1, &info, curr);
+ ST_ASSERT_EQ(ret, 0);
+ ST_ASSERT(curr->pending.signal.has(SIGUSR1));
+
+ flush_pending(curr);
+ }
+ st_pass();
+
+ /* ---- send_signal: invalid signal returns -EINVAL ---- */
+ st_begin("signal: send_signal invalid sig returns -EINVAL");
+ {
+ int ret = send_signal(0, nullptr, curr);
+ ST_ASSERT_EQ(ret, -EINVAL);
+
+ ret = send_signal(_NSIG + 1, nullptr, curr);
+ ST_ASSERT_EQ(ret, -EINVAL);
+ }
+ st_pass();
+
+ /* ---- send_signal: null task returns -ESRCH ---- */
+ st_begin("signal: send_signal null task returns -ESRCH");
+ {
+ int ret = send_signal(SIGUSR1, nullptr, nullptr);
+ ST_ASSERT_EQ(ret, -ESRCH);
+ }
+ st_pass();
+
+ /* ---- duplicate standard signal is coalesced ---- */
+ st_begin("signal: duplicate standard signal coalesced");
+ {
+ curr->pending.signal = sigset_t{};
+
+ send_signal(SIGUSR1, nullptr, curr);
+ ST_ASSERT(curr->pending.signal.has(SIGUSR1));
+
+ int ret = send_signal(SIGUSR1, nullptr, curr);
+ ST_ASSERT_EQ(ret, 0);
+
+ flush_pending(curr);
+ }
+ st_pass();
+
+ /* ---- sigset_t: add/del/has ---- */
+ st_begin("signal: sigset_t add/del/has");
+ {
+ sigset_t s{};
+ ST_ASSERT(!s.has(SIGUSR1));
+ s.add(SIGUSR1);
+ ST_ASSERT(s.has(SIGUSR1));
+ s.del(SIGUSR1);
+ ST_ASSERT(!s.has(SIGUSR1));
+ }
+ st_pass();
+
+ /* ---- sigset_t: bitwise operators ---- */
+ st_begin("signal: sigset_t bitwise OR and AND");
+ {
+ sigset_t a{};
+ a.add(SIGUSR1);
+ sigset_t b{};
+ b.add(SIGUSR2);
+
+ sigset_t c = a | b;
+ ST_ASSERT(c.has(SIGUSR1));
+ ST_ASSERT(c.has(SIGUSR2));
+
+ sigset_t d = c & a;
+ ST_ASSERT(d.has(SIGUSR1));
+ ST_ASSERT(!d.has(SIGUSR2));
+ }
+ st_pass();
+
+ /* ---- blocked mask: direct manipulation ---- */
+ st_begin("signal: blocked mask direct manipulation");
+ {
+ sigset_t saved = curr->blocked;
+ curr->blocked = sigset_t{};
+
+ curr->blocked.add(SIGUSR1);
+ curr->blocked.add(SIGUSR2);
+ ST_ASSERT(curr->blocked.has(SIGUSR1));
+ ST_ASSERT(curr->blocked.has(SIGUSR2));
+
+ curr->blocked.del(SIGUSR1);
+ ST_ASSERT(!curr->blocked.has(SIGUSR1));
+ ST_ASSERT(curr->blocked.has(SIGUSR2));
+
+ curr->blocked = saved;
+ }
+ st_pass();
+
+ /* ---- sighand action table ---- */
+ st_begin("signal: sighand action table defaults to SIG_DFL");
+ {
+ /* All handlers should default to SIG_DFL (0) */
+ kernel_sigaction* ka = &curr->sighand->action[SIGUSR1 - 1];
+ ST_ASSERT_EQ(ka->sa_handler, (uint64)SIG_DFL);
+
+ /* Set a handler and verify */
+ ka->sa_handler = 0x12345678ULL;
+ ST_ASSERT_EQ(ka->sa_handler, (uint64)0x12345678ULL);
+
+ /* Reset */
+ ka->sa_handler = SIG_DFL;
+ }
+ st_pass();
+
+ /* ---- sig_kernel_only rejects SIGKILL/SIGSTOP handlers ---- */
+ st_begin("signal: SIGKILL/SIGSTOP are kernel-only");
+ {
+ ST_ASSERT(sig_kernel_only(SIGKILL));
+ ST_ASSERT(sig_kernel_only(SIGSTOP));
+ ST_ASSERT(!sig_kernel_only(SIGUSR1));
+ ST_ASSERT(!sig_kernel_only(SIGINT));
+ }
+ st_pass();
+
+ /* ---- sig_default_action correctness ---- */
+ st_begin("signal: sig_default_action returns correct actions");
+ {
+ ST_ASSERT_EQ((int)sig_default_action(SIGCHLD), (int)SIG_ACT_IGN);
+ ST_ASSERT_EQ((int)sig_default_action(SIGKILL), (int)SIG_ACT_TERM);
+ ST_ASSERT_EQ((int)sig_default_action(SIGSTOP), (int)SIG_ACT_STOP);
+ ST_ASSERT_EQ((int)sig_default_action(SIGCONT), (int)SIG_ACT_CONT);
+ ST_ASSERT_EQ((int)sig_default_action(SIGSEGV), (int)SIG_ACT_CORE);
+ }
+ st_pass();
+
+ /* Clean up */
+ flush_pending(curr);
+ curr->blocked = sigset_t{};
+}
diff --git a/na/test/test_slab.cc b/na/test/test_slab.cc
index 2ba1a5d..8a448b4 100644
--- a/na/test/test_slab.cc
+++ b/na/test/test_slab.cc
@@ -203,7 +203,7 @@ void selftest_slab()
uint64 page_pa = va_to_pa(virt_addr(reinterpret_cast<uint64>(base_ptr))).raw;
Page* page = pa_to_page(phys_addr(page_pa));
- ST_ASSERT(page->state == PageState::Slab);
+ ST_ASSERT(page->type == PageType::Slab);
ST_ASSERT(page->slab.obj_count == 32);
// Free all items in this isolated slab page
@@ -212,7 +212,7 @@ void selftest_slab()
}
// The page MUST have been returned to the PMM (either global Free or Per-CPU Cached).
- ST_ASSERT(page->state == PageState::Free || page->state == PageState::PcpCached);
+ ST_ASSERT(page->type == PageType::Free || page->type == PageType::PcpCached);
// Clean up the drain array
for (int i = 0; i < drain_count; i++) {
diff --git a/na/test/test_smp.cc b/na/test/test_smp.cc
new file mode 100644
index 0000000..f934526
--- /dev/null
+++ b/na/test/test_smp.cc
@@ -0,0 +1,136 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_smp.cc — Post-SMP boot tests
+ *
+ * These tests run after all APs are online. They verify:
+ * 1. IPI delivery — BSP sends IPI to each AP, AP increments counter.
+ * 2. Task migration — a task moves from one runqueue to another.
+ * 3. TLB shootdown IPI — cross-CPU flush.
+ *
+ * Called from the init thread (PID 1) context, NOT from the pre-SMP
+ * self-test suite.
+ */
+
+#include <lume/selftest.h>
+#include <lume/smp.h>
+#include <lume/sched.h>
+#include <lume/atomic.h>
+#include <lume/config.h>
+#include <arch/cpu.h>
+
+/* Shared counter incremented by APs when they receive an IPI */
+static lume::atomic<uint32> g_ipi_ack_count{0};
+
+/* ------------------------------------------------------------------ */
+/* IPI delivery test */
+/* ------------------------------------------------------------------ */
+
+void selftest_ipi_delivery()
+{
+ st_begin("smp: IPI delivery to all APs");
+
+ /* Reset counter */
+ g_ipi_ack_count.store(0, __ATOMIC_RELAXED);
+
+ uint32 bsp = static_cast<uint32>(arch::cpu::id());
+ uint32 num_cpus = 4; /* TODO: derive from FDT */
+
+ /* Build mask of all APs */
+ uint64 ap_mask = 0;
+ uint32 expected = 0;
+ for (uint32 i = 0; i < num_cpus; i++) {
+ if (i == bsp) continue;
+ ap_mask |= (1ULL << i);
+ expected++;
+ }
+
+ /* Send reschedule IPI to all APs */
+ smp_send_ipi(ap_mask, IPI_RESCHEDULE);
+
+ /* Wait a short while for APs to process the interrupt.
+ * APs are in wfi (idle loop), so they'll wake immediately
+ * on interrupt delivery. We just need to give them time
+ * to run the handler. */
+ for (volatile int i = 0; i < 100000; i++) {
+ /* spin-wait */
+ }
+
+ /* We can't directly check that IPI was received because
+ * IPI_RESCHEDULE just sets need_resched — no ack.
+ * Instead verify no crash occurred and the IPI infrastructure
+ * didn't deadlock. This is a liveness test. */
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* IPI halt test (send to self, skip) */
+/* ------------------------------------------------------------------ */
+
+void selftest_ipi_self_skip()
+{
+ st_begin("smp: IPI reason bitmask accumulation");
+
+ uint32 cpu = static_cast<uint32>(arch::cpu::id());
+
+ /* Manually set multiple reasons atomically */
+ g_ipi_data[cpu].pending.store(0, __ATOMIC_RELAXED);
+ g_ipi_data[cpu].pending.fetch_or(IPI_RESCHEDULE, __ATOMIC_RELEASE);
+ g_ipi_data[cpu].pending.fetch_or(IPI_TLB_SHOOTDOWN, __ATOMIC_RELEASE);
+
+ uint32 val = g_ipi_data[cpu].pending.load(__ATOMIC_ACQUIRE);
+ ST_ASSERT_EQ(val, IPI_RESCHEDULE | IPI_TLB_SHOOTDOWN);
+
+ /* Clean up */
+ g_ipi_data[cpu].pending.store(0, __ATOMIC_RELAXED);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* TLB shootdown IPI test */
+/* ------------------------------------------------------------------ */
+
+void selftest_tlb_shootdown()
+{
+ st_begin("smp: TLB shootdown IPI (cross-CPU flush)");
+
+ uint32 bsp = static_cast<uint32>(arch::cpu::id());
+ uint32 num_cpus = 4;
+
+ /* Build AP mask */
+ uint64 ap_mask = 0;
+ for (uint32 i = 0; i < num_cpus; i++) {
+ if (i == bsp) continue;
+ ap_mask |= (1ULL << i);
+ }
+
+ /* Set TLB range on each AP's IpiData */
+ for (uint32 i = 0; i < num_cpus; i++) {
+ if (i == bsp) continue;
+ g_ipi_data[i].tlb_start = 0;
+ g_ipi_data[i].tlb_end = 0; /* 0,0 = full flush */
+ }
+
+ /* Send TLB shootdown */
+ smp_send_ipi(ap_mask, IPI_TLB_SHOOTDOWN);
+
+ /* Wait for APs to process */
+ for (volatile int i = 0; i < 100000; i++) {}
+
+ /* If we got here without panic, TLB shootdown succeeded */
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Public entry point */
+/* ------------------------------------------------------------------ */
+
+void selftest_smp()
+{
+ selftest_ipi_delivery();
+ selftest_ipi_self_skip();
+ selftest_tlb_shootdown();
+}
diff --git a/na/test/test_sys_fs_extra.cc b/na/test/test_sys_fs_extra.cc
new file mode 100644
index 0000000..705ce63
--- /dev/null
+++ b/na/test/test_sys_fs_extra.cc
@@ -0,0 +1,238 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_sys_fs_extra.cc — Self-tests for additional filesystem syscalls
+ *
+ * Tests: dup3, chdir/getcwd, writev, ioctl (stub), unlinkat (stub).
+ * Complements test_vfs_io.cc which covers the core read/write/open/close.
+ */
+
+#include <lume/selftest.h>
+#include <lume/errno.h>
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/fs_types.h>
+#include <lume/task.h>
+
+/* Syscall entry points */
+int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_dup3(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_chdir(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getcwd(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_writev(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_ioctl(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_unlinkat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_lseek(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* ================================================================== */
+/* Helpers */
+/* ================================================================== */
+
+static int64 open_dev(const char* path, int flags)
+{
+ return sys_openat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>(path),
+ static_cast<uint64>(flags), 0, 0, 0);
+}
+
+static int64 do_close(int64 fd)
+{
+ return sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+}
+
+static int64 do_read(int64 fd, void* buf, uint64 count)
+{
+ return sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), count, 0, 0, 0);
+}
+
+
+/* iovec must match sys_fs.cc definition */
+struct test_iovec {
+ void* iov_base;
+ uint64 iov_len;
+};
+
+/* ================================================================== */
+/* Tests */
+/* ================================================================== */
+
+void selftest_sys_fs_extra()
+{
+ /* ---- dup3: duplicate fd to specific target ---- */
+ st_begin("sys_fs: dup3 duplicates to specific fd");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ /* dup3 to fd 50 */
+ int64 fd2 = sys_dup3(static_cast<uint64>(fd), 50, 0, 0, 0, 0);
+ ST_ASSERT_EQ(fd2, (int64)50);
+
+ /* Both should work */
+ char buf[4];
+ int64 r = do_read(fd2, buf, 4);
+ ST_ASSERT_EQ(r, (int64)0); /* /dev/null → EOF */
+
+ do_close(fd);
+ do_close(fd2);
+ }
+ st_pass();
+
+ /* ---- dup3: same fd returns -EINVAL ---- */
+ st_begin("sys_fs: dup3 same fd returns -EINVAL");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 ret = sys_dup3(static_cast<uint64>(fd),
+ static_cast<uint64>(fd), 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- chdir to /dev, getcwd returns /dev ---- */
+ st_begin("sys_fs: chdir then getcwd returns new dir");
+ {
+ /* Save cwd */
+ char saved_cwd[64] = {};
+ sys_getcwd(reinterpret_cast<uint64>(saved_cwd), 64, 0, 0, 0, 0);
+
+ /* chdir to /dev */
+ int64 ret = sys_chdir(
+ reinterpret_cast<uint64>("/dev"), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+
+ /* getcwd should return /dev */
+ char buf[64] = {};
+ int64 len = sys_getcwd(
+ reinterpret_cast<uint64>(buf), 64, 0, 0, 0, 0);
+ ST_ASSERT(len > 0);
+ ST_ASSERT_EQ(buf[0], '/');
+ ST_ASSERT_EQ(buf[1], 'd');
+ ST_ASSERT_EQ(buf[2], 'e');
+ ST_ASSERT_EQ(buf[3], 'v');
+
+ /* Restore cwd to / */
+ sys_chdir(reinterpret_cast<uint64>("/"), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- chdir to nonexistent returns -ENOENT ---- */
+ st_begin("sys_fs: chdir nonexistent returns -ENOENT");
+ {
+ int64 ret = sys_chdir(
+ reinterpret_cast<uint64>("/no_such_dir"), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-ENOENT);
+ }
+ st_pass();
+
+ /* ---- chdir to file returns -ENOTDIR ---- */
+ st_begin("sys_fs: chdir to file returns -ENOTDIR");
+ {
+ int64 ret = sys_chdir(
+ reinterpret_cast<uint64>("/dev/null"), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-ENOTDIR);
+ }
+ st_pass();
+
+ /* ---- writev: scatter write to file ---- */
+ st_begin("sys_fs: writev scatter write");
+ {
+ /* Create a file, write with writev, read back */
+ int64 fd = sys_openat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>("/writev_test"),
+ O_CREAT | O_RDWR, 0644, 0, 0);
+ ST_ASSERT(fd >= 0);
+
+ char a[] = "AAA";
+ char b[] = "BBB";
+ test_iovec iov[2];
+ iov[0].iov_base = a;
+ iov[0].iov_len = 3;
+ iov[1].iov_base = b;
+ iov[1].iov_len = 3;
+
+ int64 w = sys_writev(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(iov), 2, 0, 0, 0);
+ ST_ASSERT_EQ(w, (int64)6);
+
+ /* Seek back to start and read */
+ sys_lseek(static_cast<uint64>(fd), 0, 0 /* SEEK_SET */, 0, 0, 0);
+
+ char buf[8] = {};
+ int64 r = do_read(fd, buf, 8);
+ ST_ASSERT_EQ(r, (int64)6);
+ ST_ASSERT_EQ(buf[0], 'A');
+ ST_ASSERT_EQ(buf[3], 'B');
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- writev: bad fd returns -EBADF ---- */
+ st_begin("sys_fs: writev bad fd returns -EBADF");
+ {
+ test_iovec iov[1];
+ char dummy[] = "x";
+ iov[0].iov_base = dummy;
+ iov[0].iov_len = 1;
+
+ int64 ret = sys_writev(999,
+ reinterpret_cast<uint64>(iov), 1, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- ioctl stub returns -ENOTTY ---- */
+ st_begin("sys_fs: ioctl stub returns -ENOTTY");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 ret = sys_ioctl(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-ENOTTY);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- unlinkat stub returns -ENOSYS ---- */
+ st_begin("sys_fs: unlinkat stub returns -ENOSYS");
+ {
+ int64 ret = sys_unlinkat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>("/some_file"),
+ 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-ENOSYS);
+ }
+ st_pass();
+
+ /* ---- getcwd: null buf returns -EINVAL ---- */
+ st_begin("sys_fs: getcwd null buf returns -EINVAL");
+ {
+ int64 ret = sys_getcwd(0, 64, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+ }
+ st_pass();
+
+ /* ---- getcwd: zero size returns -EINVAL ---- */
+ st_begin("sys_fs: getcwd zero size returns -EINVAL");
+ {
+ char buf[4];
+ int64 ret = sys_getcwd(
+ reinterpret_cast<uint64>(buf), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+ }
+ st_pass();
+}
diff --git a/na/test/test_sys_process.cc b/na/test/test_sys_process.cc
new file mode 100644
index 0000000..686bed6
--- /dev/null
+++ b/na/test/test_sys_process.cc
@@ -0,0 +1,93 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_sys_process.cc — Self-tests for process-control syscalls
+ *
+ * Tests: getpid, gettid, getppid, getuid, getgid, set_tid_address,
+ * sched_yield, uname.
+ */
+
+#include <lume/selftest.h>
+#include <lume/errno.h>
+#include <lume/task.h>
+#include <lume/types.h>
+
+/* Syscall entry points */
+int64 sys_getpid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_gettid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getppid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getuid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getgid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_set_tid_address(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_sched_yield_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+
+void selftest_sys_process()
+{
+ /* ---- getpid returns current tgid ---- */
+ st_begin("sys_process: getpid returns valid pid");
+ {
+ int64 pid = sys_getpid(0, 0, 0, 0, 0, 0);
+ /* Boot task pid is -1 but tgid may differ; just verify it returns */
+ TaskControlBlock* curr = current_task();
+ ST_ASSERT_EQ(pid, (int64)curr->tgid);
+ }
+ st_pass();
+
+ /* ---- gettid returns current pid (tid) ---- */
+ st_begin("sys_process: gettid returns current tid");
+ {
+ int64 tid = sys_gettid(0, 0, 0, 0, 0, 0);
+ TaskControlBlock* curr = current_task();
+ ST_ASSERT_EQ(tid, (int64)curr->pid);
+ }
+ st_pass();
+
+ /* ---- getppid returns parent tgid ---- */
+ st_begin("sys_process: getppid returns parent tgid");
+ {
+ int64 ppid = sys_getppid(0, 0, 0, 0, 0, 0);
+ TaskControlBlock* curr = current_task();
+ int64 expected = curr->parent ? (int64)curr->parent->tgid : 0;
+ ST_ASSERT_EQ(ppid, expected);
+ }
+ st_pass();
+
+ /* ---- getuid returns 0 (root) ---- */
+ st_begin("sys_process: getuid returns 0 (root)");
+ {
+ int64 uid = sys_getuid(0, 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(uid, (int64)0);
+ }
+ st_pass();
+
+ /* ---- getgid returns 0 (root) ---- */
+ st_begin("sys_process: getgid returns 0 (root)");
+ {
+ int64 gid = sys_getgid(0, 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(gid, (int64)0);
+ }
+ st_pass();
+
+ /* ---- set_tid_address stores pointer and returns tid ---- */
+ st_begin("sys_process: set_tid_address returns tid");
+ {
+ uint64 dummy_addr = 0x12345678ULL;
+ int64 tid = sys_set_tid_address(dummy_addr, 0, 0, 0, 0, 0);
+ TaskControlBlock* curr = current_task();
+ ST_ASSERT_EQ(tid, (int64)curr->pid);
+ ST_ASSERT_EQ(curr->clear_child_tid, dummy_addr);
+
+ /* Reset it to avoid side effects */
+ curr->clear_child_tid = 0;
+ }
+ st_pass();
+
+ /* ---- sched_yield returns 0 ---- */
+ st_begin("sys_process: sched_yield returns 0");
+ {
+ int64 ret = sys_sched_yield_wrapper(0, 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+ }
+}
+
diff --git a/na/test/test_uaccess.cc b/na/test/test_uaccess.cc
index d5795fc..c5a8c6c 100644
--- a/na/test/test_uaccess.cc
+++ b/na/test/test_uaccess.cc
@@ -7,13 +7,13 @@
#include <lume/vmm.h>
#include <lume/pmm.h>
+#include <lume/tlb.h>
+#include <lume/mm_layout.h>
#include <arch/pmap.h>
-constexpr uint64 kKernelVABase = 0xFFFFFFC000000000ULL;
-
void selftest_uaccess()
{
- /* To test valid user addresses (< kKernelVABase), we need a writable page mapped there.
+ /* To test valid user addresses (< kKernelVaBase), we need a writable page mapped there.
* We cannot use VmSpace::create() because it does not yet inherit kernel mappings,
* so switching to it hangs. We also cannot map it with the U bit because S-mode
* would need the SUM bit enabled.
@@ -33,7 +33,7 @@ void selftest_uaccess()
pmap::map(root_pa, mock_user_addr, paddr, pmap::PTE_R | pmap::PTE_W | pmap::PTE_V | pmap::PTE_A | pmap::PTE_D);
/* Flush TLB to see the new mapping */
- asm volatile("sfence.vma" : : : "memory");
+ tlb_shootdown_all();
st_begin("uaccess: copy_to_user valid range");
char kernel_buf[32] = "Hello User Space!";
@@ -67,12 +67,12 @@ void selftest_uaccess()
st_pass();
st_begin("uaccess: copy_to_user invalid range (crosses boundary)");
- ret = copy_to_user(kKernelVABase - 10, kernel_buf, 20);
+ ret = copy_to_user(kKernelVaBase - 10, kernel_buf, 20);
ST_ASSERT_EQ(ret, -EFAULT);
st_pass();
st_begin("uaccess: copy_to_user invalid range (entirely in kernel)");
- ret = copy_to_user(kKernelVABase + 0x1000, kernel_buf, 20);
+ ret = copy_to_user(kKernelVaBase + 0x1000, kernel_buf, 20);
ST_ASSERT_EQ(ret, -EFAULT);
st_pass();
@@ -82,15 +82,15 @@ void selftest_uaccess()
st_pass();
st_begin("uaccess: copy_from_user invalid ranges");
- ret = copy_from_user(read_buf, kKernelVABase - 5, 10);
+ ret = copy_from_user(read_buf, kKernelVaBase - 5, 10);
ST_ASSERT_EQ(ret, -EFAULT);
- ret = copy_from_user(read_buf, kKernelVABase, 10);
+ ret = copy_from_user(read_buf, kKernelVaBase, 10);
ST_ASSERT_EQ(ret, -EFAULT);
st_pass();
st_begin("uaccess: strncpy_from_user invalid range");
- uint64 invalid_addr = kKernelVABase + 0x1000;
+ uint64 invalid_addr = kKernelVaBase + 0x1000;
len = strncpy_from_user(str_buf, invalid_addr, 10);
ST_ASSERT_EQ(len, -EFAULT);
st_pass();
diff --git a/na/test/test_vfs_io.cc b/na/test/test_vfs_io.cc
new file mode 100644
index 0000000..3d3b125
--- /dev/null
+++ b/na/test/test_vfs_io.cc
@@ -0,0 +1,345 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_vfs_io.cc — Self-tests for VFS file I/O via syscalls
+ *
+ * Tests openat, read, write, close, lseek, dup, fstat, getdents64,
+ * getcwd, chdir, and mkdirat through the kernel syscall layer.
+ */
+
+#include <lume/selftest.h>
+#include <lume/errno.h>
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/fs_types.h>
+#include <lume/task.h>
+#include <lume/fdtable.h>
+
+/* Syscall entry points */
+int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_lseek(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_dup(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_dup3(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_fstat_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getcwd(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_chdir(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getdents64(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* Helper: open a file, returns fd or negative error */
+static int64 open_file(const char* path, int flags, uint32 mode)
+{
+ return sys_openat(
+ static_cast<uint64>(-100), /* AT_FDCWD */
+ reinterpret_cast<uint64>(path),
+ static_cast<uint64>(flags),
+ static_cast<uint64>(mode), 0, 0);
+}
+
+void selftest_vfs_io()
+{
+ /* ---- openat O_CREAT creates a file ---- */
+ st_begin("vfs_io: openat O_CREAT creates file");
+ {
+ int64 fd = open_file("/vfs_test_file", O_CREAT | O_WRONLY, 0644);
+ ST_ASSERT(fd >= 0);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- openat existing file ---- */
+ st_begin("vfs_io: openat existing file succeeds");
+ {
+ int64 fd = open_file("/vfs_test_file", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- openat nonexistent without O_CREAT returns -ENOENT ---- */
+ st_begin("vfs_io: openat nonexistent returns -ENOENT");
+ {
+ int64 fd = open_file("/no_such_file_ever", O_RDONLY, 0);
+ ST_ASSERT_EQ(fd, (int64)-ENOENT);
+ }
+ st_pass();
+
+ /* ---- write then read ---- */
+ st_begin("vfs_io: write then read back data");
+ {
+ /* Write */
+ int64 fd = open_file("/vfs_rw_test", O_CREAT | O_WRONLY, 0644);
+ ST_ASSERT(fd >= 0);
+
+ const char* msg = "Hello VFS!";
+ int64 written = sys_write(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(msg),
+ 10, 0, 0, 0);
+ ST_ASSERT_EQ(written, (int64)10);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+
+ /* Read */
+ fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[32] = {};
+ int64 nread = sys_read(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf),
+ sizeof(buf), 0, 0, 0);
+ ST_ASSERT_EQ(nread, (int64)10);
+
+ bool match = true;
+ for (int i = 0; i < 10; i++) {
+ if (buf[i] != msg[i]) { match = false; break; }
+ }
+ ST_ASSERT(match);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- read past EOF returns 0 ---- */
+ st_begin("vfs_io: read past EOF returns 0");
+ {
+ int64 fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[64];
+ /* First read: 10 bytes */
+ int64 n1 = sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), 64, 0, 0, 0);
+ ST_ASSERT_EQ(n1, (int64)10);
+
+ /* Second read: should be 0 (EOF) */
+ int64 n2 = sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), 64, 0, 0, 0);
+ ST_ASSERT_EQ(n2, (int64)0);
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- O_TRUNC clears file content ---- */
+ st_begin("vfs_io: O_TRUNC clears file content");
+ {
+ int64 fd = open_file("/vfs_rw_test", O_WRONLY | O_TRUNC, 0);
+ ST_ASSERT(fd >= 0);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+
+ fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[16];
+ int64 n = sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), 16, 0, 0, 0);
+ ST_ASSERT_EQ(n, (int64)0); /* truncated = empty */
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- close invalid fd returns -EBADF ---- */
+ st_begin("vfs_io: close bad fd returns -EBADF");
+ {
+ int64 ret = sys_close(999, 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- read/write on bad fd returns -EBADF ---- */
+ st_begin("vfs_io: read/write bad fd returns -EBADF");
+ {
+ char buf[8];
+ int64 r = sys_read(999, reinterpret_cast<uint64>(buf), 8, 0, 0, 0);
+ ST_ASSERT_EQ(r, (int64)-EBADF);
+
+ int64 w = sys_write(999, reinterpret_cast<uint64>(buf), 8, 0, 0, 0);
+ ST_ASSERT_EQ(w, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- lseek SEEK_SET / SEEK_CUR / SEEK_END ---- */
+ st_begin("vfs_io: lseek SEEK_SET/CUR/END");
+ {
+ /* Write 20 bytes */
+ int64 fd = open_file("/vfs_lseek_test", O_CREAT | O_RDWR, 0644);
+ ST_ASSERT(fd >= 0);
+
+ char data[20];
+ for (int i = 0; i < 20; i++) data[i] = static_cast<char>('A' + i);
+ sys_write(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(data), 20, 0, 0, 0);
+
+ /* SEEK_SET to 5 */
+ int64 pos = sys_lseek(static_cast<uint64>(fd), 5, SEEK_SET, 0, 0, 0);
+ ST_ASSERT_EQ(pos, (int64)5);
+
+ /* Read 1 byte, should be 'F' */
+ char c = 0;
+ sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&c), 1, 0, 0, 0);
+ ST_ASSERT_EQ(c, 'F');
+
+ /* SEEK_CUR +3 → pos 9 */
+ pos = sys_lseek(static_cast<uint64>(fd), 3, SEEK_CUR, 0, 0, 0);
+ ST_ASSERT_EQ(pos, (int64)9);
+
+ /* SEEK_END -1 → pos 19 */
+ pos = sys_lseek(static_cast<uint64>(fd), -1, SEEK_END, 0, 0, 0);
+ ST_ASSERT_EQ(pos, (int64)19);
+
+ sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&c), 1, 0, 0, 0);
+ ST_ASSERT_EQ(c, 'T');
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- lseek invalid whence returns -EINVAL ---- */
+ st_begin("vfs_io: lseek invalid whence");
+ {
+ int64 fd = open_file("/vfs_lseek_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+ int64 ret = sys_lseek(static_cast<uint64>(fd), 0, 99, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- dup: duplicate fd shares file position ---- */
+ st_begin("vfs_io: dup shares file position");
+ {
+ int64 fd1 = open_file("/vfs_lseek_test", O_RDONLY, 0);
+ ST_ASSERT(fd1 >= 0);
+
+ int64 fd2 = sys_dup(static_cast<uint64>(fd1), 0, 0, 0, 0, 0);
+ ST_ASSERT(fd2 >= 0);
+ ST_ASSERT_NE(fd1, fd2);
+
+ /* Both should work for reading */
+ char c = 0;
+ int64 n = sys_read(static_cast<uint64>(fd2),
+ reinterpret_cast<uint64>(&c), 1, 0, 0, 0);
+ ST_ASSERT_EQ(n, (int64)1);
+
+ sys_close(static_cast<uint64>(fd1), 0, 0, 0, 0, 0);
+ sys_close(static_cast<uint64>(fd2), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- fstat: basic stat ---- */
+ st_begin("vfs_io: fstat returns valid kstat");
+ {
+ int64 fd = open_file("/vfs_lseek_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ kstat st{};
+ int64 ret = sys_fstat_wrapper(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&st), 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+ ST_ASSERT(S_ISREG(st.st_mode));
+ ST_ASSERT_EQ(st.st_size, (int64)20);
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- getcwd: returns "/" ---- */
+ st_begin("vfs_io: getcwd returns /");
+ {
+ char buf[64] = {};
+ int64 len = sys_getcwd(
+ reinterpret_cast<uint64>(buf), 64, 0, 0, 0, 0);
+ ST_ASSERT(len > 0);
+ ST_ASSERT_EQ(buf[0], '/');
+ }
+ st_pass();
+
+ /* ---- mkdirat: create directory ---- */
+ st_begin("vfs_io: mkdirat creates directory");
+ {
+ int64 ret = sys_mkdirat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>("/vfs_io_dir"),
+ 0755, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+
+ /* Open it to confirm it's a directory */
+ int64 fd = open_file("/vfs_io_dir", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ kstat st{};
+ sys_fstat_wrapper(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&st), 0, 0, 0, 0);
+ ST_ASSERT(S_ISDIR(st.st_mode));
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- getdents64: list root directory ---- */
+ st_begin("vfs_io: getdents64 lists root children");
+ {
+ int64 fd = open_file("/", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[512] = {};
+ int64 n = sys_getdents64(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf),
+ 512, 0, 0, 0);
+ ST_ASSERT(n > 0); /* At least "." and ".." */
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- write to O_RDONLY fd returns -EBADF ---- */
+ st_begin("vfs_io: write to O_RDONLY returns -EBADF");
+ {
+ int64 fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[] = "x";
+ int64 w = sys_write(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), 1, 0, 0, 0);
+ ST_ASSERT_EQ(w, (int64)-EBADF);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- double close returns -EBADF ---- */
+ st_begin("vfs_io: double close returns -EBADF");
+ {
+ int64 fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ int64 r1 = sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(r1, (int64)0);
+
+ int64 r2 = sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(r2, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- fd reuse after close ---- */
+ st_begin("vfs_io: fd reuse after close");
+ {
+ int64 fd1 = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd1 >= 0);
+ sys_close(static_cast<uint64>(fd1), 0, 0, 0, 0, 0);
+
+ int64 fd2 = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd2 >= 0);
+ /* fd2 should reuse the slot of fd1 */
+ ST_ASSERT(fd2 <= fd1);
+ sys_close(static_cast<uint64>(fd2), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+}
diff --git a/na/test/test_vmm.cc b/na/test/test_vmm.cc
index 049570f..635e3f3 100644
--- a/na/test/test_vmm.cc
+++ b/na/test/test_vmm.cc
@@ -6,12 +6,17 @@
*/
#include <lume/selftest.h>
+#include <lume/klog.h>
+#include <lume/config.h>
#include <lume/addr.h>
#include <lume/vmm.h>
#include <lume/pmm.h>
#include <arch/config.h>
-/* Assuming pmap namespace is accessible or we can test basic structures */
+namespace {
+constexpr uint64 kUartBase = 0x10000000ULL;
+constexpr uint64 kPageOffsetMask = kPageSize - 1;
+}
void selftest_vmm() {
st_begin("vmm: virtual/physical address translation");
@@ -30,7 +35,7 @@ void selftest_vmm() {
st_begin("vmm: page alignment macros");
{
uint64 base = pmm_get_mem_base().raw;
- uint64 addr = base + 0xFFF;
+ uint64 addr = base + kPageOffsetMask;
ST_ASSERT_EQ(page_align_down(addr), base);
ST_ASSERT_EQ(page_align_up(addr), base + kPageSize);
ST_ASSERT_EQ(page_align_down(base), base);
@@ -40,8 +45,10 @@ void selftest_vmm() {
st_begin("vmm: mapping kernel MMIO (driver API)");
{
- uint64 mmio_pa = pmm_get_mem_base().raw;
- vmm_map_kernel_mmio(mmio_pa, 0x1000);
+ /* Use UART MMIO address (0x10000000 on RISC-V virt).
+ * Must NOT use pmm_get_mem_base() — that's PMP-protected firmware. */
+ uint64 mmio_pa = kUartBase;
+ vmm_map_kernel_mmio(mmio_pa, kPageSize);
// Verify it doesn't crash and address is reachable in higher half
uint64 va = pa_to_va(phys_addr(mmio_pa)).raw;
diff --git a/na/test/test_waitqueue.cc b/na/test/test_waitqueue.cc
index 1bf88bb..7f50d6b 100644
--- a/na/test/test_waitqueue.cc
+++ b/na/test/test_waitqueue.cc
@@ -59,7 +59,7 @@ void selftest_waitqueue()
kprintf("[test_wq] returned from yield 2\n");
ST_ASSERT(g_thread_woken == true);
-
+
st_pass();
st_begin("waitqueue: wake_all");
diff --git a/na/usr/default_cpio_list b/na/usr/default_cpio_list
new file mode 100644
index 0000000..6721365
--- /dev/null
+++ b/na/usr/default_cpio_list
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Default initramfs source — minimal, no userland.
+#
+# Mirrors Linux's usr/default_cpio_list: just enough nodes for the kernel
+# to mount a root and panic cleanly on "no init found". Real workloads
+# override CONFIG_INITRAMFS_SOURCE.
+#
+# Format (matches gen_init_cpio):
+# dir <path> <mode> <uid> <gid>
+# nod <path> <mode> <uid> <gid> <c|b> <maj> <min>
+# file <path> <hostsrc> <mode> <uid> <gid>
+# slink <path> <target> <mode> <uid> <gid>
+# Lines starting with '#' are comments. Blank lines are ignored.
+
+dir /dev 0755 0 0
+nod /dev/console 0600 0 0 c 5 1
+nod /dev/null 0666 0 0 c 1 3
+nod /dev/zero 0666 0 0 c 1 5
+dir /proc 0755 0 0
+dir /sys 0755 0 0
+dir /tmp 1777 0 0
+dir /sbin 0755 0 0
+dir /bin 0755 0 0
+dir /etc 0755 0 0
diff --git a/na/usr/gen_init_cpio.c b/na/usr/gen_init_cpio.c
new file mode 100644
index 0000000..b7296ed
--- /dev/null
+++ b/na/usr/gen_init_cpio.c
@@ -0,0 +1,781 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <ctype.h>
+#include <limits.h>
+
+/*
+ * Original work by Jeff Garzik
+ *
+ * External file lists, symlink, pipe and fifo support by Thayne Harbaugh
+ * Hard link support by Luciano Rocha
+ */
+
+#define xstr(s) #s
+#define str(s) xstr(s)
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define CPIO_HDR_LEN 110
+#define CPIO_TRAILER "TRAILER!!!"
+#define padlen(_off, _align) (((_align) - ((_off) & ((_align) - 1))) % (_align))
+
+/* zero-padding the filename field for data alignment is limited by PATH_MAX */
+static char padding[PATH_MAX];
+static unsigned int offset;
+static unsigned int ino = 721;
+static time_t default_mtime;
+static bool do_file_mtime;
+static bool do_csum = false;
+static int outfd = STDOUT_FILENO;
+static unsigned int dalign;
+
+struct file_handler {
+ const char *type;
+ int (*handler)(const char *line);
+};
+
+static int push_buf(const char *name, size_t name_len)
+{
+ ssize_t len;
+
+ len = write(outfd, name, name_len);
+ if (len != name_len)
+ return -1;
+
+ offset += name_len;
+ return 0;
+}
+
+static int push_pad(size_t padlen)
+{
+ ssize_t len = 0;
+
+ if (!padlen)
+ return 0;
+
+ if (padlen < sizeof(padding))
+ len = write(outfd, padding, padlen);
+ if (len != padlen)
+ return -1;
+
+ offset += padlen;
+ return 0;
+}
+
+static int push_rest(const char *name, size_t name_len)
+{
+ ssize_t len;
+
+ len = write(outfd, name, name_len);
+ if (len != name_len)
+ return -1;
+
+ offset += name_len;
+
+ return push_pad(padlen(name_len + CPIO_HDR_LEN, 4));
+}
+
+static int cpio_trailer(void)
+{
+ int len;
+ unsigned int namesize = sizeof(CPIO_TRAILER);
+
+ len = dprintf(outfd, "%s%08X%08X%08lX%08lX%08X%08lX"
+ "%08X%08X%08X%08X%08X%08X%08X",
+ do_csum ? "070702" : "070701", /* magic */
+ 0, /* ino */
+ 0, /* mode */
+ (long) 0, /* uid */
+ (long) 0, /* gid */
+ 1, /* nlink */
+ (long) 0, /* mtime */
+ 0, /* filesize */
+ 0, /* major */
+ 0, /* minor */
+ 0, /* rmajor */
+ 0, /* rminor */
+ namesize, /* namesize */
+ 0); /* chksum */
+ offset += len;
+
+ if (len != CPIO_HDR_LEN ||
+ push_rest(CPIO_TRAILER, namesize) < 0 ||
+ push_pad(padlen(offset, 512)) < 0)
+ return -1;
+
+ if (fsync(outfd) < 0 && errno != EINVAL)
+ return -1;
+
+ return 0;
+}
+
+static int cpio_mkslink(const char *name, const char *target,
+ unsigned int mode, uid_t uid, gid_t gid)
+{
+ int len;
+ unsigned int namesize, targetsize = strlen(target) + 1;
+
+ if (name[0] == '/')
+ name++;
+ namesize = strlen(name) + 1;
+
+ len = dprintf(outfd, "%s%08X%08X%08lX%08lX%08X%08lX"
+ "%08X%08X%08X%08X%08X%08X%08X",
+ do_csum ? "070702" : "070701", /* magic */
+ ino++, /* ino */
+ S_IFLNK | mode, /* mode */
+ (long) uid, /* uid */
+ (long) gid, /* gid */
+ 1, /* nlink */
+ (long) default_mtime, /* mtime */
+ targetsize, /* filesize */
+ 3, /* major */
+ 1, /* minor */
+ 0, /* rmajor */
+ 0, /* rminor */
+ namesize, /* namesize */
+ 0); /* chksum */
+ offset += len;
+
+ if (len != CPIO_HDR_LEN ||
+ push_buf(name, namesize) < 0 ||
+ push_pad(padlen(offset, 4)) < 0 ||
+ push_buf(target, targetsize) < 0 ||
+ push_pad(padlen(offset, 4)) < 0)
+ return -1;
+
+ return 0;
+
+}
+
+static int cpio_mkslink_line(const char *line)
+{
+ char name[PATH_MAX + 1];
+ char target[PATH_MAX + 1];
+ unsigned int mode;
+ int uid;
+ int gid;
+ int rc = -1;
+
+ if (5 != sscanf(line, "%" str(PATH_MAX) "s %" str(PATH_MAX) "s %o %d %d", name, target, &mode, &uid, &gid)) {
+ fprintf(stderr, "Unrecognized dir format '%s'", line);
+ goto fail;
+ }
+ rc = cpio_mkslink(name, target, mode, uid, gid);
+ fail:
+ return rc;
+}
+
+static int cpio_mkgeneric(const char *name, unsigned int mode,
+ uid_t uid, gid_t gid)
+{
+ int len;
+ unsigned int namesize;
+
+ if (name[0] == '/')
+ name++;
+ namesize = strlen(name) + 1;
+
+ len = dprintf(outfd, "%s%08X%08X%08lX%08lX%08X%08lX"
+ "%08X%08X%08X%08X%08X%08X%08X",
+ do_csum ? "070702" : "070701", /* magic */
+ ino++, /* ino */
+ mode, /* mode */
+ (long) uid, /* uid */
+ (long) gid, /* gid */
+ 2, /* nlink */
+ (long) default_mtime, /* mtime */
+ 0, /* filesize */
+ 3, /* major */
+ 1, /* minor */
+ 0, /* rmajor */
+ 0, /* rminor */
+ namesize, /* namesize */
+ 0); /* chksum */
+ offset += len;
+
+ if (len != CPIO_HDR_LEN ||
+ push_rest(name, namesize) < 0)
+ return -1;
+
+ return 0;
+}
+
+enum generic_types {
+ GT_DIR,
+ GT_PIPE,
+ GT_SOCK
+};
+
+struct generic_type {
+ const char *type;
+ mode_t mode;
+};
+
+static const struct generic_type generic_type_table[] = {
+ [GT_DIR] = {
+ .type = "dir",
+ .mode = S_IFDIR
+ },
+ [GT_PIPE] = {
+ .type = "pipe",
+ .mode = S_IFIFO
+ },
+ [GT_SOCK] = {
+ .type = "sock",
+ .mode = S_IFSOCK
+ }
+};
+
+static int cpio_mkgeneric_line(const char *line, enum generic_types gt)
+{
+ char name[PATH_MAX + 1];
+ unsigned int mode;
+ int uid;
+ int gid;
+ int rc = -1;
+
+ if (4 != sscanf(line, "%" str(PATH_MAX) "s %o %d %d", name, &mode, &uid, &gid)) {
+ fprintf(stderr, "Unrecognized %s format '%s'",
+ line, generic_type_table[gt].type);
+ goto fail;
+ }
+ mode |= generic_type_table[gt].mode;
+ rc = cpio_mkgeneric(name, mode, uid, gid);
+ fail:
+ return rc;
+}
+
+static int cpio_mkdir_line(const char *line)
+{
+ return cpio_mkgeneric_line(line, GT_DIR);
+}
+
+static int cpio_mkpipe_line(const char *line)
+{
+ return cpio_mkgeneric_line(line, GT_PIPE);
+}
+
+static int cpio_mksock_line(const char *line)
+{
+ return cpio_mkgeneric_line(line, GT_SOCK);
+}
+
+static int cpio_mknod(const char *name, unsigned int mode,
+ uid_t uid, gid_t gid, char dev_type,
+ unsigned int maj, unsigned int min)
+{
+ int len;
+ unsigned int namesize;
+
+ if (dev_type == 'b')
+ mode |= S_IFBLK;
+ else
+ mode |= S_IFCHR;
+
+ if (name[0] == '/')
+ name++;
+ namesize = strlen(name) + 1;
+
+ len = dprintf(outfd, "%s%08X%08X%08lX%08lX%08X%08lX"
+ "%08X%08X%08X%08X%08X%08X%08X",
+ do_csum ? "070702" : "070701", /* magic */
+ ino++, /* ino */
+ mode, /* mode */
+ (long) uid, /* uid */
+ (long) gid, /* gid */
+ 1, /* nlink */
+ (long) default_mtime, /* mtime */
+ 0, /* filesize */
+ 3, /* major */
+ 1, /* minor */
+ maj, /* rmajor */
+ min, /* rminor */
+ namesize, /* namesize */
+ 0); /* chksum */
+ offset += len;
+
+ if (len != CPIO_HDR_LEN ||
+ push_rest(name, namesize) < 0)
+ return -1;
+
+ return 0;
+}
+
+static int cpio_mknod_line(const char *line)
+{
+ char name[PATH_MAX + 1];
+ unsigned int mode;
+ int uid;
+ int gid;
+ char dev_type;
+ unsigned int maj;
+ unsigned int min;
+ int rc = -1;
+
+ if (7 != sscanf(line, "%" str(PATH_MAX) "s %o %d %d %c %u %u",
+ name, &mode, &uid, &gid, &dev_type, &maj, &min)) {
+ fprintf(stderr, "Unrecognized nod format '%s'", line);
+ goto fail;
+ }
+ rc = cpio_mknod(name, mode, uid, gid, dev_type, maj, min);
+ fail:
+ return rc;
+}
+
+static int cpio_mkfile_csum(int fd, unsigned long size, uint32_t *csum)
+{
+ while (size) {
+ unsigned char filebuf[65536];
+ ssize_t this_read;
+ size_t i, this_size = MIN(size, sizeof(filebuf));
+
+ this_read = read(fd, filebuf, this_size);
+ if (this_read <= 0 || this_read > this_size)
+ return -1;
+
+ for (i = 0; i < this_read; i++)
+ *csum += filebuf[i];
+
+ size -= this_read;
+ }
+ /* seek back to the start for data segment I/O */
+ if (lseek(fd, 0, SEEK_SET) < 0)
+ return -1;
+
+ return 0;
+}
+
+static int cpio_mkfile(const char *name, const char *location,
+ unsigned int mode, uid_t uid, gid_t gid,
+ unsigned int nlinks)
+{
+ struct stat buf;
+ unsigned long size;
+ int file, retval, len;
+ int rc = -1;
+ time_t mtime;
+ int namesize, namepadlen;
+ unsigned int i;
+ uint32_t csum = 0;
+ ssize_t this_read;
+
+ mode |= S_IFREG;
+
+ file = open (location, O_RDONLY);
+ if (file < 0) {
+ fprintf (stderr, "File %s could not be opened for reading\n", location);
+ goto error;
+ }
+
+ retval = fstat(file, &buf);
+ if (retval) {
+ fprintf(stderr, "File %s could not be stat()'ed\n", location);
+ goto error;
+ }
+
+ if (do_file_mtime) {
+ mtime = default_mtime;
+ } else {
+ mtime = buf.st_mtime;
+ if (mtime > 0xffffffff) {
+ fprintf(stderr, "%s: Timestamp exceeds maximum cpio timestamp, clipping.\n",
+ location);
+ mtime = 0xffffffff;
+ }
+
+ if (mtime < 0) {
+ fprintf(stderr, "%s: Timestamp negative, clipping.\n",
+ location);
+ mtime = 0;
+ }
+ }
+
+ if (buf.st_size > 0xffffffff) {
+ fprintf(stderr, "%s: Size exceeds maximum cpio file size\n",
+ location);
+ goto error;
+ }
+
+ if (do_csum && cpio_mkfile_csum(file, buf.st_size, &csum) < 0) {
+ fprintf(stderr, "Failed to checksum file %s\n", location);
+ goto error;
+ }
+
+ size = 0;
+ namepadlen = 0;
+ for (i = 1; i <= nlinks; i++) {
+ if (name[0] == '/')
+ name++;
+ namesize = strlen(name) + 1;
+
+ /* data goes on last link, after any alignment padding */
+ if (i == nlinks)
+ size = buf.st_size;
+
+ if (dalign && size > dalign) {
+ namepadlen = padlen(offset + CPIO_HDR_LEN + namesize,
+ dalign);
+ if (namesize + namepadlen > PATH_MAX) {
+ fprintf(stderr,
+ "%s: best-effort alignment %u missed\n",
+ name, dalign);
+ namepadlen = 0;
+ }
+ }
+
+ len = dprintf(outfd, "%s%08X%08X%08lX%08lX%08X%08lX"
+ "%08lX%08X%08X%08X%08X%08X%08X",
+ do_csum ? "070702" : "070701", /* magic */
+ ino, /* ino */
+ mode, /* mode */
+ (long) uid, /* uid */
+ (long) gid, /* gid */
+ nlinks, /* nlink */
+ (long) mtime, /* mtime */
+ size, /* filesize */
+ 3, /* major */
+ 1, /* minor */
+ 0, /* rmajor */
+ 0, /* rminor */
+ namesize + namepadlen, /* namesize */
+ size ? csum : 0); /* chksum */
+ offset += len;
+
+ if (len != CPIO_HDR_LEN ||
+ push_buf(name, namesize) < 0 ||
+ push_pad(namepadlen ? namepadlen : padlen(offset, 4)) < 0)
+ goto error;
+
+ if (size) {
+ this_read = copy_file_range(file, NULL, outfd, NULL, size, 0);
+ if (this_read > 0) {
+ if (this_read > size)
+ goto error;
+ offset += this_read;
+ size -= this_read;
+ }
+ /* short or failed copy falls back to read/write... */
+ }
+
+ while (size) {
+ unsigned char filebuf[65536];
+ size_t this_size = MIN(size, sizeof(filebuf));
+
+ this_read = read(file, filebuf, this_size);
+ if (this_read <= 0 || this_read > this_size) {
+ fprintf(stderr, "Can not read %s file\n", location);
+ goto error;
+ }
+
+ if (write(outfd, filebuf, this_read) != this_read) {
+ fprintf(stderr, "writing filebuf failed\n");
+ goto error;
+ }
+ offset += this_read;
+ size -= this_read;
+ }
+ if (push_pad(padlen(offset, 4)) < 0)
+ goto error;
+
+ name += namesize;
+ }
+ ino++;
+ rc = 0;
+
+error:
+ if (file >= 0)
+ close(file);
+ return rc;
+}
+
+static char *cpio_replace_env(char *new_location)
+{
+ char expanded[PATH_MAX + 1];
+ char *start, *end, *var;
+
+ while ((start = strstr(new_location, "${")) &&
+ (end = strchr(start + 2, '}'))) {
+ *start = *end = 0;
+ var = getenv(start + 2);
+ snprintf(expanded, sizeof expanded, "%s%s%s",
+ new_location, var ? var : "", end + 1);
+ strcpy(new_location, expanded);
+ }
+
+ return new_location;
+}
+
+static int cpio_mkfile_line(const char *line)
+{
+ char name[PATH_MAX + 1];
+ char *dname = NULL; /* malloc'ed buffer for hard links */
+ char location[PATH_MAX + 1];
+ unsigned int mode;
+ int uid;
+ int gid;
+ int nlinks = 1;
+ int end = 0, dname_len = 0;
+ int rc = -1;
+
+ if (5 > sscanf(line, "%" str(PATH_MAX) "s %" str(PATH_MAX)
+ "s %o %d %d %n",
+ name, location, &mode, &uid, &gid, &end)) {
+ fprintf(stderr, "Unrecognized file format '%s'", line);
+ goto fail;
+ }
+ if (end && isgraph(line[end])) {
+ int len;
+ int nend;
+
+ dname = malloc(strlen(line));
+ if (!dname) {
+ fprintf (stderr, "out of memory (%d)\n", dname_len);
+ goto fail;
+ }
+
+ dname_len = strlen(name) + 1;
+ memcpy(dname, name, dname_len);
+
+ do {
+ nend = 0;
+ if (sscanf(line + end, "%" str(PATH_MAX) "s %n",
+ name, &nend) < 1)
+ break;
+ len = strlen(name) + 1;
+ memcpy(dname + dname_len, name, len);
+ dname_len += len;
+ nlinks++;
+ end += nend;
+ } while (isgraph(line[end]));
+ } else {
+ dname = name;
+ }
+ rc = cpio_mkfile(dname, cpio_replace_env(location),
+ mode, uid, gid, nlinks);
+ fail:
+ if (dname_len) free(dname);
+ return rc;
+}
+
+static void usage(const char *prog)
+{
+ fprintf(stderr, "Usage:\n"
+ "\t%s [-t <timestamp>] [-c] [-o <output_file>] [-a <data_align>] <cpio_list>\n"
+ "\n"
+ "<cpio_list> is a file containing newline separated entries that\n"
+ "describe the files to be included in the initramfs archive:\n"
+ "\n"
+ "# a comment\n"
+ "file <name> <location> <mode> <uid> <gid> [<hard links>]\n"
+ "dir <name> <mode> <uid> <gid>\n"
+ "nod <name> <mode> <uid> <gid> <dev_type> <maj> <min>\n"
+ "slink <name> <target> <mode> <uid> <gid>\n"
+ "pipe <name> <mode> <uid> <gid>\n"
+ "sock <name> <mode> <uid> <gid>\n"
+ "\n"
+ "<name> name of the file/dir/nod/etc in the archive\n"
+ "<location> location of the file in the current filesystem\n"
+ " expands shell variables quoted with ${}\n"
+ "<target> link target\n"
+ "<mode> mode/permissions of the file\n"
+ "<uid> user id (0=root)\n"
+ "<gid> group id (0=root)\n"
+ "<dev_type> device type (b=block, c=character)\n"
+ "<maj> major number of nod\n"
+ "<min> minor number of nod\n"
+ "<hard links> space separated list of other links to file\n"
+ "\n"
+ "example:\n"
+ "# A simple initramfs\n"
+ "dir /dev 0755 0 0\n"
+ "nod /dev/console 0600 0 0 c 5 1\n"
+ "dir /root 0700 0 0\n"
+ "dir /sbin 0755 0 0\n"
+ "file /sbin/kinit /usr/src/klibc/kinit/kinit 0755 0 0\n"
+ "\n"
+ "<timestamp> is time in seconds since Epoch that will be used\n"
+ "as mtime for symlinks, directories, regular and special files.\n"
+ "The default is to use the current time for all files, but\n"
+ "preserve modification time for regular files.\n"
+ "-c: calculate and store 32-bit checksums for file data.\n"
+ "<output_file>: write cpio to this file instead of stdout\n"
+ "<data_align>: attempt to align file data by zero-padding the\n"
+ "filename field up to data_align. Must be a multiple of 4.\n"
+ "Alignment is best-effort; PATH_MAX limits filename padding.\n",
+ prog);
+}
+
+static const struct file_handler file_handler_table[] = {
+ {
+ .type = "file",
+ .handler = cpio_mkfile_line,
+ }, {
+ .type = "nod",
+ .handler = cpio_mknod_line,
+ }, {
+ .type = "dir",
+ .handler = cpio_mkdir_line,
+ }, {
+ .type = "slink",
+ .handler = cpio_mkslink_line,
+ }, {
+ .type = "pipe",
+ .handler = cpio_mkpipe_line,
+ }, {
+ .type = "sock",
+ .handler = cpio_mksock_line,
+ }, {
+ .type = NULL,
+ .handler = NULL,
+ }
+};
+
+#define LINE_SIZE (2 * PATH_MAX + 50)
+
+int main (int argc, char *argv[])
+{
+ FILE *cpio_list;
+ char line[LINE_SIZE];
+ char *args, *type;
+ int ec = 0;
+ int line_nr = 0;
+ const char *filename;
+
+ default_mtime = time(NULL);
+ while (1) {
+ int opt = getopt(argc, argv, "t:cho:a:");
+ char *invalid;
+
+ if (opt == -1)
+ break;
+ switch (opt) {
+ case 't':
+ default_mtime = strtol(optarg, &invalid, 10);
+ if (!*optarg || *invalid) {
+ fprintf(stderr, "Invalid timestamp: %s\n",
+ optarg);
+ usage(argv[0]);
+ exit(1);
+ }
+ do_file_mtime = true;
+ break;
+ case 'c':
+ do_csum = true;
+ break;
+ case 'o':
+ outfd = open(optarg,
+ O_WRONLY | O_CREAT | O_LARGEFILE | O_TRUNC,
+ 0600);
+ if (outfd < 0) {
+ fprintf(stderr, "failed to open %s\n", optarg);
+ usage(argv[0]);
+ exit(1);
+ }
+ break;
+ case 'a':
+ dalign = strtoul(optarg, &invalid, 10);
+ if (!*optarg || *invalid || (dalign & 3)) {
+ fprintf(stderr, "Invalid data_align: %s\n",
+ optarg);
+ usage(argv[0]);
+ exit(1);
+ }
+ break;
+ case 'h':
+ case '?':
+ usage(argv[0]);
+ exit(opt == 'h' ? 0 : 1);
+ }
+ }
+
+ /*
+ * Timestamps after 2106-02-07 06:28:15 UTC have an ascii hex time_t
+ * representation that exceeds 8 chars and breaks the cpio header
+ * specification. Negative timestamps similarly exceed 8 chars.
+ */
+ if (default_mtime > 0xffffffff || default_mtime < 0) {
+ fprintf(stderr, "ERROR: Timestamp out of range for cpio format\n");
+ exit(1);
+ }
+
+ if (argc - optind != 1) {
+ usage(argv[0]);
+ exit(1);
+ }
+ filename = argv[optind];
+ if (!strcmp(filename, "-"))
+ cpio_list = stdin;
+ else if (!(cpio_list = fopen(filename, "r"))) {
+ fprintf(stderr, "ERROR: unable to open '%s': %s\n\n",
+ filename, strerror(errno));
+ usage(argv[0]);
+ exit(1);
+ }
+
+ while (fgets(line, LINE_SIZE, cpio_list)) {
+ int type_idx;
+ size_t slen = strlen(line);
+
+ line_nr++;
+
+ if ('#' == *line) {
+ /* comment - skip to next line */
+ continue;
+ }
+
+ if (! (type = strtok(line, " \t"))) {
+ fprintf(stderr,
+ "ERROR: incorrect format, could not locate file type line %d: '%s'\n",
+ line_nr, line);
+ ec = -1;
+ break;
+ }
+
+ if ('\n' == *type) {
+ /* a blank line */
+ continue;
+ }
+
+ if (slen == strlen(type)) {
+ /* must be an empty line */
+ continue;
+ }
+
+ if (! (args = strtok(NULL, "\n"))) {
+ fprintf(stderr,
+ "ERROR: incorrect format, newline required line %d: '%s'\n",
+ line_nr, line);
+ ec = -1;
+ }
+
+ for (type_idx = 0; file_handler_table[type_idx].type; type_idx++) {
+ int rc;
+ if (! strcmp(line, file_handler_table[type_idx].type)) {
+ if ((rc = file_handler_table[type_idx].handler(args))) {
+ ec = rc;
+ fprintf(stderr, " line %d\n", line_nr);
+ }
+ break;
+ }
+ }
+
+ if (NULL == file_handler_table[type_idx].type) {
+ fprintf(stderr, "unknown file type line %d: '%s'\n",
+ line_nr, line);
+ }
+ }
+ if (ec == 0)
+ ec = cpio_trailer();
+
+ exit(ec);
+}