Compare commits

...
59 Commits
Author SHA1 Message Date
daniel 51a2b3a932 feat: add Broadcom FullMAC Wi-Fi driver 2026-10-27 07:47:57 +01:00
daniel 8b4a938164 fix: ship Pi images with MontaukOS UEFI boot entry 2026-10-26 16:31:25 +01:00
daniel be84506fdf feat: make Files app list view columns draggable horizontally 2026-10-26 16:17:06 +01:00
daniel 559c3e2875 fix: bump max file name size for Files list view to 128 bytes 2026-10-26 16:05:21 +01:00
daniel 36291cd1cc fix: make Settings app scrollbar draggable 2026-10-26 16:03:33 +01:00
daniel b781cf8c97 feat: add MontaukOS Imager tool for Raspberry Pi 2026-10-26 16:01:38 +01:00
daniel 7e036c94e7 fix: NTP syncs as soon as DHCP has a lease, random origin cookie instead of the unsynced local clock, TLS validates certificates against UTC 2026-10-26 08:19:48 +01:00
daniel 1d00c1c016 feat: add GENET v5 Ethernet driver 2026-10-26 08:10:20 +01:00
daniel 3310b3d6de fix: Pi 4 Bluetooth - hold RTS deasserted across BT_ON power-on, require Read Local Version to sync, mini UART no flow control first 2026-10-26 07:46:49 +01:00
daniel 93d8becce3 feat: wait up to 10s for a late MONTAUKROOT, retry USB enumeration after a port reset, recover halted EP0/bulk endpoints, MSC card reader settle 2026-10-25 17:57:20 +01:00
daniel ffee9002a6 feat: Raspberry Pi 4/400 Bluetooth over mini UART (H4, Broadcom patchram, firmware mailbox BT_ON); move BT stack to Drivers/Bluetooth 2026-10-25 15:59:33 +01:00
daniel 808e1d66fd feat: aarch64 port - Raspberry Pi 4/400 SD boot (SDHCI, ext2 root + first-boot grow), USB hubs, PCI INTx routing, SMP, DMA fences 2026-10-25 13:48:26 +01:00
daniel 8c09ccef66 feat: add support for aarch64 timer/interrupts, port MontaukOS userspace 2026-10-25 08:27:09 +01:00
daniel 8b6379fb7e feat: aarch64 port - fix build system obstacles, add exception handling, page tables 2026-10-25 07:43:46 +01:00
daniel aae4db01ad fix: split search results in Settings by category 2026-10-24 21:32:22 +02:00
daniel 1bae77ec84 feat: make kernel architecture-agnostic; primitive work on arm port 2026-10-24 21:25:18 +02:00
daniel dab7d5427b fix: fix Bluetooth audio delay 2026-10-24 20:38:35 +02:00
daniel 0a20385e12 feat: add flac support to Music app 2026-10-24 14:09:04 +02:00
daniel 8ae19258b1 fix: fix Files double-click bug 2026-10-24 13:34:36 +02:00
daniel 8de98f3ab3 feat: move Settings out of Files into its own searchable app with categories 2026-10-24 13:26:47 +02:00
daniel d3e49a925d feat: move Files to its own process; add window server messaging (multi-format clipboard, drag and drop), undo/redo, sidebar, breadcrumbs, thumbnails, file type icons, New menu and proper text fields 2026-10-24 12:44:20 +02:00
daniel 07191b1286 feat: Files app - add Trash 2026-10-24 08:20:18 +02:00
daniel e047c1e0c0 feat: expand Files features; add file associations 2026-10-24 07:26:50 +02:00
daniel 23cc56b933 refactor: capitalize boot entry name 2026-10-17 12:22:24 +02:00
daniel 3c2eefe7f4 refactor: move wallpaper.cache to 0:/state 2026-10-17 10:53:36 +02:00
daniel 14c7099e3b feat: remove remaining use of session.toml 2026-10-17 10:36:07 +02:00
daniel e87bd358a6 feat: Rust std target + ruststd tests; SYS_EXIT_GROUP; fix mkdir/create under missing parent 2026-10-17 10:30:24 +02:00
daniel 9e66c3b249 fix: change copyright notice to use 2025-2026 range 2026-10-14 14:59:31 +02:00
daniel 31edff3da4 docs - simplify release notes 2026-10-14 07:56:00 +02:00
daniel f3d475a0d5 feat: release - 0.2.0 2026-10-14 07:46:40 +02:00
daniel 278b208258 feat: add process termination codes and SYS_ABORT; whole-process abort() from any thread in libc and montauk-rt 2026-10-14 07:32:15 +02:00
daniel f8b8f9610f feat: add Rust userspace support; fix TLS layout for p_align < 16; kill whole process on sibling-thread faults 2026-10-14 07:16:42 +02:00
daniel a81298fb24 fix: fix faulty syscall result/status code compares 2026-10-13 07:15:05 +02:00
daniel abfd9de8f6 feat: add SYS_FUTEX_WAIT and SYS_FUTEX_WAKE syscalls; revamp desktop application categories 2026-10-13 07:11:39 +02:00
daniel 60ca9c78c7 fix: remove stray newline in dhcp log 2026-10-03 12:49:06 +02:00
daniel 2aa6360b3d feat: switch text to UTF-8; add Unicode key events, dead keys & Czech layout; fix numpad navigation; add NumLock support 2026-10-03 10:11:49 +02:00
daniel 32660ddd1a feat: add ext2 block cache 2026-10-02 15:19:41 +02:00
daniel a51ee3339f fix: desktop - remove dots from Display Settings option 2026-10-02 14:54:42 +02:00
daniel f9e21e0f13 fix: add backlight modulation support for hybrid graphics laptops 2026-10-02 14:54:00 +02:00
daniel 7979acc2eb feat: add laptop/display brightness modulation 2026-10-02 12:14:40 +02:00
daniel ef1e69ae83 fix: fix race in IwxProcessEvents; fix WiFi bugs 2026-10-02 10:21:18 +02:00
daniel c91de69189 refactor: rename montauk::StatusCodes type to montauk::StatusCode 2026-10-02 08:33:50 +02:00
daniel 41de95b277 fix: do not proceed with DHCP if no network device with an active link is present 2026-10-02 08:31:57 +02:00
daniel 24f88ec4c6 feat: power down dGPUs on hybrid graphics laptops 2026-10-01 13:09:26 +02:00
daniel 7f12e1a150 refactor: syslog viewer - update references to klog to syslog 2026-10-01 10:14:12 +02:00
daniel 63b4bd51e8 feat: make System Log quit on failed query; display message box 2026-10-01 08:36:03 +02:00
daniel 2e3055f5f8 feat: display status message on failed operations in Files app 2026-10-01 08:20:49 +02:00
daniel 5e4e8902c2 feat: make userspace StatusCodes positive; print status text along with code in shell exec; make init return BadInvocation on faulty launch 2026-09-30 16:21:32 +02:00
daniel 8a3ffb2be1 feat: unify system status codes 2026-09-30 09:43:18 +02:00
daniel 93894b8fe5 cleanup: comments in capabilities modules, default capabilities.toml, login 2026-09-30 08:40:06 +02:00
daniel 2eee6696ef cleanup: clean up docs/ directory and consent source files 2026-09-30 08:36:41 +02:00
daniel ae5e84592d feat: add permission consent prompts, drop permitted capability mask 2026-09-30 08:32:48 +02:00
daniel c54181103f refactor: clean up ProtectedPaths module 2026-09-20 16:25:29 +02:00
daniel 95e8a739d4 feat: add compositor capability 2026-09-19 10:28:54 +02:00
daniel 75e85f24ef feat: move fonts/icons/man to 0:/os, add capabilities GUI app 2026-09-19 09:50:58 +02:00
daniel 80b5beb17d feat: add 'vol' command 2026-09-19 08:54:15 +02:00
daniel 420a7c996c refactor: update intro.1 man page 2026-09-19 08:32:17 +02:00
daniel 36cbee1415 feat: add Swedish/Danish keyboard layouts 2026-09-18 08:57:28 +02:00
daniel f95bce01d1 feat: add SYS_GETCAPS syscall to retrieve process capabilities 2026-09-12 11:13:13 +02:00
591 changed files with 50158 additions and 9320 deletions

No files matched your search

+9
View File
@@ -6,12 +6,21 @@
archives/isos/
montaukos.org/downloads/*.iso
*.hdd
*.img
rpi4-firmware/
qemu.log
toolchain/local/
toolchain/build/
toolchain/src/
toolchain/sysroot/
toolchain/sysroot-*/
programs/bin/
# Non-x86 userspace builds (programs/mk/arch.mk)
programs/bin-*/
programs/obj-*/
programs/src/*/obj-*/
programs/lib/*/obj-*/
programs/lib/*/build-*/
programs/obj/
programs/src/*/obj/
programs/libs/*/obj/
+124 -20
View File
@@ -10,6 +10,32 @@ QEMUFLAGS := -smp 4 -m 2G -d int -D qemu.log -no-reboot
override IMAGE_NAME := montauk-$(ARCH)
# KERNEL_ONLY=1 builds a ramdisk holding just the boot files (no
# userspace), for bringing up a kernel on a new architecture.
KERNEL_ONLY ?= 0
# programs/ builds x86_64 into bin/, other architectures into bin-<arch>/.
ifeq ($(ARCH),x86_64)
PROGRAMS_BIN := programs/bin
else
PROGRAMS_BIN := programs/bin-$(ARCH)
endif
override EFI_LOADER_x86_64 := BOOTX64.EFI
override EFI_LOADER_aarch64 := BOOTAA64.EFI
override EFI_LOADER_riscv64 := BOOTRISCV64.EFI
override EFI_LOADER_loongarch64 := BOOTLOONGARCH64.EFI
# The host strip handles x86_64 images; other targets use their toolchain's.
ifeq ($(ARCH),x86_64)
STRIP := strip
else
STRIP := toolchain/local/bin/$(ARCH)-montauk-strip
endif
# AArch64 UEFI firmware for QEMU (Debian/Ubuntu: qemu-efi-aarch64).
AARCH64_EFI_FIRMWARE ?= /usr/share/qemu-efi-aarch64/QEMU_EFI.fd
# Toolchain for building the 'limine' executable for the host.
HOST_CC := cc
HOST_CFLAGS := -g -O2 -pipe
@@ -51,29 +77,72 @@ run-hdd-x86_64: $(IMAGE_NAME).hdd
$(QEMUFLAGS)
.PHONY: run-aarch64
# The machine mirrors the Raspberry Pi 4/400: Cortex-A72 cores, a GICv2, and
# keyboard and mouse behind a hub (every USB 2 device on the Pi is: the
# VL805's one USB 2 port carries an on-board hub). QEMU's hub is full-speed,
# so the transaction-translator path is only exercised on the real board.
# The kernel console is mirrored to the PL011 (-serial stdio).
run-aarch64: $(IMAGE_NAME).iso
qemu-system-$(ARCH) \
-M virt \
-M virt,gic-version=2 \
-cpu cortex-a72 \
-device ramfb \
-device qemu-xhci,id=xhci \
-device usb-kbd,bus=xhci.0 \
-device usb-mouse,bus=xhci.0 \
-bios /usr/share/ovmf/OVMF.fd \
-device usb-hub,bus=xhci.0,port=1 \
-device usb-kbd,bus=xhci.0,port=1.1 \
-device usb-mouse,bus=xhci.0,port=1.2 \
-bios $(AARCH64_EFI_FIRMWARE) \
-cdrom $(IMAGE_NAME).iso \
-serial stdio \
$(QEMUFLAGS)
.PHONY: run-hdd-aarch64
run-hdd-aarch64: $(IMAGE_NAME).hdd
qemu-system-$(ARCH) \
-M virt \
-M virt,gic-version=2 \
-cpu cortex-a72 \
-device ramfb \
-device qemu-xhci,id=xhci \
-device usb-kbd,bus=xhci.0 \
-device usb-mouse,bus=xhci.0 \
-bios /usr/share/ovmf/OVMF.fd \
-hda $(IMAGE_NAME).hdd \
-device usb-hub,bus=xhci.0,port=1 \
-device usb-kbd,bus=xhci.0,port=1.1 \
-device usb-mouse,bus=xhci.0,port=1.2 \
-bios $(AARCH64_EFI_FIRMWARE) \
-drive if=none,id=hd0,format=raw,file=$(IMAGE_NAME).hdd \
-device virtio-blk-pci,drive=hd0 \
-serial stdio \
$(QEMUFLAGS)
# The SD card image on QEMU, with the root read through the SDHCI driver:
# the card goes on sdhci-pci as a padded copy (QEMU wants a power-of-two card;
# RPI4_QEMU_CARD_MIB, default 4 GiB, leaves the first-boot grow room to work
# with; every run starts from a fresh copy, so every run is a first boot),
# and since QEMU's UEFI has no SD driver, the card's ESP is also
# attached alone (the card's first partition, as its own disk) over virtio
# for the firmware to boot Limine from.
RPI4_QEMU_CARD_MIB ?= 4096
.PHONY: run-rpi4-sd
run-rpi4-sd: montauk-rpi4.img
cp --sparse=always montauk-rpi4.img rpi4-sd-qemu.img
size=$$(stat -c %s rpi4-sd-qemu.img); p=$$(($(RPI4_QEMU_CARD_MIB) * 1048576)); \
while [ $$p -lt $$size ]; do p=$$((p * 2)); done; truncate -s $$p rpi4-sd-qemu.img
end=$$(PATH=$$PATH:/usr/sbin:/sbin sfdisk -d montauk-rpi4.img | awk -F'[=,]' '/montauk-rpi4.img1/ {print $$2 + $$4}'); \
dd if=montauk-rpi4.img of=rpi4-esp-qemu.img bs=512 count=$$end status=none
PATH=$$PATH:/usr/sbin:/sbin sfdisk --quiet --delete rpi4-esp-qemu.img 2 2>/dev/null || true
qemu-system-aarch64 \
-M virt,gic-version=2 \
-cpu cortex-a72 \
-device ramfb \
-device qemu-xhci,id=xhci \
-device usb-hub,bus=xhci.0,port=1 \
-device usb-kbd,bus=xhci.0,port=1.1 \
-device usb-mouse,bus=xhci.0,port=1.2 \
-bios $(AARCH64_EFI_FIRMWARE) \
-drive if=none,id=esp,format=raw,file=rpi4-esp-qemu.img \
-device virtio-blk-pci,drive=esp \
-device sdhci-pci \
-drive if=none,id=sd,format=raw,file=rpi4-sd-qemu.img \
-device sd-card,drive=sd \
-serial stdio \
$(QEMUFLAGS)
.PHONY: run-riscv64
@@ -196,13 +265,23 @@ ports: programs
fi
.PHONY: ramdisk
ifeq ($(KERNEL_ONLY),1)
ramdisk: limine/limine kernel
rm -rf .kernel-only-root
mkdir -p .kernel-only-root/boot/limine
cp -v limine/$(EFI_LOADER_$(ARCH)) limine.conf .kernel-only-root/boot/limine/
cp -v kernel/bin-$(ARCH)/kernel .kernel-only-root/boot/
./scripts/mkramdisk.sh .kernel-only-root ramdisk.tar
rm -rf .kernel-only-root
else
ramdisk: limine/limine kernel programs
mkdir -p programs/bin/boot/limine
cp -v limine/BOOTX64.EFI programs/bin/boot/limine/
cp -v limine.conf programs/bin/boot/limine/
cp -v kernel/bin-$(ARCH)/kernel programs/bin/boot/
rm -f programs/bin/boot/ramdisk.tar
./scripts/mkramdisk.sh programs/bin ramdisk.tar
mkdir -p $(PROGRAMS_BIN)/boot/limine
cp -v limine/$(EFI_LOADER_$(ARCH)) $(PROGRAMS_BIN)/boot/limine/
cp -v limine.conf $(PROGRAMS_BIN)/boot/limine/
cp -v kernel/bin-$(ARCH)/kernel $(PROGRAMS_BIN)/boot/
rm -f $(PROGRAMS_BIN)/boot/ramdisk.tar
./scripts/mkramdisk.sh $(PROGRAMS_BIN) ramdisk.tar
endif
$(IMAGE_NAME).iso: limine/limine kernel ramdisk
rm -rf iso_root
@@ -252,13 +331,17 @@ ifeq ($(ARCH),loongarch64)
endif
rm -rf iso_root
# Example and regression-test programs that are built into 0:/os for
# development but have no place on a release image.
RELEASE_EXCLUDE_PROGRAMS := rusthello.elf futextest.elf memtest.elf
.PHONY: release
release: limine/limine kernel ramdisk
rm -rf iso_root .release-tmp
mkdir -p iso_root/boot
cp kernel/bin-$(ARCH)/kernel iso_root/boot/
strip iso_root/boot/kernel
cp -r programs/bin .release-tmp
$(STRIP) iso_root/boot/kernel
cp -r $(PROGRAMS_BIN) .release-tmp
# A release ISO ships only in-tree apps, so drop any out-of-tree port bundles
# that happen to be staged in programs/bin/apps (each carries a .port marker).
# Pass RELEASE_WITH_PORTS=1 to deliberately cut a ports-included image.
@@ -281,11 +364,12 @@ ifeq ($(RELEASE_WITHOUT_SDK),1)
rm -rf .release-tmp/sdk/bin .release-tmp/sdk/lib .release-tmp/sdk/include \
.release-tmp/sdk/libexec .release-tmp/sdk/x86_64-montauk
endif
find .release-tmp -name '*.elf' -exec strip {} +
rm -f $(addprefix .release-tmp/os/,$(RELEASE_EXCLUDE_PROGRAMS))
find .release-tmp -name '*.elf' -exec $(STRIP) {} +
# The ramdisk carries its own copy of the kernel at 0:/boot/kernel -- that is
# the image the Installer lays down on the target disk -- and it is not named
# *.elf, so the find above misses it. Unstripped it is ~6 MB against ~0.7 MB.
strip .release-tmp/boot/kernel
$(STRIP) .release-tmp/boot/kernel
./scripts/mkramdisk.sh .release-tmp iso_root/boot/ramdisk.tar
rm -rf .release-tmp
mkdir -p iso_root/boot/limine
@@ -331,6 +415,26 @@ ifeq ($(ARCH),loongarch64)
endif
rm -rf iso_root
# Raspberry Pi 4/400 SD card image (UEFI firmware + Limine + MontaukOS).
# ROOTFS=ext2 (default) puts the system on an ext2 partition read through the
# kernel's SD driver; ROOTFS=ramdisk has Limine load ramdisk.tar instead.
# Phony so switching ROOTFS always rebuilds the image.
ROOTFS ?= ext2
.PHONY: rpi4-sd montauk-rpi4.img
rpi4-sd: montauk-rpi4.img
montauk-rpi4.img: limine/limine kernel ramdisk
ifneq ($(ARCH),aarch64)
$(error rpi4-sd needs ARCH=aarch64)
endif
./scripts/mk-rpi4-sd.sh kernel/bin-$(ARCH)/kernel ramdisk.tar $@ $(ROOTFS) $(PROGRAMS_BIN)-rpi
# Graphical imager: write montauk-rpi4.img to an SD card, or update the
# installation on one in place (scripts/montauk-imager.py, docs/raspberry-pi.md).
.PHONY: rpi4-imager
rpi4-imager:
./scripts/montauk-imager.py
$(IMAGE_NAME).hdd: limine/limine kernel ramdisk
rm -f $(IMAGE_NAME).hdd
dd if=/dev/zero bs=1M count=0 seek=64 of=$(IMAGE_NAME).hdd
@@ -362,7 +466,7 @@ endif
clean:
$(MAKE) -C kernel clean
# $(MAKE) -C programs clean
rm -rf iso_root .release-tmp $(IMAGE_NAME).iso $(IMAGE_NAME).hdd ramdisk.tar
rm -rf iso_root .release-tmp $(IMAGE_NAME).iso $(IMAGE_NAME).hdd montauk-rpi4.img rpi4-sd-qemu.img rpi4-esp-qemu.img ramdisk.tar
.PHONY: distclean
distclean:
+75
View File
@@ -1,3 +1,78 @@
MontaukOS 0.2.0 (October 14, 2026)
(278b20825815ec04a22fd1c2fcfedbd38e116e6d)
New
====
* Capability (application permissions) model added
* Consent prompts for missing app permissions
* New Capabilities app for reviewing and managing app permissions
* Unified system status codes; the shell and Files now print a readable message on failure
* Text is now UTF-8 throughout, with Unicode key events and dead keys
* Keyboard layout support, with English, Norwegian, Czech, Swedish, and Danish layouts
* NumLock support
* Battery percentage reporting via ACPI, with a battery indicator in the desktop panel
* Display brightness control, including on hybrid graphics laptops, with a slider in the desktop panel
* Discrete GPUs are powered down automatically on hybrid graphics laptops
* ext2 block cache with readahead, speeding up disk-heavy work such as on-OS compilation
* Userspace system logging: klog is now syslog, logs identify the user and program, and system services log through montauk::service_log
* The system log is written to disk (0:/state/logs) during shutdown
* Desktop sessions now own the processes started in them, and those processes are ended on logout
* Rust userspace support (no_std)
* Futex syscalls (SYS_FUTEX_WAIT/SYS_FUTEX_WAKE) as groundwork for Rust std and pthreads
* Process termination codes; abort() now ends the whole process from any thread
* isatty() now reports whether a terminal is actually attached
* Settings panel and a Pure Black theme in Terminal (the default for Console sessions)
* Syntax highlighting for C++, HTML, CSS, and JavaScript in edit and Text Editor
* Expanded SVG renderer
* New vol command for listing mounted volumes
* Revamped desktop application categories
* Fonts, icons, and manual pages moved to 0:/os; persistent state now lives in 0:/state
* RTL-SDR driver moved from the kernel to userspace
Fixed
=====
* Opening a directory as a file is now rejected on ext2 and FAT32, fixing on-OS gcc on installed systems
* Fixed several syscall result and status code comparisons
* Fixed a TLS layout bug for binaries whose TLS segment has an alignment below 16
* A fault in one thread now ends the whole process instead of leaving its sibling threads running
* Fixed an ACPI hardware ID endianness bug
* Fixed a race in Wi-Fi event processing and other Wi-Fi bugs
* DHCP no longer runs when no network device has an active link
* Reduced idle CPU wakeups and deferred-work polling
* Redirected consoles are now inherited by processes spawned with capabilities
* Fixed numpad navigation
* Fixed a timezone formatting bug
* Fixed the login wallpaper startup delay
* Fixed text overlap in file dialogs
* Persisted kernel logs are no longer double-spaced
* Files lists now scroll with the arrow keys, and volume labels are consistent
* mbrtowc added to libc
Included in ISO
================
Two x86_64 images are published for this release.
montauk-0.2.0-sdk-x86_64.iso additionally carries the
Montauk SDK (gcc, binutils, etc.)
* MontaukOS SMP kernel
* Userspace bootstrap (0:/os/init.elf) and other essential system utilities
* MontaukOS desktop environment and graphical apps
* NetSurf web browser (0:/apps/netsurf)
* Manual pages (0:/os/man), readable via man command
* Service and system configuration (0:/config: init.toml, capabilities.toml, ssh.toml, ntp.toml)
* System CA certificates (0:/os/certs/ca-certificates.crt)
* Intel Bluetooth controller and Wi-Fi (iwlwifi) firmware (0:/os/firmware/intel)
* Third-party license texts and notices (0:/os/licenses)
* Tiny C Compiler (tcc) with MontaukOS headers and libraries (0:/sdk/tcc)
* Lua 5.4 interpreter (lua) with headers and static library (0:/sdk/lua)
* Montauk SDK - native GCC/G++ and binutils (0:/sdk) - SDK image only
* Flat Remix icon pack
* JetBrains Mono, Noto Serif, Roboto, and C059 Roman (open Century Schoolbook) TrueType fonts
* DOOM engine with SDL_mixer audio support (game data not included)
* Experimental HTTP server (httpd) and default page (0:/www/index.html)
* Default wallpaper - blossoms by Nikhil Kumar, Unsplash (0:/os/wallpapers)
MontaukOS 0.1.9 (August 13, 2026)
(a28b8ad0b2932203b812e967f35f029f060fd6b5)
+10 -4
View File
@@ -70,10 +70,16 @@ sudo apt install build-essential nasm git curl xorriso mtools gdisk qemu-system-
### Cross-Architecture Builds
To build for a different architecture, pass `ARCH=`:
The kernel is split into architecture-neutral code and per-architecture
ports under `kernel/src/Arch/` (see [porting.md](porting.md)). x86_64 is the
only complete port. `aarch64` is a skeleton: the whole kernel compiles and
links for it, but paging, exception vectors, the GIC and the timer are not
written yet, so it does not boot. Userspace is x86_64-only.
Non-x86 kernels are built with LLVM (`clang`, plus `ld.lld` for the link):
```bash
make ARCH=aarch64
make ARCH=riscv64
make ARCH=loongarch64
make kernel ARCH=aarch64 # kernel/bin-aarch64/kernel
make kernel ARCH=aarch64 KERNEL_LD=/path/to/ld.lld
./scripts/check-arch-neutral.sh # boundary check + aarch64 compile
```
+207
View File
@@ -0,0 +1,207 @@
# Porting the Kernel to a New Architecture
The kernel is written so that everything outside `kernel/src/Arch/` is
architecture-neutral. A port is one directory, `kernel/src/Arch/<arch>/`,
implementing the interfaces declared in `kernel/src/Arch/*.hpp` (plus a few
interface headers elsewhere, listed below). The build compiles the generic
tree plus `Arch/$(ARCH)/` only.
`scripts/check-arch-neutral.sh` enforces the boundary and should pass before
every commit that touches the kernel:
- no inline assembly, assembly sources or architecture `#if`s outside
`src/Arch/` (the Limine adapter is the one exception: `limine.h` has
per-architecture SMP structures);
- no generic file includes a port's private headers (`<Arch/x86_64/...>`);
- with clang installed, the kernel compiles for `aarch64` and every symbol
the generic code references is defined by the port.
## Layout
```
kernel/src/Arch/
Arch.hpp target selection (MONTAUK_ARCH_*), MONTAUK_ARCH_NAME
Cpu.hpp inline CPU primitives (interrupt state, halt, barriers,
timestamp counter, TLB, page-table root)
Interrupts.hpp IRQ slots, handler registration, legacy line routing,
MSI message composition, IPIs
Smp.hpp Smp::CpuData (generic fields + arch sub-structs),
per-CPU base access, InitBsp / BootAPs
Context.hpp thread context: FPU area, initial kernel stack,
SwitchContext, ActivateThread, EnterUserMode
Abi.hpp user ABI: ELF machine, relocation kinds, TLS variant,
exit stub, initial user stack, EFI calling convention
Syscall.hpp SyscallFrame contract and entry-point installation
Timer.hpp per-CPU tick timer hardware
Platform.hpp boot-sequence hooks, platform device list, reboot
CpuPower.hpp frequency scaling / thermal governor (may be no-ops)
x86_64/ the complete port
aarch64/ the port in progress (see below)
```
Other interfaces a port implements:
| Header | What the port provides |
|---|---|
| `Memory/Paging.hpp` | the whole `Memory::VMM::Paging` class: page-table format, kernel and user mappings, address-space creation and teardown |
| `Io/IoPort.hpp` | port I/O (`Io::In8`...), or the absent-device fallback with `HasPortIo = false` |
| `ACPI/AcpiSleep.hpp` | `Initialize` / `IsS3Available` / `Suspend` (S3 via the FACS waking vector is an x86 mechanism) |
| `Common/Panic.hpp` | `Arch::PanicFrame` and `Arch::PrintPanicFrame` |
Interface headers that need inline definitions include the port's header of
the same name at their end (`Arch/Cpu.hpp` -> `Arch/<arch>/Cpu.hpp`); those
are `Cpu.hpp`, `IoPort.hpp`, `PerCpu.hpp`, `Context.hpp`, `Abi.hpp` and
`SyscallFrame.hpp`. Everything else is an ordinary out-of-line function.
## Boot sequence
`kmain` (`Boot/Main.cpp`) owns the order and calls the port at fixed points:
1. `Arch::EarlyInitialize()` - descriptor tables, FPU/SIMD enable. No allocator.
2. Frame allocator and heap come up (generic).
3. `Arch::InitializeTraps()` - exception vectors.
4. `Paging::Init()` switches to the kernel's own page tables, then
`Arch::InitializeMemoryAttributes()` (x86: PAT for write-combining).
5. ACPI tables are parsed (generic). `Arch::InitializeInterrupts(xsdt)` brings
up interrupt controllers, calls `Smp::InitBsp()`, idle and power
management; kmain then calls `Arch::EnableInterrupts()`.
6. PCI, AML, drivers (generic); `Timekeeping::TimerInitialize()` registers
`IRQ_TIMER` and calls `Arch::Timer::InitializeBsp(1)`.
7. `Arch::InitializePlatformDevices()` (x86: PS/2).
8. `Arch::PrepareUserMode()`, `montauk::abi::InitializeSyscalls()` (calls
`Arch::InitializeSyscallEntry()`), scheduler, IPC.
9. `Smp::BootAPs()`, `Arch::LateInitialize()`, first process, idle loop.
## Assumptions the generic kernel makes
- 64-bit, little-endian, 4 KiB pages.
- A higher-half direct map of physical memory (`Memory::HHDM`), as Limine
provides on every architecture.
- User space in the lower half of each address space, kernel in the upper
half; an address space is identified by the physical address of its root
table (on ARM, the TTBR0 table; the kernel half lives in TTBR1).
- Firmware tables via ACPI (`Hal::ACPI`); device-tree-only platforms would
need a discovery path first.
- Interrupts are numbered IRQ slots (`Arch::IRQ_COUNT`); slot numbers for
MSI-capable drivers are fixed in the drivers.
## The aarch64 port
Real: CPU primitives (DAIF, WFI, `CNTVCT_EL0`, TLBI, TTBR0), per-CPU data via
`TPIDR_EL1`, the context switch and EL0 entry (`Context.S`), TLS through
`TPIDR_EL0`, the ELF/TLS/exit-stub ABI (syscall number in x8), the IRQ slot
dispatch table (`ArchIrqDispatch`), panic-frame printing, and:
- Exception vectors (`Vectors.S`, installed in `EarlyInitialize`). Every
exception saves one 288-byte frame that is both `Arch::PanicFrame` and
`Arch::SyscallFrame`. EL0 `svc` goes to `SyscallDispatch`; other
synchronous exceptions go to `ArchSyncException` (`Exceptions.cpp`), which
grows user stacks and lazy anonymous memory, kills faulting processes with
a crash report, and panics on kernel faults.
- Translation tables (`Paging.cpp`): 4 KiB granule, 48-bit VAs. The kernel
half lives in its own TTBR1 table; `KernelRoot()` is the TTBR0 table of
kernel contexts (EFI identity mappings only, so null pointers fault). The
HHDM maps only the memory map's RAM and firmware regions, never the holes
between them (device space on ARM). MAIR: 0 Normal WB, 1 Normal NC
(`MapWC`), 2 Device-nGnRE (`MapMMIO`).
- GICv2 (`Interrupts.cpp`, from the MADT): IRQ slots map onto INTIDs --
`IRQ_TIMER` onto the generic timer's PPI, the MSI slots (24-45) onto the
SPIs of a GICv2m MSI frame (`ComposeMsi`), the IPI slots onto SGIs 0/1,
and the platform lines (1-23) onto the SPIs `AssignPlatformLine` gives
them (see "Interrupt lines" below).
Interrupts are completed (EOI) before the handler runs, as on x86; a
level-triggered source drops its line first in an ack hook.
- Generic timer tick (`Timer.cpp`): the EL1 virtual timer at
`CNTFRQ_EL0`, its PPI from the GTDT, re-armed from the ack hook.
- PSCI `SYSTEM_RESET` / `SYSTEM_OFF` (`Arch::Reboot` / `Arch::PowerOff`)
through the conduit the FADT's ARM_BOOT_ARCH names. Hardware-reduced
ACPI has no PM1 block, so `AcpiShutdown` leaves power-off to the port.
- `Arch::SyncInstructionCache` after the ELF loader writes code: ARM
instruction fetch does not snoop data writes (QEMU hides this; real
cores do not). EL0 may maintain caches itself (`SCTLR_EL1.UCI`).
- SMP (`Smp.cpp`): Limine wakes each secondary CPU (PSCI `CPU_ON`
underneath) on the bootloader's tables; it programs its own system
registers and the kernel translation regime (`Bringup.hpp`), its banked
GIC CPU interface and timer PPI, then idles until a reschedule SGI.
TLB maintenance is broadcast (`tlbi ...is`).
The generic kernel is compiled `-fsigned-char` for AArch64: it was written
for x86's signed plain `char`.
Building it needs `clang` and `ld.lld`; without a system `ld.lld` the
kernel makefile uses the one rustup ships
(`~/.rustup/toolchains/*/lib/rustlib/*/bin/gcc-ld/ld.lld`), and `KERNEL_LD=`
overrides both. `make run ARCH=aarch64` boots the full image (desktop, apps,
USB keyboard and mouse) on QEMU `virt` with a GICv2 and Cortex-A72 cores, the
Raspberry Pi 4/400's configuration; it needs `qemu-system-aarch64` and
`qemu-efi-aarch64`, and the userspace compiler
(`./toolchain/build-montauk-toolchain.sh aarch64`). `KERNEL_ONLY=1` builds an
image without userspace.
The kernel console is mirrored to the serial port the ACPI SPCR table names
(PL011, SBSA UART, 16550 or the BCM2835 mini UART; `Drivers/Serial/Uart.cpp`),
including the log written before the port was found. On QEMU that is
`-serial stdio`.
## Interrupt lines
Drivers for PCI functions without MSI read the Interrupt Line register and
pass it to `Arch::EnableLegacyIrq`. On x86 the firmware fills that register
with the function's I/O APIC input (`Arch::PlatformLinesAreFixed()`). On
other platforms `Pci::AssignInterruptLines` (`Pci/PciAcpi.cpp`) fills it
instead: it finds each pin's global system interrupt in the root bridge's
`_PRT` (swizzling through PCI-to-PCI bridges, resolving PNP0C0F link
devices through `_CRS`), binds the GSI to a platform line with
`Hal::GsiRouting::LineForGsi` and writes the line number into the register.
Devices that ACPI describes directly (platform devices) get their line from
`LineForGsi` too.
## DMA
`Memory/Dma.hpp` serves devices whose DMA does not snoop the CPU caches
(ACPI `_CCA = 0`): pages for rings and descriptors come uncached, and
`SyncForDevice` / `SyncForCpu` clean and invalidate streaming buffers
around a transfer (`Arch::CleanDataCache` / `InvalidateDataCache`). For
coherent devices every call is the plain allocation or a no-op. The xHCI
driver uses it; it also orders its TRB writes with `Arch::DmaWriteFence`
(the cycle bit last, a control TD's Setup TRB after its Data and Status
TRBs) and its event reads with `Arch::DmaReadFence`. Those fences cost
nothing on x86 and are required on ARM, where stores to normal memory and
device registers may otherwise be observed out of order. The other PCI
drivers (AHCI, NVMe, e1000, HDA) do not have them yet: they work on QEMU,
which does not reorder, but need them before they meet real ARM hardware.
## Raspberry Pi 4 / 400
See `docs/raspberry-pi.md`: `make ARCH=aarch64 rpi4-sd`, the firmware it
expects and how the Pi's hardware is reached.
## Userspace
The programs are architecture-neutral C/C++ built by `<arch>-montauk-gcc`
(see `toolchain/README.md`). `programs/mk/arch.mk` holds everything
architecture-specific in the build: the toolchain prefix, FP/SIMD flag sets
(`ARCH_FP_FLAGS` for GUI apps, `ARCH_NOFP_FLAGS` for system programs) and the
output layout. x86_64 keeps `bin/`, `obj/` and libraries next to their
sources; other architectures build beside them into `bin-<arch>/`,
`obj-<arch>/` and `lib/<name>/build-<arch>/`, so both can be built from one
tree. The few architecture-specific sources are the raw system-call
wrappers (`#if defined(__x86_64__)` / `__aarch64__` blocks in
`montauk/syscall.h`, `libc/montauk.h`, `lib/libc/libc.c` and
`lib/libc/crt/crt1.c`), `lib/libc/arch/setjmp-<arch>.S` and the scanout
store fence in `gui/framebuffer.hpp`.
Not built for AArch64: tcc (it generates x86 code), the on-OS SDK and the
Rust programs (their `x86_64-unknown-montauk` target).
## Adding another architecture
1. Create `kernel/src/Arch/<arch>/` with the six inline headers listed above
and stubs for the out-of-line functions (copy `aarch64/` as a template).
2. Add the target to `Arch/Arch.hpp` and to the `#if` chains at the end of
the dispatching interface headers.
3. Add `KernelStartSymbol` / `KernelEndSymbol` to `kernel/linker-<arch>.ld`,
and the `ld.lld` emulation to `LD_EMULATION_<arch>` in `kernel/GNUmakefile`.
4. Teach `Boot/Protocols/LimineProtocol.cpp` the architecture's MP fields.
5. Run `./scripts/check-arch-neutral.sh <arch>` until it passes.
-168
View File
@@ -1,168 +0,0 @@
# CPU Power and Thermal Management
MontaukOS manages CPU frequency, idle states, and package temperature on
Intel hardware. Without this, a modern many-core laptop part (the reference
target is a TUXEDO Gemini Gen2 with an i9-13900HX, 24 cores / 32 threads)
runs at whatever P-state the firmware left it, never drops below C1 when
idle, and heats until the hardware throttles at TjMax - or until the
embedded controller intervenes (on Clevo boards: forced full-volume beeping,
then a hard power-off).
Everything below is feature-gated by CPUID, so virtual machines (KVM masks
the power bits in CPUID.06H) and non-Intel CPUs skip it safely.
## Components
### HWP frequency scaling (`Hal/CpuPower.cpp`)
`Hal::CpuPower::InitializeBsp()` enables Hardware P-states
(IA32_PM_ENABLE), reads the performance range from IA32_HWP_CAPABILITIES,
and programs IA32_HWP_REQUEST with:
- min = lowest performance level, max = highest
- desired = 0 (fully autonomous hardware frequency selection)
- EPP = 0x80 (balanced; same as Linux "balance_performance")
This is the same mechanism intel_pstate uses on Linux and is the single
biggest thermal win: the CPU idles near its minimum frequency and ramps
only while there is work. APs apply the same request in `ApEntry` via
`InitializeAp()` (HWP request MSRs are per-logical-CPU).
CPUs with EPB but no HWP get IA32_ENERGY_PERF_BIAS = 6 (balanced) instead.
### C1E promotion
MSR_POWER_CTL bit 1 lets HLT-idle drop core voltage/frequency to minimum
(C1E) instead of parking at the current ratio. Set per-core on the BSP and
every AP. Gated on the DTS CPUID bit as a real-hardware heuristic, because
MSR_POWER_CTL is not architectural and hypervisors may not emulate it.
### Thermal governor (`Hal/CpuPower.cpp`)
A closed-loop passive-cooling controller on the BSP, stepped from
`Sched::RunBspMaintenance()` (reached from both the idle loop and the
timer tick, so it keeps running under full load). Every 500 ms it reads
the package digital thermal sensor (IA32_PACKAGE_THERM_STATUS readout
relative to TjMax from MSR_TEMPERATURE_TARGET) and adjusts the HWP
performance ceiling:
- **>= 85 C (passive):** step the ceiling down 2 ratio units (~200 MHz)
- **>= 94 C (hot):** step down 8 units per interval
- **<= 78 C (resume):** step back up 1 unit per interval
- between resume and passive: hold (hysteresis)
The ceiling propagates through a policy epoch counter: each CPU compares
the epoch in `Sched::Tick()` (and the AP idle loop) against the last one
it applied and rewrites its own IA32_HWP_REQUEST when it changed, since
MSR writes only affect the executing core. APs tick every 10 ms, so a
throttle decision reaches all cores within one tick.
Throttle engage/release transitions are logged (WARNING/OK, "CpuPower").
### Deep AP idle (`ACPI/CpuIdle.cpp`)
APs use deep MWAIT C-states while idle; C6 power-gates the core entirely,
which is where most of the idle-heat win lives on many-core parts.
The hint comes from ACPI `_CST` FFH entries (the GAS address low byte is
the MWAIT hint, same convention as Linux), validated against the CPUID.05H
sub-state enumeration and capped at the C6 family (0x2F). With no usable
`_CST`, the deepest CPUID-enumerated hint (up to C6) is used.
**The BSP stays in C1 (HLT).** On some laptops deep MWAIT states freeze
the LAPIC timer even though ARAT is advertised, and the BSP's periodic
1 ms tick is the system timekeeper (see the comment in
`Timekeeping::IdleOnce`). APs have no timekeeping duty: an idle AP is
woken by the reschedule IPI, and its coarse tick resumes on wake.
### S3 wake
IA32_PM_ENABLE, HWP requests, and MSR_POWER_CTL do not survive suspend.
`Hal::CpuPower::ReapplyAfterWake()` runs in the S3 resume path, redoes the
BSP setup, and bumps the policy epoch so every AP re-applies its HWP
request (and re-checks C1E) on its next tick.
### Battery (`ACPI/Battery.cpp`, `ACPI/EmbeddedController.cpp`)
Battery state comes from the ACPI Control Method Battery
(`_HID PNP0C0A`) and, for the AC adapter, `ACPI0003`:
- `_STA` bit 4 says whether a pack is installed. A laptop with the pack
removed still has the device, which is why "has a battery device" and
"has a battery" are separate answers.
- `_BIX` (or `_BIF` on older firmware) gives the power unit (mWh vs mAh),
the design capacity, and the last full charge capacity.
- `_BST` gives the charge/discharge state, the present rate, the remaining
capacity and the present voltage.
- `_PSR` on the adapter says whether it is plugged in.
Percentage is remaining / last-full, clamped to 100 and falling back to the
design capacity when firmware reports last-full as zero. Results are cached
for two seconds so that a panel repaint does not turn into EC traffic.
Almost all of this reaches the CPU through the **embedded controller**.
`Hal::EmbeddedController` finds the EC's command/data port pair from the
ECDT table when one exists, and otherwise from `_CRS` on the `PNP0C09`
device, then drives the polled read/write protocol (`0x80`/`0x81` with
OBF/IBF handshaking). It is wired into the AML interpreter as the
`EmbeddedControl` region space, so any `OperationRegion` in the DSDT that
targets the EC now works -- battery methods being the first user.
Supporting `_BST` also needed real package element storage in the AML
interpreter. Firmware overwhelmingly builds its result package with
`Store(x, Index(BST0, n))`, which requires `Index()` to name a writable
element rather than yield a value. `AML::PackageStore` provides that: a
persistent region for packages declared with `Name()`, and a transient ring
rewound at each top-level evaluation for packages built inside a method, so
polling `_BST` cannot exhaust it.
## Diagnostics
`SYS_POWERINFO` (149) fills a `PowerInfo` struct: package temperature,
TjMax, HWP state and performance range, the governor's current ceiling,
CPUID base/turbo frequency, the measured average active frequency
(APERF/MPERF over the 500 ms governor window), and the AP idle MWAIT hint.
Returns -1 when the CPU exposes no power features (e.g. under QEMU).
`SYS_BATTERYINFO` (188) fills a `BatteryInfo` struct: percentage, charge
state, AC presence, capacities, present rate, voltage and an estimated time
remaining. It returns -1 on a machine with no battery device at all, which
is how the desktop panel decides not to draw an indicator. Like
`SYS_POWERINFO` it is ungated read-only telemetry: the panel indicator has
to work in an unprivileged session, which holds no device capability to
spend on it.
The `power` program (`programs/src/power`) prints both:
```
power one-shot snapshot
power watch 1 Hz samples for 60 s (temp, avg freq, ceiling)
power watch 300 same, for 300 s
```
Boot log lines to look for (Kernel Log app):
```
[CpuPower] HWP enabled: perf range 4-54, EPP=0x80
[CpuPower] Thermal governor armed: TjMax=100C, passive=85C, hot=94C, ...
[CpuIdle] AP deep idle: MWAIT hint 0x20
[EC] Embedded Controller at cmd=66 data=62
[Battery] ACPI battery at \_SB.PCI0.LPCB.EC0.BAT0 (AC adapter present)
```
The desktop panel shows a battery icon left of the Wi-Fi and network icons,
using the icon pack's `battery-level-N[-charging]-symbolic` glyphs (one per
10% step, reloaded only when the step changes). Clicking it opens a popout
with the percentage in numbers, the charge state, time remaining and raw
capacity. The icon is absent entirely when `SYS_BATTERYINFO` fails, so a
desktop machine shows nothing there.
## Tuning
The governor constants live at the top of `Hal/CpuPower.cpp`
(`PassiveTempC`, `HotTempC`, `ResumeTempC`, `GovernorIntervalMs`, step
sizes) and were chosen to keep the package well below both TjMax (100 C
on the i9-13900HX) and the Clevo EC's emergency thresholds. If the fans
still spin up aggressively under sustained load, lower `PassiveTempC`
first; if builds feel throttled while cool, raise the recovery rate
instead of the trip points.
+344
View File
@@ -0,0 +1,344 @@
# Raspberry Pi 4 / 400
MontaukOS runs on the Raspberry Pi 4 and 400 through the Pi Firmware Task
Force's UEFI firmware ([pftf/RPi4](https://github.com/pftf/RPi4), an EDK2
build) in ACPI mode, booting Limine like any other UEFI machine.
## Building and writing the SD card
```bash
./toolchain/build-montauk-toolchain.sh aarch64 # once
make ARCH=aarch64 rpi4-sd # -> montauk-rpi4.img
sudo dd if=montauk-rpi4.img of=/dev/sdX bs=4M conv=fsync
```
Or use the imager, which also updates a card in place:
```bash
make rpi4-imager # = ./scripts/montauk-imager.py
```
### The imager
`scripts/montauk-imager.py` is a Tk window (`python3-tk`) that lists the SD
cards and USB drives attached (never the host's own disks) and what is on
them, and either writes a fresh image or updates the MontaukOS on a card. It
runs as the user and does the writing by starting itself again through
`pkexec` (it asks for the password each time). The same operations work from
a terminal: `list`, and as root `flash`, `update` and `expand`
(`--help` for the options). Images can be raw or `.gz`, `.xz` or `.zip`
compressed; **Build** runs `make ARCH=aarch64 rpi4-sd`.
- **Write a fresh image** erases the card, writes the image, clears the last
MiB of the card (a backup GPT from an earlier OS) and reads the card back to
verify it. *Expand* grows the root to fill the card right away instead of
leaving it to `firstboot.elf`.
- **Update** leaves the partition table alone and finds both partitions by
label, so a root that has grown to fill the card (or was resized by hand)
stays as it is. On the ESP every file is written except `RPI_EFI.fd`, which
holds the firmware's own settings (*Update the UEFI firmware* replaces it);
a changed `config.txt` is kept as `config.txt.bak`. On the root, `os/`,
`apps/`, `sdk/`, `boot/` get the image's new and changed files; `config/`
and `www/` only get files the card lacks, `users/` and `state/` are not
touched. `init.toml` becomes the image's, keeping each service's `enabled`
from the card and appending services only the card has; `capabilities.toml`
becomes the image's plus grants only the card has (old copies: `.old`).
`os/firstboot.elf` is only written if the card still has it, so a grown
card does not grow again. *Remove system files the new image no longer
ships* prunes `os/`, `apps/`, `sdk/` and `boot/` (keeping `os/btkeys.bin`,
the Bluetooth bonds). Unchanged files are skipped and each file is
replaced by a rename; the volume is checked with `e2fsck` before and after,
and its features against what `Fs/Ext2.cpp` mounts.
- **Expand** (a host-side first boot) grows the root partition to the end of
the card, at most 1 TiB, and the volume with `resize2fs`; `firstboot.elf`
then finds nothing to do and deletes itself.
While it works, a runtime udev rule keeps the desktop from automounting the
card's partitions (Linux writes to an ext2 volume as soon as it mounts it).
`scripts/mk-rpi4-sd.sh` downloads the firmware release once into
`rpi4-firmware/` (`PFTF_VERSION=v1.53` by default) and builds an MBR image:
| Partition | Type | Contents |
|---|---|---|
| 1 | 0xEF, FAT32 | the firmware files, Limine (`EFI/BOOT/BOOTAA64.EFI`), `boot/kernel` |
| 2 | 0x83, ext2, label `MONTAUKROOT` | the system tree (the contents of `ramdisk.tar`) |
Limine loads only the kernel; the `limine.conf` on the card has no ramdisk
module. Finding no ramdisk, the kernel mounts the partition labelled
`MONTAUKROOT` as `0:/` (`Fs/Boot.cpp`), read through the SD driver
(`Drivers/Storage/Sdhci`), and the ESP after it. Files load as they are
used instead of the firmware reading the whole 80 MB tree before boot.
The card can also boot from a USB card reader (or the image from a USB
stick): the root then comes through `Drivers/USB/MassStorage.cpp`. A USB
root can register after the boot scan (a SuperSpeed link still training, a
reader still bringing its card up), so a boot that finds no `MONTAUKROOT`
waits up to 10 s for one, servicing USB hot-plug meanwhile.
Unlike the ramdisk, the root is persistent: changes to `0:/` survive a
reboot, so shut down from the desktop rather than pulling power.
The image is sized to its contents (`ROOT_FREE_MIB`, default 64, sets the
free space on the root) and grows to fill the card on first boot.
### First boot: growing the root
Only the SD image ships `0:/os/firstboot.elf` (`programs/src/firstboot`,
built into `programs/bin-aarch64-rpi/`, which the script lays over the
system tree), and the script adds its init service, ahead of all others,
and its capability grant (`storage_admin`, `system_image`) to the image's
`init.toml` and `capabilities.toml`. On first boot it shows a status card
in the login screen's style while it calls `SYS_FS_GROW` on drive 0 in a
loop:
1. The kernel grows the MBR entry of the root partition as far as the
filesystem can use (to the end of the card, or 1 TiB), on a 1 MiB
boundary (`Gpt::ResizeMbrPartition`).
2. The ext2 driver grows the mounted volume (`Fs/Ext2.cpp`, "Online
grow"): each call writes the bitmaps and zeroed inode tables of a few
new block groups past the old end, then the last call commits the
bitmaps, descriptor table and superblock. The descriptor table grows
into the blocks `mke2fs -O resize_inode -E resize=2^28` reserved; the
leftover reservation is freed and `resize_inode` cleared, so the volume
grows once.
The new groups' inode tables are the bulk of the writes (0.1% of the card,
one inode per 256 KiB), written as long multi-block runs. Once the volume has grown, `firstboot.elf` deletes
itself and init skips its (optional) service from then on. If it fails it
stays and the next boot tries again; an interrupted grow is safe, since the
new groups are only referenced once the superblock is written, and a
partition larger than its filesystem is valid.
`make ARCH=aarch64 rpi4-sd ROOTFS=ramdisk` builds the old layout instead:
one FAT32 partition with `boot/ramdisk.tar`, which Limine loads through the
firmware. It boots slowly but does not depend on the kernel's SD driver.
`make ARCH=aarch64 run-rpi4-sd` boots the ext2 image on QEMU with the card
on `sdhci-pci`, so the root comes through the same driver. QEMU's UEFI has
no SD driver, so the card's ESP is also attached on its own over virtio for
the firmware to load Limine from. The card is padded to
`RPI4_QEMU_CARD_MIB` (default 4096), and every run starts from a fresh
copy, so every run is a first boot.
The image's `RPI_EFI.fd` comes with a **MontaukOS** boot entry first in the
firmware's boot order (`scripts/rpi4-efi-bootorder.py` writes it into the
release's empty variable store), so the card boots on its own instead of
falling behind the PXE/HTTP entries the firmware creates. The entry names the
ESP by the image's MBR disk ID and partition number, so it works from the SD
slot or a USB drive. A card whose `RPI_EFI.fd` predates it keeps its old
boot order until it is updated with *Update the UEFI firmware*
(`--firmware`), which also copies the image's disk ID to the card.
The firmware defaults are what MontaukOS needs; leave them:
- **System Table Mode: ACPI.** The kernel reads ACPI only.
- **Limit RAM to 3 GB: enabled.** The BCM2711's PCIe can only reach the
first 3 GB (its tables say so in `_DMA`). The kernel warns at boot when
RAM extends past a device's `_DMA` range.
## Serial console
The kernel console (the whole boot log, including what was printed before
the port was found) is mirrored to the UART the firmware's SPCR table names.
The firmware's `config.txt` applies the `miniuart-bt` overlay, which puts the
PL011 on GPIO 14/15: header pins 8 (TX) and 10 (RX), ground on pin 6, 115200
8N1, 3.3 V levels. On the Pi 400 the header is on the back.
## How the hardware is reached
| Part | Source | Driver |
|---|---|---|
| 4x Cortex-A72 | MADT, PSCI (TF-A, SMC) | `Arch/aarch64` SMP via Limine MP |
| GIC-400 (GICv2) | MADT | `Arch/aarch64/Interrupts.cpp` |
| Generic timer | GTDT | `Arch/aarch64/Timer.cpp` |
| Display | UEFI GOP (VideoCore) | Limine framebuffer |
| Keyboard (built in), USB ports | VL805 xHCI as ACPI `\_SB.SCB0.XHC0` (`PNP0D10`) | `Drivers/USB/Xhci.cpp` platform probe |
| USB 2 devices (built-in keyboard too) | VIA hub on the VL805's USB 2 root port | `Drivers/USB/Hub.cpp` |
| Reset, power-off | PSCI | `Arch::Reboot`, `Arch::PowerOff` |
| Bluetooth (CYW43455 on the 4B, CYW43456 on the 400) | `BCM2EA6` (`\_SB.GDV0.BTH0`) on the mini UART (`BCM2836`) | `Drivers/Bluetooth/Broadcom.cpp`, `Drivers/Serial/BtUart.cpp` |
| VideoCore firmware mailbox (radio power, clocks, board MAC) | `BCM2849` (`RPIQ`) | `Drivers/Platform/RpiFirmware.cpp` |
| GPIO pin functions | `BCM2845` (`GPI0`) | `Drivers/Platform/RpiGpio.cpp` |
| Ethernet (GENET v5 MAC, BCM54213PE PHY) | `BCM6E4E` (`\_SB.ETH0`) | `Drivers/Net/Genet.cpp` |
| Wi-Fi (the same CYW4345x, SDIO) | Arasan SD controller `BCM2847`, GPIO 34-39 | `Drivers/Net/Wifi/Brcmf*.cpp` |
The VL805 sits behind the BCM2711's PCIe, whose config space is not ECAM
(the root bridge's `_DSD` says `linux-ecam-quirk-id = "bcm2711"`), so the
kernel does not enumerate PCI there. The firmware has already brought up the
link and the VL805 and describes the controller as a platform device: a
register block at `0x600000000`, GSI 175, and `_CCA = 0`, i.e. its DMA does
not snoop the CPU caches. `XHC0._INI` (run by the AML interpreter) enables
the controller's PCI memory decoding and bus mastering. The xHCI driver then
keeps its rings and contexts in uncached memory, bounces control-transfer
data through an uncached page, and cleans/invalidates the caller buffers of
bulk transfers (`Memory/Dma.hpp`). It also watches the event ring from the
timer tick, so input keeps working if the INTx line never fires.
The VL805 has one USB 2 root port (port 1) and four USB 3 ones. An on-board
VIA hub (`2109:3431`) sits on the USB 2 port, and the USB 2 lines of every
socket, and the Pi 400's internal keyboard (`04d9:0007`), hang off it: a
keyboard or mouse is always a low/full-speed device behind a high-speed hub,
reached through that hub's transaction translator.
## Bluetooth
The radio's Bluetooth half speaks HCI over a UART (H4) on GPIO 30-33 (CTS,
RTS, TXD, RXD); its power switch, `BT_ON`, is line 0 of the VideoCore
firmware's GPIO expander, reached through the mailbox. The firmware tables
describe it as `BTH0`, whose `UARTSerialBus` names the UART it uses: with
the `miniuart-bt` overlay the PL011 is the console, so Bluetooth has the mini
UART. The kernel refuses to take a UART that is the SPCR console.
At boot the driver probe routes the pins (`ALT5` for the mini UART, `ALT3`
for the PL011), starts the UART (interrupt driven, 16 KiB receive and
transmit rings) and the H4 transport under the shared HCI layer. Once the
root is mounted the idle loop finishes, as for USB adapters:
1. power-cycle the controller through `BT_ON`, HCI Reset at 115200 baud;
2. Read Local Version: the ROM's LMP subversion picks the patch file,
`0:/os/firmware/brcm/BCM4345C0.hcd` (0x6119, 4B) or `BCM4345C5.hcd`
(0x6606, 400), shipped unmodified from Raspberry Pi OS's bluez-firmware;
3. raise both ends to the operating speed (vendor `0xFC18`);
4. Download Minidriver (`0xFC2E`), then every record of the `.hcd` file
(Write RAM, then Launch RAM), as Linux's `btbcm` does;
5. the patched firmware restarts at 115200: reset, raise the speed again.
The mini UART's FIFO is 8 bytes deep. Its hardware RTS is tried first; if
the controller answers with it on, the link runs at 921600 baud, otherwise
at 460800 without flow control (what Raspberry Pi OS uses), and at 115200
if the UART has no interrupt. The baud rate derives from the VPU core
clock, so `mk-rpi4-sd.sh` pins it (`core_freq=500`, `core_freq_min=500` in
`config.txt`), as the overlay's documentation requires. A failed fast
bring-up is retried entirely at 115200.
The CYW43456 reports a chip-wide default address (`43:45:C5:00:1F:AC`);
the driver replaces it with the board's Ethernet MAC from the firmware.
`0:/config/bluetooth.toml` still overrides both (Broadcom `0xFC01`).
## Ethernet
The BCM2711's own Gigabit MAC, GENET v5 (a UniMAC behind a DMA engine),
talks RGMII to a BCM54213PE PHY at MDIO address 1. The firmware describes
it as `BCM6E4E`: a 64 KiB register block, two interrupts (the driver uses
the first, which carries the default queue), `_CCA = 0`, and a `_DSD` with
`phy-mode` (`rgmii-rxid`: the PHY delays the receive clock, the MAC the
transmit clock) and `brcm,max-dma-burst-size`.
The driver uses only the default queue (ring 16), with all 256 descriptors
of each direction. The descriptors live in the MAC's own SRAM, so only the
2 KiB packet buffers need cache maintenance. Received frames start two
bytes into their buffer (`RBUF_ALIGN_2B`); neither direction uses the
64-byte status blocks, so there is no checksum offload. The UniMAC accepts
broadcast and its own address only (the MDF filter).
The interrupt only acknowledges and masks; frames are dispatched from the
idle bottom half, which also checks the ring itself, so receive works if
the interrupt never arrives. The PHY is polled once a second (its interrupt
pin is not described); a link change reprograms the RGMII block and the
UniMAC's speed and duplex. A link the firmware already negotiated is kept,
otherwise the PHY is reset and advertises 10/100 and 1000 full duplex.
The MAC address is the board's, read from OTP through the firmware mailbox
(the one the Bluetooth driver uses too); the address the firmware's own
driver programmed is the fallback. `dhcp.elf` waits up to 10 s for a wired
link, since autonegotiation takes about 3 s after the PHY reset.
## Wi-Fi
The radio's WLAN half is an SDIO card on the BCM2711's older SD controller,
the Arasan-derived EMMC (`BCM2847`, `0x7E300000`); the card slot is on
EMMC2, so the two never share a bus. Its pins are GPIO 34-39 (ALT3; clock
without a pull, the rest pulled up, as Linux's `sdio_pins`) and its power
switch, `WL_ON`, is line 1 of the firmware's GPIO expander. The probe
claims the controller only while the slot is on EMMC2 (`BRCME88C` present);
if the tables do not describe the Arasan, its address is taken as EMMC2's
less 256 KiB.
It is a FullMAC radio: the firmware does the 802.11 MLME itself and the host
sees Ethernet frames. The driver follows Linux's brcmfmac (ISC license,
Broadcom; its notice is in `NOTICES.txt`):
| File | Part |
|---|---|
| `BrcmfSdio.cpp` | the Arasan as an SDIO host: CMD5/3/7, 4-bit at 25 MHz, CMD52/CMD53, polled PIO, 32-bit register access |
| `BrcmfChip.cpp` | backplane window (function 1), clocks, the enumeration ROM's core list, firmware + NVRAM download, function 2, the firmware console |
| `BrcmfProto.cpp` | SDPCM framing and transmit credit, CDC ioctls, BDC data, events, the receive pump, the transmit queue |
| `BrcmfConnect.cpp` | preinit and CLM, escan, join, keys, link state |
Once the root is mounted the idle loop:
1. routes the pins, switches the EMMC clock on, pulses `WL_ON`, enumerates
the card (function 1 blocks of 64 bytes, function 2 of 512);
2. requests the ALP clock, reads the chip ID: `0x4345` revision 9 is the
CYW43456 (Pi 400), any other revision the CYW43455 (Pi 4B), which picks
`0:/os/firmware/brcm/brcmfmac43456-sdio.*` or `brcmfmac43455-sdio.*`
(shipped unmodified from Raspberry Pi OS's firmware-nonfree);
3. halts the ARM Cortex-R4, holds the 802.11 core in reset, sizes the RAM
from the CR4's banks, writes the firmware at `0x198000` and the NVRAM
(stripped to `key=value\0` pairs and a length token) at the top of RAM,
reading both back to verify, puts the reset vector at address 0 and
releases the CPU;
4. waits for the HT clock, sends the SDPCM protocol version through the
mailbox, enables function 2, the F2 watermark and MES busy signalling of
Linux's CYW43455 settings, save/restore if the chip has it, and the card
interrupt;
5. reads `cur_etheraddr` (the NVRAM's shared placeholder address
`b8:27:eb:74:f2:6c` is replaced with the board address + 1, as on
Raspberry Pi OS), loads the CLM blob (`clmload`, 1400-byte chunks: these
firmware builds carry no regulatory data of their own), turns off glom,
roaming, ARP/ND offload and power save, enables the events it handles and
brings the interface up (`WLC_UP`).
Nothing uses the controller's interrupt: the bus is swept from the idle
loop (and from the timer tick when the cores never idle), every 10 ms while
a scan or a connection is active and every 250 ms otherwise, and at once when
the controller shows the card interrupt. Every sweep reads the SDIO core's
interrupt status, then every frame waiting in function 2 (a 4-byte header
read, then the rest). Ioctls wait for transmit credit and their answer by
sweeping the bus themselves. Frames are delivered with the bus unlocked, and
transmitting never waits for the bus: data frames go into a 32-frame queue
that whoever holds the bus, or the next sweep, sends.
Scans are escans (all channels, active, firmware dwell times); each result
event carries a `brcmf_bss_info_le` whose information elements go into the
same scan table the Intel driver fills from beacons. A join sets `wpaie`
(the RSN element our supplicant builds), `wpa_auth`, `auth` and `wsec`, then
`join` with the BSSID and the chanspec escan reported. The 4-way handshake
runs in the host's supplicant (`Wpa.cpp`) as for the Intel adapter: EAPOL
frames are ordinary Ethernet frames here, and the keys go to the firmware
with `wsec_key` once message 4 has left the transmit queue. The radio does
AES-CCMP only: GCMP networks are refused, as are WPA3-only (SAE), WEP and
TKIP ones.
Unanswered ioctls (a leaky bucket of three), a halt reported through
the mailbox, or repeated bus errors mark the radio failed: the firmware's
console and any trap record are printed to the kernel log (`WiFi-FW`), and
the facade's recovery powers the radio off and repeats the bring-up (at
most three times).
`tests/wifi/run.sh` drives `BrcmfProto.cpp` and `BrcmfConnect.cpp` with the
real supplicant against a simulated firmware: preinit and the CLM chunks,
escan parsing, open and WPA2 joins (an independent authenticator checks
messages 2 and 4 and the installed keys), the wrong passphrase, refusals,
join timeouts and a firmware that stops answering.
## Not supported yet
- SD high-speed mode: the driver transfers by polled PIO at 25 MHz on a
4-bit bus (multi-block CMD18/CMD25 with Auto CMD12), at most 12.5 MB/s;
switching the card to 50 MHz (CMD6) would double that.
- Wi-Fi beyond 802.11 station mode on one interface: no access point mode,
no SDIO high speed (50 MHz), no glom, no firmware supplicant.
- Audio (HDMI via VideoCore, the 3.5 mm jack via PWM).
## Status
Bluetooth works on a Pi 400 (CYW43456), including A2DP music to
headphones. Ethernet has not run yet: QEMU has no GENET model. Wi-Fi has
not run on hardware yet either (QEMU has no model of the radio); only its
protocol and connection layers are tested, in the host harness. It is untested on a Pi 4B (CYW43455), and QEMU has no model of
the radio.
Everything else above is verified on QEMU only, including the Pi-specific path:
the firmware's own XHC0 table, retargeted at QEMU's xHCI and loaded with the
`bcm2711` quirk on QEMU's root bridge, boots to the desktop with non-coherent
DMA. Real hardware has not been tried yet. The ext2 root on the SD card boots on QEMU (`run-rpi4-sd`); the
first-boot grow has been tested only against the kernel's ext2 code in a
host harness (images grown, then checked with `e2fsck`).
-100
View File
@@ -1,100 +0,0 @@
to-do: rewrite & convert to html for docs pages
# SSH
MontaukOS ships an SSH-2 server, `sshd`, that gives a remote client a shell on
the machine. It speaks enough of the protocol to interoperate with stock
OpenSSH clients:
```
ssh dan@montauk-box interactive shell
ssh dan@montauk-box 'ls 0:/' run one command
```
Remote access is **off by default**. Turn it on from Settings > SSH Server,
which flips `services.ssh.enabled` in `0:/config/init.toml`; `init` starts the
daemon on the next boot.
Two gates stand in front of a login: the account must exist in the MontaukOS
user database with a valid password, and it must be listed in the `[allow]`
table of `0:/config/ssh.toml`. A user absent from that table is refused even
with the right password. The Settings applet maintains the table; `sshd` never
writes it.
## Layout
```
programs/src/sshd/
main.cpp transport framing, key exchange, auth, session/channel loop
crypto.hpp host key storage, DH group 14 arithmetic, cipher/MAC wrappers
programs/src/sshserver/
main.cpp Settings applet: enable the service, edit the allow table
programs/include/montauk/ssh.h
shared policy helpers used by both
```
The crypto primitives (SHA-256, HMAC, AES-CTR, RSA signing, the DRBG) come from
BearSSL. The modular exponentiation for Diffie-Hellman is in `crypto.hpp`
because BearSSL exposes no public bignum API.
## What it implements
| | |
|---|---|
| Key exchange | `diffie-hellman-group14-sha256` |
| Host key | `rsa-sha2-256`, 2048-bit, generated on first start |
| Cipher | `aes128-ctr` |
| MAC | `hmac-sha2-256` |
| Compression | none |
| Auth | password only |
Client-initiated rekeying works, so long sessions survive OpenSSH's 1 GiB /
one-hour rekey threshold. Channel windows are tracked in both directions, so
sessions that move more than a window's worth of data keep flowing.
## What it does not
No public key authentication, no port or X11 forwarding, no SFTP or SCP. One
session channel per connection, and connections are served one at a time --
there is no `fork`, so a second client waits for the first to finish. A
pre-authentication timeout keeps an idle peer from holding the daemon shut.
`exec` requests are run by typing the command into an interactive shell rather
than executing it directly, so the prompt and the echoed command come back
mixed into the output. This is why `scp` and friends will not work as-is.
## The host key
Generated on first start and written to `0:/config/ssh_hostkey.toml`, separate
from the policy file so the unprivileged Settings applet -- which rewrites
`ssh.toml` whenever the allow table changes -- never round-trips private key
material.
That separation is hygiene, not protection: **the filesystem has no permission
model**, so any local process can read the host key. Closing that hole needs
FS-level access control, at which point the key file should be restricted to
the account `sshd` runs as. A key found in an older `ssh.toml` is migrated to
the new file on first start so the host key does not change under existing
clients.
## Debugging
`sshd` calls `montauk::print`, which reaches the boot console only. **It has no
usable diagnostics once the desktop is up**, pending the unified syslog.
Worth understanding, because it catches out anything written as a daemon:
`montauk::print` is `SYS_PRINT`, which writes the kernel *terminal*, not the
kernel *log*. Only in-kernel `KernelLogStream` writes raise `g_kernelLogDepth`,
and only those append to the ring buffer `SYS_LOG` reads -- so daemon output
never shows up in `klog`. On top of that, `Sys_Print` returns early once
`g_suppressKernelLog` is set, which the desktop does at startup, so the output is
discarded outright from then on. `init` spawns services with `spawn` rather than
`spawn_redir`, so there is no stream to capture either.
Until there is somewhere for it to go, debugging a handshake means watching the
boot console, or temporarily pointing the print calls at a file.
Failures print a numeric code -- `key exchange failed (N)`, `authentication
failed (N)` -- where N identifies the step; see the `kex_error` assignments in
`main.cpp`. Running the client with `ssh -vvv` and lining its trace up against
those codes is usually the fastest way to find where a handshake diverged.
-395
View File
@@ -1,395 +0,0 @@
to-do: rewrite & convert to html for docs pages
# Wi-Fi
MontaukOS drives Intel AX210-family adapters (the reference part is the AX211).
The driver scans, joins open and WPA2/WPA3-PSK networks, and presents itself to
the network stack as an ordinary Ethernet interface, so `dhcp`, `ping`, `nslookup`
and anything speaking sockets work over Wi-Fi exactly as they do over a cable.
```
wifi scan list nearby networks
wifi connect <ssid> <passphrase> join one
dhcp pick up an address
wifi status what you are connected to
wifi saved / wifi forget <ssid> networks remembered for next time
```
There is a graphical path too: a Wi-Fi entry in the desktop panel and a
Wi-Fi tab in the Network app. See "The desktop side" below.
## Layout
```
kernel/src/Drivers/Net/Wifi/
IwxTrans.cpp PCIe transport: MMIO, MSI-X, DMA rings, firmware boot,
host commands, RX processing, frame TX, key installation
IwxFw.cpp .ucode / .pnvm TLV parsing
IwxMvm.cpp post-ALIVE init, NVM, UMAC scan, RX dispatch
IwxConnect.cpp MLME: contexts, authenticate, associate, 802.11 <-> Ethernet
Wpa.cpp WPA2/WPA3-PSK supplicant (EAPOL 4-way + group rekey)
Ieee80211.hpp frame, element and RSN constants
Wifi.cpp subsystem facade: probe, scan table, syscalls, netif hooks
kernel/src/Libraries/Crypto.cpp SHA-1/SHA-256, HMAC, PBKDF2, AES, CMAC
kernel/src/Net/NetIf.cpp interface registry the Ethernet layer uses
```
## Joining a network
`SYS_WIFI_CONNECT` blocks until the link is up or the attempt fails, and
returns a `WIFI_ERR_*` code the `wifi` tool turns into a specific message
(wrong passphrase, unsupported security, AP out of range, and so on).
The sequence:
1. **Look up the BSS.** The SSID is matched against the scan table, strongest
signal first. If it is not there, one scan is run automatically and the
lookup retried, so `wifi connect` works without scanning first.
2. **Negotiate ciphers.** The AP's RSN element (kept verbatim in the scan
table) picks the pairwise cipher and AKM. CCMP is preferred over GCMP,
plain PSK over PSK-SHA256.
3. **Derive the PMK.** PBKDF2-HMAC-SHA1 over the passphrase with the SSID as
salt, 4096 iterations. A 64-character hex string is taken as a raw PSK
instead.
4. **Bring up firmware contexts.** PHY, MAC, binding and station, then one TX
queue on the management TID.
5. **Authenticate and associate.** Open-system authentication, then an
association request carrying the SSID, supported rates and, for encrypted
networks, the RSN element the supplicant built. Both are retransmitted up
to four times at 400 ms.
6. **Run the 4-way handshake.** EAPOL-Key messages 1-4, then the pairwise key
and the group key go into the firmware with `ADD_STA_KEY`.
7. **Report the link up.** Only now does `NetIf` see `wlan0` as usable.
## The data path
Once associated the driver translates between 802.11 and Ethernet II:
- **TX** - an Ethernet frame becomes a to-DS 802.11 data header plus an
RFC 1042 LLC/SNAP shim carrying the EtherType. The protected bit is set
once keys are installed and the firmware does the CCMP encryption.
- **RX** - the firmware decrypts and strips the MIC but leaves the 8-byte
CCMP header, which is skipped; the LLC/SNAP shim is replaced by an Ethernet
header built from addresses 1 and 3. EAPOL frames are diverted to the
supplicant instead.
One TX queue carries management frames, EAPOL and non-QoS data. The firmware
maps non-QoS data onto the management TID anyway, and the driver never
negotiates block-ack sessions that would need per-TID queues.
### Threading
The RX path runs under `IwxProcessEvents()`'s reentrancy guard and **must not
send a host command** - the command's completion is pumped by the very
function it would be re-entering. Anything needing a command (post-association
context updates, key installation, and therefore the whole EAPOL handshake) is
queued and applied from `IwxConnectService()`, which every idling core calls
and which is itself serialized. Transmitting is safe from either context: it
only writes a descriptor and rings the doorbell.
### Never wait on the clock under a spinlock
`kcp::Spinlock::Acquire()` does `cli`, and `Timekeeping::GetMilliseconds()` is
driven by the APIC timer interrupt. A wall-clock timeout inside a spinlock
therefore cannot expire: the counter never advances, the loop never ends, and
the machine locks solid with interrupts off - no mouse, no keyboard, no
scheduler. This is the same trap `ApicTimer.cpp` documents for the idle path.
`IwxSendCmd` holds a lock across a wait for the firmware's reply, so that lock
is a `kcp::Mutex` (which keeps interrupts enabled), and the wait carries a spin
cap as well as the clock check so it terminates even if the clock is somehow
stuck. It also bails immediately once the firmware is known to have asserted,
because nothing after that will ever be answered.
A firmware command that goes unanswered now dumps the firmware's own error
table on the first timeout - that names the command that asserted - and gives
up on the adapter after three, rather than stalling for seconds per command.
### Sample the clock after the work, not before it
`IwxConnectService()` sends host commands, and every one of them is a round
trip to the adapter that takes real milliseconds. Each step also stamps the
timestamp its deadline is measured from - `EnterState()` sets
`g_stateEnteredMs`, transmitting sets `g_lastTxMs`. A `now` read at the top of
the pass is therefore *older* than the stamps it is about to be compared
against, and because these are unsigned counters, `now - g_stateEnteredMs`
wraps to about 2^64 instead of going negative. Every deadline in the pass then
reads as long expired.
The symptom was a join that failed the instant it succeeded:
```
WiFi: [OK] Associated, AID 3
WiFi: [WARNING] Connection failed: timed out while joining the network
WiFi: [INFO] EAPOL frame received (99 bytes)
```
The two post-association context commands moved the clock forward, the timeout
check compared a stale `now` against the `g_stateEnteredMs` they had just set,
and the contexts came down before the access point's first EAPOL frame could
arrive - which is why message 1 shows up *after* the failure. So the clock is
read only once the command-sending work in the pass is done, and the
comparisons go through `Elapsed()`, which refuses to underflow.
### Teardown unwinds contexts in the order they depend on each other
Firmware 89 asserts when a context is taken away while another still points at
it, and the assert names the command rather than the reason. Three of these
have been hit so far:
| UMAC error | Cause |
|---|---|
| 0x2010330F | PHY binding and link activation folded into one LINK_CONFIG |
| 0x2010330E | link removed while its station still existed |
| 0x2000320F | link deactivated while the MAC was still marked associated |
The last one is the teardown side of the same rule. While
`MAC_CONFIG.is_assoc` is set, the firmware's MAC context owns the link carrying
the BSS, so `TearDown()` sends a `MAC_CONFIG` MODIFY clearing `is_assoc` first,
and only then removes the station, deactivates the link, removes the link,
removes the MAC and drops the PHY context - the order
`iwl_mvm_mld_vif_cfg_changed_station` and the paths below it use on the way
down. `tests/wifi/ap_mlme.py` pins that order.
### A bad access point must not become a bad computer
Three separate defects turned "the Wi-Fi connection went wrong" into "the whole
machine went wrong". They are worth keeping straight because they have nothing
to do with each other beyond sharing a trigger.
**The command wait is a busy spin on a core the scheduler has been told not to
touch.** `ServiceEvents()` runs from `ApicTimer::ServiceDeferredWork()`, which
sets `cpu->reservedForKernelWork` so a bottom half holding a process-context
mutex cannot be preempted into the process that would wait on it. The
scheduler honours that by refusing to place any process on that core and by
skipping it for the reschedule IPI, and on the BSP `RunBspMaintenance()` - the
thing that wakes sleeping processes - is not reached until the pass returns.
`IwxSendCmd` then waits for the firmware by spinning on `IwxDelayUs`, up to a
second. One pass of `TearDown()` is eight commands. Against an adapter that has
stopped answering, that pinned a core for the better part of ten seconds. The
symptom is not a Wi-Fi symptom at all: the cursor crawls, windows stop
repainting, everything stutters.
So a service pass now carries a budget - `IwxBeginServicePass()` /
`IwxEndServicePass()` - of roughly one command's worth of waiting in total.
Commands that do not fit are not sent at all (an abandoned command still holds
its ring slot, and a late answer would be misread as the *next* command's
completion) and are retried on the next trip round the idle loop. The bring-up
path does not bracket itself: it reserves its CPU deliberately and has nothing
to starve, so it keeps the full per-command timeout.
**Counting consecutive failures never fires on the failure that matters.** The
give-up rule was three unanswered commands in a row, with any success resetting
the count. An adapter that answers some commands and drops others - which is
exactly what a marginal link leaves the firmware doing - therefore never
reached the cutoff, while every drop still cost a full timeout. The stall was
not a one-off; it repeated indefinitely, and the driver never concluded
anything was wrong. It is a leaky bucket now: a failure adds one, a success
drains one, so a firmware failing even a fraction of its commands trips the
cutoff in bounded time.
**`IwxFwState::Error` was a one-way door.** Nothing anywhere cleared it. A
single firmware assert left `StartJoin()` returning `WIFI_ERR_NO_ADAPTER` -
which the user sees as *no adapter is ready* - for the rest of the uptime, and
the only way back was a reboot. Disconnecting and reconnecting could not help:
there was nothing to reconnect with. Since `IwxReadFirmware()` is idempotent
and the parsed image stays resident, the cure is to stop the device and run the
same bring-up again, which `ServiceRecovery()` now does. It is rate-limited to
one attempt per five seconds and capped at three, because the reset has its own
multi-second handshakes and an adapter that will not come back after three
tries is genuinely broken - repeating it forever would be its own kind of
stall.
### The link is up only while the access point says so
`IwxLinkUp()` was nothing but `g_state == Connected`, and nothing moved it off
that state unless the access point was polite enough to send a
deauthentication frame. An access point that simply stops being there - a phone
hotspot that sleeps, wanders off channel, or drops the station without saying
so - left the link reported as up indefinitely. `NetIf::Active()` went on
choosing `wlan0`, every packet went into the void, and the desktop showed a
healthy connection while nothing resolved and nothing connected.
Beacon loss cannot be used to notice this: once associated, `MacConfigCmd`
stops asking for beacons and the firmware tracks them itself. What is
observable is that our own frames stop being acknowledged, since `IwxTxComplete`
gets a status per frame. A long enough run of failures - with any
acknowledgement, or any frame received from the BSS, restarting the count -
means the access point is gone, and the link comes down so the stack can fall
back to a cable and the panel can report the truth.
The threshold is counted in frames rather than seconds on purpose: a link that
is merely idle has nothing to send and must not be torn down for it. And
`IwxTxComplete` runs inside the RX pump, so it only sets a flag; the teardown
happens in `IwxConnectService()`, where sending the commands it needs is
allowed. `tests/wifi/ap_mlme.py` pins both halves of that.
## The desktop side
### Nothing in the GUI may block
`SYS_WIFI_SCAN` sweeps for up to twenty seconds and `SYS_WIFI_CONNECT` waits
out a whole handshake. Either one called from `desktop.elf` would freeze the
compositor - and with it the mouse, the panel and every window - for seconds at
a time. So the same work has a second, non-blocking entry point:
| | |
|---|---|
| `SYS_WIFI_SCAN_START` | starts a sweep and returns immediately |
| `SYS_WIFI_RESULTS` | copies the scan table out without touching the radio |
| `SYS_WIFI_CONNECT_ASYNC` | starts a join and returns immediately |
| `SYS_NETIFS` | lists the registered interfaces (see below) |
`WifiInfo` grew the fields that make polling enough to follow along:
`scanning` and `scanGeneration` for the sweep, `joining` and `connState` for
the handshake, and `lastError` for how the last join ended. The GUI reads
`SYS_WIFI_INFO` every 400 ms while something is in flight and every three
seconds otherwise.
The deadlines belong to the kernel, not the caller: `ServiceAsync()` runs from
`ServiceEvents()` - after the RX pump has returned, so it is allowed to send
commands - and aborts a scan that overruns, tears down a join that stalls, and
records `lastError` when one fails. A failed join is unwound the moment it
fails, so `connState` is back to idle by the time anyone looks; `lastError` is
what survives to be reported.
### Wired and wireless are separate on the panel
The IP configuration is global to the stack, so "which interface does this
address belong to?" is not a question `SYS_GETNETCFG` can answer. `SYS_NETIFS`
reports each registered interface with its name, MAC, link state and an
`active` flag - the one `NetIf::Active()` currently sends through. The Ethernet
popup shows the address only while the wired interface is the active one, and
says "Not in use" when the cable is up but Wi-Fi is carrying the traffic; the
Wi-Fi popup does the mirror image. Each icon appears only when its hardware
does: no wired interface, no Ethernet icon; no adapter, no Wi-Fi icon.
The Wi-Fi icon stays white whatever the radio is doing. State belongs in the
popup, and an icon that changes colour next to the clock is just noise.
Picking a network that needs a passphrase opens a real window - created with
`desktop_create_window()` and the four callbacks, exactly like the reboot and
shutdown dialogs in `dialogs.cpp` - rather than something painted into the
panel overlay. It therefore has a title bar, can be dragged and closed, appears
in the window list, and gets its text field, checkboxes and buttons from the
same `mtk` widgets the settings apps use.
### What runs at startup
`desktop.elf` keeps looking for an adapter until one appears (firmware loads
well after login), starts one scan as soon as the firmware reports ready, and
when the results land joins the strongest saved network that is in range. Once
the link is up, and only if no address is configured, it spawns `dhcp.elf`.
### Saved networks
`0:/config/wifi.toml` holds them:
```toml
[wifi]
autoconnect = true
[network.0]
ssid = "Home"
psk = "passphrase"
```
**No default copy of this file ships in the image.** It is created on the first
save, the way `bluetooth.toml`, `display.toml` and `session.toml` are. A shipped
default looks harmless - it only documents the schema - but it is laid down
again by anything that refreshes the system files, and it takes the user's saved
networks with it when it lands. That is exactly what happened the first time
this was written, and it is why the schema is documented here instead.
`programs/include/montauk/wifi.h` is the one implementation of reading,
writing and searching that file, shared by the panel, the Network app and the
`wifi` command, so all three agree on the schema. The passphrase is stored as
typed because that is what the join needs - the kernel derives the PMK from it,
or takes a 64-character hex string as a raw PSK. There is no key store to hide
it in: anyone who can read `0:/config` can read the passphrases.
## The interface registry
`Net::NetIf` replaced the Ethernet layer's direct calls into the E1000
drivers. Drivers register a name, a kind, and three function pointers; the
stack sends through `NetIf::Active()`, which prefers a wired interface with a
link and otherwise takes the first interface reporting one. A Wi-Fi-only
machine therefore has no link until it joins a network, and a machine with a
cable plugged in keeps using it.
`SYS_NETSTATUS` reports whichever interface is active, so `ifconfig` shows the
wireless counters once Wi-Fi is carrying traffic.
## What is supported
| | |
|---|---|
| Open networks | yes |
| WPA2-PSK, CCMP or GCMP | yes |
| WPA2-PSK-SHA256 | yes |
| WPA3 transition mode (PSK advertised alongside SAE) | yes, joins via PSK |
| Group key rekeying | yes |
| WPA3-only (SAE) | no |
| Management frame protection required (MFPR) | no |
| WEP, original WPA / TKIP | no |
| 802.1X enterprise (EAP) | no |
| Block-ack aggregation, HT/VHT/HE rates | no - legacy rates only |
SAE needs finite-field or elliptic-curve arithmetic that does not belong in
this kernel, and MFP needs BIP. Both are rejected up front with a specific
log line rather than failing partway through a handshake. Mixed WPA/WPA2
networks that still broadcast under TKIP are refused for the same reason: the
pairwise key would install but every broadcast frame would be dropped, which
looks like a working connection that cannot get a DHCP lease.
## Crypto
`kernel/src/Libraries/Crypto.cpp` exists because the supplicant runs in the
kernel and BearSSL is a userspace library. It provides SHA-1, SHA-256, HMAC
over both, PBKDF2-HMAC-SHA1, AES-128/256, RFC 3394 key wrap/unwrap and
AES-CMAC. It is not a general-purpose crypto library and should not be used
as one.
## Testing
`./tests/wifi/run.sh` compiles the shipping sources for the host against a
small shim and drives them from Python. It is the real `Crypto.cpp`,
`Wpa.cpp` and `IwxConnect.cpp`, not a copy, with only the transport stubbed.
- **Crypto primitives** against the published vectors - FIPS-197 for AES,
RFC 2202/4231 for HMAC, RFC 3394 for key wrap, RFC 4493 for CMAC, and the
IEEE 802.11i Annex H.4 WPA passphrase vectors for PBKDF2.
- **The supplicant** against an independent authenticator using `hashlib` and
`cryptography`: messages 2 and 4 carry MICs that verify under a PTK the AP
derived itself, the installed TK and GTK match the AP's, a wrong passphrase
produces a MIC the AP rejects, group rekeys and message-3 retransmissions
are answered, and RSN negotiation picks the right suites across eight
real-world information elements.
- **The MLME and data path** against a simulated AP that decodes every frame
the driver emits: the authentication request, the association request and
its elements (SSID, rates, capabilities, RSN), the handshake carried inside
real 802.11 data frames, key installation arguments, and the encapsulation
both ways - to-DS addressing, the protected bit, LLC/SNAP, sequence numbers,
broadcast delivery, and the filtering of foreign-BSSID and null-data frames.
Also the branches: open networks, retransmission and give-up when the AP is
silent, authentication and association rejections, and an AP-initiated
deauthentication bringing the link down.
- **Firmware context ordering** - the MLD command sizes and field offsets
against the decoded Linux trace, and the order the teardown unwinds the
contexts in, which is what the asserts above are about.
The harness clock advances on every host command
(`CMD_ROUND_TRIP_MS` in `mlme_harness.cpp`) rather than standing still. That
detail matters: a frozen clock makes every elapsed-time comparison in the
service loop trivially true or trivially false, and hid the underflow described
under "Sample the clock after the work, not before it" - the host tests passed
while the adapter could not join a network at all. Anything that reads
`Timekeeping::GetMilliseconds()` should be tested with time actually moving.
What is left needs the adapter, because it is the firmware's opinion rather
than the driver's logic: whether the firmware accepts the TX command and TFD
layout and actually radiates the frames, whether `ADD_STA_KEY` installs the
keys the driver asks for, whether the RX MPDU descriptor is read correctly off
real receptions, and whether association succeeds against a real AP's timing
and rate expectations. None of that can be exercised in QEMU, which has no
AX210-family device to emulate.
+53 -2
View File
@@ -27,6 +27,20 @@ ifeq ($(ARCH),x86_64)
endif
endif
# The other architectures have no in-tree cross GCC: default to LLVM, which
# cross-compiles to every target (linking needs ld.lld; see docs/porting.md).
ifneq ($(ARCH),x86_64)
ifeq ($(filter command line environment,$(origin CC)),)
CC := clang
endif
ifeq ($(filter command line environment,$(origin CXX)),)
CXX := clang++
endif
ifeq ($(filter command line environment,$(origin AR)),)
AR := ar
endif
endif
# User controllable C compiler command.
CC ?= cc
@@ -139,8 +153,10 @@ ifeq ($(ARCH),aarch64)
override CXX += \
-target aarch64-unknown-none
endif
# Plain char is signed on x86; the generic kernel is written for that.
override SHARED_FLAGS += \
-mgeneral-regs-only
-mgeneral-regs-only \
-fsigned-char
override LDFLAGS += \
-Wl,-m,aarch64elf
endif
@@ -197,8 +213,10 @@ override CXXFLAGS += \
-std=gnu++20 \
-fno-rtti \
-fno-exceptions \
-fmodules-ts \
$(SHARED_FLAGS)
ifneq ($(CXX_IS_CLANG),1)
override CXXFLAGS += -fmodules-ts
endif
# Internal linker flags that should not be changed by the user.
override LDFLAGS += \
@@ -212,6 +230,9 @@ override LDFLAGS += \
# Use "find" to glob all *.c, *.cpp, *.S, and *.asm files in the tree and obtain the
# object and header dependency file names.
override SRCFILES := $(shell cd src && find -L * -type f | LC_ALL=C sort)
# Architecture-specific code lives in src/Arch/<arch>/; only the target's
# directory is built. Everything else in src/ is architecture-neutral.
override SRCFILES := $(filter-out Arch/%,$(SRCFILES)) $(filter Arch/$(ARCH)/%,$(SRCFILES))
override CFILES := $(filter %.c,$(SRCFILES))
override CXXFILES := $(filter %.cpp,$(SRCFILES))
override OBJCFILES := $(filter %.mm,$(SRCFILES))
@@ -253,6 +274,11 @@ $(BUILDNO_HDR): $(filter-out $(BUILDNO_OBJ),$(OBJ)) $(PROG_SRC)
# on a clean build (where no .d dependency exists yet).
$(BUILDNO_OBJ): $(BUILDNO_HDR)
# Compile everything without linking (used by scripts/check-arch-neutral.sh
# to verify a port's objects before a cross linker is available).
.PHONY: objects
objects: $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a
# Link rules for building the C compiler runtime.
cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/*
rm -rf cc-runtime-$(ARCH)
@@ -263,10 +289,35 @@ cc-runtime-$(ARCH)/cc-runtime.a: GNUmakefile cc-runtime/*
CFLAGS="$(CFLAGS)" \
CPPFLAGS='-isystem ../freestnd-c-hdrs/$(ARCH)/include -DCC_RUNTIME_NO_FLOAT'
# Clang's generic-ELF driver links cross targets through the host's GCC,
# so a clang-built non-x86 kernel is linked by ld.lld directly.
ifneq ($(ARCH),x86_64)
ifeq ($(CXX_IS_CLANG),1)
# Without a system lld, fall back to the one rustup ships.
KERNEL_LD ?= $(firstword $(shell command -v ld.lld) \
$(wildcard $(HOME)/.rustup/toolchains/*/lib/rustlib/*/bin/gcc-ld/ld.lld))
override LD_EMULATION_aarch64 := aarch64elf
override LD_EMULATION_riscv64 := elf64lriscv
override LD_EMULATION_loongarch64 := elf64loongarch
override KERNEL_LDFLAGS := \
-m $(LD_EMULATION_$(ARCH)) \
--build-id=none \
--no-relax \
-static \
-z max-page-size=0x1000 \
--gc-sections \
-T linker-$(ARCH).ld
endif
endif
# Link rules for the final executable.
bin-$(ARCH)/$(OUTPUT): GNUmakefile linker-$(ARCH).ld $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a
mkdir -p "$$(dirname $@)"
ifeq ($(KERNEL_LD),)
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a -o $@
else
$(KERNEL_LD) $(KERNEL_LDFLAGS) $(OBJ) cc-runtime-$(ARCH)/cc-runtime.a -o $@
endif
# Compilation rules for *.c files.
obj-$(ARCH)/%.c.o: src/%.c GNUmakefile
+3
View File
@@ -22,6 +22,7 @@ SECTIONS
/* Any address in this region will do, but often 0xffffffff80000000 is chosen as */
/* that is the beginning of the region. */
. = 0xffffffff80000000;
KernelStartSymbol = .;
/* Define a section to contain the Limine requests and assign it to its own PHDR */
.limine_requests : {
@@ -74,4 +75,6 @@ SECTIONS
*(.eh_frame*)
*(.note .note.*)
}
KernelEndSymbol = .;
}
File diff suppressed because it is too large. Load diff
+204 -30
View File
@@ -97,6 +97,35 @@ namespace Hal {
static constexpr uint8_t ConcatResOp = 0x84;
static constexpr uint8_t ToHexStringOp = 0x98;
static constexpr uint8_t ToDecimalStringOp = 0x97;
static constexpr uint8_t ExternalOp = 0x15;
static constexpr uint8_t RefOfOp = 0x71;
static constexpr uint8_t FindSetLeftBitOp = 0x81;
static constexpr uint8_t FindSetRightBitOp = 0x82;
static constexpr uint8_t NotifyOp = 0x86;
static constexpr uint8_t MatchOp = 0x89;
static constexpr uint8_t ObjectTypeOp = 0x8E;
static constexpr uint8_t CreateQWordFieldOp = 0x8F;
static constexpr uint8_t ToStringOp = 0x9C;
static constexpr uint8_t CopyObjectOp = 0x9D;
static constexpr uint8_t MidOp = 0x9E;
static constexpr uint8_t ContinueOp = 0x9F;
static constexpr uint8_t BreakPointOp = 0xCC;
// More extended opcodes (after ExtOpPrefix)
static constexpr uint8_t CondRefOfOp = 0x12;
static constexpr uint8_t CreateFieldOp = 0x13;
static constexpr uint8_t LoadTableOp = 0x1F;
static constexpr uint8_t LoadOp = 0x20;
static constexpr uint8_t SignalOp = 0x24;
static constexpr uint8_t WaitOp = 0x25;
static constexpr uint8_t ResetOp = 0x26;
static constexpr uint8_t FromBCDOp = 0x28;
static constexpr uint8_t ToBCDOp = 0x29;
static constexpr uint8_t UnloadOp = 0x2A;
static constexpr uint8_t DebugOp = 0x31;
static constexpr uint8_t FatalOp = 0x32;
static constexpr uint8_t TimerOp = 0x33;
static constexpr uint8_t DataRegionOp = 0x88;
// ============================================================================
@@ -104,7 +133,11 @@ namespace Hal {
// ============================================================================
static constexpr int MaxCallDepth = 16;
static constexpr int MaxLoopIterations = 1024;
// ACPICA's ceiling. Firmware polls hardware in While loops with a
// Sleep or Stall in the body (link training, power-good, L2/L3 ready)
// and expects more than a token number of passes.
static constexpr int MaxLoopIterations = 0xFFFF;
// Backstop cap on evaluation nesting. This counter is shared by
// expression evaluation AND block execution, and block nesting is
@@ -133,8 +166,10 @@ namespace Hal {
public:
Interpreter();
// Parse a DSDT or SSDT table into the namespace.
// tableData points to the CommonSDTHeader (HHDM-mapped).
// Parse a DSDT or SSDT table into the namespace. Tables may be
// loaded one after another; each method remembers which table
// its body lives in. tableData points to the CommonSDTHeader
// (HHDM-mapped) and must stay mapped.
bool LoadTable(void* tableData);
// Evaluate a named object, returning its value.
@@ -144,6 +179,28 @@ namespace Hal {
// Evaluate a method with arguments.
bool EvaluateMethod(const char* path, const Object* args, int argCount, Object& result);
// Evaluate a node: a method is run with `args`, a field is read,
// anything else hands back its value (non-data objects as a
// Reference to the node).
bool EvaluateNode(int32_t node, const Object* args, int argCount, Object& result);
// Find the child of `parent` named `seg` ("_PS3", "_ON"), -1 if
// there is none. Short names are padded with '_'.
int32_t FindChild(int32_t parent, const char* seg) const;
// EvaluateNode() on a named child. False when it does not exist.
bool EvaluateChild(int32_t parent, const char* seg, const Object* args,
int argCount, Object& result);
// Turn a Reference, or a NameRef/String naming an object relative
// to `scopeNode`, into a node index. -1 when it names nothing.
int32_t ResolveReference(const Object& obj, int32_t scopeNode) const;
// The PCI function a Device node describes: its _ADR on the bus
// behind its parent (a root bridge's _BBN, or a PCI-to-PCI
// bridge's secondary bus). False when it cannot be placed.
bool GetPciAddress(int32_t deviceNode, uint8_t& bus, uint8_t& dev, uint8_t& fn);
// Read a field value. Returns integer.
bool ReadField(int32_t nodeIndex, uint64_t& value);
@@ -157,6 +214,10 @@ namespace Hal {
// Check if the interpreter has been initialized
bool IsInitialized() const { return m_initialized; }
// Nodes [0, DsdtNodeCount()) came from the DSDT. Nodes past it were
// added by SSDTs (or by methods run since).
int32_t DsdtNodeCount() const { return m_dsdtNodeCount; }
// Log every opcode executed, with its offset, for the next
// `budget` opcodes. The only way to see where a method that runs to
// completion without returning a value actually goes.
@@ -170,8 +231,10 @@ namespace Hal {
// Read one element of a Package object. Works for both a package
// whose elements were computed at run time (materialized in the
// package store) and one that is still just the constant AML bytes
// it was declared with. Returns false when the object is not a
// package or the index is out of range.
// it was declared with. A name in a constant package comes back as
// a NameRef unless it resolves (from the root) to plain data, in
// which case its value is returned. Returns false when the object
// is not a package or the index is out of range.
bool GetPackageElement(const Object& package, int index, Object& result);
// Number of elements a Package declares.
@@ -183,17 +246,35 @@ namespace Hal {
int32_t MaterializeNodePackage(int32_t nodeIndex);
private:
struct ExecContext;
// ============================================================================
// Parsing (table load)
// ============================================================================
// ctx is the method being executed when a definition appears in
// a method body, nullptr at table load. Operands that are TermArgs
// (region offsets, buffer sizes, bank values) are evaluated in it.
bool ParseBlock(const uint8_t* aml, uint32_t offset, uint32_t endOffset,
int32_t scopeNode);
bool ParseNamedObject(const uint8_t* aml, uint32_t* pos, uint32_t endOffset,
int32_t scopeNode);
int32_t scopeNode, ExecContext* ctx = nullptr);
bool ParseExtendedOp(const uint8_t* aml, uint32_t* pos, uint32_t endOffset,
int32_t scopeNode);
int32_t scopeNode, ExecContext* ctx = nullptr);
// The FieldList shared by Field, IndexField and BankField.
void ParseFieldList(const uint8_t* aml, uint32_t* pos, uint32_t fieldEnd,
int32_t scopeNode, uint8_t fieldFlags, const Object& proto);
// Evaluate a TermArg operand of a definition: in the running
// method, or in the table-load context.
bool EvalOperand(ExecContext* ctx, const uint8_t* aml, uint32_t* pos,
uint32_t endOffset, int32_t scopeNode, Object& result);
// CreateBitField .. CreateQWordField and CreateField.
bool CreateBufferField(ExecContext& ctx, uint32_t* pos, uint32_t endOffset,
uint8_t op);
// ============================================================================
@@ -214,6 +295,10 @@ namespace Hal {
char* outRelative = nullptr, int relLen = 0,
bool* outSearchable = nullptr);
// Read a NameString and resolve it with the search rule. -1 when
// it names nothing.
int32_t ReadAndResolveName(const uint8_t* aml, uint32_t* pos, int32_t scopeNode);
// Read a single 4-char NameSeg from AML. Advances *pos.
void ReadNameSeg(const uint8_t* aml, uint32_t* pos, char* outSeg);
@@ -225,11 +310,10 @@ namespace Hal {
uint32_t DecodePkgLength(const uint8_t* aml, uint32_t* pos);
// Decode one constant package element out of raw AML bytes and
// advance *pos past it. Non-constant elements (name references,
// locals) decode to integer 0, which is what they were worth
// before the package store existed.
// advance *pos past it. Names come back as NameRef; locals and
// anything else that needs a running method decode to integer 0.
bool DecodeConstElement(const uint8_t* data, uint32_t length,
uint32_t* pos, Object& out);
uint32_t* pos, Object& out) const;
uint64_t DecodeInteger(const uint8_t* aml, uint32_t* pos);
// ============================================================================
@@ -247,6 +331,8 @@ namespace Hal {
Object ReturnValue;
bool Returned;
bool Broken;
bool Continued;
bool Aborted; // undecodable AML: unwind the method
int Depth; // method-call nesting
int ExprDepth; // expression/block nesting
};
@@ -259,36 +345,117 @@ namespace Hal {
~DepthGuard() { Value--; }
};
// Where a Store lands.
struct Target {
enum class Kind : uint8_t { None, Node, Local, Arg, PkgElement, BufByte };
Kind What = Kind::None;
int32_t Node = -1;
int Index = 0; // local/arg number, or element index
int32_t PkgStore = -1;
Object* Buf = nullptr; // BufByte: the buffer being indexed
};
bool ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset);
bool ExecuteOpcode(ExecContext& ctx, uint32_t* pos, uint32_t endOffset);
// Evaluate a term (expression that produces a value) within an execution context.
bool EvalTerm(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
// Evaluate a "SuperName" target for Store operations.
// Returns the node index for named targets, or handles locals/args.
// If isLocal/isArg is set, localIdx/argIdx contains the index.
// A target may also be Index(package, n), which names a package
// element rather than a node, local or arg. pkgStore/pkgElement
// carry that case (pkgStore > 0 when it applies).
bool EvalTarget(ExecContext& ctx, uint32_t* pos,
int32_t& nodeIndex, bool& isLocal, int& localIdx,
bool& isArg, int& argIdx,
int32_t& pkgStore, int& pkgElement);
// Extended (0x5B-prefixed) opcodes in expression or statement position.
bool EvalExtOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
// Store a value to a target (node, local, or arg).
void StoreToTarget(ExecContext& ctx, const Object& value,
int32_t nodeIndex, bool isLocal, int localIdx,
bool isArg, int argIdx,
int32_t pkgStore = -1, int pkgElement = 0);
// EvalTerm's opcode handlers. They live out of line so the
// recursive path only carries the locals of the one running.
bool EvalBufferOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalPackageOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalBinary(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalUnary(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalDivide(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalStoreOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalReference(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalIndexOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalMatchOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalMidOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool EvalNameRef(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
bool CallMethod(ExecContext& ctx, int32_t node, uint32_t* pos, uint32_t endOffset,
Object& result);
bool IndexElement(const Object& source, uint64_t idx, Object& out);
void ReportUnresolved(ExecContext& ctx, uint32_t namePos);
// Target forms that need operand evaluation of their own.
bool EvalIndexTarget(ExecContext& ctx, uint32_t* pos, Target& target);
bool EvalDerefTarget(ExecContext& ctx, uint32_t* pos, Target& target);
// A package element: like EvalTerm, except that a name is a
// reference to the object rather than a call or a read.
bool EvalPackageElement(ExecContext& ctx, uint32_t* pos, uint32_t endOffset,
Object& result);
// Parse a SuperName/Target operand.
bool EvalTarget(ExecContext& ctx, uint32_t* pos, Target& target);
// Store a value to a target, with ACPI's implicit conversion for
// named data objects.
void StoreToTarget(ExecContext& ctx, const Object& value, const Target& target);
// Parse a Target operand and store `value` there.
void StoreResult(ExecContext& ctx, uint32_t* pos, const Object& value);
// The current value behind a target (for Increment and friends).
Object ReadTarget(ExecContext& ctx, const Target& target);
// Run a method node. Caller holds m_evalLock.
bool InvokeMethodNode(int32_t node, const Object* args, int argCount,
Object& result, int depth, int exprDepth);
// Run a method in a context already acquired and holding its
// arguments; releases the context.
bool RunMethodIn(ExecContext* ctx, int32_t node, int depth, int exprDepth,
Object& result);
// The value of a node as an operand: fields read, methods run
// without arguments, non-data objects become References.
bool NodeValue(int32_t node, Object& result, int depth, int exprDepth);
// Log an opcode we cannot decode and stop the method: carrying on
// past it would execute operand bytes as code.
bool Abort(ExecContext& ctx, uint32_t pos, const char* why);
// Native methods (\_OSI).
bool RunNativeMethod(uint8_t which, const Object* args, int argCount, Object& result);
// Install the OS-defined objects (\_OSI, \_REV, \_OS_, \_GL_).
void InstallPredefined();
// Device identification (for PCI_Config regions).
bool IntegerChild(int32_t device, const char* seg, uint64_t fallback,
uint64_t& value, int depth);
bool IsPciRootBridge(int32_t device, int depth);
bool ResolvePciAddress(int32_t node, uint8_t& bus, uint8_t& dev,
uint8_t& fn, int depth);
// ============================================================================
// Field I/O
// ============================================================================
bool ReadRegion(RegionSpace space, uint64_t address, uint32_t bitWidth, uint64_t& value);
bool WriteRegion(RegionSpace space, uint64_t address, uint32_t bitWidth, uint64_t value);
// Access `bits` (little-endian, bitLength long) of a field unit.
bool AccessField(int32_t fieldNode, bool write, uint8_t* bits);
bool ReadFieldObject(int32_t fieldNode, Object& value);
bool WriteFieldObject(int32_t fieldNode, const Object& value);
// One aligned access unit of a field's backing store.
bool FieldUnitIo(const Object& field, uint64_t byteOffset, uint32_t width,
bool write, uint64_t& value);
// Region access at byte `offset` into the region, `width` bytes.
bool ReadRegion(int32_t regionNode, uint64_t offset, uint32_t width, uint64_t& value);
bool WriteRegion(int32_t regionNode, uint64_t offset, uint32_t width, uint64_t value);
bool RegionIo(int32_t regionNode, uint64_t offset, uint32_t width,
bool write, uint64_t& value);
bool ReadBufferField(const Object& field, Object& value);
bool WriteBufferField(const Object& field, const Object& value);
// ============================================================================
@@ -310,16 +477,23 @@ namespace Hal {
ExecContext* AcquireContext();
void ReleaseContext();
// Run `body` as a top-level evaluation: serialized, with the stack
// budget anchored here.
template<typename Fn> bool TopLevel(Fn body);
Namespace m_ns;
const uint8_t* m_dsdt;
uint32_t m_dsdtLength;
bool m_initialized;
bool m_predefined;
int32_t m_dsdtNodeCount;
ExecContext* m_ctxPool; // MaxCallDepth + 1 entries, heap
int m_ctxInUse;
int m_evalNesting; // top-level evaluations in progress
uint64_t m_stackFloor; // stack address the interpreter stops at
int m_traceBudget; // opcodes left to log, 0 = off
ExecContext* m_loadCtx; // table-load operand context
uint64_t m_delayedUs; // total Sleep/Stall time requested
int m_methodScan; // >0 while scanning a method body at load
kcp::Mutex m_evalLock;
};
+10
View File
@@ -89,6 +89,10 @@ namespace Hal {
s_transientNext = 0;
}
bool IsPersistent(int32_t index) {
return index > 0 && index <= MaxPersistent;
}
static Entry* Lookup(int32_t index) {
if (index <= 0) return nullptr;
int32_t i = index - 1;
@@ -293,6 +297,12 @@ namespace Hal {
return -1;
}
int32_t Namespace::FindChild(int32_t parentIndex, const char* seg) const {
char padded[MaxNameSegLen + 1];
PadSegment(seg, padded);
return FindChildByName(parentIndex, padded);
}
int32_t Namespace::CreateNode(const char* absolutePath) {
EnsureRoot();
char segments[MaxPathDepth][MaxNameSegLen + 1];
+66 -3
View File
@@ -31,6 +31,16 @@ namespace Hal {
ThermalZone,
PowerResource,
BufferField,
Event,
// A reference to a namespace node: what RefOf() yields, and what a
// name evaluates to when it names something that is not data (a
// device or power resource listed in a _PRx package, say).
Reference,
// A name inside a constant package, kept as written ("PG00",
// "\_SB.PCI0.PEG0") because its scope is only known to whoever
// reads the package. Interpreter::ResolveReference() turns it
// into a node.
NameRef,
};
// ============================================================================
@@ -63,7 +73,7 @@ namespace Hal {
static constexpr int InitialChildren = 8;
static constexpr int MaxStringLen = 64;
static constexpr int MaxBufferLen = 256;
static constexpr int MaxPackageElements = 32;
static constexpr int MaxPackageElements = 64;
static constexpr int MaxMethodArgs = 7;
static constexpr int MaxMethodLocals = 8;
@@ -102,10 +112,23 @@ namespace Hal {
uint32_t Length;
} Buffer;
// A package's constant form: its element bytes, left in the
// table they were declared in, and the declared element count. ACPI tables stay
// mapped for the life of the kernel, so pointing at them is
// both cheaper and free of the size cap a copy would carry --
// a GPIO community table runs well past MaxBufferLen.
struct {
const uint8_t* Data; // first element
uint32_t Length; // bytes of element encodings
uint32_t Count; // NumElements
} Package;
struct {
uint8_t ArgCount; // bits 0-2 of method flags
bool Serialized; // bit 3
uint32_t AmlOffset; // offset into DSDT AML where the method body starts
uint8_t Native; // NativeMethod; 0 = AML
const uint8_t* Table; // the table the body lives in
uint32_t AmlOffset; // offset into Table where the method body starts
uint32_t AmlLength; // length of the method body
} Method;
@@ -116,12 +139,33 @@ namespace Hal {
} Region;
struct {
uint32_t RegionNodeIndex; // index of the parent OperationRegion node
// Field / BankField: the OperationRegion node.
// IndexField: the index register field.
uint32_t RegionNodeIndex;
uint32_t BitOffset;
uint32_t BitLength;
uint8_t AccessType; // 0=Any, 1=Byte, 2=Word, 3=DWord, 4=QWord, 5=Buffer
uint8_t UpdateRule; // 0=Preserve, 1=WriteAsOnes, 2=WriteAsZeros
uint8_t Kind; // FieldKind
int32_t DataNodeIndex; // IndexField: the data register field
int32_t BankNodeIndex; // BankField: the bank select field
uint64_t BankValue; // BankField: value selecting this bank
} Field;
// CreateXxxField over a buffer. Source points at the object
// holding the buffer -- a namespace node's, or a local/arg slot
// of the method that created the field -- so reads and writes
// go through to it.
struct {
Object* Source;
uint32_t BitOffset;
uint32_t BitLength;
} BufferField;
struct {
int32_t Node;
} Reference;
struct {
uint8_t ProcId;
uint32_t PblkAddr;
@@ -132,6 +176,18 @@ namespace Hal {
Object() : Type(ObjectType::None), PkgIndex(-1), Integer(0) {}
};
enum class FieldKind : uint8_t {
Normal = 0,
Index = 1,
Bank = 2,
};
// Methods the OS provides rather than the firmware.
enum class NativeMethod : uint8_t {
None = 0,
Osi = 1, // \_OSI
};
// ============================================================================
// Package element store
@@ -155,6 +211,9 @@ namespace Hal {
// Rewind the transient ring. Called once per top-level evaluation.
void ResetTransient();
// True for an entry that outlives the evaluation that made it.
bool IsPersistent(int32_t index);
// Element access. Returns nullptr for an unknown entry or an
// out-of-range element.
Object* Element(int32_t index, int element);
@@ -219,6 +278,10 @@ namespace Hal {
// rebased onto a scope. Returns -1 if no scope contains it.
int32_t SearchUpward(const char* relative, int32_t scopeNodeIndex) const;
// Find the child of `parent` named `seg`; a short name is padded
// with '_' the way node names are stored. Returns -1 if absent.
int32_t FindChild(int32_t parentIndex, const char* seg) const;
// Get a node by index.
NamespaceNode* GetNode(int32_t index);
const NamespaceNode* GetNode(int32_t index) const;
+132 -1
View File
@@ -9,6 +9,8 @@
#include <ACPI/ACPI.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Heap.hpp>
using namespace Kt;
@@ -239,15 +241,144 @@ namespace Hal {
// Full interpreter initialization
// ============================================================================
void InitializeInterpreter(void* dsdtData) {
void InitializeInterpreter(void* dsdtData, ACPI::CommonSDTHeader* xsdt) {
auto& interp = GetInterpreter();
if (!interp.LoadTable(dsdtData)) {
KernelLogStream(ERROR, "AML") << "Failed to load DSDT into AML interpreter";
}
// SSDTs extend the DSDT's namespace and are loaded after it, in
// XSDT order. They are where most laptops keep CPU power tables,
// and -- the reason they matter here -- the discrete GPU's power
// control (_PR3 power resources, _PS3, NVIDIA/AMD _DSM).
int ssdts = 0;
if (xsdt) {
uint32_t entryCount = (xsdt->Length - sizeof(ACPI::CommonSDTHeader)) / 8;
auto* entries = (uint64_t*)((uint64_t)xsdt + sizeof(ACPI::CommonSDTHeader));
for (uint32_t i = 0; i < entryCount; i++) {
if (entries[i] == 0) continue;
auto* table = (ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
const char* sig = table->Signature;
bool isSsdt = sig[0] == 'S' && sig[1] == 'S' && sig[2] == 'D' && sig[3] == 'T';
bool isPsdt = sig[0] == 'P' && sig[1] == 'S' && sig[2] == 'D' && sig[3] == 'T';
if (!isSsdt && !isPsdt) continue;
if (table->Length <= sizeof(ACPI::CommonSDTHeader)) continue;
if (interp.LoadTable(table)) ssdts++;
}
}
KernelLogStream(OK, "AML") << "Loaded DSDT + " << base::dec << (uint64_t)ssdts
<< " SSDT(s)";
auto& ns = interp.GetNamespace();
KernelLogStream(OK, "AML") << "Namespace: " << base::dec
<< (uint64_t)ns.NodeCount() << " / " << (uint64_t)ns.MaxCapacity() << " nodes (dynamic)";
}
// ============================================================================
// Namespace initialization (_REG, _INI)
// ============================================================================
static bool SegIs(const char* name, const char* seg) {
return name[0] == seg[0] && name[1] == seg[1] &&
name[2] == seg[2] && name[3] == seg[3];
}
void InitializeNamespaceObjects() {
auto& interp = GetInterpreter();
if (!interp.IsInitialized()) return;
auto& ns = interp.GetNamespace();
// Collect first, evaluate second: executing AML creates nodes,
// which reallocates a parent's child array under a live walk.
int32_t capacity = ns.NodeCount();
auto* regs = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * capacity);
auto* devices = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * capacity);
if (!regs || !devices) {
if (regs) Memory::g_heap->Free(regs);
if (devices) Memory::g_heap->Free(devices);
return;
}
int regCount = 0, deviceCount = 0;
ns.WalkDescendants(ns.RootIndex(), ObjectType::None,
[&](int32_t idx, const NamespaceNode* node) {
if (node->Obj.Type == ObjectType::Method && SegIs(node->Name, "_REG") &&
regCount < capacity)
regs[regCount++] = idx;
if (node->Obj.Type == ObjectType::Device && deviceCount < capacity)
devices[deviceCount++] = idx;
});
// _REG(PCI_Config, connect). A _REG for another space checks Arg0
// and ignores the call, as with the EC's.
Object args[2]{};
args[0].Type = ObjectType::Integer;
args[0].Integer = (uint64_t)RegionSpace::PciConfig;
args[1].Type = ObjectType::Integer;
args[1].Integer = 1;
for (int i = 0; i < regCount; i++) {
Object result{};
interp.EvaluateNode(regs[i], args, 2, result);
}
// \_SB._INI runs before any device's.
int32_t sb = ns.FindNode("\\_SB_");
if (sb >= 0) {
Object result{};
if (interp.EvaluateChild(sb, "_INI", nullptr, 0, result))
KernelLogStream(INFO, "AML") << "Ran \\_SB._INI";
}
// Then each device, parents first (WalkDescendants is pre-order).
// A device that is neither present nor functional takes its whole
// subtree with it; one that is functional but absent skips only
// its own _INI.
static constexpr int MaxSkipped = 256;
int32_t skipped[MaxSkipped];
int skippedCount = 0;
int ran = 0;
for (int i = 0; i < deviceCount; i++) {
int32_t dev = devices[i];
bool underSkipped = false;
for (int32_t p = ns.GetNode(dev) ? ns.GetNode(dev)->ParentIndex : -1;
p > 0 && !underSkipped; p = ns.GetNode(p) ? ns.GetNode(p)->ParentIndex : -1) {
for (int k = 0; k < skippedCount; k++)
if (skipped[k] == p) { underSkipped = true; break; }
}
if (underSkipped) continue;
uint64_t sta = 0x0F;
Object staObj{};
if (interp.EvaluateChild(dev, "_STA", nullptr, 0, staObj) &&
staObj.Type == ObjectType::Integer)
sta = staObj.Integer;
if (!(sta & 0x01)) {
if (!(sta & 0x08) && skippedCount < MaxSkipped)
skipped[skippedCount++] = dev;
continue;
}
if (interp.FindChild(dev, "_INI") < 0) continue;
Object result{};
bool ok = interp.EvaluateChild(dev, "_INI", nullptr, 0, result);
ran++;
char path[128];
ns.GetNodePath(dev, path, sizeof(path));
KernelLogStream(DEBUG, "AML") << " _INI " << path << (ok ? "" : " FAILED");
}
KernelLogStream(OK, "AML") << "Namespace initialized: " << base::dec
<< (uint64_t)regCount << " _REG, " << (uint64_t)ran << " device _INI";
Memory::g_heap->Free(regs);
Memory::g_heap->Free(devices);
}
};
};
+13 -3
View File
@@ -5,6 +5,7 @@
*/
#pragma once
#include <ACPI/ACPI.hpp>
#include <cstdint>
namespace Hal {
@@ -27,11 +28,20 @@ namespace Hal {
// interpreter or namespace.
SleepObject FindSleepState(void* dsdtData, int state);
// Initialize the AML interpreter with the DSDT.
// This loads the full table into the namespace and enables
// Initialize the AML interpreter with the DSDT, then every SSDT the
// XSDT lists (xsdt may be null to load the DSDT alone).
// This loads the full tables into the namespace and enables
// method evaluation, device enumeration, and field access.
// Should be called during boot after ACPI table discovery.
void InitializeInterpreter(void* dsdtData);
void InitializeInterpreter(void* dsdtData, ACPI::CommonSDTHeader* xsdt);
// Run the namespace initialization the OS owes the firmware once its
// address-space handlers are up: _REG for PCI_Config, then \_SB._INI
// and every present device's _INI, parents first. Firmware works out
// which OS it is running under (OSYS, from _OSI) in these, and gates
// much of its later behaviour -- device power control included -- on
// the answer. Call once the Embedded Controller and PCI are up.
void InitializeNamespaceObjects();
};
};
+7 -8
View File
@@ -453,15 +453,14 @@ namespace Hal {
auto* nsNode = ns.GetNode(node);
if (!nsNode) return result;
if (nsNode->Obj.Type == AML::ObjectType::Package) {
// Parse the raw package buffer to extract SLP_TYP values
const uint8_t* data = nsNode->Obj.Buffer.Data;
uint32_t len = nsNode->Obj.Buffer.Length;
if (len < 2) return result;
if (nsNode->Obj.Type == AML::ObjectType::Package && nsNode->Obj.Package.Data) {
// Parse the raw package elements to extract SLP_TYP values
const uint8_t* data = nsNode->Obj.Package.Data;
uint32_t len = nsNode->Obj.Package.Length;
if (len < 1) return result;
// NumElements
uint8_t numElements = data[0];
uint32_t pos = 1;
uint32_t numElements = nsNode->Obj.Package.Count;
uint32_t pos = 0;
if (numElements >= 1 && pos < len) {
// Decode first element
+7 -12
View File
@@ -7,9 +7,7 @@
#include "AcpiEvents.hpp"
#include <ACPI/AcpiSleep.hpp>
#include <Io/IoPort.hpp>
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/IoApic.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Arch/Interrupts.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -191,24 +189,21 @@ namespace Hal {
// routed. Clear stale status only after the enables are down.
DisableAndClearGpes();
// Route SCI to an IRQ vector. The SCI is level-triggered,
// Route the SCI to its IRQ slot. The SCI is level-triggered,
// active-low (ACPI spec requirement). The MADT may have an
// Interrupt Source Override for this IRQ that already sets
// these flags; RouteIrq applies overrides automatically.
uint8_t sciVector = IRQ_VECTOR_BASE + (uint8_t)g_sciIrq;
uint8_t bspApicId = (uint8_t)LocalApic::GetId();
// these flags; RouteLegacyIrq applies overrides automatically.
// Register the handler before routing to avoid missing events
RegisterIrqHandler((uint8_t)g_sciIrq, SciHandler);
Arch::RegisterIrqHandler((uint8_t)g_sciIrq, SciHandler);
// Route via IoApic (applies MADT overrides for polarity/trigger)
IoApic::RouteIrq((uint8_t)g_sciIrq, sciVector, bspApicId);
// Route to the boot CPU (applies firmware polarity/trigger overrides)
Arch::RouteLegacyIrq((uint8_t)g_sciIrq);
g_initialized = true;
KernelLogStream(OK, "ACPI") << "ACPI events initialized (SCI IRQ "
<< base::dec << (uint64_t)g_sciIrq << ", vector "
<< (uint64_t)sciVector << ")";
<< base::dec << (uint64_t)g_sciIrq << ")";
}
void Reinitialize() {
+336
View File
@@ -0,0 +1,336 @@
/*
* AcpiPower.cpp
* ACPI device power management: _PSx methods, _PRx power resources,
* _DSM calls, and finding the namespace node behind a PCI function
* Copyright (c) 2026 Daniel Hammer
*/
#include "AcpiPower.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <Memory/Heap.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
namespace Hal {
namespace AcpiPower {
// ============================================================================
// PCI <-> ACPI
// ============================================================================
struct PciMapEntry {
int32_t Node;
uint8_t Bus, Dev, Fn;
uint8_t Score;
};
static PciMapEntry* g_map = nullptr;
static int g_mapCount = 0;
static bool g_mapBuilt = false;
bool HasObject(int32_t device, const char* name) {
return AML::GetInterpreter().FindChild(device, name) >= 0;
}
// How much power management a node carries. Picks the real node out
// of duplicates at one address.
static uint8_t PowerScore(int32_t node) {
uint8_t score = 0;
if (HasObject(node, "_PR0") || HasObject(node, "_PR3")) score += 8;
if (HasObject(node, "_PS3")) score += 4;
if (HasObject(node, "_DSM")) score += 2;
if (HasObject(node, "_OFF") || HasObject(node, "_PS0")) score += 1;
return score;
}
static void BuildPciMap() {
auto& interp = AML::GetInterpreter();
auto& ns = interp.GetNamespace();
g_mapBuilt = true;
g_mapCount = 0;
if (!interp.IsInitialized()) return;
// Device nodes with an _ADR, collected before anything is
// evaluated (evaluation may add nodes).
int32_t capacity = ns.NodeCount();
auto* nodes = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * capacity);
if (!nodes) return;
int count = 0;
int32_t sb = ns.FindNode("\\_SB_");
if (sb < 0) sb = ns.RootIndex();
ns.WalkDescendants(sb, AML::ObjectType::Device,
[&](int32_t idx, const AML::NamespaceNode*) {
if (count < capacity && ns.FindChild(idx, "_ADR") >= 0)
nodes[count++] = idx;
});
if (g_map) Memory::g_heap->Free(g_map);
g_map = (PciMapEntry*)Memory::g_heap->Request(sizeof(PciMapEntry) * (count ? count : 1));
if (!g_map) { Memory::g_heap->Free(nodes); return; }
for (int i = 0; i < count; i++) {
uint8_t bus, dev, fn;
if (!interp.GetPciAddress(nodes[i], bus, dev, fn)) continue;
g_map[g_mapCount++] = { nodes[i], bus, dev, fn, PowerScore(nodes[i]) };
}
Memory::g_heap->Free(nodes);
}
void InvalidatePciMap() {
g_mapBuilt = false;
}
int32_t FindPciDevice(uint8_t bus, uint8_t dev, uint8_t fn) {
if (!g_mapBuilt) BuildPciMap();
int32_t best = -1;
int bestScore = -1;
for (int i = 0; i < g_mapCount; i++) {
const auto& e = g_map[i];
if (e.Bus != bus || e.Dev != dev || e.Fn != fn) continue;
if (e.Score > bestScore) {
best = e.Node;
bestScore = e.Score;
}
}
return best;
}
// ============================================================================
// Power resources
// ============================================================================
static int AddUnique(int32_t* out, int count, int max, int32_t node) {
if (node < 0) return count;
for (int i = 0; i < count; i++)
if (out[i] == node) return count;
if (count < max) out[count++] = node;
return count;
}
// The resources one _PRx object lists. The list is copied out before
// anything else is evaluated: a computed package lives in storage the
// next evaluation reuses.
static int CollectList(int32_t device, const char* name, int32_t* out, int count, int max) {
auto& interp = AML::GetInterpreter();
auto& ns = interp.GetNamespace();
AML::Object pkg{};
if (!interp.EvaluateChild(device, name, nullptr, 0, pkg)) return count;
if (pkg.Type != AML::ObjectType::Package) return count;
int n = interp.GetPackageCount(pkg);
int32_t found[MaxResources];
int foundCount = 0;
for (int i = 0; i < n && foundCount < MaxResources; i++) {
AML::Object elem{};
if (!interp.GetPackageElement(pkg, i, elem)) break;
int32_t node = interp.ResolveReference(elem, device);
auto* nn = ns.GetNode(node);
if (nn && nn->Obj.Type == AML::ObjectType::PowerResource)
found[foundCount++] = node;
}
for (int i = 0; i < foundCount; i++)
count = AddUnique(out, count, max, found[i]);
return count;
}
int CollectResources(int32_t device, int32_t* out, int max) {
int count = 0;
count = CollectList(device, "_PR0", out, count, max);
count = CollectList(device, "_PR1", out, count, max);
count = CollectList(device, "_PR2", out, count, max);
count = CollectList(device, "_PR3", out, count, max);
return count;
}
int ResourceState(int32_t resource) {
AML::Object sta{};
if (!AML::GetInterpreter().EvaluateChild(resource, "_STA", nullptr, 0, sta))
return -1;
return (ResultInteger(sta) & 1) ? 1 : 0;
}
// Does a present device outside `owners` list this resource for D0?
// Turning off a rail another device is running on is how a GPU
// power-down would take out, say, an NVMe drive sharing a slot rail.
static bool NeededElsewhere(int32_t resource, const int32_t* owners, int ownerCount) {
auto& interp = AML::GetInterpreter();
auto& ns = interp.GetNamespace();
int32_t capacity = ns.NodeCount();
auto* users = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * capacity);
if (!users) return true; // cannot tell: assume shared
int count = 0;
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
[&](int32_t idx, const AML::NamespaceNode*) {
if (count < capacity && ns.FindChild(idx, "_PR0") >= 0)
users[count++] = idx;
});
bool shared = false;
for (int i = 0; i < count && !shared; i++) {
int32_t dev = users[i];
// Everything at or below an owner goes down with it: the
// card's own child nodes, a duplicate placeholder node at the
// same address, a function the firmware hid from PCI.
bool own = false;
for (int32_t a = dev; a > 0 && !own; ) {
for (int k = 0; k < ownerCount; k++)
if (owners[k] == a) { own = true; break; }
auto* an = ns.GetNode(a);
a = an ? an->ParentIndex : -1;
}
if (own) continue;
int32_t list[MaxResources];
int n = CollectList(dev, "_PR0", list, 0, MaxResources);
bool listed = false;
for (int k = 0; k < n; k++)
if (list[k] == resource) { listed = true; break; }
if (!listed) continue;
// Only a device that is actually there counts.
AML::Object sta{};
uint64_t status = 0x0F;
if (interp.EvaluateChild(dev, "_STA", nullptr, 0, sta))
status = ResultInteger(sta);
if (status & 0x01) {
char path[128];
ns.GetNodePath(dev, path, sizeof(path));
KernelLogStream(INFO, "AcpiPower") << " power resource also used by "
<< path << "; leaving it on";
shared = true;
}
}
Memory::g_heap->Free(users);
return shared;
}
// ============================================================================
// Device power transitions
// ============================================================================
void EnterD3Cold(int32_t device, const int32_t* owners, int ownerCount,
D3ColdReport& report) {
auto& interp = AML::GetInterpreter();
auto& ns = interp.GetNamespace();
report = D3ColdReport{};
if (device < 0) return;
char path[128];
ns.GetNodePath(device, path, sizeof(path));
// Gather the resources first: _PS3 may change what _PRx reports.
int32_t resources[MaxResources];
int count = CollectResources(device, resources, MaxResources);
if (HasObject(device, "_PS3")) {
AML::Object result{};
bool ok = interp.EvaluateChild(device, "_PS3", nullptr, 0, result);
report.RanPs3 = true;
KernelLogStream(INFO, "AcpiPower") << " " << path << "._PS3"
<< (ok ? "" : " (failed)");
}
// Off in reverse order of listing (ACPI turns resources off in
// reverse ResourceOrder; firmware lists them in that order).
for (int i = count - 1; i >= 0; i--) {
int32_t res = resources[i];
char rpath[128];
ns.GetNodePath(res, rpath, sizeof(rpath));
if (ResourceState(res) == 0) {
KernelLogStream(INFO, "AcpiPower") << " " << rpath << " already off";
continue;
}
if (NeededElsewhere(res, owners, ownerCount)) {
report.ResourcesShared++;
continue;
}
AML::Object result{};
bool ok = interp.EvaluateChild(res, "_OFF", nullptr, 0, result);
int after = ResourceState(res);
if (ok) report.ResourcesOff++;
if (after == 1) report.ResourcesStillOn++;
KernelLogStream(INFO, "AcpiPower") << " " << rpath << "._OFF"
<< (ok ? "" : " (failed)")
<< (after == 0 ? ", now off" : after == 1 ? ", still on" : "");
}
}
// ============================================================================
// _DSM
// ============================================================================
AML::Object IntegerArg(uint64_t value) {
AML::Object o{};
o.Type = AML::ObjectType::Integer;
o.Integer = value;
return o;
}
AML::Object BufferArg(const uint8_t* bytes, uint32_t length) {
AML::Object o{};
o.Type = AML::ObjectType::Buffer;
if (length > AML::MaxBufferLen) length = AML::MaxBufferLen;
memcpy(o.Buffer.Data, bytes, length);
o.Buffer.Length = length;
return o;
}
AML::Object EmptyPackageArg() {
AML::Object o{};
o.Type = AML::ObjectType::Package;
o.Package.Data = nullptr;
o.Package.Length = 0;
o.Package.Count = 0;
return o;
}
uint64_t ResultInteger(const AML::Object& result) {
if (result.Type == AML::ObjectType::Integer) return result.Integer;
if (result.Type == AML::ObjectType::Buffer) {
uint64_t v = 0;
for (uint32_t i = 0; i < result.Buffer.Length && i < 8; i++)
v |= (uint64_t)result.Buffer.Data[i] << (8 * i);
return v;
}
return 0;
}
bool EvaluateDsm(int32_t device, const uint8_t uuid[16], uint64_t revision,
uint64_t function, const AML::Object& arg3, AML::Object& result) {
auto& interp = AML::GetInterpreter();
if (device < 0 || interp.FindChild(device, "_DSM") < 0) return false;
AML::Object args[4];
args[0] = BufferArg(uuid, 16);
args[1] = IntegerArg(revision);
args[2] = IntegerArg(function);
args[3] = arg3;
return interp.EvaluateChild(device, "_DSM", args, 4, result);
}
uint64_t DsmFunctions(int32_t device, const uint8_t uuid[16], uint64_t revision,
const AML::Object& arg3) {
AML::Object result{};
if (!EvaluateDsm(device, uuid, revision, 0, arg3, result)) return 0;
uint64_t mask = ResultInteger(result);
// Bit 0 clear means "no functions for this UUID/revision".
return (mask & 1) ? mask : 0;
}
};
};
+92
View File
@@ -0,0 +1,92 @@
/*
* AcpiPower.hpp
* ACPI device power management: _PSx methods, _PRx power resources,
* _DSM calls, and finding the namespace node behind a PCI function
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <ACPI/AML/AmlNamespace.hpp>
namespace Hal {
namespace AcpiPower {
// ============================================================================
// PCI <-> ACPI
// ============================================================================
// The Device node describing PCI function bus:dev.fn, or -1. When
// firmware declares more than one node at the same address (a generic
// placeholder plus a vendor SSDT's real one is common for GPUs), the
// one carrying power-management objects wins.
int32_t FindPciDevice(uint8_t bus, uint8_t dev, uint8_t fn);
// Forget the PCI address map built by FindPciDevice (bus numbers may
// change across a resume).
void InvalidatePciMap();
// True when `device` has a child object named `name` ("_PS3").
bool HasObject(int32_t device, const char* name);
// ============================================================================
// Power resources
// ============================================================================
static constexpr int MaxResources = 8;
// The power resources the device lists in _PR0.._PR3 (deduplicated).
int CollectResources(int32_t device, int32_t* out, int max);
// _STA of a power resource: 1 on, 0 off, -1 unknown.
int ResourceState(int32_t resource);
// ============================================================================
// Device power transitions
// ============================================================================
struct D3ColdReport {
bool RanPs3;
int ResourcesOff; // resources whose _OFF was run
int ResourcesShared; // left on: some other present device needs them
int ResourcesStillOn; // _OFF ran but _STA still says on
};
// Put an ACPI device into D3cold the way the spec orders it: _PS3,
// then every power resource it depends on off, last-listed first. A
// resource that a device outside `owners` also depends on is left
// on -- `owners` lists the nodes being powered down together (the
// functions of one card and the port above them), which may freely
// share, as may anything below them in the namespace. The PCI side
// (PMCSR to D3hot) is the caller's to do first.
void EnterD3Cold(int32_t device, const int32_t* owners, int ownerCount,
D3ColdReport& report);
// ============================================================================
// _DSM
// ============================================================================
// Evaluate _DSM(uuid, revision, function, arg3) on a device. False
// when the device has no _DSM or it failed.
bool EvaluateDsm(int32_t device, const uint8_t uuid[16], uint64_t revision,
uint64_t function, const AML::Object& arg3, AML::Object& result);
// The function bitmap _DSM function 0 reports for (uuid, revision),
// 0 when unsupported. Bit 0 set means "some functions exist".
uint64_t DsmFunctions(int32_t device, const uint8_t uuid[16], uint64_t revision,
const AML::Object& arg3);
// Helpers for building _DSM / method arguments.
AML::Object IntegerArg(uint64_t value);
AML::Object BufferArg(const uint8_t* bytes, uint32_t length);
AML::Object EmptyPackageArg();
// Integer value of a method result: an integer as is, a buffer read
// little-endian (up to 8 bytes).
uint64_t ResultInteger(const AML::Object& result);
};
};
+21 -21
View File
@@ -5,6 +5,7 @@
*/
#include "AcpiShutdown.hpp"
#include <Arch/Cpu.hpp>
#include "AcpiSleep.hpp"
#include "AcpiEvents.hpp"
#include <ACPI/FADT.hpp>
@@ -37,8 +38,24 @@ namespace Hal {
return;
}
// Map and parse the DSDT to find \_S5_
auto* dsdt = (void*)Memory::HHDM(fadt.DsdtAddress);
AML::InitializeInterpreter(dsdt, xsdt);
if (AML::GetInterpreter().IsInitialized()) {
AcpiDevices::DeviceList devices{};
AcpiDevices::EnumerateAll(devices);
}
AcpiSleep::Initialize(xsdt);
// Hardware-reduced ACPI (ARM) has no PM1 registers: the
// architecture powers off instead (Arch::PowerOff).
if ((fadt.Flags & FADT::FlagHardwareReduced) || fadt.PM1aControlBlock == 0) {
KernelLogStream(INFO, "ACPI") << "No PM1 control block - ACPI S5 shutdown unavailable";
return;
}
AML::S5Object s5 = AML::FindS5(dsdt);
if (!s5.Valid) {
@@ -54,23 +71,6 @@ namespace Hal {
KernelLogStream(OK, "ACPI") << "ACPI shutdown initialized (PM1a=" << base::hex
<< (uint64_t)g_pm1aControlBlock << " SLP_TYPa=" << (uint64_t)g_slpTypA << ")";
// Initialize the full AML interpreter with the DSDT.
// This loads the entire DSDT namespace, enabling device enumeration,
// method evaluation, and field I/O for the rest of the kernel.
AML::InitializeInterpreter(dsdt);
// Enumerate ACPI devices now that the namespace is loaded
if (AML::GetInterpreter().IsInitialized()) {
AcpiDevices::DeviceList devices{};
AcpiDevices::EnumerateAll(devices);
}
// Initialize S3 suspend support (reads FACS and \_S3_ from namespace)
AcpiSleep::Initialize(xsdt);
// Note: ACPI event handling (SCI, power button) is initialized
// from Main.cpp after the APIC subsystem is ready.
}
bool IsAvailable() {
@@ -95,7 +95,7 @@ namespace Hal {
}
}
asm volatile("cli");
Arch::DisableInterrupts();
// Phase 1: Write SLP_TYP to PM1x_CNT without SLP_EN,
// preserving existing register bits
@@ -110,7 +110,7 @@ namespace Hal {
}
// Flush all caches before entering sleep state
asm volatile("wbinvd");
Arch::WritebackInvalidateCaches();
// Phase 2: Assert SLP_EN to trigger the transition
Io::Out16(pm1a | SLP_EN, (uint16_t)g_pm1aControlBlock);
@@ -122,7 +122,7 @@ namespace Hal {
// Halt and wait for the hardware to power off
for (;;) {
asm volatile("hlt");
Arch::Halt();
}
}
};
+4 -15
View File
@@ -75,21 +75,6 @@ namespace Hal {
// CPU State (saved across S3)
// ============================================================================
// Layout must match S3Wake.asm offsets exactly.
// No FPU/SSE state — the kernel is compiled with -mno-sse.
struct CpuState {
uint64_t Rax, Rbx, Rcx, Rdx; // 0x00 - 0x18
uint64_t Rsi, Rdi, Rbp, Rsp; // 0x20 - 0x38
uint64_t R8, R9, R10, R11; // 0x40 - 0x58
uint64_t R12, R13, R14, R15; // 0x60 - 0x78
uint64_t Rflags; // 0x80
uint64_t Rip; // 0x88
uint64_t Cr3; // 0x90
uint64_t Cr0, Cr4; // 0x98, 0xA0
uint8_t GdtPtr[10]; // 0xA8
uint8_t IdtPtr[10]; // 0xB2
} __attribute__((aligned(16)));
// ============================================================================
@@ -109,5 +94,9 @@ namespace Hal {
// Returns 0 on successful resume, or -1 if S3 is unavailable.
int Suspend();
// Initialize/IsS3Available/Suspend are implemented by the
// architecture (the FACS waking-vector path is an x86 platform
// mechanism); other targets report S3 as unavailable.
};
};
+212
View File
@@ -0,0 +1,212 @@
/*
* AcpiVideo.cpp
* ACPI video extensions (_BCL/_BCM/_BQC) panel brightness fallback
* Copyright (c) 2026 Daniel Hammer
*/
#include "AcpiVideo.hpp"
#include "AcpiPower.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <Pci/Pci.hpp>
#include <CppLib/Spinlock.hpp>
#include <CppLib/Stream.hpp>
#include <Terminal/Terminal.hpp>
using namespace Kt;
namespace Hal {
namespace AcpiVideo {
// ============================================================================
// State
// ============================================================================
static constexpr int MaxLevels = 101;
static kcp::Mutex g_lock;
static bool g_probed = false;
static int32_t g_output = -1; // output device carrying _BCM
static bool g_hasBqc = false;
static uint32_t g_levels[MaxLevels]; // ascending, de-duplicated
static int g_levelCount = 0;
static int g_percent = -1; // last level set, when no _BQC
// ============================================================================
// Discovery
// ============================================================================
static bool HasMethod(int32_t node, const char* name) {
return AML::GetInterpreter().FindChild(node, name) >= 0;
}
// _ADR display output IDs (ACPI 6.x, B.4.2): bits 11:8 are the type,
// 4 = internal flat panel. The panel is the output to drive.
static int32_t FindOutput() {
auto& interp = AML::GetInterpreter();
auto& ns = interp.GetNamespace();
int32_t fallback = -1;
auto& devices = Pci::GetDevices();
for (uint64_t i = 0; i < devices.size(); i++) {
const auto& d = devices[i];
if (d.ClassCode != 0x03) continue;
int32_t gpu = AcpiPower::FindPciDevice(d.Bus, d.Device, d.Function);
if (gpu < 0) continue;
int32_t panel = -1;
ns.ForEachChild(gpu, AML::ObjectType::Device,
[&](int32_t idx, const AML::NamespaceNode*) {
if (!HasMethod(idx, "_BCM") || !HasMethod(idx, "_BCL")) return true;
AML::Object adr{};
uint64_t id = interp.EvaluateChild(idx, "_ADR", nullptr, 0, adr)
? AcpiPower::ResultInteger(adr) : 0;
if (((id >> 8) & 0xF) == 0x4) {
panel = idx;
return false;
}
if (fallback < 0) fallback = idx;
return true;
});
if (panel >= 0) return panel;
}
if (fallback >= 0) return fallback;
// Some firmware hangs the output device off a bridge the PCI scan
// could not map; any _BCM in the namespace is the only one there is.
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Method,
[&](int32_t idx, const AML::NamespaceNode* n) {
if (fallback >= 0 || n->Name[0] != '_' || n->Name[1] != 'B'
|| n->Name[2] != 'C' || n->Name[3] != 'M')
return;
const AML::NamespaceNode* node = ns.GetNode(idx);
if (node && node->ParentIndex >= 0 && HasMethod(node->ParentIndex, "_BCL"))
fallback = node->ParentIndex;
});
return fallback;
}
// _BCL: { level on AC, level on battery, supported levels... }. Only
// the supported list counts; firmware often repeats or misorders it.
static bool ReadLevels() {
auto& interp = AML::GetInterpreter();
AML::Object bcl{};
if (!interp.EvaluateChild(g_output, "_BCL", nullptr, 0, bcl)
|| bcl.Type != AML::ObjectType::Package)
return false;
g_levelCount = 0;
int count = interp.GetPackageCount(bcl);
for (int i = 2; i < count; i++) {
AML::Object elem{};
if (!interp.GetPackageElement(bcl, i, elem)
|| elem.Type != AML::ObjectType::Integer)
continue;
uint32_t level = (uint32_t)elem.Integer;
int pos = 0;
while (pos < g_levelCount && g_levels[pos] < level) pos++;
if (pos < g_levelCount && g_levels[pos] == level) continue;
if (g_levelCount == MaxLevels) break;
for (int j = g_levelCount; j > pos; j--) g_levels[j] = g_levels[j - 1];
g_levels[pos] = level;
g_levelCount++;
}
return g_levelCount >= 2 && g_levels[g_levelCount - 1] > 0;
}
static void Probe() {
g_probed = true;
auto& interp = AML::GetInterpreter();
if (!interp.IsInitialized()) return;
g_output = FindOutput();
if (g_output < 0) return;
if (!ReadLevels()) {
g_output = -1;
return;
}
g_hasBqc = HasMethod(g_output, "_BQC");
// Without _BQC the level is only known once we set one; until
// then firmware's boot level is assumed to be full brightness.
if (!g_hasBqc) g_percent = 100;
char path[128];
interp.GetNamespace().GetNodePath(g_output, path, sizeof(path));
KernelLogStream(INFO, "AcpiVideo") << "Brightness via " << path
<< "._BCM, " << base::dec << (uint64_t)g_levelCount << " levels"
<< (g_hasBqc ? ", _BQC" : "");
}
// ============================================================================
// Levels
// ============================================================================
static int LevelToPercent(uint32_t level) {
uint32_t top = g_levels[g_levelCount - 1];
if (level > top) level = top;
return (int)(((uint64_t)level * 100 + top / 2) / top);
}
// The supported level closest to the requested share of the top one.
static uint32_t PercentToLevel(int percent) {
uint64_t target = (uint64_t)g_levels[g_levelCount - 1] * (uint32_t)percent / 100;
uint32_t best = g_levels[0];
uint64_t bestDistance = ~0ULL;
for (int i = 0; i < g_levelCount; i++) {
uint64_t distance = g_levels[i] > target ? g_levels[i] - target
: target - g_levels[i];
if (distance < bestDistance) {
bestDistance = distance;
best = g_levels[i];
}
}
return best;
}
static int Query() {
if (g_hasBqc) {
AML::Object result{};
if (AML::GetInterpreter().EvaluateChild(g_output, "_BQC", nullptr, 0, result)
&& result.Type == AML::ObjectType::Integer)
return LevelToPercent((uint32_t)result.Integer);
}
return g_percent;
}
// ============================================================================
// Public API
// ============================================================================
int Brightness(int percent) {
g_lock.Acquire();
if (!g_probed) Probe();
if (g_output < 0) {
g_lock.Release();
return -1;
}
int result;
if (percent < 0) {
result = Query();
} else {
if (percent > 100) percent = 100;
uint32_t level = PercentToLevel(percent);
AML::Object arg = AcpiPower::IntegerArg(level);
AML::Object ignored{};
if (AML::GetInterpreter().EvaluateChild(g_output, "_BCM", &arg, 1, ignored)) {
g_percent = LevelToPercent(level);
int reported = Query();
result = reported >= 0 ? reported : g_percent;
} else {
result = -2;
}
}
g_lock.Release();
return result;
}
};
};
+23
View File
@@ -0,0 +1,23 @@
/*
* AcpiVideo.hpp
* ACPI video extensions (_BCL/_BCM/_BQC) panel brightness fallback
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal {
namespace AcpiVideo {
// Panel brightness through the firmware's ACPI video output device,
// for machines no native backlight driver covers (a panel wired to a
// discrete GPU, a GPU Montauk has no driver for). percent < 0 queries;
// otherwise the nearest level the firmware lists is selected. Returns
// the resulting 0..100, -1 when the firmware has no brightness
// methods, -2 when evaluating them failed. Discovery runs on first
// use. May sleep (AML).
int Brightness(int percent);
};
};
+127
View File
@@ -0,0 +1,127 @@
/*
* AcpiWmi.cpp
* ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call
* its WMxx method
* Copyright (c) 2026 Daniel Hammer
*/
#include "AcpiWmi.hpp"
#include "AcpiDevices.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <Libraries/Memory.hpp>
#include <CppLib/Stream.hpp>
#include <Terminal/Terminal.hpp>
using namespace Kt;
namespace Hal {
namespace AcpiWmi {
static constexpr const char* HidWmi = "PNP0C14";
static constexpr uint32_t WdgEntrySize = 20; // GUID, id, count, flags
static int HexDigit(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;
}
bool ParseGuid(const char* text, uint8_t out[16]) {
// Bytes in text order, then the first three fields byte-swapped.
uint8_t bytes[16];
int count = 0;
for (int i = 0; text[i]; i++) {
if (text[i] == '-') continue;
int hi = HexDigit(text[i]);
int lo = text[i + 1] ? HexDigit(text[i + 1]) : -1;
if (hi < 0 || lo < 0 || count == 16) return false;
bytes[count++] = (uint8_t)(hi << 4 | lo);
i++;
}
if (count != 16) return false;
static constexpr int order[16] = {3, 2, 1, 0, 5, 4, 7, 6,
8, 9, 10, 11, 12, 13, 14, 15};
for (int i = 0; i < 16; i++) out[i] = bytes[order[i]];
return true;
}
static bool SearchDevice(int32_t device, const uint8_t guid[16], Block& out) {
auto& interp = AML::GetInterpreter();
AML::Object wdg{};
if (!interp.EvaluateChild(device, "_WDG", nullptr, 0, wdg)
|| wdg.Type != AML::ObjectType::Buffer)
return false;
// The interpreter caps buffers; a _WDG that fills the cap may have
// lost entries past it.
if (wdg.Buffer.Length >= (uint32_t)AML::MaxBufferLen) {
char path[128];
interp.GetNamespace().GetNodePath(device, path, sizeof(path));
KernelLogStream(WARNING, "WMI") << path << "._WDG may be truncated ("
<< base::dec << (uint64_t)wdg.Buffer.Length << " bytes read)";
}
for (uint32_t off = 0; off + WdgEntrySize <= wdg.Buffer.Length; off += WdgEntrySize) {
const uint8_t* entry = wdg.Buffer.Data + off;
if (memcmp(entry, guid, 16) != 0) continue;
out.Device = device;
out.ObjectId[0] = (char)entry[16];
out.ObjectId[1] = (char)entry[17];
out.InstanceCount = entry[18];
out.Flags = entry[19];
return true;
}
return false;
}
bool Find(const uint8_t guid[16], Block& out) {
auto& interp = AML::GetInterpreter();
if (!interp.IsInitialized()) return false;
auto& ns = interp.GetNamespace();
bool found = false;
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
[&](int32_t idx, const AML::NamespaceNode*) {
if (found) return;
char hid[16];
if (!AcpiDevices::EvaluateHid(idx, hid, sizeof(hid))) return;
if (memcmp(hid, HidWmi, sizeof("PNP0C14")) != 0) return;
if (!(AcpiDevices::EvaluateSta(idx) & AcpiDevices::STA_PRESENT)) return;
found = SearchDevice(idx, guid, out);
});
return found;
}
bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId,
const uint8_t* input, uint32_t inputLength,
AML::Object& result) {
if (!(block.Flags & FlagMethod) || instance >= block.InstanceCount)
return false;
AML::Object args[3];
args[0].Type = AML::ObjectType::Integer;
args[0].Integer = instance;
args[1].Type = AML::ObjectType::Integer;
args[1].Integer = methodId;
if (block.Flags & FlagString) {
uint32_t n = inputLength;
if (n >= (uint32_t)AML::MaxStringLen) n = AML::MaxStringLen - 1;
args[2].Type = AML::ObjectType::String;
if (n) memcpy(args[2].String.Data, input, n);
args[2].String.Data[n] = '\0';
args[2].String.Length = (uint16_t)n;
} else {
uint32_t n = inputLength;
if (n > (uint32_t)AML::MaxBufferLen) n = AML::MaxBufferLen;
args[2].Type = AML::ObjectType::Buffer;
if (n) memcpy(args[2].Buffer.Data, input, n);
args[2].Buffer.Length = n;
}
char method[5] = {'W', 'M', block.ObjectId[0], block.ObjectId[1], '\0'};
return AML::GetInterpreter().EvaluateChild(block.Device, method, args, 3, result);
}
};
};
+44
View File
@@ -0,0 +1,44 @@
/*
* AcpiWmi.hpp
* ACPI-WMI mapper (PNP0C14): find a WMI GUID's data block in _WDG and call
* its WMxx method
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <ACPI/AML/AmlNamespace.hpp>
namespace Hal {
namespace AcpiWmi {
// _WDG flags (Microsoft "Windows Instrumentation: WMI and ACPI").
static constexpr uint8_t FlagExpensive = 0x1;
static constexpr uint8_t FlagMethod = 0x2;
static constexpr uint8_t FlagString = 0x4;
static constexpr uint8_t FlagEvent = 0x8;
// One _WDG entry, bound to the PNP0C14 device that declared it.
struct Block {
int32_t Device; // namespace node of the PNP0C14 device
char ObjectId[2]; // names the WQxx / WSxx / WMxx methods
uint8_t InstanceCount;
uint8_t Flags;
};
// Parse "603E9613-EF25-4338-A3D0-C46177516DB7" into the byte order
// _WDG stores (first three fields little-endian). False if malformed.
bool ParseGuid(const char* text, uint8_t out[16]);
// Search every present PNP0C14 device's _WDG for `guid`. The first
// declaration wins, as on Linux.
bool Find(const uint8_t guid[16], Block& out);
// Evaluate WMxx(instance, methodId, input) on a method block. The input
// is passed as a String instead of a Buffer when the block says so.
bool EvaluateMethod(const Block& block, uint8_t instance, uint32_t methodId,
const uint8_t* input, uint32_t inputLength,
AML::Object& result);
};
};
+1 -1
View File
@@ -9,7 +9,7 @@
#include "AcpiDevices.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <CppLib/Spinlock.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
+2
View File
@@ -107,6 +107,8 @@ namespace Hal {
result.AcpiEnable = fadt->AcpiEnable;
result.AcpiDisable = fadt->AcpiDisable;
result.Flags = fadt->Flags;
result.ArmBootArchFlags = fadt->Header.Length >= offsetof(Table, ArmBootArchFlags) + 2
? fadt->ArmBootArchFlags : 0;
result.Valid = true;
KernelLogStream(INFO, "FADT") << "DSDT at " << base::hex << result.DsdtAddress;
+8
View File
@@ -69,6 +69,13 @@ namespace Hal {
uint8_t X_GPE1Block[12];
} __attribute__((packed));
// FADT Flags
constexpr uint32_t FlagHardwareReduced = 1u << 20;
// ARM_BOOT_ARCH flags (ACPI 5.1+)
constexpr uint16_t ArmBootPsciCompliant = 1u << 0;
constexpr uint16_t ArmBootPsciUseHvc = 1u << 1;
struct ParsedFADT {
uint64_t DsdtAddress;
uint64_t FacsAddress;
@@ -91,6 +98,7 @@ namespace Hal {
uint8_t AcpiEnable;
uint8_t AcpiDisable;
uint32_t Flags;
uint16_t ArmBootArchFlags; // 0 when the table predates the field
bool Valid;
};
+41
View File
@@ -0,0 +1,41 @@
/*
* GsiRouting.cpp
* Platform interrupt lines for ACPI global system interrupts
* Copyright (c) 2026 Daniel Hammer
*/
#include "GsiRouting.hpp"
#include <Arch/Interrupts.hpp>
#include <CppLib/Spinlock.hpp>
namespace Hal::GsiRouting {
static constexpr uint8_t FirstLine = 1;
static constexpr uint8_t LineCount = 24;
static uint32_t g_lineGsi[LineCount] = {};
static uint8_t g_nextLine = FirstLine;
static kcp::Mutex g_lock;
uint8_t LineForGsi(uint32_t gsi, bool levelTriggered, bool activeLow) {
if (Arch::PlatformLinesAreFixed()) return 0;
g_lock.Acquire();
for (uint8_t line = FirstLine; line < g_nextLine; line++) {
if (g_lineGsi[line] == gsi) {
g_lock.Release();
return line;
}
}
uint8_t line = 0;
if (g_nextLine < LineCount &&
Arch::AssignPlatformLine(g_nextLine, gsi, levelTriggered, activeLow)) {
line = g_nextLine++;
g_lineGsi[line] = gsi;
}
g_lock.Release();
return line;
}
}
+19
View File
@@ -0,0 +1,19 @@
/*
* GsiRouting.hpp
* Platform interrupt lines for ACPI global system interrupts
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal::GsiRouting {
// The platform line (1-23) that delivers global system interrupt `gsi`,
// assigning one on first use (Arch::AssignPlatformLine); every device on
// one GSI shares its line. Drivers register their handler on the line
// and call Arch::EnableLegacyIrq. Returns 0 where the platform's lines
// are wired by the hardware (x86) or when none is left.
uint8_t LineForGsi(uint32_t gsi, bool levelTriggered, bool activeLow);
}
+160
View File
@@ -0,0 +1,160 @@
/*
* WmiEcBacklight.cpp
* Firmware (EC) panel backlight behind the WMI interface NVIDIA specified
* for display-mux laptops
* Copyright (c) 2026 Daniel Hammer
*/
#include "WmiEcBacklight.hpp"
#include "AcpiWmi.hpp"
#include "AcpiPower.hpp"
#include <ACPI/AML/AmlInterpreter.hpp>
#include <CppLib/Spinlock.hpp>
#include <CppLib/Stream.hpp>
#include <Terminal/Terminal.hpp>
using namespace Kt;
namespace Hal {
namespace WmiEcBacklight {
// ============================================================================
// Interface
// ============================================================================
static constexpr const char* Guid = "603E9613-EF25-4338-A3D0-C46177516DB7";
// WMxx method IDs.
static constexpr uint32_t MethodLevel = 1;
static constexpr uint32_t MethodSource = 2;
// `mode` values in the argument block.
static constexpr uint32_t ModeGet = 0;
static constexpr uint32_t ModeSet = 1;
static constexpr uint32_t ModeGetMaxLevel = 2;
static constexpr uint64_t SourceEc = 2; // 1 = GPU, 3 = eDP AUX
// Lowest level driven, so the panel never goes fully dark.
static constexpr int MinPercent = 1;
// ============================================================================
// State
// ============================================================================
static kcp::Mutex g_lock;
static bool g_probed = false;
static bool g_available = false;
static AcpiWmi::Block g_block{};
static uint32_t g_maxLevel = 0;
// Argument block: { u32 mode; u32 val; u32 ret; u32 reserved[3]; }.
// The method answers with an Integer. Linux reads it back as the
// `ret` field of the same block, which is where ACPICA's copy-out of
// an Integer puts the value; a firmware returning the block itself as
// a Buffer is read at that same offset.
static bool Call(uint32_t method, uint32_t mode, uint32_t value, uint32_t& out) {
uint8_t args[24] = {};
args[0] = (uint8_t)mode;
args[1] = (uint8_t)(mode >> 8);
args[2] = (uint8_t)(mode >> 16);
args[3] = (uint8_t)(mode >> 24);
args[4] = (uint8_t)value;
args[5] = (uint8_t)(value >> 8);
args[6] = (uint8_t)(value >> 16);
args[7] = (uint8_t)(value >> 24);
AML::Object result{};
if (!AcpiWmi::EvaluateMethod(g_block, 0, method, args, sizeof(args), result))
return false;
if (result.Type == AML::ObjectType::Integer) {
out = (uint32_t)result.Integer;
return true;
}
if (result.Type == AML::ObjectType::Buffer && result.Buffer.Length >= 12) {
const uint8_t* r = result.Buffer.Data + 8;
out = r[0] | (uint32_t)r[1] << 8 | (uint32_t)r[2] << 16 | (uint32_t)r[3] << 24;
return true;
}
// A set may legitimately return nothing.
out = 0;
return mode == ModeSet;
}
static void Probe() {
g_probed = true;
uint8_t guid[16];
if (!AcpiWmi::ParseGuid(Guid, guid) || !AcpiWmi::Find(guid, g_block)) return;
if (!(g_block.Flags & AcpiWmi::FlagMethod) || g_block.InstanceCount == 0) return;
char path[128];
AML::GetInterpreter().GetNamespace().GetNodePath(g_block.Device, path, sizeof(path));
// The interface also exists on machines where the GPU drives the
// backlight; there it must be left alone.
uint32_t source = 0;
if (!Call(MethodSource, ModeGet, 0, source)) {
KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path
<< ": source query failed";
return;
}
if (source != SourceEc) {
KernelLogStream(INFO, "Backlight") << "WMI EC backlight at " << path
<< " reports source " << base::dec << (uint64_t)source << ", not EC";
return;
}
uint32_t maximum = 0;
if (!Call(MethodLevel, ModeGetMaxLevel, 0, maximum) || maximum == 0) {
KernelLogStream(WARNING, "Backlight") << "WMI EC backlight at " << path
<< ": no maximum level";
return;
}
g_maxLevel = maximum;
g_available = true;
char method[5] = {'W', 'M', g_block.ObjectId[0], g_block.ObjectId[1], '\0'};
KernelLogStream(OK, "Backlight") << "Panel brightness via EC (WMI "
<< path << "." << method << ", " << base::dec << (uint64_t)maximum
<< " levels)";
}
static int ToPercent(uint32_t level) {
if (level > g_maxLevel) level = g_maxLevel;
return (int)(((uint64_t)level * 100 + g_maxLevel / 2) / g_maxLevel);
}
// ============================================================================
// Public API
// ============================================================================
int Brightness(int percent) {
g_lock.Acquire();
if (!g_probed) Probe();
int result = -1;
if (g_available) {
uint32_t level = 0;
if (percent >= 0) {
if (percent > 100) percent = 100;
if (percent < MinPercent) percent = MinPercent;
uint32_t target = (uint32_t)(((uint64_t)g_maxLevel * (uint32_t)percent + 50) / 100);
if (target == 0) target = 1;
uint32_t ignored = 0;
if (!Call(MethodLevel, ModeSet, target, ignored)) {
result = -2;
} else {
result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level)
: ToPercent(target);
}
} else {
result = Call(MethodLevel, ModeGet, 0, level) ? ToPercent(level) : -2;
}
}
g_lock.Release();
return result;
}
};
};
+28
View File
@@ -0,0 +1,28 @@
/*
* WmiEcBacklight.hpp
* Firmware (EC) panel backlight behind the WMI interface NVIDIA specified
* for display-mux laptops
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal {
namespace WmiEcBacklight {
// On a laptop whose panel can be switched between GPUs, firmware may
// wire the backlight to the embedded controller instead of either
// GPU and expose it through WMI GUID 603E9613-EF25-4338-A3D0-
// C46177516DB7. Despite the name the interface is pure firmware: it
// works with the discrete GPU powered off. It is used only when the
// firmware itself reports the EC as the brightness source, which is
// also when GPU PWM writes stop reaching the panel.
//
// percent < 0 queries. Returns 0..100, -1 when this interface is not
// the panel's brightness source, -2 when the firmware call failed.
// Discovery runs on first use. May sleep (AML).
int Brightness(int percent);
};
};
+8 -8
View File
@@ -11,8 +11,8 @@
#include <Drivers/Audio/IntelHda.hpp>
#include <Drivers/Audio/Mixer.hpp>
#include <Terminal/Terminal.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/USB/Bluetooth/A2dp.hpp>
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Bluetooth/A2dp.hpp>
#include <Sched/Scheduler.hpp>
#include "Syscall.hpp"
@@ -36,12 +36,12 @@ namespace montauk::abi {
bitsPerSample, pid, name);
}
return -1;
return NoDevice;
}
static int64_t Sys_AudioClose(int handle) {
Drivers::Audio::Mixer::Close(handle);
return 0;
return Success;
}
static int64_t Sys_AudioWrite(int handle, const uint8_t* data, uint32_t size) {
@@ -55,10 +55,10 @@ namespace montauk::abi {
return Drivers::Audio::Mixer::SetOutput(
(Drivers::Audio::Mixer::Output)value);
if (cmd == AUDIO_CTL_BT_STATUS) {
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
if (Drivers::USB::Bluetooth::A2dp::IsReady()) return 2;
return Drivers::USB::Bluetooth::A2dp::GetState()
== Drivers::USB::Bluetooth::A2dp::State::Idle
if (!Drivers::Bluetooth::IsInitialized()) return 0;
if (Drivers::Bluetooth::A2dp::IsReady()) return 2;
return Drivers::Bluetooth::A2dp::GetState()
== Drivers::Bluetooth::A2dp::State::Idle
? 0 : 1;
}
+30 -30
View File
@@ -6,8 +6,8 @@
#pragma once
#include <cstdint>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/USB/Bluetooth/Hci.hpp>
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Bluetooth/Hci.hpp>
#include <Libraries/Memory.hpp>
#include <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp>
@@ -18,15 +18,15 @@
namespace montauk::abi {
static int64_t Sys_BtScan(BtScanResult* buf, int maxCount, uint32_t timeoutMs) {
if (!buf || maxCount <= 0) return -1;
if (!Drivers::USB::Bluetooth::IsInitialized()) return -1;
if (!buf || maxCount <= 0) return InvalidArgument;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
// Use a stack buffer for kernel-side results (max 16)
Drivers::USB::Bluetooth::Hci::InquiryDevice tmpBuf[16];
Drivers::Bluetooth::Hci::InquiryDevice tmpBuf[16];
int kMax = maxCount;
if (kMax > 16) kMax = 16;
int found = Drivers::USB::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
int found = Drivers::Bluetooth::Scan(tmpBuf, kMax, timeoutMs);
if (found < 0) return found;
// Convert kernel InquiryDevice to userspace BtScanResult
@@ -46,39 +46,39 @@ namespace montauk::abi {
}
static int64_t Sys_BtConnect(const uint8_t* bdAddr) {
if (!bdAddr) return -1;
if (!Drivers::USB::Bluetooth::IsInitialized()) return -1;
if (!bdAddr) return InvalidArgument;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
return (int64_t)Drivers::USB::Bluetooth::Connect(bdAddr);
return (int64_t)Drivers::Bluetooth::Connect(bdAddr);
}
static int64_t Sys_BtDisconnect(const uint8_t* bdAddr) {
if (!bdAddr) return -1;
if (!Drivers::USB::Bluetooth::IsInitialized()) return -1;
if (!bdAddr) return InvalidArgument;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
<< (uint64_t)Sched::GetCurrentPid()
<< " entered Bluetooth disconnect stage";
return (int64_t)Drivers::USB::Bluetooth::Disconnect(bdAddr);
return (int64_t)Drivers::Bluetooth::Disconnect(bdAddr);
}
static int64_t Sys_BtSetAddr(const uint8_t* bdAddr) {
if (!bdAddr) return -1;
if (!Drivers::USB::Bluetooth::IsInitialized()) return -1;
if (!bdAddr) return InvalidArgument;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
// bdAddr is a 6-byte buffer, addr[0] = least-significant octet.
return Drivers::USB::Bluetooth::SetAddress(bdAddr) ? 0 : -1;
return Drivers::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
}
static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) {
if (!buf || maxCount <= 0) return 0;
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
if (!Drivers::Bluetooth::IsInitialized()) return 0;
Drivers::USB::Bluetooth::Hci::BondInfo tmpBuf[8];
Drivers::Bluetooth::Hci::BondInfo tmpBuf[8];
int kMax = maxCount;
if (kMax > 8) kMax = 8;
int count = Drivers::USB::Bluetooth::ListBonded(tmpBuf, kMax);
int count = Drivers::Bluetooth::ListBonded(tmpBuf, kMax);
for (int i = 0; i < count; i++) {
memcpy(buf[i].bdAddr, tmpBuf[i].Addr, 6);
@@ -90,21 +90,21 @@ namespace montauk::abi {
}
static int64_t Sys_BtForget(const uint8_t* bdAddr) {
if (!bdAddr) return -1;
if (!Drivers::USB::Bluetooth::IsInitialized()) return -1;
if (!bdAddr) return InvalidArgument;
if (!Drivers::Bluetooth::IsInitialized()) return NoDevice;
return (int64_t)Drivers::USB::Bluetooth::ForgetDevice(bdAddr);
return (int64_t)Drivers::Bluetooth::ForgetDevice(bdAddr);
}
static int64_t Sys_BtList(BtDevInfo* buf, int maxCount) {
if (!buf || maxCount <= 0) return 0;
if (!Drivers::USB::Bluetooth::IsInitialized()) return 0;
if (!Drivers::Bluetooth::IsInitialized()) return 0;
Drivers::USB::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
Drivers::Bluetooth::Hci::ConnectionInfo tmpBuf[4];
int kMax = maxCount;
if (kMax > 4) kMax = 4;
int count = Drivers::USB::Bluetooth::ListConnected(tmpBuf, kMax);
int count = Drivers::Bluetooth::ListConnected(tmpBuf, kMax);
for (int i = 0; i < count; i++) {
memcpy(buf[i].bdAddr, tmpBuf[i].BdAddr, 6);
@@ -119,18 +119,18 @@ namespace montauk::abi {
}
static int64_t Sys_BtInfo(BtAdapterInfo* buf) {
if (!buf) return -1;
if (!buf) return InvalidArgument;
memset(buf, 0, sizeof(BtAdapterInfo));
if (!Drivers::USB::Bluetooth::IsInitialized()) {
if (!Drivers::Bluetooth::IsInitialized()) {
buf->initialized = 0;
return -1;
return NoDevice;
}
buf->initialized = 1;
memcpy(buf->bdAddr, Drivers::USB::Bluetooth::GetBdAddr(), 6);
buf->scanning = Drivers::USB::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
memcpy(buf->bdAddr, Drivers::Bluetooth::GetBdAddr(), 6);
buf->scanning = Drivers::Bluetooth::Hci::IsInquiryActive() ? 1 : 0;
// Copy adapter name
const char* name = "MontaukOS";
@@ -138,7 +138,7 @@ namespace montauk::abi {
for (; i < 63 && name[i]; i++) buf->name[i] = name[i];
buf->name[i] = '\0';
return 0;
return Success;
}
}
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 220
#define MONTAUK_BUILD_NUMBER 123
+97 -72
View File
@@ -1,6 +1,6 @@
/*
* Clipboard.hpp
* SYS_CLIPBOARD_* plain-text clipboard syscalls
* SYS_CLIPBOARD_* syscalls: per-user multi-format clipboard
* Copyright (c) 2026 Daniel Hammer
*/
@@ -11,24 +11,25 @@
#include <CppLib/Spinlock.hpp>
#include "Syscall.hpp"
#include "DataTransfer.hpp"
namespace montauk::abi {
static constexpr int ClipboardMaxSlots = 32;
// One clipboard per user. It holds a multi-format transfer; the text
// syscalls read and replace its TRANSFER_MIME_TEXT format, so text-only
// apps and multi-format apps share the same clipboard.
struct ClipboardSlot {
bool inUse;
char user[32];
char* text;
uint32_t len;
DataTransfer::Transfer transfer;
uint64_t serial;
};
static ClipboardSlot g_clipboardSlots[ClipboardMaxSlots];
static kcp::Mutex g_clipboardLock;
static int Sys_ClipboardClear();
static const char* ClipboardCurrentUser() {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return nullptr;
@@ -81,66 +82,84 @@ namespace montauk::abi {
freeSlot->inUse = true;
ClipboardCopyUser(freeSlot->user, user);
freeSlot->text = nullptr;
freeSlot->len = 0;
freeSlot->transfer.count = 0;
freeSlot->serial = 0;
return freeSlot;
}
static int Sys_ClipboardSetText(const char* data, uint32_t len) {
if (len == 0)
return Sys_ClipboardClear();
if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES || Memory::g_heap == nullptr)
return -1;
for (uint32_t i = 0; i < len; i++) {
if (data[i] == '\0')
return -1;
}
// Swap `incoming` into the caller's clipboard (it becomes empty) and free
// what was there. An empty transfer clears the clipboard.
static int ClipboardReplace(DataTransfer::Transfer& incoming) {
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return -1;
if (currentUser == nullptr) {
DataTransfer::Free(incoming);
return GenericFailure;
}
char user[32];
ClipboardCopyUser(user, currentUser);
char* newText = (char*)Memory::g_heap->Request(len + 1);
if (newText == nullptr) return -1;
for (uint32_t i = 0; i < len; i++)
newText[i] = data[i];
newText[len] = '\0';
char* oldText = nullptr;
DataTransfer::Transfer old;
old.count = 0;
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
if (slot == nullptr) {
g_clipboardLock.Release();
Memory::g_heap->Free(newText);
return -1;
DataTransfer::Free(incoming);
return Busy;
}
oldText = slot->text;
slot->text = newText;
slot->len = len;
old = slot->transfer;
slot->transfer = incoming;
slot->serial = ClipboardNextSerial(slot->serial);
g_clipboardLock.Release();
if (oldText != nullptr)
Memory::g_heap->Free(oldText);
incoming.count = 0;
DataTransfer::Free(old);
return Success;
}
return 0;
static int Sys_ClipboardClear() {
DataTransfer::Transfer empty;
empty.count = 0;
return ClipboardReplace(empty);
}
static int Sys_ClipboardSetText(const char* data, uint32_t len) {
if (len == 0)
return Sys_ClipboardClear();
if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES)
return InvalidArgument;
for (uint32_t i = 0; i < len; i++) {
if (data[i] == '\0')
return InvalidArgument;
}
DataTransfer::Transfer transfer;
int rc = DataTransfer::BuildText(data, len, transfer);
if (rc != Success) return rc;
return ClipboardReplace(transfer);
}
static int Sys_ClipboardSetData(const TransferItem* items, uint32_t count) {
if (count == 0) return Sys_ClipboardClear();
DataTransfer::Transfer transfer;
int rc = DataTransfer::BuildFromUser(items, count, transfer);
if (rc != Success) return rc;
return ClipboardReplace(transfer);
}
static int Sys_ClipboardGetInfo(ClipboardInfo* out) {
if (out == nullptr) return -1;
if (out == nullptr) return InvalidArgument;
out->textBytes = 0;
out->_pad = 0;
out->serial = 0;
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return -1;
if (currentUser == nullptr) return GenericFailure;
char user[32];
ClipboardCopyUser(user, currentUser);
@@ -148,73 +167,79 @@ namespace montauk::abi {
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
if (slot != nullptr) {
out->textBytes = slot->len;
const DataTransfer::Format* text = DataTransfer::Find(slot->transfer, TRANSFER_MIME_TEXT);
out->textBytes = text ? text->len : 0;
out->serial = slot->serial;
}
g_clipboardLock.Release();
return 0;
return Success;
}
static int Sys_ClipboardGetText(char* buffer, uint32_t bufferLen, uint32_t* outLen, uint64_t* outSerial) {
if (outLen == nullptr) return -1;
if (outLen == nullptr) return InvalidArgument;
*outLen = 0;
if (outSerial != nullptr) *outSerial = 0;
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return -1;
if (currentUser == nullptr) return GenericFailure;
char user[32];
ClipboardCopyUser(user, currentUser);
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
if (slot == nullptr || slot->len == 0) {
if (slot != nullptr && outSerial != nullptr)
*outSerial = slot->serial;
if (slot != nullptr && outSerial != nullptr)
*outSerial = slot->serial;
const DataTransfer::Format* text =
slot ? DataTransfer::Find(slot->transfer, TRANSFER_MIME_TEXT) : nullptr;
if (text == nullptr || text->len == 0) {
g_clipboardLock.Release();
return 0;
return Success;
}
*outLen = slot->len;
if (outSerial != nullptr) *outSerial = slot->serial;
if (buffer == nullptr || bufferLen < slot->len) {
g_clipboardLock.Release();
return -1;
}
for (uint32_t i = 0; i < slot->len; i++)
buffer[i] = slot->text[i];
int rc = DataTransfer::CopyOut(*text, buffer, bufferLen, outLen);
g_clipboardLock.Release();
return 0;
return rc;
}
static int Sys_ClipboardClear() {
static int Sys_ClipboardFormats(TransferFormatInfo* out, uint32_t max, uint64_t* outSerial) {
if (outSerial != nullptr) *outSerial = 0;
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return -1;
if (currentUser == nullptr) return GenericFailure;
char user[32];
ClipboardCopyUser(user, currentUser);
char* oldText = nullptr;
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
uint32_t n = 0;
if (slot != nullptr) {
if (outSerial != nullptr) *outSerial = slot->serial;
n = out ? DataTransfer::ListFormats(slot->transfer, out, max) : 0;
if (out == nullptr || max == 0) n = slot->transfer.count;
}
g_clipboardLock.Release();
return (int)n;
}
static int Sys_ClipboardGetData(const char* mime, void* buffer, uint32_t bufferLen, uint32_t* outLen) {
if (outLen == nullptr) return InvalidArgument;
*outLen = 0;
const char* currentUser = ClipboardCurrentUser();
if (currentUser == nullptr) return GenericFailure;
char user[32];
ClipboardCopyUser(user, currentUser);
g_clipboardLock.Acquire();
ClipboardSlot* slot = ClipboardEnsureSlotLocked(user);
if (slot == nullptr) {
g_clipboardLock.Release();
return -1;
}
oldText = slot->text;
slot->text = nullptr;
slot->len = 0;
slot->serial = ClipboardNextSerial(slot->serial);
ClipboardSlot* slot = ClipboardFindSlotLocked(user);
const DataTransfer::Format* format = slot ? DataTransfer::Find(slot->transfer, mime) : nullptr;
int rc = format ? DataTransfer::CopyOut(*format, buffer, bufferLen, outLen) : NotFound;
g_clipboardLock.Release();
if (oldText != nullptr)
Memory::g_heap->Free(oldText);
return 0;
return rc;
}
}
+156
View File
@@ -0,0 +1,156 @@
/*
* DataTransfer.cpp
* Multi-format payloads shared by the clipboard and drag and drop
* Copyright (c) 2026 Daniel Hammer
*/
#include "DataTransfer.hpp"
#include "UserMemory.hpp"
#include <Memory/Heap.hpp>
namespace DataTransfer {
using namespace montauk::abi;
static bool MimeEquals(const char* a, const char* b) {
int i = 0;
for (; a[i] && b[i]; i++) {
char ca = (a[i] >= 'A' && a[i] <= 'Z') ? a[i] + 32 : a[i];
char cb = (b[i] >= 'A' && b[i] <= 'Z') ? b[i] + 32 : b[i];
if (ca != cb) return false;
}
return a[i] == b[i];
}
// Length of the "type/subtype" part, before any ";parameters".
static int MimeBaseLen(const char* mime) {
int n = 0;
while (mime[n] && mime[n] != ';') n++;
while (n > 0 && mime[n - 1] == ' ') n--;
return n;
}
static bool MimeBaseEquals(const char* a, const char* b) {
int la = MimeBaseLen(a);
int lb = MimeBaseLen(b);
if (la != lb) return false;
for (int i = 0; i < la; i++) {
char ca = (a[i] >= 'A' && a[i] <= 'Z') ? a[i] + 32 : a[i];
char cb = (b[i] >= 'A' && b[i] <= 'Z') ? b[i] + 32 : b[i];
if (ca != cb) return false;
}
return true;
}
bool ValidMime(const char* mime) {
if (mime == nullptr) return false;
bool slash = false;
uint32_t i = 0;
for (; mime[i]; i++) {
if (i + 1 >= TRANSFER_MIME_MAX) return false;
char c = mime[i];
if (c < 0x20 || c > 0x7E) return false;
if (c == '/') slash = true;
}
return i > 0 && slash;
}
void Free(Transfer& transfer) {
for (uint32_t i = 0; i < transfer.count; i++) {
if (transfer.formats[i].data != nullptr) Memory::g_heap->Free(transfer.formats[i].data);
transfer.formats[i].data = nullptr;
transfer.formats[i].len = 0;
}
transfer.count = 0;
}
static int AddFormat(Transfer& transfer, const char* mime, const uint8_t* data, uint32_t len) {
if (transfer.count >= TRANSFER_MAX_FORMATS) return InvalidArgument;
Format& f = transfer.formats[transfer.count];
uint32_t m = 0;
for (; mime[m] && m + 1 < TRANSFER_MIME_MAX; m++) f.mime[m] = mime[m];
f.mime[m] = '\0';
// Never hand out a null pointer for an empty format; one byte keeps
// "present but empty" distinct from "absent".
f.data = (uint8_t*)Memory::g_heap->Request(len > 0 ? len : 1);
if (f.data == nullptr) return OutOfMemory;
for (uint32_t i = 0; i < len; i++) f.data[i] = data[i];
f.len = len;
transfer.count++;
return Success;
}
int BuildFromUser(const TransferItem* items, uint32_t count, Transfer& out) {
out.count = 0;
if (items == nullptr || count == 0 || count > TRANSFER_MAX_FORMATS) return InvalidArgument;
if (Memory::g_heap == nullptr) return OutOfMemory;
// Validate everything before allocating anything.
uint64_t total = 0;
for (uint32_t i = 0; i < count; i++) {
if (!UserMemory::String(items[i].mime, TRANSFER_MIME_MAX)) return BadPointer;
if (!ValidMime((const char*)items[i].mime)) return InvalidArgument;
if (items[i].len > 0 && !UserMemory::Range(items[i].data, items[i].len, false)) return BadPointer;
total += items[i].len;
for (uint32_t j = 0; j < i; j++) {
if (MimeEquals((const char*)items[i].mime, (const char*)items[j].mime))
return InvalidArgument;
}
}
if (total > TRANSFER_MAX_BYTES) return InvalidArgument;
for (uint32_t i = 0; i < count; i++) {
int rc = AddFormat(out, (const char*)items[i].mime,
(const uint8_t*)items[i].data, items[i].len);
if (rc != Success) {
Free(out);
return rc;
}
}
return Success;
}
int BuildText(const char* text, uint32_t len, Transfer& out) {
out.count = 0;
if (Memory::g_heap == nullptr) return OutOfMemory;
int rc = AddFormat(out, TRANSFER_MIME_TEXT, (const uint8_t*)text, len);
if (rc != Success) Free(out);
return rc;
}
const Format* Find(const Transfer& transfer, const char* mime) {
if (mime == nullptr) return nullptr;
for (uint32_t i = 0; i < transfer.count; i++) {
if (MimeEquals(transfer.formats[i].mime, mime)) return &transfer.formats[i];
}
bool has_params = false;
for (int i = 0; mime[i]; i++) {
if (mime[i] == ';') { has_params = true; break; }
}
if (has_params) return nullptr;
for (uint32_t i = 0; i < transfer.count; i++) {
if (MimeBaseEquals(transfer.formats[i].mime, mime)) return &transfer.formats[i];
}
return nullptr;
}
uint32_t ListFormats(const Transfer& transfer, TransferFormatInfo* out, uint32_t max) {
uint32_t n = 0;
for (; n < transfer.count && n < max; n++) {
for (uint32_t c = 0; c < TRANSFER_MIME_MAX; c++) out[n].mime[c] = transfer.formats[n].mime[c];
out[n].len = transfer.formats[n].len;
out[n]._reserved = 0;
}
return n;
}
int CopyOut(const Format& format, void* buffer, uint32_t bufferLen, uint32_t* outLen) {
if (outLen != nullptr) *outLen = format.len;
if (format.len == 0) return Success;
if (buffer == nullptr || bufferLen < format.len) return BufferTooSmall;
uint8_t* dst = (uint8_t*)buffer;
for (uint32_t i = 0; i < format.len; i++) dst[i] = format.data[i];
return Success;
}
}
+52
View File
@@ -0,0 +1,52 @@
/*
* DataTransfer.hpp
* Multi-format payloads shared by the clipboard and drag and drop
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include "Syscall.hpp"
// A transfer is a small set of formats, each one MIME type plus bytes, all
// owned by the kernel heap. Producers offer every representation they can
// (a file list and its paths as text, say) and each consumer reads the one it
// understands. The clipboard holds one transfer per user; the window server
// holds the active drag's.
namespace DataTransfer {
struct Format {
char mime[montauk::abi::TRANSFER_MIME_MAX];
uint8_t* data;
uint32_t len;
};
struct Transfer {
Format formats[montauk::abi::TRANSFER_MAX_FORMATS];
uint32_t count;
};
// Copy `count` formats in from user memory, validating every pointer the
// items carry (the item array itself must already be validated). On
// failure `out` is left empty. Returns a StatusCodes value.
int BuildFromUser(const montauk::abi::TransferItem* items, uint32_t count, Transfer& out);
// A transfer holding just `text` as TRANSFER_MIME_TEXT.
int BuildText(const char* text, uint32_t len, Transfer& out);
void Free(Transfer& transfer);
// Exact match first; a request without parameters ("text/plain") also
// matches a stored format that has them ("text/plain;charset=utf-8").
const Format* Find(const Transfer& transfer, const char* mime);
uint32_t ListFormats(const Transfer& transfer, montauk::abi::TransferFormatInfo* out, uint32_t max);
// Copy a format to a (validated) user buffer. *outLen always receives the
// full length; BufferTooSmall when it does not fit, nothing copied then.
int CopyOut(const Format& format, void* buffer, uint32_t bufferLen, uint32_t* outLen);
// MIME string checks shared by every entry point: "type/subtype" with
// optional parameters, printable ASCII, shorter than TRANSFER_MIME_MAX.
bool ValidMime(const char* mime);
}
+34 -22
View File
@@ -6,18 +6,20 @@
#pragma once
#include <cstdint>
#include <Hal/Apic/ApicInit.hpp>
#include <Drivers/PS2/PS2Controller.hpp>
#include <Drivers/USB/Xhci.hpp>
#include <Drivers/USB/UsbDevice.hpp>
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/Bluetooth/Bluetooth.hpp>
#include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp>
#include <Drivers/Net/Genet.hpp>
#include <Drivers/Net/Wifi/Wifi.hpp>
#include <Drivers/Graphics/IntelGPU.hpp>
#include <Drivers/Storage/Ahci.hpp>
#include <Drivers/Audio/IntelHda.hpp>
#include <Pci/Pci.hpp>
#include <Arch/Arch.hpp>
#include <Arch/Platform.hpp>
#include <Arch/Smp.hpp>
#include "Syscall.hpp"
#include <Drivers/Storage/BlockDevice.hpp>
@@ -69,28 +71,21 @@ namespace montauk::abi {
};
// CPU cores (category 0)
int cpuCount = Hal::GetDetectedCpuCount();
int cpuCount = Smp::GetDetectedCpuCount();
if (cpuCount > 0) {
char detail[48];
int p = 0;
p = dl_append(detail, p, "x86_64, ", 48);
p = dl_append(detail, p, MONTAUK_ARCH_NAME ", ", 48);
p = dl_append_dec(detail, p, cpuCount, 48);
p = dl_append(detail, p, " core(s)", 48);
add(0, "Processor", detail);
}
// Interrupt controllers (category 1)
add(1, "Local APIC", "");
add(1, "I/O APIC", "");
// Timer (category 2)
add(2, "LAPIC Timer", "Local APIC periodic timer");
// PS/2 Input (category 3)
add(3, "PS/2 Keyboard", "IRQ 1");
if (Drivers::PS2::IsDualChannel()) {
add(3, "PS/2 Mouse", "IRQ 12");
}
// Interrupt controllers (category 1), timer (2), platform input (3)
Arch::ListPlatformDevices([](void* ctx, uint8_t category,
const char* name, const char* detail) {
(*(decltype(add)*)ctx)(category, name, detail);
}, &add);
// USB devices (category 4)
if (Drivers::USB::Xhci::IsInitialized()) {
@@ -128,11 +123,24 @@ namespace montauk::abi {
if (Drivers::Net::E1000E::IsInitialized()) {
add(5, "Intel E1000E", "Gigabit Ethernet (82574L)");
}
if (Drivers::Net::Genet::IsInitialized()) {
uint32_t speed = Drivers::Net::Genet::GetLinkSpeed();
add(5, "Broadcom GENET", speed == 1000 ? "Gigabit Ethernet (1000 Mbit/s)"
: speed == 100 ? "Gigabit Ethernet (100 Mbit/s)"
: speed == 10 ? "Gigabit Ethernet (10 Mbit/s)"
: "Gigabit Ethernet (no link)");
}
if (Drivers::Net::Wifi::IsPresent()) {
add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp()
? "802.11 wireless (connected)"
: "802.11 wireless");
}
// An on-board UART Bluetooth controller (Raspberry Pi) is no USB
// device, so it is listed here; USB adapters appear above.
if (Drivers::Bluetooth::IsInitialized() &&
Drivers::Bluetooth::Hci::GetTransport() == Drivers::Bluetooth::Hci::Transport::Uart) {
add(5, "Bluetooth Adapter", "Broadcom, HCI over UART");
}
// Display (category 6)
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
@@ -154,7 +162,7 @@ namespace montauk::abi {
add(9, "Intel HDA", detail);
}
if (Drivers::USB::Bluetooth::IsInitialized()) {
if (Drivers::Bluetooth::IsInitialized()) {
add(9, "Bluetooth Audio", "A2DP (SBC)");
}
@@ -225,10 +233,10 @@ namespace montauk::abi {
}
static int Sys_DiskInfo(DiskInfo* buf, int blockDevIndex) {
if (buf == nullptr) return -1;
if (buf == nullptr) return InvalidArgument;
auto* bdev = Drivers::Storage::GetBlockDevice(blockDevIndex);
if (!bdev) return -1;
if (!bdev) return NoDevice;
// Zero the struct first so SATA-specific fields default to 0
for (unsigned i = 0; i < sizeof(DiskInfo); i++)
@@ -254,6 +262,10 @@ namespace montauk::abi {
case Drivers::Storage::BLOCK_KIND_USB_MSC:
buf->type = 4;
break;
case Drivers::Storage::BLOCK_KIND_SD:
buf->type = 5;
buf->rpm = 1;
break;
default:
break;
}
@@ -303,8 +315,8 @@ namespace montauk::abi {
}
// If type is still 0, this block device is not recognized
if (buf->type == 0) return -1;
if (buf->type == 0) return UnsupportedOp;
return 0;
return Success;
}
};
+53 -24
View File
@@ -20,14 +20,33 @@
#include <Fs/ProtectedPaths.hpp>
namespace montauk::abi {
// Writes refused here return SystemIntegrityError: the path is protected
// by Fs/ProtectedPaths, not merely gated on an ordinary capability.
static bool CanModifyFilePath(const char* resolved) {
uint64_t required = Fs::RequiredFileWriteCapability(resolved);
return required == 0 || Sched::HasCapability(required);
}
// Drivers fail a create under a missing parent with a bare -1; report it
// as NoSuchPath so callers can tell it apart (Rust's create_dir_all only
// creates the missing ancestors when mkdir says NotFound).
static bool ParentDirExists(const char* resolved) {
char parent[256];
int len = Lib::strlen(resolved);
if (len >= (int)sizeof(parent)) return false;
memcpy(parent, resolved, len + 1);
while (len > 0 && parent[len - 1] == '/') len--;
while (len > 0 && parent[len - 1] != '/') len--;
while (len > 0 && parent[len - 1] == '/') len--;
if (len == 0 || parent[len - 1] == ':') return true; // drive root
parent[len] = '\0';
Fs::Vfs::StatInfo info;
return Fs::Vfs::VfsStat(parent, info) == 0 && info.isDir;
}
static int Sys_Open(const char* path) {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
return Ipc::OpenFileHandle(resolved);
}
@@ -53,8 +72,8 @@ namespace montauk::abi {
static int Sys_ReadDirAt(const char* path, const char** outNames, int maxEntries,
int startIndex) {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (startIndex < 0) return -1;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (startIndex < 0) return InvalidArgument;
// Get entries from VFS into a kernel-local array
const char* kernelNames[256];
@@ -65,7 +84,7 @@ namespace montauk::abi {
// Allocate a user-accessible page for string data via process heap
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return -1;
if (proc == nullptr) return GenericFailure;
// Use a rotating ring of scratch pages below the heap instead of
// extending the heap high-water mark on every call. This keeps repeated
@@ -75,16 +94,16 @@ namespace montauk::abi {
proc->readdirCursor = (slot + 1) % Sched::UserReadDirSlots;
uint64_t userVa = Sched::UserReadDirBase + (uint64_t)slot * 0x1000ULL;
uint64_t physAddr = Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, userVa);
uint64_t physAddr = Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot, userVa);
uint8_t* pageBuf = nullptr;
if (physAddr == 0) {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) return -1;
if (page == nullptr) return OutOfMemory;
physAddr = Memory::SubHHDM((uint64_t)page);
if (!Memory::VMM::Paging::MapUserIn(proc->pml4Phys, physAddr, userVa)) {
if (!Memory::VMM::Paging::MapUserIn(proc->pageTableRoot, physAddr, userVa)) {
Memory::g_pfa->Free(page);
return -1;
return OutOfMemory;
}
pageBuf = (uint8_t*)page;
} else {
@@ -113,25 +132,26 @@ namespace montauk::abi {
static int Sys_FCreate(const char* path) {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
if (!ParentDirExists(resolved)) return NoSuchPath;
return Ipc::CreateFileHandle(resolved);
}
static int Sys_FDelete(const char* path) {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
return Fs::Vfs::VfsDelete(resolved);
}
static int Sys_Stat(const char* path, FileStat* out) {
if (out == nullptr) return -1;
if (out == nullptr) return InvalidArgument;
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
Fs::Vfs::StatInfo info;
if (Fs::Vfs::VfsStat(resolved, info) != 0) return -1;
if (Fs::Vfs::VfsStat(resolved, info) != 0) return NoSuchPath;
out->size = info.size;
out->mtime = info.mtime;
@@ -139,38 +159,47 @@ namespace montauk::abi {
out->atime = info.atime;
out->mode = info.mode;
out->isDir = info.isDir ? 1u : 0u;
return 0;
return Success;
}
static int Sys_Utime(const char* path, int64_t atime, int64_t mtime,
bool useCurrent) {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
// Timestamps are file state like any other: a protected path must not
// be mutable through a side door that skips the write check.
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
if (useCurrent) {
int64_t now = Timekeeping::GetUnixTimestamp();
atime = now;
mtime = now;
}
return Fs::Vfs::VfsSetTimes(resolved, atime, mtime);
// VfsSetTimes has its own codes: -1 not found, -2 unsupported by the
// filesystem, -3 timestamp out of range, -4 I/O error.
switch (Fs::Vfs::VfsSetTimes(resolved, atime, mtime)) {
case 0: return Success;
case -2: return UnsupportedOp;
case -3: return InvalidArgument;
case -4: return IoFailure;
default: return NoSuchPath;
}
}
static int Sys_FMkdir(const char* path) {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
if (!CanModifyFilePath(resolved)) return SystemIntegrityError;
if (!ParentDirExists(resolved)) return NoSuchPath;
return Fs::Vfs::VfsMkdir(resolved);
}
static int Sys_FRename(const char* oldPath, const char* newPath) {
char resolvedOld[256];
char resolvedNew[256];
if (!ResolveProcessPath(oldPath, resolvedOld, sizeof(resolvedOld))) return -1;
if (!ResolveProcessPath(newPath, resolvedNew, sizeof(resolvedNew))) return -1;
if (!ResolveProcessPath(oldPath, resolvedOld, sizeof(resolvedOld))) return InvalidArgument;
if (!ResolveProcessPath(newPath, resolvedNew, sizeof(resolvedNew))) return InvalidArgument;
if (!CanModifyFilePath(resolvedOld) || !CanModifyFilePath(resolvedNew))
return SYS_ERR_PERMISSION;
return SystemIntegrityError;
return Fs::Vfs::VfsRename(resolvedOld, resolvedNew);
}
+50 -8
View File
@@ -11,6 +11,8 @@
#include <Memory/HHDM.hpp>
#include <Graphics/Framebuffer.hpp>
#include <Drivers/Graphics/IntelGPU.hpp>
#include <ACPI/AcpiVideo.hpp>
#include <ACPI/WmiEcBacklight.hpp>
#include <Terminal/Terminal.hpp>
#include "Syscall.hpp"
@@ -81,7 +83,7 @@ namespace montauk::abi {
for (uint64_t i = 0; i < numPages; i++) {
if (!Memory::VMM::Paging::MapUserInWC(
proc->pml4Phys,
proc->pageTableRoot,
fbPhys + i * 0x1000,
userVa + i * 0x1000
)) {
@@ -96,7 +98,7 @@ namespace montauk::abi {
uint64_t buf1Phys = Drivers::Graphics::IntelGPU::GetBufferPhys(1);
for (uint64_t i = 0; i < numPages; i++) {
if (!Memory::VMM::Paging::MapUserInWC(
proc->pml4Phys,
proc->pageTableRoot,
buf1Phys + i * 0x1000,
userVa + (numPages + i) * 0x1000
)) {
@@ -118,18 +120,49 @@ namespace montauk::abi {
if ((int64_t)index == -1) {
return GPU::FlipSupported() ? 1 : 0;
}
if (!GPU::FlipSupported()) return -1;
if (!GPU::FlipSupported()) return UnsupportedOp;
auto* proc = Sched::GetCurrentProcessPtr();
if ((int64_t)index == -2) {
return proc != nullptr ? GPU::AcquireFlip(proc->pid) : -1;
return proc != nullptr ? GPU::AcquireFlip(proc->pid) : GenericFailure;
}
if (proc == nullptr) return -1;
if (proc == nullptr) return GenericFailure;
return GPU::Flip((int)index, (flags & 1) != 0, proc->pid);
}
// Firmware brightness interfaces (EC via WMI, ACPI _BCM) drive the
// built-in panel, so they apply only while that panel is what is on
// screen (or no native driver knows what is): never for an external
// monitor, where they would silently dim a closed lid instead.
static bool PanelIsActiveOutput() {
if (!Drivers::Graphics::IntelGPU::IsInitialized()) return true;
uint8_t type = Drivers::Graphics::IntelGPU::ActiveConnectorType();
return type == DISPLAY_CONNECTOR_EDP || type == DISPLAY_CONNECTOR_LVDS;
}
// Order follows who actually owns the backlight. A firmware that names
// the EC as the source has taken it away from the GPU, whose PWM then
// still accepts writes that reach nothing; so the EC interface goes
// first, then the GPU's own controls, then ACPI _BCM as a last resort.
static int DisplayBrightness(int percent) {
bool panel = PanelIsActiveOutput();
if (panel) {
int rc = Hal::WmiEcBacklight::Brightness(percent);
if (rc != -1) return rc;
}
int rc = Drivers::Graphics::IntelGPU::Brightness(percent);
if (rc == -1 && panel) rc = Hal::AcpiVideo::Brightness(percent);
return rc;
}
static int64_t Sys_DisplayInfo(DisplayInfo* out) {
return Drivers::Graphics::IntelGPU::GetDisplayInfo(out) ? 0 : -1;
if (!Drivers::Graphics::IntelGPU::GetDisplayInfo(out)) return NoDevice;
// The driver only knows its own backends; report what a set would use.
int level = DisplayBrightness(-1);
out->brightness = level >= 0 ? level : -1;
if (level >= 0) out->capabilities |= DISPLAY_CAP_BRIGHTNESS;
else out->capabilities &= ~DISPLAY_CAP_BRIGHTNESS;
return Success;
}
static int64_t Sys_DisplayModes(DisplayModeInfo* out, int maxCount) {
@@ -137,11 +170,20 @@ namespace montauk::abi {
}
static int64_t Sys_DisplaySetMode(int modeIndex) {
return Drivers::Graphics::IntelGPU::SetDisplayMode(modeIndex);
// The driver answers -2 for a mode that would need link retraining
// and -3 for a modeset that failed and was rolled back.
int64_t rc = Drivers::Graphics::IntelGPU::SetDisplayMode(modeIndex);
if (rc == -2) return UnsupportedOp;
if (rc == -3) return IoFailure;
return rc;
}
// percent < 0 queries. Returns 0..100, NoDevice when the active output
// has no controllable backlight, IoFailure when the hardware refused.
static int64_t Sys_DisplayBrightness(int percent) {
return Drivers::Graphics::IntelGPU::Brightness(percent);
int rc = DisplayBrightness(percent);
if (rc >= 0) return rc;
return rc == -1 ? NoDevice : IoFailure;
}
static uint64_t Sys_TermSize() {
+24 -24
View File
@@ -107,7 +107,7 @@ namespace montauk::abi {
if (page == nullptr) break;
uint64_t phys = Memory::SubHHDM((uint64_t)page);
if (!Memory::VMM::Paging::MapUserInPermissions(
proc->pml4Phys, phys, pageVa, writable, executable)) {
proc->pageTableRoot, phys, pageVa, writable, executable)) {
Memory::g_pfa->Free(page);
break;
}
@@ -131,7 +131,7 @@ namespace montauk::abi {
// Reset heap allocation tracking for a process slot.
// The actual physical pages are freed by Paging::FreeUserHalf() during process cleanup.
inline void CleanupHeapForSlot(int slot, uint64_t /*pml4Phys*/) {
inline void CleanupHeapForSlot(int slot, uint64_t /*pageTableRoot*/) {
if (slot < 0 || slot >= Sched::MaxProcesses) return;
g_heapLocks[slot].Acquire();
HeapAlloc* area = g_heapAllocs[slot];
@@ -182,7 +182,7 @@ namespace montauk::abi {
}
for (uint64_t i = 0; i < area->numPages; i++)
if (Memory::VMM::Paging::GetPhysAddr(
proc->pml4Phys, area->va + i * 0x1000ULL) != 0)
proc->pageTableRoot, area->va + i * 0x1000ULL) != 0)
resident++;
*link = area->next;
area->next = released;
@@ -194,7 +194,7 @@ namespace montauk::abi {
// user TLB entry can otherwise corrupt the frame's next owner.
while (released != nullptr) {
HeapAlloc* next = released->next;
Memory::UnmapAndFreeUserRange(proc->pml4Phys, released->va,
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, released->va,
released->numPages);
Sched::ReleaseUserHeapRange(slot, released->va,
released->numPages * 0x1000ULL);
@@ -211,9 +211,9 @@ namespace montauk::abi {
auto* proc = Sched::GetCurrentProcessPtr();
int slot = GetCurrentSlot();
if (proc == nullptr || slot < 0 || size == 0 || (addr & 0xFFFULL) != 0 ||
size > UINT64_MAX - 0xFFFULL) return -1;
size > UINT64_MAX - 0xFFFULL) return InvalidArgument;
size = (size + 0xFFFULL) & ~0xFFFULL;
if (addr > UINT64_MAX - size) return -1;
if (addr > UINT64_MAX - size) return InvalidArgument;
g_heapLocks[slot].Acquire();
uint64_t firstPage = 0;
@@ -229,7 +229,7 @@ namespace montauk::abi {
}
link = &area->next;
}
if (*link == nullptr) { g_heapLocks[slot].Release(); return -1; }
if (*link == nullptr) { g_heapLocks[slot].Release(); return InvalidArgument; }
HeapAlloc* area = *link;
uint64_t oldPages = area->numPages;
@@ -253,16 +253,16 @@ namespace montauk::abi {
uint64_t resident = 0;
for (uint64_t i = 0; i < pages; i++)
if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, addr + i * 0x1000ULL) != 0)
if (Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot, addr + i * 0x1000ULL) != 0)
resident++;
g_heapLocks[slot].Release();
Memory::UnmapAndFreeUserRange(proc->pml4Phys, addr, pages);
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, addr, pages);
Sched::ReleaseUserHeapRange(slot, addr, size);
g_heapLocks[slot].Acquire();
Sched::g_allocatedPages[slot] -= resident;
g_heapLocks[slot].Release();
return 0;
return Success;
}
inline int64_t Sys_Protect(uint64_t addr, uint64_t size, uint64_t prot) {
@@ -272,9 +272,9 @@ namespace montauk::abi {
size > UINT64_MAX - 0xFFFULL ||
(prot & ~(VmProtRead | VmProtWrite | VmProtExec)) != 0 ||
(prot & VmProtRead) == 0 ||
((prot & VmProtWrite) != 0 && (prot & VmProtExec) != 0)) return -1;
((prot & VmProtWrite) != 0 && (prot & VmProtExec) != 0)) return InvalidArgument;
size = (size + 0xFFFULL) & ~0xFFFULL;
if (addr > UINT64_MAX - size) return -1;
if (addr > UINT64_MAX - size) return InvalidArgument;
g_heapLocks[slot].Acquire();
uint64_t pages = size / 0x1000ULL;
@@ -293,7 +293,7 @@ namespace montauk::abi {
break;
}
}
if (!covered) { g_heapLocks[slot].Release(); return -1; }
if (!covered) { g_heapLocks[slot].Release(); return InvalidArgument; }
}
uint64_t end = addr + size;
@@ -336,28 +336,28 @@ namespace montauk::abi {
bool writable = (prot & VmProtWrite) != 0;
bool executable = (prot & VmProtExec) != 0;
for (uint64_t i = 0; i < pages; i++) {
if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys,
if (Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot,
addr + i * 0x1000ULL) == 0)
continue;
if (!Memory::VMM::Paging::ProtectUserIn(proc->pml4Phys,
if (!Memory::VMM::Paging::ProtectUserIn(proc->pageTableRoot,
addr + i * 0x1000ULL,
writable, executable)) {
g_heapLocks[slot].Release();
return -1;
return GenericFailure;
}
}
g_heapLocks[slot].Release();
Memory::ShootdownUserRange(proc->pml4Phys, addr, (uint32_t)pages);
return 0;
Memory::ShootdownUserRange(proc->pageTableRoot, addr, (uint32_t)pages);
return Success;
}
// Resolve a non-present access inside an anonymous VMA. This is used for
// both ring-3 faults and kernel copies to untouched userspace buffers.
inline bool TryHandleAnonymousPageFault(uint64_t faultAddr, uint64_t errorCode) {
inline bool TryHandleAnonymousPageFault(uint64_t faultAddr, const Memory::VMM::PageFault& fault) {
auto* proc = Sched::GetCurrentProcessPtr();
int slot = GetCurrentSlot();
if (proc == nullptr || slot < 0 || faultAddr < Sched::UserHeapBase ||
faultAddr >= Sched::UserHeapLimit || (errorCode & 1) != 0) return false;
faultAddr >= Sched::UserHeapLimit || fault.present) return false;
uint64_t pageVa = faultAddr & ~0xFFFULL;
g_heapLocks[slot].Acquire();
@@ -370,14 +370,14 @@ namespace montauk::abi {
}
}
if (found == nullptr ||
((errorCode & (1ULL << 1)) != 0 && (found->prot & VmProtWrite) == 0) ||
((errorCode & (1ULL << 4)) != 0 && (found->prot & VmProtExec) == 0)) {
(fault.write && (found->prot & VmProtWrite) == 0) ||
(fault.execute && (found->prot & VmProtExec) == 0)) {
g_heapLocks[slot].Release();
return false;
}
// A sibling may have resolved the same fault while this CPU waited.
if (Memory::VMM::Paging::GetPhysAddr(proc->pml4Phys, pageVa) != 0) {
if (Memory::VMM::Paging::GetPhysAddr(proc->pageTableRoot, pageVa) != 0) {
g_heapLocks[slot].Release();
return true;
}
@@ -389,7 +389,7 @@ namespace montauk::abi {
}
uint64_t phys = Memory::SubHHDM((uint64_t)page);
bool mapped = Memory::VMM::Paging::MapUserInPermissions(
proc->pml4Phys, phys, pageVa,
proc->pageTableRoot, phys, pageVa,
(found->prot & VmProtWrite) != 0, (found->prot & VmProtExec) != 0);
if (!mapped) Memory::g_pfa->Free(page);
else Sched::g_allocatedPages[slot]++;
+2 -2
View File
@@ -17,13 +17,13 @@ namespace montauk::abi {
// Copy strings into fixed-size arrays (user-accessible)
const char* name = "MontaukOS";
const char* ver = "0.2.0";
const char* ver = "0.2.1";
for (int i = 0; name[i]; i++) outInfo->osName[i] = name[i];
outInfo->osName[9] = '\0';
for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i];
outInfo->osVersion[5] = '\0';
outInfo->apiVersion = 11;
outInfo->apiVersion = 12;
outInfo->maxProcesses = Sched::MaxProcesses;
outInfo->buildNumber = MONTAUK_BUILD_NUMBER;
}
+39 -22
View File
@@ -19,18 +19,18 @@ namespace montauk::abi {
const char* path, const char* args,
const SpawnCapabilities* capabilities) {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
int parentSlot = Ipc::CurrentSlot();
int childPid = Sched::Spawn(resolved, args, false, nullptr, 0,
capabilities);
if (childPid < 0) return -1;
if (childPid < 0) return childPid;
auto* child = Sched::GetProcessByPid(childPid);
int childSlot = Ipc::SlotForPid(childPid);
if (child == nullptr || childSlot < 0 || parentSlot < 0) {
Sched::KillProcess(childPid);
return -1;
return GenericFailure;
}
Ipc::Stream* outStream = Ipc::CreateStream(RedirOutputStreamCapacity);
@@ -41,7 +41,7 @@ namespace montauk::abi {
if (inStream != nullptr) Ipc::ReleaseStream(inStream, false, false);
if (keyMailbox != nullptr) Ipc::ReleaseMailbox(keyMailbox, false, false);
Sched::KillProcess(childPid);
return -1;
return OutOfMemory;
}
int parentOutHandle = Ipc::InstallHandleForSlot(
@@ -71,7 +71,7 @@ namespace montauk::abi {
if (child->ioInHandle < 0) Ipc::ReleaseStream(inStream, false, false);
if (child->ioKeyHandle < 0) Ipc::ReleaseMailbox(keyMailbox, false, false);
Sched::KillProcess(childPid);
return -1;
return GenericFailure;
}
child->redirected = true;
@@ -80,7 +80,7 @@ namespace montauk::abi {
child->termRows = 25;
if (Sched::StartProcess(childPid) < 0) {
Sched::KillProcess(childPid);
return -1;
return GenericFailure;
}
return childPid;
@@ -93,11 +93,11 @@ namespace montauk::abi {
static int Sys_SpawnRedirCaps(const char* path, const char* args,
const SpawnCapabilities* requested) {
auto* parent = Sched::GetCurrentProcessPtr();
if (parent == nullptr || requested == nullptr) return -1;
if (parent == nullptr || requested == nullptr) return InvalidArgument;
SpawnCapabilities copy = *requested;
if (!ValidCapabilityDelegation(copy, parent->delegableCaps)) {
return SYS_ERR_PERMISSION;
return CapabilityDenied;
}
return Sys_SpawnRedirInternal(path, args, &copy);
}
@@ -105,51 +105,68 @@ namespace montauk::abi {
static int Sys_ChildIoRead(int childPid, char* buf, int maxLen) {
auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
return SYS_ERR_PERMISSION;
return PermissionDenied;
Ipc::HandleSnapshot snapshot;
Ipc::Stream* stream = GetRedirOutStream(child, snapshot);
if (!child->redirected || stream == nullptr) return -1;
if (!child->redirected || stream == nullptr) return InvalidArgument;
return Ipc::StreamRead(stream, (uint8_t*)buf, maxLen, true);
}
static int Sys_ChildIoWrite(int childPid, const char* data, int len) {
auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
return SYS_ERR_PERMISSION;
return PermissionDenied;
Ipc::HandleSnapshot snapshot;
Ipc::Stream* stream = GetRedirInStream(child, snapshot);
if (!child->redirected || stream == nullptr) return -1;
if (!child->redirected || stream == nullptr) return InvalidArgument;
return WriteAllToStream(stream, (const uint8_t*)data, len);
}
static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) {
if (key == nullptr) return -1;
// Queue a key event (raw bytes, `len` of them) on a redirected child's key
// mailbox. Readers accept both the 9-byte legacy and the full format.
static int ChildIoQueueKey(int childPid, const void* key, uint16_t len) {
auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
return SYS_ERR_PERMISSION;
return PermissionDenied;
Ipc::HandleSnapshot snapshot;
Ipc::Mailbox* mailbox = GetRedirKeyMailbox(child, snapshot);
if (!child->redirected || mailbox == nullptr) return -1;
if (!child->redirected || mailbox == nullptr) return InvalidArgument;
for (;;) {
uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
int rc = Ipc::MailboxSend(mailbox, 0, key, sizeof(KeyEvent));
if (rc < 0) return -1;
int rc = Ipc::MailboxSend(mailbox, 0, key, len);
if (rc < 0) return GenericFailure;
if (rc == 0) {
Sched::BlockOnObjectSince(mailbox, 0, observedWake);
continue;
}
return 0;
return Success;
}
}
static int Sys_ChildIoWriteKey(int childPid, const KeyEventV1* key) {
if (key == nullptr) return InvalidArgument;
KeyEventV1 copy = *key;
return ChildIoQueueKey(childPid, &copy, sizeof(copy));
}
// Size-versioned: copies min(size, sizeof(KeyEvent)) bytes (validated
// readable by the dispatcher) and zero-fills the rest.
static int Sys_ChildIoWriteKeyEx(int childPid, const void* key, uint64_t size) {
if (key == nullptr || size < sizeof(KeyEventV1)) return InvalidArgument;
KeyEvent ev;
memset(&ev, 0, sizeof(ev));
memcpy(&ev, key, size < sizeof(ev) ? size : sizeof(ev));
return ChildIoQueueKey(childPid, &ev, sizeof(ev));
}
static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) {
auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
return SYS_ERR_PERMISSION;
if (!child->redirected) return -1;
return PermissionDenied;
if (!child->redirected) return InvalidArgument;
child->termCols = cols;
child->termRows = rows;
return 0;
return Success;
}
};
+6 -6
View File
@@ -20,15 +20,15 @@ namespace montauk::abi {
}
static int Sys_StreamCreate(int* outReadHandle, int* outWriteHandle, uint32_t capacity) {
if (outReadHandle == nullptr || outWriteHandle == nullptr) return -1;
if (outReadHandle == nullptr || outWriteHandle == nullptr) return InvalidArgument;
int readHandle = -1;
int writeHandle = -1;
if (Ipc::CreateStreamHandlePair(capacity, readHandle, writeHandle) < 0) return -1;
if (Ipc::CreateStreamHandlePair(capacity, readHandle, writeHandle) < 0) return GenericFailure;
*outReadHandle = readHandle;
*outWriteHandle = writeHandle;
return 0;
return Success;
}
static int Sys_StreamRead(int handle, uint8_t* buffer, int maxLen) {
@@ -40,15 +40,15 @@ namespace montauk::abi {
}
static int Sys_MailboxCreate(int* outSendHandle, int* outRecvHandle) {
if (outSendHandle == nullptr || outRecvHandle == nullptr) return -1;
if (outSendHandle == nullptr || outRecvHandle == nullptr) return InvalidArgument;
int sendHandle = -1;
int recvHandle = -1;
if (Ipc::CreateMailboxHandlePair(sendHandle, recvHandle) < 0) return -1;
if (Ipc::CreateMailboxHandlePair(sendHandle, recvHandle) < 0) return GenericFailure;
*outSendHandle = sendHandle;
*outRecvHandle = recvHandle;
return 0;
return Success;
}
static int Sys_MailboxSend(int handle, uint32_t msgType, const void* data, uint16_t len, int attachHandle) {
+137 -22
View File
@@ -12,6 +12,107 @@
#include "Common.hpp"
namespace montauk::abi {
// ---- Key text ----------------------------------------------------------
// Keyboard layouts are applied in userspace (desktop, terminal), which send
// KeyEvents carrying the Unicode codepoint. Binaries built before that send
// 9-byte KeyEventV1s whose `ascii` is a Windows-1252 byte; both kinds share
// a redirected child's key mailbox, told apart by message length.
static uint32_t KeyCp1252ToUnicode(uint8_t b) {
static constexpr uint16_t high[32] = {
0x20AC, 0x81, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021,
0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x8D, 0x017D, 0x8F,
0x90, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014,
0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x9D, 0x017E, 0x0178,
};
return (b >= 0x80 && b <= 0x9F) ? high[b - 0x80] : b;
}
// The character a key event types, or 0 for control/navigation keys.
static uint32_t KeyTextCodepoint(const KeyEvent& ev) {
if (ev.codepoint) return ev.codepoint;
uint8_t b = (uint8_t)ev.ascii;
if (b < 0x20 || b == 0x7F) return 0;
return KeyCp1252ToUnicode(b);
}
static int KeyEncodeUtf8(uint32_t cp, uint8_t* out) {
if (cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) cp = 0xFFFD;
if (cp < 0x80) { out[0] = (uint8_t)cp; return 1; }
if (cp < 0x800) {
out[0] = (uint8_t)(0xC0 | (cp >> 6));
out[1] = (uint8_t)(0x80 | (cp & 0x3F));
return 2;
}
if (cp < 0x10000) {
out[0] = (uint8_t)(0xE0 | (cp >> 12));
out[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[2] = (uint8_t)(0x80 | (cp & 0x3F));
return 3;
}
out[0] = (uint8_t)(0xF0 | (cp >> 18));
out[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
out[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[3] = (uint8_t)(0x80 | (cp & 0x3F));
return 4;
}
static void KeyFromDriver(const Drivers::PS2::Keyboard::KeyEvent& k, KeyEvent* out) {
memset(out, 0, sizeof(KeyEvent));
out->scancode = k.Scancode;
out->ascii = k.Ascii;
out->pressed = k.Pressed;
out->shift = k.Shift;
out->ctrl = k.Ctrl;
out->alt = k.Alt;
out->capslock = k.CapsLock;
out->altgr = k.AltGr;
out->extended = k.Extended;
// The kernel's own scancode table is US ASCII.
uint8_t a = (uint8_t)k.Ascii;
out->codepoint = (a >= 0x20 && a < 0x7F) ? a : 0;
}
// Receive one key from a redirected child's mailbox as a full KeyEvent,
// upgrading legacy 9-byte messages. Returns MailboxRecv's result.
static int KeyMailboxRecv(Ipc::Mailbox* mailbox, KeyEvent* out) {
memset(out, 0, sizeof(KeyEvent));
uint16_t len = sizeof(KeyEvent);
int rc = Ipc::MailboxRecv(mailbox, nullptr, out, &len, true);
if (rc > 0 && len < sizeof(KeyEvent)) {
memset((uint8_t*)out + len, 0, sizeof(KeyEvent) - len);
out->codepoint = KeyTextCodepoint(*out);
}
return rc;
}
// Turn a pressed key into the next getchar byte. Characters above ASCII are
// returned as UTF-8, one byte per call; the rest is parked in the process.
static bool KeyTakeChar(Sched::Process* proc, const KeyEvent& ev, char* out) {
if (!ev.pressed) return false;
uint32_t cp = KeyTextCodepoint(ev);
if (cp >= 0x80 && proc != nullptr) {
int n = KeyEncodeUtf8(cp, proc->getcharUtf8);
proc->getcharUtf8Len = (uint8_t)n;
proc->getcharUtf8Pos = 1;
*out = (char)proc->getcharUtf8[0];
return true;
}
if (ev.ascii != 0 && ev.ascii != KEY_ASCII_SUBSTITUTE) {
*out = ev.ascii;
return true;
}
return false;
}
static bool KeyPendingChar(Sched::Process* proc, char* out) {
if (proc == nullptr || proc->getcharUtf8Pos >= proc->getcharUtf8Len) return false;
*out = (char)proc->getcharUtf8[proc->getcharUtf8Pos++];
return true;
}
// ---- Syscalls ------------------------------------------------------------
static bool Sys_IsKeyAvailable() {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc && proc->redirected) {
@@ -27,8 +128,8 @@ namespace montauk::abi {
return Drivers::PS2::Keyboard::IsKeyAvailable();
}
static void Sys_GetKey(KeyEvent* outEvent) {
if (outEvent == nullptr) return;
// Blocking read of the next key event, in the full format.
static void ReadKeyEvent(KeyEvent* outEvent) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc && proc->redirected) {
Ipc::HandleSnapshot snapshot;
@@ -36,8 +137,7 @@ namespace montauk::abi {
if (mailbox != nullptr) {
for (;;) {
uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
uint16_t len = sizeof(KeyEvent);
int rc = Ipc::MailboxRecv(mailbox, nullptr, outEvent, &len, true);
int rc = KeyMailboxRecv(mailbox, outEvent);
if (rc > 0) return;
if (rc < 0) {
memset(outEvent, 0, sizeof(KeyEvent));
@@ -47,20 +147,33 @@ namespace montauk::abi {
}
}
}
auto k = Drivers::PS2::Keyboard::GetKey();
outEvent->scancode = k.Scancode;
outEvent->ascii = k.Ascii;
outEvent->pressed = k.Pressed;
outEvent->shift = k.Shift;
outEvent->ctrl = k.Ctrl;
outEvent->alt = k.Alt;
outEvent->capslock = k.CapsLock;
outEvent->altgr = k.AltGr;
outEvent->extended = k.Extended;
KeyFromDriver(Drivers::PS2::Keyboard::GetKey(), outEvent);
}
// SYS_GETKEY: legacy 9-byte event. A new-format event's first 9 bytes are
// already a valid KeyEventV1 (ascii holds the 1252 byte or SUB).
static void Sys_GetKey(KeyEventV1* outEvent) {
if (outEvent == nullptr) return;
KeyEvent ev;
ReadKeyEvent(&ev);
memcpy(outEvent, &ev, sizeof(KeyEventV1));
}
// SYS_GETKEY_EX: size-versioned. The caller's size was validated by the
// dispatcher (>= sizeof(KeyEventV1), writable).
static int Sys_GetKeyEx(void* outEvent, uint64_t size) {
KeyEvent ev;
ReadKeyEvent(&ev);
uint64_t n = size < sizeof(KeyEvent) ? size : sizeof(KeyEvent);
memcpy(outEvent, &ev, n);
if (size > n) memset((uint8_t*)outEvent + n, 0, size - n);
return Success;
}
static char Sys_GetChar() {
auto* proc = Sched::GetCurrentProcessPtr();
char pending;
if (KeyPendingChar(proc, &pending)) return pending;
if (proc && proc->redirected) {
Ipc::HandleSnapshot inputSnapshot;
Ipc::HandleSnapshot mailboxSnapshot;
@@ -78,11 +191,11 @@ namespace montauk::abi {
}
if (mailbox != nullptr) {
KeyEvent ev{};
uint16_t len = sizeof(ev);
int rc = Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true);
KeyEvent ev;
int rc = KeyMailboxRecv(mailbox, &ev);
if (rc > 0) {
if (ev.pressed && ev.ascii != 0) return ev.ascii;
char c;
if (KeyTakeChar(proc, ev, &c)) return c;
continue;
}
if (rc < 0 && input == nullptr) return 0;
@@ -115,6 +228,8 @@ namespace montauk::abi {
*/
static char Sys_GetCharNb() {
auto* proc = Sched::GetCurrentProcessPtr();
char pending;
if (KeyPendingChar(proc, &pending)) return pending;
if (proc && proc->redirected) {
Ipc::HandleSnapshot inputSnapshot;
Ipc::HandleSnapshot mailboxSnapshot;
@@ -127,10 +242,10 @@ namespace montauk::abi {
}
while (mailbox != nullptr) {
KeyEvent ev{};
uint16_t len = sizeof(ev);
if (Ipc::MailboxRecv(mailbox, nullptr, &ev, &len, true) <= 0) break;
if (ev.pressed && ev.ascii != 0) return ev.ascii;
KeyEvent ev;
if (KeyMailboxRecv(mailbox, &ev) <= 0) break;
char c;
if (KeyTakeChar(proc, ev, &c)) return c;
}
return 0;
}
+7 -7
View File
@@ -91,7 +91,7 @@ namespace montauk::abi {
}
// Load the library into the process's address space
uint64_t loadBias = Sched::ElfLoadLib(path, proc->pml4Phys, libSlot);
uint64_t loadBias = Sched::ElfLoadLib(path, proc->pageTableRoot, libSlot);
if (loadBias == 0) {
Kt::KernelLogStream(Kt::ERROR, "Lib") << "Failed to load library: " << path;
g_libLocks[slot].Release();
@@ -115,15 +115,15 @@ namespace montauk::abi {
// Decrements refcount; actually unmaps when refcount reaches 0.
inline int Sys_UnloadLib(uint64_t handle) {
int slot = GetCurrentSlot();
if (slot < 0) return -1;
if (slot < 0) return GenericFailure;
int libSlot = (int)handle - 1;
if (libSlot < 0 || libSlot >= MaxLibsPerProcess) return -1;
if (libSlot < 0 || libSlot >= MaxLibsPerProcess) return InvalidArgument;
g_libLocks[slot].Acquire();
if (!g_libTable[slot][libSlot].inUse) {
g_libLocks[slot].Release();
return -1;
return InvalidArgument;
}
g_libTable[slot][libSlot].refcount--;
@@ -134,7 +134,7 @@ namespace montauk::abi {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc != nullptr) {
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, libBase,
(libEnd - libBase) / 0x1000ULL);
}
@@ -144,7 +144,7 @@ namespace montauk::abi {
}
g_libLocks[slot].Release();
return 0;
return Success;
}
// Resolve a symbol in a loaded library.
@@ -203,7 +203,7 @@ namespace montauk::abi {
uint64_t libBase = GetLibSlotBase(i);
uint64_t libEnd = libBase + Sched::LIB_MAX_SIZE;
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
Memory::UnmapAndFreeUserRange(proc->pageTableRoot, libBase,
(libEnd - libBase) / 0x1000ULL);
g_libTable[slot][i].inUse = false;
+9 -4
View File
@@ -9,7 +9,7 @@
#pragma once
#include <cstdint>
#include <Sched/Scheduler.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
#include <Net/Icmp.hpp>
#include <Net/Dns.hpp>
#include <Net/Ipv4.hpp>
@@ -18,6 +18,7 @@
#include <Net/NetIf.hpp>
#include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp>
#include <Drivers/Net/Genet.hpp>
#include <Drivers/Net/Wifi/Wifi.hpp>
#include "Syscall.hpp"
@@ -157,6 +158,10 @@ namespace montauk::abi {
out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount();
out->txPackets = Drivers::Net::E1000E::GetTxPacketCount();
CopyNetStatusDriver(out->driver, "Intel e1000e");
} else if (Drivers::Net::Genet::IsInitialized()) {
out->rxPackets = Drivers::Net::Genet::GetRxPacketCount();
out->txPackets = Drivers::Net::Genet::GetTxPacketCount();
CopyNetStatusDriver(out->driver, "Broadcom GENET");
}
}
@@ -164,7 +169,7 @@ namespace montauk::abi {
// wired and wireless status apart: the IP configuration is global, so the
// only way to say which interface owns it is `active`.
static int Sys_NetIfs(NetIfInfo* out, int maxCount) {
if (out == nullptr || maxCount <= 0) return -1;
if (out == nullptr || maxCount <= 0) return InvalidArgument;
const auto* active = ::Net::NetIf::Active();
int count = ::Net::NetIf::Count();
@@ -193,12 +198,12 @@ namespace montauk::abi {
}
static int Sys_SetNetCfg(const NetCfg* in) {
if (in == nullptr) return -1;
if (in == nullptr) return InvalidArgument;
Net::SetIpAddress(in->ipAddress);
Net::SetSubnetMask(in->subnetMask);
Net::SetGateway(in->gateway);
Net::SetDnsServer(in->dnsServer);
return 0;
return Success;
}
// ---- DNS resolve ----
+21 -21
View File
@@ -8,13 +8,13 @@
#include <cstdint>
#include <Efi/UEFI.hpp>
#include <Memory/Paging.hpp>
#include <Io/IoPort.hpp>
#include <Graphics/Framebuffer.hpp>
#include <ACPI/AcpiShutdown.hpp>
#include <ACPI/AcpiSleep.hpp>
#include <ACPI/CpuIdle.hpp>
#include <ACPI/Battery.hpp>
#include <Hal/CpuPower.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/CpuPower.hpp>
#include <Arch/Platform.hpp>
#include <Sched/Scheduler.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -49,22 +49,20 @@ namespace montauk::abi {
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
<< (uint64_t)Sched::GetCurrentPid()
<< " posted graceful power request " << (uint64_t)action;
return 0;
return Success;
}
static void Sys_Reset() {
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
<< (uint64_t)Sched::GetCurrentPid() << " entered final reboot stage";
if (Efi::g_ResetSystem) {
/* Switch to kernel PML4 which has identity-mapped UEFI runtime regions */
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4);
/* Switch to the kernel address space, which has identity-mapped UEFI runtime regions */
Memory::VMM::ActivateKernelAddressSpace();
Efi::g_ResetSystem(Efi::EfiResetCold, 0, 0, nullptr);
}
/* Fallback: triple fault via null IDT */
struct [[gnu::packed]] { uint16_t limit; uint64_t base; } nullIdt = {0, 0};
asm volatile("lidt %0; int $0x03" :: "m"(nullIdt));
__builtin_unreachable();
/* Fallback: architecture-specific reset */
Arch::Reboot();
}
static void Sys_Shutdown() {
@@ -75,20 +73,22 @@ namespace montauk::abi {
Hal::AcpiShutdown::Shutdown();
}
/* Hardware-reduced platforms (PSCI on ARM) */
Arch::PowerOff();
/* Fallback: EFI ResetSystem */
if (Efi::g_ResetSystem) {
Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4);
Memory::VMM::ActivateKernelAddressSpace();
Efi::g_ResetSystem(Efi::EfiResetShutdown, 0, 0, nullptr);
}
/* Last resort: halt the CPU */
asm volatile("cli; hlt");
__builtin_unreachable();
Arch::HaltForever();
}
static int64_t Sys_Suspend() {
if (!Hal::AcpiSleep::IsS3Available()) {
return -1; // S3 not supported
return UnsupportedOp; // S3 not supported
}
return (int64_t)Hal::AcpiSleep::Suspend();
}
@@ -96,9 +96,9 @@ namespace montauk::abi {
// SYS_POWERINFO. Fills a CPU power/thermal snapshot. Returns -1 when the
// CPU exposes no power management features (e.g. under QEMU).
static int64_t Sys_PowerInfo(PowerInfo* out) {
Hal::CpuPower::Snapshot s{};
if (!Hal::CpuPower::GetSnapshot(s)) {
return -1;
Arch::CpuPower::Snapshot s{};
if (!Arch::CpuPower::GetSnapshot(s)) {
return NoDevice;
}
out->hwpActive = s.HwpActive ? 1 : 0;
@@ -112,8 +112,8 @@ namespace montauk::abi {
out->baseMHz = s.BaseMHz;
out->maxMHz = s.MaxMHz;
out->effMHz = s.EffMHz;
out->apIdleHint = Hal::CpuIdle::GetApIdleHint();
return 0;
out->apIdleHint = s.ApIdleHint;
return Success;
}
// SYS_BATTERYINFO. Fills a battery snapshot. Returns -1 on a machine with
@@ -122,7 +122,7 @@ namespace montauk::abi {
static int64_t Sys_BatteryInfo(BatteryInfo* out) {
Hal::Battery::Status s{};
if (!Hal::Battery::GetStatus(s)) {
return -1;
return NoDevice;
}
out->present = s.Present ? 1 : 0;
@@ -139,6 +139,6 @@ namespace montauk::abi {
out->presentRate = s.PresentRate;
out->voltageMv = s.VoltageMv;
out->minutesRemaining = s.MinutesRemaining;
return 0;
return Success;
}
};
+65 -30
View File
@@ -7,7 +7,7 @@
#pragma once
#include <Sched/Scheduler.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
#include <Memory/Paging.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp>
@@ -49,13 +49,13 @@ namespace montauk::abi {
// stream, whether or not it also carries a capability grant. The child is
// created suspended (startReady == false) so its first instruction cannot
// run before the channels exist, and is started here once they do.
// Returns childPid, or kills the child and returns -1 on failure.
// Returns childPid, or kills the child and returns GenericFailure.
static int InheritRedirection(int childPid, Sched::Process* parent, int parentSlot) {
auto* child = Sched::GetProcessByPid(childPid);
int childSlot = Ipc::SlotForPid(childPid);
if (child == nullptr || childSlot < 0 || parentSlot < 0) {
Sched::KillProcess(childPid);
return -1;
return GenericFailure;
}
child->ioOutHandle = DuplicateHandleBetweenSlots(parentSlot, parent->ioOutHandle, childSlot);
@@ -65,7 +65,7 @@ namespace montauk::abi {
if (child->ioOutHandle < 0 || child->ioInHandle < 0 || child->ioKeyHandle < 0 ||
!ConfigureRedirWaitsetForSlot(childSlot, child)) {
Sched::KillProcess(childPid);
return -1;
return GenericFailure;
}
child->redirected = true;
@@ -74,7 +74,7 @@ namespace montauk::abi {
child->termRows = parent->termRows;
if (Sched::StartProcess(childPid) < 0) {
Sched::KillProcess(childPid);
return -1;
return GenericFailure;
}
return childPid;
@@ -83,7 +83,7 @@ namespace montauk::abi {
static int Sys_Spawn(const char* path, const char* args,
const char* environment = nullptr, uint32_t environmentLength = 0) {
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
auto* parent = Sched::GetCurrentProcessPtr();
int parentSlot = Ipc::CurrentSlot();
@@ -101,7 +101,7 @@ namespace montauk::abi {
static int Sys_SpawnCaps(const char* path, const char* args,
const char* user, const SpawnCapabilities* requested) {
auto* parent = Sched::GetCurrentProcessPtr();
if (parent == nullptr || requested == nullptr) return -1;
if (parent == nullptr || requested == nullptr) return InvalidArgument;
// Snapshot all security-sensitive userspace inputs before evaluating
// them. This prevents another thread from changing a mask or owner
@@ -111,7 +111,7 @@ namespace montauk::abi {
const char* userOverride = nullptr;
if (user != nullptr) {
if (!Sched::HasCapability(CAP_USER_ADMIN))
return SYS_ERR_PERMISSION;
return CapabilityDenied;
int i = 0;
for (; i < 31 && user[i]; i++) childUser[i] = user[i];
childUser[i] = '\0';
@@ -122,11 +122,11 @@ namespace montauk::abi {
// possessing a capability is insufficient to pass it: the parent must
// also hold it in its delegable set.
if (!ValidCapabilityDelegation(copy, parent->delegableCaps)) {
return SYS_ERR_PERMISSION;
return CapabilityDenied;
}
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
int parentSlot = Ipc::CurrentSlot();
bool inheritRedirection = parent->redirected;
@@ -140,10 +140,19 @@ namespace montauk::abi {
return childPid;
}
// The calling process's own capability masks.
static int Sys_GetCaps(SpawnCapabilities* out) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || out == nullptr) return InvalidArgument;
out->effective = proc->effectiveCaps;
out->delegable = proc->delegableCaps;
return Success;
}
// Copy the absolute path this process was spawned from (argv[0]).
static int Sys_GetExecPath(char* buf, uint64_t maxLen) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || buf == nullptr || maxLen == 0) return -1;
if (proc == nullptr || buf == nullptr || maxLen == 0) return InvalidArgument;
int i = 0;
for (; i < (int)maxLen - 1 && proc->name[i]; i++) {
buf[i] = proc->name[i];
@@ -154,7 +163,7 @@ namespace montauk::abi {
static int Sys_GetArgs(char* buf, uint64_t maxLen) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || buf == nullptr || maxLen == 0) return -1;
if (proc == nullptr || buf == nullptr || maxLen == 0) return InvalidArgument;
int i = 0;
for (; i < (int)maxLen - 1 && proc->args[i]; i++) {
buf[i] = proc->args[i];
@@ -165,8 +174,8 @@ namespace montauk::abi {
static int Sys_GetEnviron(char* buf, uint64_t maxLen) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || buf == nullptr || maxLen == 0) return -1;
if (maxLen < proc->environmentLength) return -2;
if (proc == nullptr || buf == nullptr || maxLen == 0) return InvalidArgument;
if (maxLen < proc->environmentLength) return BufferTooSmall;
for (uint32_t i = 0; i < proc->environmentLength; i++)
buf[i] = proc->environment[i];
return (int)proc->environmentLength;
@@ -175,10 +184,10 @@ namespace montauk::abi {
static int Sys_SetEnviron(const char* buf, uint64_t len) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || buf == nullptr || len == 0 ||
len > Sched::Process::EnvironmentBytes || buf[len - 1] != '\0') return -1;
len > Sched::Process::EnvironmentBytes || buf[len - 1] != '\0') return InvalidArgument;
for (uint64_t i = 0; i < len; i++) proc->environment[i] = buf[i];
proc->environmentLength = (uint32_t)len;
return 0;
return Success;
}
static int Sys_ProcList(ProcInfo* buf, int maxCount) {
@@ -204,7 +213,6 @@ namespace montauk::abi {
}
buf[count].heapUsed = Sched::g_allocatedPages[i] * 0x1000;
buf[count].cpuTimeMs = proc->cpuTimeMs;
buf[count].permittedCaps = proc->permittedCaps;
buf[count].effectiveCaps = proc->effectiveCaps;
buf[count].delegableCaps = proc->delegableCaps;
count++;
@@ -212,7 +220,26 @@ namespace montauk::abi {
return count;
}
// Flag every thread of the caller's process with TerminatedAborted, then
// exit this one. A sibling's ExitProcess ends only itself; the main
// thread and the other siblings follow via killPending.
static void Sys_Abort() {
Sched::KillProcess(Sched::GetCurrentPid(), TerminatedAborted);
Sched::ExitProcess();
}
// Like Sys_Abort, but a normal exit with the caller's code: every thread
// goes, and waiters see exitCode & 0xFF, the same as SYS_EXIT on the main
// thread.
static void Sys_ExitGroup(int exitCode) {
Sched::KillProcess(Sched::GetCurrentPid(), exitCode & 0xFF);
Sched::ExitProcess();
}
static int Sys_Kill(int pid) {
// A process may always kill itself (kill(getpid(), ...)); waiters
// see TerminatedKilled. abort() uses SYS_ABORT instead.
if (pid == Sched::GetCurrentPid()) return Sched::KillProcess(pid);
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) {
int ancestor = pid;
bool descendant = false;
@@ -225,7 +252,7 @@ namespace montauk::abi {
}
ancestor = target->parentPid;
}
if (!descendant) return SYS_ERR_PERMISSION;
if (!descendant) return PermissionDenied;
}
return Sched::KillProcess(pid);
}
@@ -239,19 +266,19 @@ namespace montauk::abi {
}
static int Sys_SetUser(int pid, const char* name) {
if (name == nullptr) return -1;
if (name == nullptr) return InvalidArgument;
auto* target = Sched::GetProcessByPid(pid);
if (target == nullptr) return -1;
if (target == nullptr) return InvalidArgument;
int i = 0;
for (; i < 31 && name[i]; i++)
target->user[i] = name[i];
target->user[i] = '\0';
return 0;
return Success;
}
static int Sys_GetUser(char* buf, uint64_t maxLen) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || buf == nullptr || maxLen == 0) return -1;
if (proc == nullptr || buf == nullptr || maxLen == 0) return InvalidArgument;
int i = 0;
for (; i < (int)maxLen - 1 && proc->user[i]; i++)
buf[i] = proc->user[i];
@@ -261,15 +288,15 @@ namespace montauk::abi {
static int Sys_GetCwd(char* buf, uint64_t maxLen) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || buf == nullptr || maxLen == 0) return -1;
if (proc == nullptr || buf == nullptr || maxLen == 0) return InvalidArgument;
const char* cwd = proc->cwd[0] ? proc->cwd : "0:/";
uint64_t cwdLen = 0;
while (cwd[cwdLen] != '\0') cwdLen++;
// Reserve -1 for non-size failures so libc can distinguish them
// from a buffer that can be retried with a larger allocation.
if (maxLen <= cwdLen) return -2;
// BufferTooSmall tells libc the call can be retried with a larger
// allocation (ERANGE), unlike the other failures.
if (maxLen <= cwdLen) return BufferTooSmall;
for (uint64_t i = 0; i <= cwdLen; i++) buf[i] = cwd[i];
return (int)cwdLen;
@@ -294,12 +321,20 @@ namespace montauk::abi {
return Sched::GetCurrentTid();
}
static int Sys_FutexWait(uint64_t addr, uint32_t expected, uint64_t timeoutNs) {
return Sched::FutexWait(addr, expected, timeoutNs);
}
static int Sys_FutexWake(uint64_t addr, uint32_t count) {
return Sched::FutexWake(addr, count);
}
static int Sys_Chdir(const char* path) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || path == nullptr) return -1;
if (proc == nullptr || path == nullptr) return InvalidArgument;
char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return InvalidArgument;
// ReadDir doubles as the directory-existence probe: it fails
// for nonexistent paths and for regular files (directories are
@@ -307,12 +342,12 @@ namespace montauk::abi {
// would reject every valid target).
{
const char* entries[1];
if (Fs::Vfs::VfsReadDir(resolved, entries, 1) < 0) return -1;
if (Fs::Vfs::VfsReadDir(resolved, entries, 1) < 0) return NoSuchPath;
}
int i = 0;
for (; i < 255 && resolved[i]; i++) proc->cwd[i] = resolved[i];
proc->cwd[i] = '\0';
return 0;
return Success;
}
};
+8 -8
View File
@@ -6,23 +6,23 @@
#pragma once
#include <cstdint>
#include <Arch/Cpu.hpp>
namespace montauk::abi {
// ---- Random number generation ----
// Uses RDTSC mixed with xorshift64* PRNG for entropy.
// RDRAND is intentionally avoided: some firmware disables the RDRAND
// hardware unit while CPUID still advertises support (bit 30 of ECX),
// causing #UD on real hardware. RDTSC-based entropy is sufficient for
// seeding BearSSL's PRNG for TLS session keys.
// Uses the CPU timestamp counter mixed with xorshift64* PRNG for entropy.
// RDRAND is intentionally avoided on x86: some firmware disables the
// RDRAND hardware unit while CPUID still advertises support (bit 30 of
// ECX), causing #UD on real hardware. Timestamp-based entropy is
// sufficient for seeding BearSSL's PRNG for TLS session keys.
static int64_t Sys_GetRandom(uint8_t* buf, uint64_t len) {
uint64_t tsc;
asm volatile("rdtsc; shl $32, %%rdx; or %%rdx, %%rax" : "=a"(tsc) :: "rdx");
uint64_t tsc = Arch::ReadTimestamp();
uint64_t state = tsc;
for (uint64_t i = 0; i < len; i += 8) {
asm volatile("rdtsc; shl $32, %%rdx; or %%rdx, %%rax" : "=a"(tsc) :: "rdx");
tsc = Arch::ReadTimestamp();
state ^= tsc;
state ^= state >> 12;
state ^= state << 25;
+104 -23
View File
@@ -29,7 +29,7 @@ namespace montauk::abi {
// Fill a user buffer with partition info. Returns the number of entries written.
static int Sys_PartList(PartInfo* buf, int maxCount) {
if (buf == nullptr || maxCount <= 0) return 0;
if (!UserMemory::Range((uint64_t)buf, (uint64_t)maxCount * sizeof(PartInfo), true)) return -1;
if (!UserMemory::Range((uint64_t)buf, (uint64_t)maxCount * sizeof(PartInfo), true)) return BadPointer;
int total = Drivers::Storage::Gpt::GetPartitionCount();
int count = total < maxCount ? total : maxCount;
@@ -57,47 +57,89 @@ namespace montauk::abi {
return count;
}
// Read sectors from a block device. Returns bytes read, or -1 on error.
// Fill a user buffer with one entry per mounted volume. Returns the number
// of entries written, or BadPointer for a bad buffer.
static int Sys_VolInfo(VolInfo* buf, int maxCount) {
if (buf == nullptr || maxCount <= 0) return 0;
if (!UserMemory::Range((uint64_t)buf, (uint64_t)maxCount * sizeof(VolInfo), true)) return BadPointer;
int drives[Fs::Vfs::MaxDrives];
int n = Fs::Vfs::VfsDriveList(drives, Fs::Vfs::MaxDrives);
if (n > maxCount) n = maxCount;
for (int i = 0; i < n; i++) {
VolInfo& v = buf[i];
v = VolInfo{};
v.drive = drives[i];
v.blockDev = -1;
v.partIndex = -1;
Fs::Vfs::VfsDriveLabel(v.drive, v.label, sizeof(v.label));
Fs::Vfs::VfsDriveFsType(v.drive, v.fsType, sizeof(v.fsType));
v.partIndex = Fs::FsProbe::GetPartitionForDrive(v.drive);
if (v.partIndex < 0) continue; // ramdisk: no backing partition
auto* part = Drivers::Storage::Gpt::GetPartition(v.partIndex);
if (part == nullptr) continue;
v.blockDev = part->BlockDevIndex;
v.startLba = part->StartLba;
v.sectorCount = part->SectorCount;
dl_strcpy_s(v.partName, part->Name, sizeof(v.partName));
auto* dev = Drivers::Storage::GetBlockDevice(v.blockDev);
if (dev == nullptr) continue;
v.kind = dev->Kind;
v.sectorSize = dev->SectorSize;
dl_strcpy_s(v.model, dev->Model, sizeof(v.model));
}
return n;
}
// Read sectors from a block device. Returns bytes read, or a status code.
static int64_t Sys_DiskRead(int blockDev, uint64_t lba, uint32_t count, void* buffer) {
if (buffer == nullptr || count == 0) return -1;
if (count > 128) return -1; // same limit as AHCI
if (buffer == nullptr || count == 0) return InvalidArgument;
if (count > 128) return InvalidArgument; // same limit as AHCI
auto* dev = Drivers::Storage::GetBlockDevice(blockDev);
if (!dev) return -1;
if (!dev) return NoDevice;
if (lba >= dev->SectorCount || count > dev->SectorCount - lba) return -1;
if (!UserMemory::Range((uint64_t)buffer, (uint64_t)count * dev->SectorSize, true)) return -1;
if (lba >= dev->SectorCount || count > dev->SectorCount - lba) return InvalidArgument;
if (!UserMemory::Range((uint64_t)buffer, (uint64_t)count * dev->SectorSize, true)) return BadPointer;
if (!dev->ReadSectors(dev->Ctx, lba, count, buffer)) return -1;
if (!dev->ReadSectors(dev->Ctx, lba, count, buffer)) return IoFailure;
return (int64_t)(count * dev->SectorSize);
}
// Write sectors to a block device. Returns bytes written, or -1 on error.
// Write sectors to a block device. Returns bytes written, or a status code.
static int64_t Sys_DiskWrite(int blockDev, uint64_t lba, uint32_t count, const void* buffer) {
if (buffer == nullptr || count == 0) return -1;
if (count > 128) return -1;
if (buffer == nullptr || count == 0) return InvalidArgument;
if (count > 128) return InvalidArgument;
auto* dev = Drivers::Storage::GetBlockDevice(blockDev);
if (!dev) return -1;
if (!dev) return NoDevice;
if (lba >= dev->SectorCount || count > dev->SectorCount - lba) return -1;
if (!UserMemory::Range((uint64_t)buffer, (uint64_t)count * dev->SectorSize, false)) return -1;
if (lba >= dev->SectorCount || count > dev->SectorCount - lba) return InvalidArgument;
if (!UserMemory::Range((uint64_t)buffer, (uint64_t)count * dev->SectorSize, false)) return BadPointer;
if (!dev->WriteSectors(dev->Ctx, lba, count, buffer)) return -1;
if (!dev->WriteSectors(dev->Ctx, lba, count, buffer)) return IoFailure;
return (int64_t)(count * dev->SectorSize);
}
// Initialize a new GPT on a block device. Returns 0 on success, -1 on error.
// Initialize a new GPT on a block device. Returns 0 on success, GenericFailure on error.
static int64_t Sys_GptInit(int blockDev) {
Fs::FsProbe::UnmountPartitionsForBlockDevice(blockDev);
return (int64_t)Drivers::Storage::Gpt::InitializeGpt(blockDev);
}
// Add a partition to an existing GPT. Returns partition index on success, -1 on error.
// Add a partition to an existing GPT. Returns partition index on success, GenericFailure on error.
static int64_t Sys_GptAdd(const GptAddParams* params) {
if (params == nullptr) return -1;
if (params == nullptr) return InvalidArgument;
Drivers::Storage::Gpt::Guid typeGuid;
__builtin_memcpy(&typeGuid, &params->typeGuid, sizeof(typeGuid));
@@ -107,7 +149,7 @@ namespace montauk::abi {
typeGuid, params->name);
}
// Mount a partition as a VFS drive. Returns 0 on success, -1 on error.
// Mount a partition as a VFS drive. Returns 0 on success, GenericFailure on error.
static int64_t Sys_FsMount(int partIndex, int driveNum) {
return (int64_t)Fs::FsProbe::MountPartition(partIndex, driveNum);
}
@@ -121,12 +163,51 @@ namespace montauk::abi {
return (int64_t)Fs::FsProbe::SyncAndUnmountAll();
}
// Format a partition with a filesystem. Returns 0 on success, -1 on error.
// Grow a mounted volume to fill the free space after its partition: the
// MBR entry first, only as far as the filesystem can use, then the
// filesystem a slice per call. Interrupted grows resume from the next
// call (or boot), since a partition larger than its filesystem is valid.
static int64_t Sys_FsGrow(int drive, FsGrowStatus* out, bool apply) {
int partIndex = Fs::FsProbe::GetPartitionForDrive(drive);
auto* part = Drivers::Storage::Gpt::GetPartition(partIndex);
if (part == nullptr) return UnsupportedOp; // ramdisk, or no such drive
uint64_t maxSectors = Drivers::Storage::Gpt::GetMbrPartitionMaxSectors(partIndex);
if (maxSectors < part->SectorCount) maxSectors = part->SectorCount;
Fs::Vfs::GrowStatus gs{};
int rc = Fs::Vfs::VfsGrow(drive, maxSectors, false, &gs);
if (rc == -2) return UnsupportedOp;
if (rc < 0) return IoFailure;
// Leave whatever the filesystem cannot use unpartitioned, with the end
// on a 1 MiB boundary like the partitions mk-rpi4-sd.sh makes.
uint64_t wanted = (part->StartLba + (gs.targetBytes + 511) / 512 + 2047) / 2048 * 2048
- part->StartLba;
if (wanted > maxSectors) wanted = maxSectors;
if (apply && wanted > part->SectorCount) {
if (Drivers::Storage::Gpt::ResizeMbrPartition(partIndex, wanted) != 0) return IoFailure;
}
rc = apply ? Fs::Vfs::VfsGrow(drive, part->SectorCount, true, &gs) : 0;
if (rc == -2) return UnsupportedOp;
if (rc < 0) return IoFailure;
out->fsBytes = gs.fsBytes;
out->partBytes = part->SectorCount * 512;
out->targetBytes = gs.targetBytes;
out->stepsDone = gs.stepsDone;
out->stepsTotal = gs.stepsTotal;
return rc;
}
// Format a partition with a filesystem. Returns 0 on success, GenericFailure on error.
static int64_t Sys_FsFormat(const FsFormatParams* params) {
if (params == nullptr) return -1;
if (params == nullptr) return InvalidArgument;
auto* part = Drivers::Storage::Gpt::GetPartition(params->partIndex);
if (!part) return -1;
if (!part) return InvalidArgument;
switch (params->fsType) {
case FS_TYPE_FAT32:
@@ -138,7 +219,7 @@ namespace montauk::abi {
part->BlockDevIndex, part->StartLba, part->SectorCount,
params->label[0] ? params->label : nullptr);
default:
return -1; // unknown filesystem type
return UnsupportedOp; // unknown filesystem type
}
}
+280 -212
View File
@@ -6,18 +6,17 @@
#include "Syscall.hpp"
#include <Terminal/Terminal.hpp>
#include <Hal/MSR.hpp>
#include <Hal/GDT.hpp>
/* For common functions used by multiple syscall implementations*/
#include "Common.hpp"
#include <Sched/Consent.hpp>
/* Syscall impl. includes */
#include "Process.hpp" // SYS_EXIT, SYS_YIELD, SYS_SLEEP_MS, SYS_GETPID, SYS_WAITPID, SYS_SPAWN, SYS_GETARGS, SYS_PROCLIST, SYS_KILL
#include "Terminal.hpp" // SYS_PRINT, SYS_PUTCHAR
#include "Filesystem.hpp" // SYS_OPEN, SYS_READ, SYS_GETSIZE, SYS_CLOSE, SYS_READDIR, SYS_FWRITE, SYS_FCREATE
#include "Heap.hpp" // SYS_ALLOC, SYS_FREE
#include "Time.hpp" // SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME
#include "Time.hpp" // SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME, SYS_GETUNIXTIME
#include "Keyboard.hpp" // SYS_ISKEYAVAILABLE, SYS_GETKEY, SYS_GETCHAR
#include "Info.hpp" // SYS_GETINFO
#include "Graphics.hpp" // SYS_FBINFO, SYS_FBMAP, SYS_TERMSIZE, SYS_TERMSCALE
@@ -29,8 +28,8 @@
#include "MemInfo.hpp" // SYS_MEMSTATS
#include "Device.hpp" // SYS_DEVLIST, SYS_DISKINFO
#include "Input.hpp" // SYS_INPUT_WAIT
#include "Storage.hpp" // SYS_PARTLIST, SYS_DISKREAD, SYS_DISKWRITE
#include "Window.hpp" // SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE
#include "Storage.hpp" // SYS_PARTLIST, SYS_VOLINFO, SYS_DISKREAD, SYS_DISKWRITE
#include "Window.hpp" // SYS_WIN_REQUEST*, SYS_WIN_DRAG_*, SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL, SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE, SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE
#include "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL
#include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO
#include "Usb.hpp" // generic process-owned USB interface access
@@ -38,12 +37,9 @@
#include "IpcSyscall.hpp" // SYS_DUPHANDLE, SYS_WAIT_HANDLE, SYS_STREAM_CREATE, SYS_STREAM_READ, SYS_STREAM_WRITE, SYS_MAILBOX_CREATE, SYS_MAILBOX_SEND, SYS_MAILBOX_RECV, SYS_WAITSET_CREATE, SYS_WAITSET_ADD, SYS_WAITSET_REMOVE, SYS_WAITSET_WAIT, SYS_PROC_OPEN, SYS_SURFACE_CREATE, SYS_SURFACE_MAP, SYS_SURFACE_RESIZE
#include "LibSyscall.hpp" // SYS_LOAD_LIB, SYS_UNLOAD_LIB, SYS_DLSYM
#include "CrashReportSyscall.hpp" // SYS_CRASH_REPORT
#include "Clipboard.hpp" // SYS_CLIPBOARD_*
#include "Clipboard.hpp" // SYS_CLIPBOARD_* (text and multi-format)
#include "UserMemory.hpp"
// Assembly entry point
extern "C" void SyscallEntry();
namespace montauk::abi {
static constexpr uint64_t kMaxPrintableStringBytes = 4096;
@@ -61,54 +57,54 @@ namespace montauk::abi {
switch (frame->syscall_nr) {
case SYS_EXIT: {
Sys_Exit((int)frame->arg1);
return 0;
return Success;
}
case SYS_YIELD:
Sys_Yield();
return 0;
return Success;
case SYS_SLEEP_MS:
Sys_SleepMs(frame->arg1);
return 0;
return Success;
case SYS_GETPID:
return (int64_t)Sys_GetPid();
case SYS_PRINT:
if (!UserMemory::String(frame->arg1, kMaxPrintableStringBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPrintableStringBytes)) return BadPointer;
Sys_Print((const char*)frame->arg1);
return 0;
return Success;
case SYS_PUTCHAR:
Sys_Putchar((char)frame->arg1);
return 0;
return Success;
case SYS_OPEN:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
return (int64_t)Sys_Open((const char*)frame->arg1);
case SYS_READ:
if (!UserMemory::Range(frame->arg2, frame->arg4, true)) return -1;
if (!UserMemory::Range(frame->arg2, frame->arg4, true)) return BadPointer;
return (int64_t)Sys_Read((int)frame->arg1, (uint8_t*)frame->arg2,
frame->arg3, frame->arg4);
case SYS_GETSIZE:
return (int64_t)Sys_GetSize((int)frame->arg1);
case SYS_CLOSE:
Sys_Close((int)frame->arg1);
return 0;
return Success;
case SYS_READDIR:
if ((int64_t)frame->arg3 < 0) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if ((int64_t)frame->arg3 < 0) return InvalidArgument;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
if (!UserMemory::Range(frame->arg2,
(uint64_t)((frame->arg3 > 256) ? 256 : frame->arg3) * sizeof(const char*),
true)) {
return -1;
return BadPointer;
}
return (int64_t)Sys_ReadDir((const char*)frame->arg1,
(const char**)frame->arg2,
(int)frame->arg3);
case SYS_READDIR_AT:
if ((int64_t)frame->arg3 < 0) return -1;
if ((int64_t)frame->arg4 < 0) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if ((int64_t)frame->arg3 < 0) return InvalidArgument;
if ((int64_t)frame->arg4 < 0) return InvalidArgument;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
if (!UserMemory::Range(frame->arg2,
(uint64_t)((frame->arg3 > 256) ? 256 : frame->arg3) * sizeof(const char*),
true)) {
return -1;
return BadPointer;
}
return (int64_t)Sys_ReadDirAt((const char*)frame->arg1,
(const char**)frame->arg2,
@@ -120,7 +116,7 @@ namespace montauk::abi {
return (int64_t)Sys_AllocEager(frame->arg1);
case SYS_FREE:
Sys_Free(frame->arg1);
return 0;
return Success;
case SYS_MMAP_ANON:
return (int64_t)Sys_MapAnonymous(frame->arg1, frame->arg2);
case SYS_MUNMAP:
@@ -132,15 +128,15 @@ namespace montauk::abi {
case SYS_GETMILLISECONDS:
return (int64_t)Sys_GetMilliseconds();
case SYS_GETINFO:
if (!UserMemory::Writable<SysInfo>(frame->arg1)) return -1;
if (!UserMemory::Writable<SysInfo>(frame->arg1)) return BadPointer;
Sys_GetInfo((SysInfo*)frame->arg1);
return 0;
return Success;
case SYS_ISKEYAVAILABLE:
return (int64_t)Sys_IsKeyAvailable();
case SYS_GETKEY:
if (!UserMemory::Writable<KeyEvent>(frame->arg1)) return -1;
Sys_GetKey((KeyEvent*)frame->arg1);
return 0;
if (!UserMemory::Writable<KeyEventV1>(frame->arg1)) return BadPointer;
Sys_GetKey((KeyEventV1*)frame->arg1);
return Success;
case SYS_GETCHAR:
return (int64_t)Sys_GetChar();
case SYS_GETCHAR_NB:
@@ -148,73 +144,75 @@ namespace montauk::abi {
case SYS_PING:
return (int64_t)Sys_Ping((uint32_t)frame->arg1, (uint32_t)frame->arg2);
case SYS_SPAWN:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return BadPointer;
return (int64_t)Sys_Spawn((const char*)frame->arg1,
UserMemory::IsUserPtr(frame->arg2) ? (const char*)frame->arg2 : nullptr);
case SYS_SPAWN_CAPS:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
if (frame->arg3 != 0 && !UserMemory::String(frame->arg3, 32)) return -1;
if (!UserMemory::Readable<SpawnCapabilities>(frame->arg4)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return BadPointer;
if (frame->arg3 != 0 && !UserMemory::String(frame->arg3, 32)) return BadPointer;
if (!UserMemory::Readable<SpawnCapabilities>(frame->arg4)) return BadPointer;
return (int64_t)Sys_SpawnCaps(
(const char*)frame->arg1,
frame->arg2 ? (const char*)frame->arg2 : nullptr,
frame->arg3 ? (const char*)frame->arg3 : nullptr,
(const SpawnCapabilities*)frame->arg4);
case SYS_SPAWN_ENV:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
if (frame->arg3 == 0 || frame->arg4 == 0 ||
frame->arg4 > Sched::Process::EnvironmentBytes ||
!UserMemory::Range(frame->arg3, frame->arg4, false) ||
((const char*)frame->arg3)[frame->arg4 - 1] != '\0') return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return BadPointer;
if (frame->arg4 == 0 || frame->arg4 > Sched::Process::EnvironmentBytes)
return InvalidArgument;
if (frame->arg3 == 0 || !UserMemory::Range(frame->arg3, frame->arg4, false))
return BadPointer;
if (((const char*)frame->arg3)[frame->arg4 - 1] != '\0') return InvalidArgument;
return (int64_t)Sys_Spawn((const char*)frame->arg1,
UserMemory::IsUserPtr(frame->arg2) ? (const char*)frame->arg2 : nullptr,
(const char*)frame->arg3, (uint32_t)frame->arg4);
case SYS_WAITPID:
return Sys_WaitPid((int)frame->arg1);
case SYS_GETENVIRON:
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
return Sys_GetEnviron((char*)frame->arg1, frame->arg2);
case SYS_SETENVIRON:
if (frame->arg2 == 0 || frame->arg2 > Sched::Process::EnvironmentBytes ||
!UserMemory::Range(frame->arg1, frame->arg2, false)) return -1;
if (frame->arg2 == 0 || frame->arg2 > Sched::Process::EnvironmentBytes)
return InvalidArgument;
if (!UserMemory::Range(frame->arg1, frame->arg2, false)) return BadPointer;
return Sys_SetEnviron((const char*)frame->arg1, frame->arg2);
case SYS_FBINFO:
if (!UserMemory::Writable<FbInfo>(frame->arg1)) return -1;
if (!UserMemory::Writable<FbInfo>(frame->arg1)) return BadPointer;
Sys_FbInfo((FbInfo*)frame->arg1);
return 0;
return Success;
case SYS_FBMAP:
return (int64_t)Sys_FbMap();
case SYS_FBFLIP:
return Sys_FbFlip(frame->arg1, frame->arg2);
case SYS_DISPLAYINFO:
if (!UserMemory::Writable<DisplayInfo>(frame->arg1)) return -1;
if (!UserMemory::Writable<DisplayInfo>(frame->arg1)) return BadPointer;
return Sys_DisplayInfo((DisplayInfo*)frame->arg1);
case SYS_DISPLAYMODES:
// The Intel driver publishes at most 32 modes. Bounding this
// here also keeps the userspace range calculation overflow-free.
if ((int64_t)frame->arg2 < 0 || frame->arg2 > 32) return -1;
if ((int64_t)frame->arg2 < 0 || frame->arg2 > 32) return InvalidArgument;
if (!UserMemory::Range(frame->arg1,
(uint64_t)frame->arg2 * sizeof(DisplayModeInfo), true)) return -1;
(uint64_t)frame->arg2 * sizeof(DisplayModeInfo), true)) return BadPointer;
return Sys_DisplayModes((DisplayModeInfo*)frame->arg1, (int)frame->arg2);
case SYS_DISPLAYSETMODE:
if (!Sched::HasCapability(CAP_DISPLAY_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_DISPLAY_ADMIN)) return CapabilityDenied;
return Sys_DisplaySetMode((int)frame->arg1);
case SYS_DISPLAYBRIGHTNESS:
if ((int64_t)frame->arg1 >= 0 &&
!Sched::HasCapability(CAP_DISPLAY_ADMIN)) return SYS_ERR_PERMISSION;
!Sched::Consent::Require(CAP_BRIGHTNESS)) return CapabilityDenied;
return Sys_DisplayBrightness((int)frame->arg1);
case SYS_GETEXECPATH:
if (!UserMemory::Range(frame->arg2 ? frame->arg1 : frame->arg1, frame->arg2, true)) return -1;
if (!UserMemory::Range(frame->arg2 ? frame->arg1 : frame->arg1, frame->arg2, true)) return BadPointer;
return Sys_GetExecPath((char*)frame->arg1, frame->arg2);
case SYS_STAT:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (!UserMemory::Writable<FileStat>(frame->arg2)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
if (!UserMemory::Writable<FileStat>(frame->arg2)) return BadPointer;
return (int64_t)Sys_Stat((const char*)frame->arg1, (FileStat*)frame->arg2);
case SYS_UTIME:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
return (int64_t)Sys_Utime((const char*)frame->arg1,
(int64_t)frame->arg2,
(int64_t)frame->arg3,
@@ -222,33 +220,35 @@ namespace montauk::abi {
case SYS_TERMSIZE:
return (int64_t)Sys_TermSize();
case SYS_GETARGS:
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
return (int64_t)Sys_GetArgs((char*)frame->arg1, frame->arg2);
case SYS_RESET:
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return SYS_ERR_PERMISSION;
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return CapabilityDenied;
Sys_Reset();
return 0;
return Success;
case SYS_SHUTDOWN:
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return SYS_ERR_PERMISSION;
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return CapabilityDenied;
Sys_Shutdown();
return 0;
return Success;
case SYS_POWER_REQUEST:
if (frame->arg1 != POWER_REQ_QUERY &&
frame->arg1 != POWER_REQ_SHUTDOWN &&
frame->arg1 != POWER_REQ_REBOOT &&
frame->arg1 != POWER_REQ_PEEK) return -1;
frame->arg1 != POWER_REQ_PEEK) return InvalidArgument;
if (frame->arg1 == POWER_REQ_QUERY) {
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return SYS_ERR_PERMISSION;
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return CapabilityDenied;
} else if (!Sched::HasCapability(CAP_POWER_REQUEST)) {
return SYS_ERR_PERMISSION;
return CapabilityDenied;
}
return Sys_PowerRequest((int)frame->arg1);
case SYS_GETTIME:
if (!UserMemory::Writable<DateTime>(frame->arg1)) return -1;
if (!UserMemory::Writable<DateTime>(frame->arg1)) return BadPointer;
Sys_GetTime((DateTime*)frame->arg1);
return 0;
return Success;
case SYS_GETUNIXTIME:
return Sys_GetUnixTime();
case SYS_SETUNIXTIME:
if (!Sched::HasCapability(CAP_SET_TIME)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_SET_TIME)) return CapabilityDenied;
return Sys_SetUnixTime((int64_t)frame->arg1);
case SYS_SOCKET:
return (int64_t)Sys_Socket((int)frame->arg1);
@@ -261,114 +261,114 @@ namespace montauk::abi {
case SYS_ACCEPT:
return (int64_t)Sys_Accept((int)frame->arg1);
case SYS_SEND:
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return -1;
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
return (int64_t)Sys_Send((int)frame->arg1, (const uint8_t*)frame->arg2, (uint32_t)frame->arg3);
case SYS_RECV:
if (!UserMemory::Range(frame->arg2, frame->arg3, true)) return -1;
if (!UserMemory::Range(frame->arg2, frame->arg3, true)) return BadPointer;
return (int64_t)Sys_Recv((int)frame->arg1, (uint8_t*)frame->arg2, (uint32_t)frame->arg3);
case SYS_CLOSESOCK:
Sys_CloseSock((int)frame->arg1);
return 0;
return Success;
case SYS_GETNETCFG:
if (!UserMemory::Writable<NetCfg>(frame->arg1)) return -1;
if (!UserMemory::Writable<NetCfg>(frame->arg1)) return BadPointer;
Sys_GetNetCfg((NetCfg*)frame->arg1);
return 0;
return Success;
case SYS_SETNETCFG:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Readable<NetCfg>(frame->arg1)) return -1;
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return CapabilityDenied;
if (!UserMemory::Readable<NetCfg>(frame->arg1)) return BadPointer;
return (int64_t)Sys_SetNetCfg((const NetCfg*)frame->arg1);
case SYS_NETSTATUS:
if (!UserMemory::Writable<NetStatus>(frame->arg1)) return -1;
if (!UserMemory::Writable<NetStatus>(frame->arg1)) return BadPointer;
Sys_NetStatus((NetStatus*)frame->arg1);
return 0;
return Success;
case SYS_SENDTO:
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return -1;
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
return (int64_t)Sys_SendTo((int)frame->arg1, (const uint8_t*)frame->arg2,
(uint32_t)frame->arg3, (uint32_t)frame->arg4,
(uint16_t)frame->arg5);
case SYS_RECVFROM:
if (!UserMemory::Range(frame->arg2, frame->arg3, true)) return -1;
if (!UserMemory::OptionalRange(frame->arg4, sizeof(uint32_t), true)) return -1;
if (!UserMemory::OptionalRange(frame->arg5, sizeof(uint16_t), true)) return -1;
if (!UserMemory::Range(frame->arg2, frame->arg3, true)) return BadPointer;
if (!UserMemory::OptionalRange(frame->arg4, sizeof(uint32_t), true)) return BadPointer;
if (!UserMemory::OptionalRange(frame->arg5, sizeof(uint16_t), true)) return BadPointer;
return (int64_t)Sys_RecvFrom((int)frame->arg1, (uint8_t*)frame->arg2,
(uint32_t)frame->arg3,
UserMemory::IsUserPtr(frame->arg4) ? (uint32_t*)frame->arg4 : nullptr,
UserMemory::IsUserPtr(frame->arg5) ? (uint16_t*)frame->arg5 : nullptr);
case SYS_FWRITE:
if (!UserMemory::Range(frame->arg2, frame->arg4, false)) return -1;
if (!UserMemory::Range(frame->arg2, frame->arg4, false)) return BadPointer;
return (int64_t)Sys_FWrite((int)frame->arg1, (const uint8_t*)frame->arg2,
frame->arg3, frame->arg4);
case SYS_FCREATE:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
return (int64_t)Sys_FCreate((const char*)frame->arg1);
case SYS_FDELETE:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
return (int64_t)Sys_FDelete((const char*)frame->arg1);
case SYS_FMKDIR:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
return (int64_t)Sys_FMkdir((const char*)frame->arg1);
case SYS_FRENAME:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (!UserMemory::String(frame->arg2, kMaxPathBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
if (!UserMemory::String(frame->arg2, kMaxPathBytes)) return BadPointer;
return (int64_t)Sys_FRename((const char*)frame->arg1, (const char*)frame->arg2);
case SYS_DRIVELIST:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(int), true)) return -1;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(int), true)) return BadPointer;
return (int64_t)Sys_DriveList((int*)frame->arg1, (int)frame->arg2);
case SYS_DRIVELABEL:
if ((int64_t)frame->arg3 <= 0) return -1;
if (!UserMemory::Range(frame->arg2, frame->arg3, true)) return -1;
if ((int64_t)frame->arg3 <= 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg2, frame->arg3, true)) return BadPointer;
return (int64_t)Sys_DriveLabel((int)frame->arg1, (char*)frame->arg2, (int)frame->arg3);
case SYS_DRIVEKIND:
return (int64_t)Sys_DriveKind((int)frame->arg1);
case SYS_TERMSCALE:
return Sys_TermScale(frame->arg1, frame->arg2);
case SYS_RESOLVE:
if (!UserMemory::String(frame->arg1, kMaxHostnameBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxHostnameBytes)) return BadPointer;
return Sys_Resolve((const char*)frame->arg1);
case SYS_GETRANDOM:
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
return Sys_GetRandom((uint8_t*)frame->arg1, frame->arg2);
case SYS_LOG:
if (!Sched::HasCapability(CAP_LOG_READ)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
if (!Sched::Consent::Require(CAP_LOG_READ)) return CapabilityDenied;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
return Kt::ReadKernelLogBuffer((char*)frame->arg1, frame->arg2);
case SYS_MOUSESTATE:
if (!UserMemory::Writable<MouseState>(frame->arg1)) return -1;
if (!UserMemory::Writable<MouseState>(frame->arg1)) return BadPointer;
Sys_MouseState((MouseState*)frame->arg1);
return 0;
return Success;
case SYS_SETMOUSEBOUNDS:
Sys_SetMouseBounds((int32_t)frame->arg1, (int32_t)frame->arg2);
return 0;
return Success;
case SYS_SPAWN_REDIR:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return BadPointer;
return (int64_t)Sys_SpawnRedir((const char*)frame->arg1,
UserMemory::IsUserPtr(frame->arg2) ? (const char*)frame->arg2 : nullptr);
case SYS_SPAWN_REDIR_CAPS:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
if (!UserMemory::Readable<SpawnCapabilities>(frame->arg3)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return BadPointer;
if (!UserMemory::Readable<SpawnCapabilities>(frame->arg3)) return BadPointer;
return (int64_t)Sys_SpawnRedirCaps(
(const char*)frame->arg1,
frame->arg2 ? (const char*)frame->arg2 : nullptr,
(const SpawnCapabilities*)frame->arg3);
case SYS_CHILDIO_READ:
if ((int64_t)frame->arg3 < 0) return -1;
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, true)) return -1;
if ((int64_t)frame->arg3 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, true)) return BadPointer;
return (int64_t)Sys_ChildIoRead((int)frame->arg1, (char*)frame->arg2, (int)frame->arg3);
case SYS_CHILDIO_WRITE:
if ((int64_t)frame->arg3 < 0) return -1;
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return -1;
if ((int64_t)frame->arg3 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
return (int64_t)Sys_ChildIoWrite((int)frame->arg1, (const char*)frame->arg2, (int)frame->arg3);
case SYS_CHILDIO_WRITEKEY:
if (!UserMemory::Readable<KeyEvent>(frame->arg2)) return -1;
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEvent*)frame->arg2);
if (!UserMemory::Readable<KeyEventV1>(frame->arg2)) return BadPointer;
return (int64_t)Sys_ChildIoWriteKey((int)frame->arg1, (const KeyEventV1*)frame->arg2);
case SYS_CHILDIO_SETTERMSZ:
return (int64_t)Sys_ChildIoSetTermsz((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
case SYS_WINCREATE:
if (!UserMemory::String(frame->arg1, kMaxWindowTitleBytes)) return -1;
if (!UserMemory::Writable<WinCreateResult>(frame->arg4)) return -1;
if (!UserMemory::String(frame->arg1, kMaxWindowTitleBytes)) return BadPointer;
if (!UserMemory::Writable<WinCreateResult>(frame->arg4)) return BadPointer;
return (int64_t)Sys_WinCreate((const char*)frame->arg1, (int)frame->arg2,
(int)frame->arg3, (WinCreateResult*)frame->arg4);
case SYS_WINDESTROY:
@@ -376,38 +376,44 @@ namespace montauk::abi {
case SYS_WINPRESENT:
return (int64_t)Sys_WinPresent((int)frame->arg1);
case SYS_WINPOLL:
if (!UserMemory::Writable<WinEvent>(frame->arg2)) return -1;
if (!UserMemory::Writable<WinEvent>(frame->arg2)) return BadPointer;
return (int64_t)Sys_WinPoll((int)frame->arg1, (WinEvent*)frame->arg2);
case SYS_WINENUM:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WinInfo), true)) return -1;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WinInfo), true)) return BadPointer;
return (int64_t)Sys_WinEnum((WinInfo*)frame->arg1, (int)frame->arg2);
case SYS_WINMAP:
return (int64_t)Sys_WinMap((int)frame->arg1);
case SYS_WINUNMAP:
return (int64_t)Sys_WinUnmap((int)frame->arg1);
case SYS_WINSENDEVENT:
if (!UserMemory::Readable<WinEvent>(frame->arg2)) return -1;
if (!UserMemory::Readable<WinEvent>(frame->arg2)) return BadPointer;
return (int64_t)Sys_WinSendEvent((int)frame->arg1, (const WinEvent*)frame->arg2);
case SYS_WINRESIZE:
return (int64_t)Sys_WinResize((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
case SYS_PROCLIST:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(ProcInfo), true)) return -1;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(ProcInfo), true)) return BadPointer;
return (int64_t)Sys_ProcList((ProcInfo*)frame->arg1, (int)frame->arg2);
case SYS_KILL:
return (int64_t)Sys_Kill((int)frame->arg1);
case SYS_ABORT:
Sys_Abort();
return Success;
case SYS_EXIT_GROUP:
Sys_ExitGroup((int)frame->arg1);
return Success;
case SYS_SETSESSION:
return (int64_t)Sys_SetSession();
case SYS_KILLSESSION:
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) return CapabilityDenied;
return (int64_t)Sys_KillSession((int)frame->arg1);
case SYS_DEVLIST:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(DevInfo), true)) return -1;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(DevInfo), true)) return BadPointer;
return (int64_t)Sys_DevList((DevInfo*)frame->arg1, (int)frame->arg2);
case SYS_DISKINFO:
if (!UserMemory::Writable<DiskInfo>(frame->arg1)) return -1;
if (!UserMemory::Writable<DiskInfo>(frame->arg1)) return BadPointer;
return (int64_t)Sys_DiskInfo((DiskInfo*)frame->arg1, (int)frame->arg2);
case SYS_WINSETSCALE:
return (int64_t)Sys_WinSetScale((int)frame->arg1);
@@ -418,50 +424,58 @@ namespace montauk::abi {
case SYS_WINSETFLAGS:
return (int64_t)Sys_WinSetFlags((int)frame->arg1, (uint32_t)frame->arg2);
case SYS_MEMSTATS:
if (!UserMemory::Writable<MemStats>(frame->arg1)) return -1;
if (!UserMemory::Writable<MemStats>(frame->arg1)) return BadPointer;
Sys_MemStats((MemStats*)frame->arg1);
return 0;
return Success;
case SYS_PARTLIST:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(PartInfo), true)) return -1;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(PartInfo), true)) return BadPointer;
return (int64_t)Sys_PartList((PartInfo*)frame->arg1, (int)frame->arg2);
case SYS_VOLINFO:
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(VolInfo), true)) return BadPointer;
return (int64_t)Sys_VolInfo((VolInfo*)frame->arg1, (int)frame->arg2);
case SYS_DISKREAD:
if (!Sched::HasCapability(CAP_RAW_STORAGE)) return SYS_ERR_PERMISSION;
if (!Sched::HasCapability(CAP_RAW_STORAGE)) return CapabilityDenied;
return (int64_t)Sys_DiskRead((int)frame->arg1, frame->arg2,
(uint32_t)frame->arg3, (void*)frame->arg4);
case SYS_DISKWRITE:
if (!Sched::HasCapability(CAP_RAW_STORAGE)) return SYS_ERR_PERMISSION;
if (!Sched::HasCapability(CAP_RAW_STORAGE)) return CapabilityDenied;
return (int64_t)Sys_DiskWrite((int)frame->arg1, frame->arg2,
(uint32_t)frame->arg3, (const void*)frame->arg4);
case SYS_GPTINIT:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
return (int64_t)Sys_GptInit((int)frame->arg1);
case SYS_GPTADD:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Readable<GptAddParams>(frame->arg1)) return -1;
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
if (!UserMemory::Readable<GptAddParams>(frame->arg1)) return BadPointer;
return (int64_t)Sys_GptAdd((const GptAddParams*)frame->arg1);
case SYS_FSMOUNT:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
return (int64_t)Sys_FsMount((int)frame->arg1, (int)frame->arg2);
case SYS_FS_SYNC:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
return Sys_FsSync();
case SYS_FS_GROW:
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
if (!UserMemory::Range(frame->arg2, sizeof(FsGrowStatus), true)) return BadPointer;
return Sys_FsGrow((int)frame->arg1, (FsGrowStatus*)frame->arg2, frame->arg3 != 0);
case SYS_FSFORMAT:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return -1;
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return BadPointer;
return (int64_t)Sys_FsFormat((const FsFormatParams*)frame->arg1);
case SYS_AUDIOOPEN:
return Sys_AudioOpen((uint32_t)frame->arg1, (uint8_t)frame->arg2, (uint8_t)frame->arg3);
case SYS_AUDIOCLOSE:
return Sys_AudioClose((int)frame->arg1);
case SYS_AUDIOWRITE:
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return -1;
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
return Sys_AudioWrite((int)frame->arg1, (const uint8_t*)frame->arg2, (uint32_t)frame->arg3);
case SYS_AUDIOCTL:
return Sys_AudioCtl((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
case SYS_AUDIOLIST:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(AudioStreamInfo), true)) return -1;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(AudioStreamInfo), true)) return BadPointer;
return Sys_AudioList((AudioStreamInfo*)frame->arg1, (int)frame->arg2);
case SYS_AUDIOWAIT:
return Sys_AudioWait(frame->arg1, frame->arg2);
@@ -502,58 +516,83 @@ namespace montauk::abi {
return Sys_UsbBulkInRead((int)frame->arg1, (uint8_t*)frame->arg2,
(uint32_t)frame->arg3);
case SYS_POWERINFO:
if (!UserMemory::Writable<PowerInfo>(frame->arg1)) return -1;
if (!UserMemory::Writable<PowerInfo>(frame->arg1)) return BadPointer;
return Sys_PowerInfo((PowerInfo*)frame->arg1);
case SYS_BATTERYINFO:
// Read-only power telemetry, ungated like SYS_POWERINFO: the
// panel indicator has to work in an unprivileged session, which
// holds no device capability to spend on it.
if (!UserMemory::Writable<BatteryInfo>(frame->arg1)) return -1;
if (!UserMemory::Writable<BatteryInfo>(frame->arg1)) return BadPointer;
return Sys_BatteryInfo((BatteryInfo*)frame->arg1);
case SYS_GETCAPS:
if (!UserMemory::Writable<SpawnCapabilities>(frame->arg1)) return BadPointer;
return Sys_GetCaps((SpawnCapabilities*)frame->arg1);
case SYS_CONSENT_REGISTER:
return Sched::Consent::Register();
case SYS_CONSENT_NEXT:
if (!UserMemory::Writable<ConsentRequest>(frame->arg1)) return BadPointer;
return Sched::Consent::Next((ConsentRequest*)frame->arg1);
case SYS_CONSENT_REPLY:
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
case SYS_GETKEY_EX:
if (frame->arg2 < sizeof(KeyEventV1) || frame->arg2 > 4096) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
return Sys_GetKeyEx((void*)frame->arg1, frame->arg2);
case SYS_CHILDIO_WRITEKEY_EX:
if (frame->arg3 < sizeof(KeyEventV1) || frame->arg3 > 4096) return InvalidArgument;
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
return (int64_t)Sys_ChildIoWriteKeyEx((int)frame->arg1, (const void*)frame->arg2, frame->arg3);
case SYS_THREAD_SPAWN:
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
case SYS_THREAD_EXIT:
Sys_ThreadExit((int)frame->arg1);
return 0;
return Success;
case SYS_THREAD_JOIN:
if (frame->arg2 != 0 && !UserMemory::Writable<int>(frame->arg2)) return -1;
if (frame->arg2 != 0 && !UserMemory::Writable<int>(frame->arg2)) return BadPointer;
return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2);
case SYS_THREAD_SELF:
return Sys_ThreadSelf();
case SYS_FUTEX_WAIT:
// Validation demand-commits the page so the scheduler can
// read the word with its lock held without faulting.
if (!UserMemory::Readable<uint32_t>(frame->arg1)) return BadPointer;
return Sys_FutexWait(frame->arg1, (uint32_t)frame->arg2, frame->arg3);
case SYS_FUTEX_WAKE:
return Sys_FutexWake(frame->arg1, (uint32_t)frame->arg2);
case SYS_BTSCAN:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return -1;
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return BadPointer;
return Sys_BtScan((BtScanResult*)frame->arg1, (int)frame->arg2, (uint32_t)frame->arg3);
case SYS_BTCONNECT:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
if (!UserMemory::Range(frame->arg1, 6, false)) return BadPointer;
return Sys_BtConnect((const uint8_t*)frame->arg1);
case SYS_BTDISCONNECT:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
if (!UserMemory::Range(frame->arg1, 6, false)) return BadPointer;
return Sys_BtDisconnect((const uint8_t*)frame->arg1);
case SYS_BTSETADDR:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
if (!UserMemory::Range(frame->arg1, 6, false)) return BadPointer;
return Sys_BtSetAddr((const uint8_t*)frame->arg1);
case SYS_BTBONDS:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtBondInfo), true)) return -1;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtBondInfo), true)) return BadPointer;
return Sys_BtBonds((BtBondInfo*)frame->arg1, (int)frame->arg2);
case SYS_BTFORGET:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
if (!UserMemory::Range(frame->arg1, 6, false)) return BadPointer;
return Sys_BtForget((const uint8_t*)frame->arg1);
case SYS_BTLIST:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtDevInfo), true)) return -1;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtDevInfo), true)) return BadPointer;
return Sys_BtList((BtDevInfo*)frame->arg1, (int)frame->arg2);
case SYS_BTINFO:
if (!UserMemory::Writable<BtAdapterInfo>(frame->arg1)) return -1;
if (!UserMemory::Writable<BtAdapterInfo>(frame->arg1)) return BadPointer;
return Sys_BtInfo((BtAdapterInfo*)frame->arg1);
case SYS_WIFI_SCAN:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WifiNetwork), true)) return -1;
return Sys_WifiScan((WifiNetwork*)frame->arg1, (int)frame->arg2, (uint32_t)frame->arg3);
@@ -561,81 +600,81 @@ namespace montauk::abi {
if (!UserMemory::Writable<WifiInfo>(frame->arg1)) return -1;
return Sys_WifiInfo((WifiInfo*)frame->arg1);
case SYS_WIFI_CONNECT:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::String(frame->arg1, 64)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1;
return Sys_WifiConnect((const char*)frame->arg1, (const char*)frame->arg2);
case SYS_WIFI_DISCONNECT:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
return Sys_WifiDisconnect();
case SYS_WIFI_SCAN_START:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
return Sys_WifiScanStart((uint32_t)frame->arg1);
case SYS_WIFI_RESULTS:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WifiNetwork), true)) return -1;
return Sys_WifiResults((WifiNetwork*)frame->arg1, (int)frame->arg2);
case SYS_WIFI_CONNECT_ASYNC:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::String(frame->arg1, 64)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1;
return Sys_WifiConnectAsync((const char*)frame->arg1, (const char*)frame->arg2);
case SYS_NETIFS:
if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(NetIfInfo), true)) return -1;
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(NetIfInfo), true)) return BadPointer;
return Sys_NetIfs((NetIfInfo*)frame->arg1, (int)frame->arg2);
case SYS_SUSPEND:
if (!Sched::HasCapability(CAP_SUSPEND)) return SYS_ERR_PERMISSION;
if (!Sched::HasCapability(CAP_SUSPEND)) return CapabilityDenied;
return Sys_Suspend();
case SYS_SETTZ:
if (!Sched::HasCapability(CAP_SET_TIME)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_SET_TIME)) return CapabilityDenied;
return Sys_SetTZ((int32_t)frame->arg1);
case SYS_GETTZ:
return Sys_GetTZ();
case SYS_SETUSER:
if (!Sched::HasCapability(CAP_USER_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::String(frame->arg2, kMaxUserNameBytes)) return -1;
if (!Sched::HasCapability(CAP_USER_ADMIN)) return CapabilityDenied;
if (!UserMemory::String(frame->arg2, kMaxUserNameBytes)) return BadPointer;
return Sys_SetUser((int)frame->arg1, (const char*)frame->arg2);
case SYS_GETUSER:
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
return Sys_GetUser((char*)frame->arg1, frame->arg2);
case SYS_GETCWD:
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
return Sys_GetCwd((char*)frame->arg1, frame->arg2);
case SYS_CHDIR:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
return Sys_Chdir((const char*)frame->arg1);
case SYS_DUPHANDLE:
return Sys_DupHandle((int)frame->arg1);
case SYS_WAIT_HANDLE:
return (int64_t)Sys_WaitHandle((int)frame->arg1, (uint32_t)frame->arg2, frame->arg3);
case SYS_STREAM_CREATE:
if (!UserMemory::Writable<int>(frame->arg1) || !UserMemory::Writable<int>(frame->arg2)) return -1;
if (!UserMemory::Writable<int>(frame->arg1) || !UserMemory::Writable<int>(frame->arg2)) return BadPointer;
return Sys_StreamCreate((int*)frame->arg1, (int*)frame->arg2, (uint32_t)frame->arg3);
case SYS_STREAM_READ:
if ((int64_t)frame->arg3 < 0) return -1;
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, true)) return -1;
if ((int64_t)frame->arg3 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, true)) return BadPointer;
return Sys_StreamRead((int)frame->arg1, (uint8_t*)frame->arg2, (int)frame->arg3);
case SYS_STREAM_WRITE:
if ((int64_t)frame->arg3 < 0) return -1;
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return -1;
if ((int64_t)frame->arg3 < 0) return InvalidArgument;
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
return Sys_StreamWrite((int)frame->arg1, (const uint8_t*)frame->arg2, (int)frame->arg3);
case SYS_MAILBOX_CREATE:
if (!UserMemory::Writable<int>(frame->arg1) || !UserMemory::Writable<int>(frame->arg2)) return -1;
if (!UserMemory::Writable<int>(frame->arg1) || !UserMemory::Writable<int>(frame->arg2)) return BadPointer;
return Sys_MailboxCreate((int*)frame->arg1, (int*)frame->arg2);
case SYS_MAILBOX_SEND:
if (frame->arg4 > 0 && !UserMemory::Range(frame->arg3, frame->arg4, false)) return -1;
if (frame->arg4 > 0 && !UserMemory::Range(frame->arg3, frame->arg4, false)) return BadPointer;
return Sys_MailboxSend((int)frame->arg1, (uint32_t)frame->arg2,
(const void*)frame->arg3, (uint16_t)frame->arg4,
(int)frame->arg5);
case SYS_MAILBOX_RECV:
if (!UserMemory::Writable<uint16_t>(frame->arg4)) return -1;
if (frame->arg2 != 0 && !UserMemory::Writable<uint32_t>(frame->arg2)) return -1;
if (!UserMemory::Writable<uint16_t>(frame->arg4)) return BadPointer;
if (frame->arg2 != 0 && !UserMemory::Writable<uint32_t>(frame->arg2)) return BadPointer;
if (frame->arg3 != 0) {
uint16_t maxCopy = *(uint16_t*)frame->arg4;
if (!UserMemory::Range(frame->arg3, maxCopy, true)) return -1;
if (!UserMemory::Range(frame->arg3, maxCopy, true)) return BadPointer;
}
if (frame->arg5 != 0 && !UserMemory::Writable<int>(frame->arg5)) return -1;
if (frame->arg5 != 0 && !UserMemory::Writable<int>(frame->arg5)) return BadPointer;
return Sys_MailboxRecv((int)frame->arg1,
UserMemory::IsUserPtr(frame->arg2) ? (uint32_t*)frame->arg2 : nullptr,
UserMemory::IsUserPtr(frame->arg3) ? (void*)frame->arg3 : nullptr,
@@ -648,7 +687,7 @@ namespace montauk::abi {
case SYS_WAITSET_REMOVE:
return Sys_WaitsetRemove((int)frame->arg1, (int)frame->arg2);
case SYS_WAITSET_WAIT:
if (frame->arg2 != 0 && !UserMemory::Writable<IpcWaitResult>(frame->arg2)) return -1;
if (frame->arg2 != 0 && !UserMemory::Writable<IpcWaitResult>(frame->arg2)) return BadPointer;
return Sys_WaitsetWait((int)frame->arg1,
UserMemory::IsUserPtr(frame->arg2) ? (IpcWaitResult*)frame->arg2 : nullptr,
frame->arg3);
@@ -670,28 +709,73 @@ namespace montauk::abi {
case SYS_GETLIBBASE:
return (int64_t)Sys_GetLibBase(frame->arg1);
case SYS_CRASH_REPORT:
if (!UserMemory::Writable<CrashReportInfo>(frame->arg1)) return -1;
if (!UserMemory::Writable<CrashReportInfo>(frame->arg1)) return BadPointer;
return Sys_CrashReport((CrashReportInfo*)frame->arg1);
case SYS_CLIPBOARD_SET_TEXT:
if (frame->arg2 > 0 && !UserMemory::Range(frame->arg1, frame->arg2, false)) return -1;
if (frame->arg2 > 0 && !UserMemory::Range(frame->arg1, frame->arg2, false)) return BadPointer;
return Sys_ClipboardSetText((const char*)frame->arg1, (uint32_t)frame->arg2);
case SYS_CLIPBOARD_GET_INFO:
if (!UserMemory::Writable<ClipboardInfo>(frame->arg1)) return -1;
if (!UserMemory::Writable<ClipboardInfo>(frame->arg1)) return BadPointer;
return Sys_ClipboardGetInfo((ClipboardInfo*)frame->arg1);
case SYS_CLIPBOARD_GET_TEXT:
if (frame->arg2 > 0 && !UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
if (!UserMemory::Writable<uint32_t>(frame->arg3)) return -1;
if (frame->arg4 != 0 && !UserMemory::Writable<uint64_t>(frame->arg4)) return -1;
if (frame->arg2 > 0 && !UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
if (!UserMemory::Writable<uint32_t>(frame->arg3)) return BadPointer;
if (frame->arg4 != 0 && !UserMemory::Writable<uint64_t>(frame->arg4)) return BadPointer;
return Sys_ClipboardGetText(UserMemory::IsUserPtr(frame->arg1) ? (char*)frame->arg1 : nullptr,
(uint32_t)frame->arg2,
(uint32_t*)frame->arg3,
UserMemory::IsUserPtr(frame->arg4) ? (uint64_t*)frame->arg4 : nullptr);
case SYS_CLIPBOARD_CLEAR:
return Sys_ClipboardClear();
case SYS_CLIPBOARD_SET_DATA:
if (frame->arg2 > TRANSFER_MAX_FORMATS) return InvalidArgument;
if (!UserMemory::Range(frame->arg1, frame->arg2 * sizeof(TransferItem), false)) return BadPointer;
return Sys_ClipboardSetData((const TransferItem*)frame->arg1, (uint32_t)frame->arg2);
case SYS_CLIPBOARD_FORMATS: {
uint64_t max = frame->arg2 > TRANSFER_MAX_FORMATS ? TRANSFER_MAX_FORMATS : frame->arg2;
if (!UserMemory::OptionalRange(frame->arg1, max * sizeof(TransferFormatInfo), true)) return BadPointer;
if (!UserMemory::OptionalRange(frame->arg3, sizeof(uint64_t), true)) return BadPointer;
return Sys_ClipboardFormats(frame->arg1 ? (TransferFormatInfo*)frame->arg1 : nullptr,
(uint32_t)max,
frame->arg3 ? (uint64_t*)frame->arg3 : nullptr);
}
case SYS_CLIPBOARD_GET_DATA:
if (!UserMemory::String(frame->arg1, TRANSFER_MIME_MAX)) return BadPointer;
if (frame->arg3 > 0 && !UserMemory::Range(frame->arg2, frame->arg3, true)) return BadPointer;
if (!UserMemory::Writable<uint32_t>(frame->arg4)) return BadPointer;
return Sys_ClipboardGetData((const char*)frame->arg1,
frame->arg3 > 0 ? (void*)frame->arg2 : nullptr,
(uint32_t)frame->arg3, (uint32_t*)frame->arg4);
case SYS_WIN_REQUEST:
if (!UserMemory::Readable<WinRequest>(frame->arg2)) return BadPointer;
return Sys_WinRequest((int)frame->arg1, (const WinRequest*)frame->arg2);
case SYS_WIN_REQUEST_POLL:
if (!UserMemory::Writable<WinRequest>(frame->arg1)) return BadPointer;
return Sys_WinRequestPoll((WinRequest*)frame->arg1);
case SYS_WIN_DRAG_BEGIN:
if (!UserMemory::Readable<WinDragBegin>(frame->arg2)) return BadPointer;
if (frame->arg4 == 0 || frame->arg4 > TRANSFER_MAX_FORMATS) return InvalidArgument;
if (!UserMemory::Range(frame->arg3, frame->arg4 * sizeof(TransferItem), false)) return BadPointer;
return Sys_WinDragBegin((int)frame->arg1, (const WinDragBegin*)frame->arg2,
(const TransferItem*)frame->arg3, (uint32_t)frame->arg4);
case SYS_WIN_DRAG_FORMATS: {
uint64_t max = frame->arg3 > TRANSFER_MAX_FORMATS ? TRANSFER_MAX_FORMATS : frame->arg3;
if (!UserMemory::OptionalRange(frame->arg2, max * sizeof(TransferFormatInfo), true)) return BadPointer;
return Sys_WinDragFormats((uint32_t)frame->arg1,
frame->arg2 ? (TransferFormatInfo*)frame->arg2 : nullptr,
(uint32_t)max);
}
case SYS_WIN_DRAG_GET_DATA:
if (!UserMemory::String(frame->arg2, TRANSFER_MIME_MAX)) return BadPointer;
if (frame->arg4 > 0 && !UserMemory::Range(frame->arg3, frame->arg4, true)) return BadPointer;
if (!UserMemory::Writable<uint32_t>(frame->arg5)) return BadPointer;
return Sys_WinDragGetData((uint32_t)frame->arg1, (const char*)frame->arg2,
frame->arg4 > 0 ? (void*)frame->arg3 : nullptr,
(uint32_t)frame->arg4, (uint32_t*)frame->arg5);
case SYS_INPUT_WAIT:
return (int64_t)Sys_InputWait(frame->arg1, frame->arg2);
case SYS_LOG_WRITE: {
if (!UserMemory::String(frame->arg1, kMaxUserspaceLogEntryBytes)) return -1;
if (!UserMemory::String(frame->arg1, kMaxUserspaceLogEntryBytes)) return BadPointer;
auto* process = Sched::GetCurrentProcessPtr();
const char* username = process != nullptr && process->user[0] != '\0'
@@ -704,38 +788,22 @@ namespace montauk::abi {
Kt::UserspaceLogStream(imageName, username)
<< (const char*)frame->arg1;
return 0;
return Success;
}
case SYS_TERMINAL_ATTACHED:
return Sys_TerminalAttached();
default:
return -1;
return BadInvocation;
}
}
// ---- SYSCALL MSR initialization ----
// ---- Entry point initialization ----
void InitializeSyscalls() {
// Enable SYSCALL/SYSRET and no-execute page permissions in EFER.
uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER);
efer |= 1 | (1ULL << 11); // SCE | NXE
Hal::WriteMSR(Hal::IA32_EFER, efer);
Arch::InitializeSyscallEntry();
// STAR: kernel CS in [47:32], sysret base in [63:48]
// SYSCALL: CS=0x08, SS=0x10
// SYSRET: CS=0x10+16=0x20|RPL3=0x23, SS=0x10+8=0x18|RPL3=0x1B
uint64_t star = (0x0010ULL << 48) | (0x0008ULL << 32);
Hal::WriteMSR(Hal::IA32_STAR, star);
// LSTAR: SYSCALL entry point
Hal::WriteMSR(Hal::IA32_LSTAR, (uint64_t)SyscallEntry);
// FMASK: mask IF on SYSCALL entry (bit 9 = IF)
Hal::WriteMSR(Hal::IA32_FMASK, 0x200);
Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", "
<< (SYS_SPAWN_REDIR_CAPS + 1) << " syscall slots)";
Kt::KernelLogStream(Kt::OK, "Syscall") << "System call entry installed ("
<< kcp::dec << (SYS_SPAWN_REDIR_CAPS + 1) << " syscall slots)";
}
}
+337 -24
View File
@@ -7,6 +7,8 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <Common/StatusCodes.hpp>
#include <Arch/Syscall.hpp>
namespace montauk::abi {
@@ -346,6 +348,66 @@ namespace montauk::abi {
/* Battery.hpp -- ACPI battery status */
static constexpr uint64_t SYS_BATTERYINFO = 188; // (BatteryInfo*) -> 0, -1 no battery device
/* Process.hpp -- the caller's own capability masks. */
static constexpr uint64_t SYS_GETCAPS = 189; // (SpawnCapabilities*) -> 0, -1 bad pointer
/* Storage.hpp */
static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer
/* Consent.hpp */
static constexpr uint64_t SYS_CONSENT_REGISTER = 191; // () -> 0, -13 lacks CAP_COMPOSITOR, -1 another agent is live
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
/* Unicode key events. Size-versioned: the caller passes sizeof(its
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
rest, so the struct can grow without another syscall. */
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
/* Futexes: process-private wait queues keyed by a user address. WAIT
blocks only if *addr still equals `expected` (checked atomically with
enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check
only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken,
WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the
number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */
static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status
static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status
static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL;
static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu;
/* Abnormal termination from any thread: ends the whole process (every
thread) with exit code TerminatedAborted (256+SIGABRT). SYS_EXIT from a
sibling thread ends only that thread, so abort() needs its own call. */
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
/* Normal termination of the whole process from any thread, with an exit
code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc
exit() and Rust std::process::exit() use it, so exiting from a sibling
thread ends the process rather than only that thread. */
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
/* Multi-format clipboard (see "Window server messaging" below). The text
calls above operate on its TRANSFER_MIME_TEXT format. */
static constexpr uint64_t SYS_CLIPBOARD_SET_DATA = 200; // (const TransferItem*, count) -> status
static constexpr uint64_t SYS_CLIPBOARD_FORMATS = 201; // (TransferFormatInfo*, max, uint64_t* serial) -> count
static constexpr uint64_t SYS_CLIPBOARD_GET_DATA = 202; // (mime, buf, max, uint32_t* len) -> status
/* Window server requests and drag and drop. */
static constexpr uint64_t SYS_WIN_REQUEST = 203; // (windowId, const WinRequest*) -> status
static constexpr uint64_t SYS_WIN_REQUEST_POLL = 204; // (WinRequest*) -> 1 / 0; compositor only
static constexpr uint64_t SYS_WIN_DRAG_BEGIN = 205; // (windowId or -1 for the compositor's own surface, const WinDragBegin*, const TransferItem*, count) -> serial
static constexpr uint64_t SYS_WIN_DRAG_FORMATS = 206; // (serial, TransferFormatInfo*, max) -> count
static constexpr uint64_t SYS_WIN_DRAG_GET_DATA = 207; // (serial, mime, buf, max, uint32_t* len) -> status
/* Storage.hpp -- grow a mounted volume (and its MBR partition) to fill the
free space after it. apply=0 only reports; apply=1 does one bounded slice
and returns 1 while more remains, 0 once done. CAP_STORAGE_ADMIN. */
static constexpr uint64_t SYS_FS_GROW = 208; // (drive, FsGrowStatus*, apply) -> 1 / 0 / status
/* Time.hpp -- wall clock as UTC unix seconds (SYS_GETTIME is local time). */
static constexpr uint64_t SYS_GETUNIXTIME = 209; // () -> seconds
/* Kernel-owned process capabilities. User identities may namespace
per-user resources, but never participate in authorization decisions. */
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
@@ -366,8 +428,21 @@ namespace montauk::abi {
whatever that image is granted at its next launch. Formatting a data
volume must not carry that authority with it. */
static constexpr uint64_t CAP_SYSTEM_IMAGE = 1ULL << 12;
static constexpr uint64_t CAP_ALL = (1ULL << 13) - 1;
static constexpr uint64_t CAP_STANDARD_SESSION = CAP_POWER_REQUEST | CAP_SUSPEND;
/* Map other processes' windows and deliver input events to them
(SYS_WINMAP, SYS_WINSENDEVENT): the desktop compositor's authority.
Anything holding it can read every window's pixels and click on any
window, including a consent prompt it is itself waiting on, so it is
never part of a session mask, never part of "all", and never
delegated by the compositor to what it launches. */
static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13;
/* Set the backlight of the active display (SYS_DISPLAYBRIGHTNESS).
A seat preference rather than display administration, so every
session holds it; CAP_DISPLAY_ADMIN keeps mode setting. Reading the
level needs nothing. */
static constexpr uint64_t CAP_BRIGHTNESS = 1ULL << 14;
static constexpr uint64_t CAP_ALL = (1ULL << 15) - 1;
static constexpr uint64_t CAP_STANDARD_SESSION =
CAP_POWER_REQUEST | CAP_SUSPEND | CAP_BRIGHTNESS;
static constexpr uint64_t CAP_ADMIN_SESSION =
CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN |
CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN |
@@ -378,28 +453,63 @@ namespace montauk::abi {
static_assert((CAP_ADMIN_SESSION & CAP_SYSTEM_IMAGE) == 0,
"an admin session must not imply authority to rewrite the "
"programs it launches; grant CAP_SYSTEM_IMAGE per binary");
static_assert((CAP_ADMIN_SESSION & CAP_COMPOSITOR) == 0,
"a session must not delegate the compositor's authority; "
"login grants it to the desktop directly");
// Capability denial for the Wi-Fi and USB syscalls only, which keep their
// own WIFI_ERR_* / USB_ERR_* spaces; everything else returns
// CapabilityDenied from StatusCodes.
static constexpr int SYS_ERR_PERMISSION = -13;
// A process holds two independent sets: what it may use (effective) and
// what it may pass to children (delegable). A launcher can delegate what
// it never uses itself, so neither set bounds the other.
struct SpawnCapabilities {
uint64_t permitted;
uint64_t effective;
uint64_t delegable;
};
// Both of the child's sets must come out of the parent's delegable set.
constexpr bool ValidCapabilityDelegation(const SpawnCapabilities& child,
uint64_t parentDelegable) {
return (child.permitted & ~CAP_ALL) == 0 &&
(child.effective & ~child.permitted) == 0 &&
(child.delegable & ~child.permitted) == 0 &&
(child.permitted & ~parentDelegable) == 0 &&
return ((child.effective | child.delegable) & ~CAP_ALL) == 0 &&
(child.effective & ~parentDelegable) == 0 &&
(child.delegable & ~parentDelegable) == 0;
}
static_assert(ValidCapabilityDelegation(
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN, 0}, CAP_NETWORK_ADMIN));
{CAP_NETWORK_ADMIN, 0}, CAP_NETWORK_ADMIN));
static_assert(ValidCapabilityDelegation(
{0, CAP_NETWORK_ADMIN}, CAP_NETWORK_ADMIN));
static_assert(!ValidCapabilityDelegation(
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN}, 0));
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN}, 0));
static_assert(!ValidCapabilityDelegation(
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN | CAP_SET_TIME, 0}, CAP_ALL));
{CAP_NETWORK_ADMIN | CAP_SET_TIME, 0}, CAP_NETWORK_ADMIN));
static_assert(!ValidCapabilityDelegation(
{0, CAP_NETWORK_ADMIN | CAP_SET_TIME}, CAP_NETWORK_ADMIN));
/* Capabilities a user can grant at first use through a consent prompt.
A consent grant never exceeds what the process's parent could have
delegated to it at spawn (its consent ceiling), is never delegable
onwards, and is issued only by the session's consent agent -- so it is
deferred delegation, not amplification. */
static constexpr uint64_t CAP_CONSENT_ELIGIBLE =
CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME |
CAP_DISPLAY_ADMIN | CAP_LOG_READ | CAP_BRIGHTNESS;
static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0,
"consent can only hand out authority an admin session holds");
static constexpr int CONSENT_DENY = 0;
static constexpr int CONSENT_ALLOW = 1;
// A request left unanswered this long is denied (and not remembered).
static constexpr uint64_t CONSENT_TIMEOUT_MS = 120000;
struct ConsentRequest {
uint64_t id;
uint64_t capability; // exactly one CAP_* bit
int32_t pid;
uint32_t imageProtected; // 1 when writing the image needs CAP_SYSTEM_IMAGE
char imagePath[256];
};
// Generic USB errors. Claims are restricted to interfaces without a
@@ -540,7 +650,10 @@ namespace montauk::abi {
uint64_t txPackets;
};
struct KeyEvent {
// Legacy 9-byte key event, read by SYS_GETKEY and written by
// SYS_CHILDIO_WRITEKEY. Kept so binaries built before codepoints still work.
// `ascii` is a Windows-1252 byte.
struct KeyEventV1 {
uint8_t scancode;
char ascii;
bool pressed;
@@ -556,6 +669,32 @@ namespace montauk::abi {
bool extended;
};
// Key event with the Unicode text it types (SYS_GETKEY_EX,
// SYS_CHILDIO_WRITEKEY_EX, WinEvent). Its first 9 bytes ARE a KeyEventV1, so
// a legacy reader sees a valid event. `codepoint` is the character the key
// produces (0 for control/navigation keys and dead keys). `ascii` is its
// Windows-1252 byte, or KEY_ASCII_SUBSTITUTE when 1252 has none.
struct KeyEvent {
uint8_t scancode;
char ascii;
bool pressed;
bool shift;
bool ctrl;
bool alt;
bool capslock;
bool altgr;
bool extended;
uint8_t _reserved[3];
uint32_t codepoint;
};
static_assert(sizeof(KeyEventV1) == 9, "KeyEventV1 is frozen ABI");
static_assert(sizeof(KeyEvent) == 16, "KeyEvent must fit the WinEvent union");
// ASCII SUB: `ascii` of a text key whose character has no Windows-1252 byte.
// A control character, so legacy code that only accepts ascii >= 0x20 skips
// it, while code testing `ascii == 0` for navigation keys is not confused.
static constexpr char KEY_ASCII_SUBSTITUTE = 0x1A;
struct MouseState {
int32_t x;
int32_t y;
@@ -589,16 +728,55 @@ namespace montauk::abi {
};
// Window server shared types
enum WinEventType : uint8_t {
WIN_EVENT_KEY = 0,
WIN_EVENT_MOUSE = 1,
WIN_EVENT_RESIZE = 2,
WIN_EVENT_CLOSE = 3,
WIN_EVENT_SCALE = 4,
// Drag and drop (WinEvent::drag). A window may read the drag's data
// (SYS_WIN_DRAG_*) once it has received one of these for that serial.
WIN_EVENT_DRAG_ENTER = 5, // pointer entered the window during a drag
WIN_EVENT_DRAG_OVER = 6, // pointer moved; coalesced like mouse moves
WIN_EVENT_DRAG_LEAVE = 7, // pointer left, or the drag was cancelled
WIN_EVENT_DROP = 8, // released here; drag.actions = chosen action
WIN_EVENT_DRAG_END = 9, // to the source only; drag.actions = action
// taken (0 = cancelled / not accepted)
};
// Modifier keys held at the time of a drag event (WinEvent::drag.modifiers).
static constexpr uint8_t WIN_MOD_SHIFT = 1u << 0;
static constexpr uint8_t WIN_MOD_CTRL = 1u << 1;
static constexpr uint8_t WIN_MOD_ALT = 1u << 2;
// Drag actions; a source offers a set, a drop carries exactly one.
static constexpr uint8_t WIN_DRAG_COPY = 1u << 0;
static constexpr uint8_t WIN_DRAG_MOVE = 1u << 1;
static constexpr uint8_t WIN_DRAG_LINK = 1u << 2;
struct WinDragEvent {
int32_t x, y; // window-local; -1,-1 when not over the window
uint32_t serial; // identifies the drag
uint8_t actions; // WIN_DRAG_* (see WinEventType)
uint8_t modifiers; // WIN_MOD_*
uint8_t _pad[2];
};
static_assert(sizeof(WinDragEvent) == 16, "WinDragEvent must fit the WinEvent union");
struct WinEvent {
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
uint8_t type; // WinEventType
uint8_t _pad[3];
union {
KeyEvent key;
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
KeyEvent key; // 16 bytes: the union was already that size (mouse)
// modifiers (WIN_MOD_*) was padding before; compositors that
// predate it send 0.
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons, modifiers; } mouse;
struct { int32_t w, h; } resize;
struct { int32_t scale; } scale;
WinDragEvent drag; // WIN_EVENT_DRAG_* / WIN_EVENT_DROP
};
};
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
struct WinInfo {
int32_t id;
@@ -619,6 +797,116 @@ namespace montauk::abi {
uint64_t pixelVa; // VA of pixel buffer in caller's address space
};
// ---- Window server messaging ----------------------------------------
//
// Three pieces, shared by the compositor (the desktop) and every app:
// * WinEvent types 5-9 below: drag-and-drop events, compositor -> app.
// * WinRequest: typed records an app posts about one of its own windows
// (SYS_WIN_REQUEST); only the compositor can collect them
// (SYS_WIN_REQUEST_POLL). Unknown types are ignored, so new requests
// can be added without breaking older compositors or apps.
// * Transfers: multi-format payloads (one MIME type per format) carried
// by the clipboard (SYS_CLIPBOARD_*_DATA) and by drags
// (SYS_WIN_DRAG_*). The older text clipboard calls read and write the
// TRANSFER_MIME_TEXT format of the same clipboard.
// Transfers -----------------------------------------------------------
static constexpr uint32_t TRANSFER_MAX_FORMATS = 8;
static constexpr uint32_t TRANSFER_MIME_MAX = 64; // incl. NUL
static constexpr uint32_t TRANSFER_MAX_BYTES = 256 * 1024; // all formats together
// Well-known formats. Any other "type/subtype" string is allowed.
static constexpr const char* TRANSFER_MIME_TEXT = "text/plain;charset=utf-8";
// UTF-8 absolute paths ("0:/..."), one per line.
static constexpr const char* TRANSFER_MIME_FILE_LIST = "application/x-montauk-file-list";
// Clipboard companion to a file list: "copy" or "cut".
static constexpr const char* TRANSFER_MIME_FILE_OP = "application/x-montauk-file-op";
// One format to store. Pointers are carried as 64-bit values so the
// layout is the same for every caller.
struct TransferItem {
uint64_t mime; // const char*, NUL-terminated, < TRANSFER_MIME_MAX
uint64_t data; // const void*
uint32_t len;
uint32_t _reserved;
};
static_assert(sizeof(TransferItem) == 24, "TransferItem is ABI");
struct TransferFormatInfo {
char mime[TRANSFER_MIME_MAX];
uint32_t len;
uint32_t _reserved;
};
static_assert(sizeof(TransferFormatInfo) == 72, "TransferFormatInfo is ABI");
// Requests --------------------------------------------------------------
static constexpr uint32_t WIN_REQUEST_PAYLOAD_MAX = 240;
struct WinRequest {
uint16_t type; // WinRequestType
uint16_t size; // payload bytes in use
int32_t windowId; // window the request is about (the sender must own it
// when posting); -1 if it was destroyed since
int32_t senderPid; // filled in by the kernel
uint32_t serial; // filled in by the kernel; increases per request
uint8_t payload[WIN_REQUEST_PAYLOAD_MAX];
};
static_assert(sizeof(WinRequest) == 256, "WinRequest is ABI");
enum WinRequestType : uint16_t {
WIN_REQ_LAUNCH_ITEM = 1, // WinReqLaunchItem
WIN_REQ_SET_PARENT = 2, // WinReqSetParent
WIN_REQ_DRAG_STATUS = 3, // WinReqDragStatus
WIN_REQ_ACTIVATE = 4, // no payload: raise and focus the window
// Types from here on are generated by the kernel; SYS_WIN_REQUEST
// refuses them so an app cannot forge one.
WIN_REQ_KERNEL_BASE = 0x8000,
WIN_REQ_DRAG_BEGIN = 0x8001, // WinReqDragBegin (from SYS_WIN_DRAG_BEGIN)
};
// Open a desktop catalog item (an app manifest id, or a built-in such as
// "desktop-settings"), as if the user had picked it from the launcher, so
// it runs with the authority the desktop would give it. `args` reach only
// apps whose manifest declares files.types (they take a document path);
// anything else starts without them, so a request cannot steer a
// privileged app through its command line.
struct WinReqLaunchItem {
char itemId[48];
char args[192];
};
// Mark the window as a dialog of another window owned by the same
// process: the compositor centres it over the parent.
struct WinReqSetParent {
int32_t parentWindowId;
};
// Answer to DRAG_ENTER/DRAG_OVER: the action this window would perform
// on a drop at the current position (0 = would refuse). Advisory; it
// drives the compositor's drag feedback.
struct WinReqDragStatus {
uint32_t serial;
uint8_t action; // one WIN_DRAG_* or 0
uint8_t _pad[3];
};
// Describes a drag for the compositor's feedback. Supplied by the app to
// SYS_WIN_DRAG_BEGIN and relayed (with the serial) as WIN_REQ_DRAG_BEGIN.
struct WinDragBegin {
uint8_t actions; // WIN_DRAG_* offered
uint8_t _pad[3];
uint32_t itemCount; // for a "3 items" badge; 0 = do not show
char label[64]; // short description, e.g. the file name
};
static_assert(sizeof(WinDragBegin) == 72, "WinDragBegin is ABI");
struct WinReqDragBegin {
uint32_t serial;
WinDragBegin info;
};
struct MemStats {
uint64_t totalBytes;
uint64_t freeBytes;
@@ -635,7 +923,7 @@ namespace montauk::abi {
struct DiskInfo {
uint8_t port; // block device index
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage
uint8_t type; // 0=none, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB mass storage, 5=SD card
uint8_t sataGen; // SATA gen (1/2/3)
uint8_t _pad0;
uint64_t sectorCount; // Total user-addressable sectors
@@ -676,6 +964,28 @@ namespace montauk::abi {
char typeName[24]; // human-readable type name
};
// One mounted volume (returned by SYS_VOLINFO). A volume is a single
// mounted filesystem, which is what a drive number names: one physical
// disk with three partitions occupies three drive numbers.
//
// A volume with no backing partition -- the boot ramdisk -- reports
// blockDev and partIndex as -1 and leaves the geometry fields zero.
struct VolInfo {
int32_t drive; // VFS drive number ("N:/")
int32_t blockDev; // backing block device index, -1 if none
int32_t partIndex; // global GPT partition index, -1 if none
uint32_t kind; // BlockDeviceKind: 0=none/ramdisk, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB MSC
uint64_t startLba; // partition start LBA, 0 if none
uint64_t sectorCount; // partition length in sectors, 0 if none
uint32_t sectorSize; // logical sector size in bytes, 0 if none
uint32_t _pad0;
char label[36]; // filesystem volume label, "" if unset
char fsType[12]; // "ext2", "FAT32", "ramdisk", "" if unknown
char partName[72]; // GPT partition name, "" if none
char model[41]; // backing disk model, "" if none
char _pad1[3];
};
struct GptAddParams {
int32_t blockDev;
uint32_t _pad0;
@@ -695,6 +1005,14 @@ namespace montauk::abi {
char label[32]; // volume label
};
struct FsGrowStatus {
uint64_t fsBytes; // filesystem size now
uint64_t partBytes; // partition size now
uint64_t targetBytes; // filesystem size once grown; == fsBytes if it cannot grow
uint32_t stepsDone; // progress of the grow, out of stepsTotal
uint32_t stepsTotal;
};
struct ProcInfo {
int32_t pid;
int32_t parentPid;
@@ -703,7 +1021,6 @@ namespace montauk::abi {
char name[64];
uint64_t heapUsed; // Distance from UserHeapBase to high-water mark
uint64_t cpuTimeMs; // accumulated scheduler runtime
uint64_t permittedCaps;
uint64_t effectiveCaps;
uint64_t delegableCaps;
};
@@ -929,15 +1246,11 @@ namespace montauk::abi {
uint64_t timestampTick;
};
// Stack frame pushed by SyscallEntry.asm
struct SyscallFrame {
uint64_t r15, r14, r13, r12, rbp, rbx; // callee-saved
uint64_t arg6, arg5, arg4, arg3, arg2, arg1;
uint64_t syscall_nr;
uint64_t user_rflags, user_rip, user_rsp;
};
// Saved user context of a system call; its layout is the
// architecture's (see Arch/Syscall.hpp).
using SyscallFrame = Arch::SyscallFrame;
// Kernel-only: set up SYSCALL MSRs and initialize dispatch
// Kernel-only: install the system-call entry point and initialize dispatch
void InitializeSyscalls();
}
+8 -4
View File
@@ -1,11 +1,11 @@
/*
* Time.hpp
* SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME syscalls
* SYS_GETTICKS, SYS_GETMILLISECONDS, SYS_GETTIME, SYS_GETUNIXTIME syscalls
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <Timekeeping/ApicTimer.hpp>
#include <Timekeeping/Timer.hpp>
#include <Timekeeping/Time.hpp>
#include "Syscall.hpp"
@@ -32,14 +32,18 @@ namespace montauk::abi {
static int64_t Sys_SetTZ(int32_t offsetMinutes) {
Timekeeping::SetTZOffset(offsetMinutes);
return 0;
return Success;
}
static int64_t Sys_GetTZ() {
return (int64_t)Timekeeping::GetTZOffset();
}
static int64_t Sys_GetUnixTime() {
return Timekeeping::GetUnixTimestamp();
}
static int64_t Sys_SetUnixTime(int64_t unixSeconds) {
return Timekeeping::SetUnixTimestamp(unixSeconds) ? 0 : -1;
return Timekeeping::SetUnixTimestamp(unixSeconds) ? Success : InvalidArgument;
}
};
+12 -12
View File
@@ -19,9 +19,9 @@ namespace montauk::abi::UserMemory {
return addr != 0 && addr < USER_SPACE_END;
}
inline uint64_t CurrentPml4() {
inline uint64_t CurrentRoot() {
auto* proc = Sched::GetCurrentProcessPtr();
return proc ? proc->pml4Phys : 0;
return proc ? proc->pageTableRoot : 0;
}
inline bool Range(uint64_t addr, uint64_t size, bool requireWrite = false) {
@@ -30,10 +30,10 @@ namespace montauk::abi::UserMemory {
if (addr + size < addr) return false;
if (addr + size > USER_SPACE_END) return false;
uint64_t pml4Phys = CurrentPml4();
if (pml4Phys == 0) return false;
uint64_t pageTableRoot = CurrentRoot();
if (pageTableRoot == 0) return false;
if (Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr, size, requireWrite))
if (Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr, size, requireWrite))
return true;
// Anonymous VMAs are demand-committed. Validation precedes the actual
@@ -42,13 +42,13 @@ namespace montauk::abi::UserMemory {
uint64_t page = addr & ~0xFFFULL;
uint64_t last = (addr + size - 1) & ~0xFFFULL;
for (;;) {
if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, page, 1, requireWrite) &&
!TryHandleAnonymousPageFault(page, requireWrite ? 2ULL : 0ULL))
if (!Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, page, 1, requireWrite) &&
!TryHandleAnonymousPageFault(page, {false, requireWrite, false}))
return false;
if (page == last) break;
page += 0x1000ULL;
}
return Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr, size, requireWrite);
return Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr, size, requireWrite);
}
inline bool OptionalRange(uint64_t addr, uint64_t size, bool requireWrite = false) {
@@ -68,13 +68,13 @@ namespace montauk::abi::UserMemory {
inline bool String(uint64_t addr, uint64_t maxLen) {
if (!ValidUserPtr(addr) || maxLen == 0) return false;
uint64_t pml4Phys = CurrentPml4();
if (pml4Phys == 0) return false;
uint64_t pageTableRoot = CurrentRoot();
if (pageTableRoot == 0) return false;
const char* str = (const char*)addr;
for (uint64_t i = 0; i < maxLen; i++) {
if (!Memory::VMM::Paging::IsUserRangeAccessible(pml4Phys, addr + i, 1, false)) {
if (!TryHandleAnonymousPageFault(addr + i, 0)) return false;
if (!Memory::VMM::Paging::IsUserRangeAccessible(pageTableRoot, addr + i, 1, false)) {
if (!TryHandleAnonymousPageFault(addr + i, {false, false, false})) return false;
}
if (str[i] == '\0') return true;
}
+253 -56
View File
@@ -54,14 +54,14 @@ namespace WinServer {
Ipc::ReleaseSurface(surface);
}
static int UnmapRetiredSnapshotsLocked(int windowId, int callerPid, uint64_t callerPml4) {
int result = -1;
static int UnmapRetiredSnapshotsLocked(int windowId, int callerPid, uint64_t callerRoot) {
int result = InvalidArgument;
for (int i = 0; i < MaxRetiredSnapshots; i++) {
if (!g_retiredSnapshots[i].used || g_retiredSnapshots[i].windowId != windowId) continue;
if (g_retiredSnapshots[i].surface != nullptr) {
if (Ipc::UnmapSurfaceForPid(g_retiredSnapshots[i].surface, callerPid, callerPml4) == 0) {
result = 0;
if (Ipc::UnmapSurfaceForPid(g_retiredSnapshots[i].surface, callerPid, callerRoot) == 0) {
result = Success;
}
Ipc::ReleaseSurface(g_retiredSnapshots[i].surface);
}
@@ -73,11 +73,18 @@ namespace WinServer {
return result;
}
static void OrphanRequestsLocked(int windowId);
static void ReleaseSlotResourcesLocked(WindowSlot& slot, bool unmapOwner) {
// Queued requests stay deliverable (a dialog may post "open this
// app" and close at once), but no longer name a window that could be
// handed to someone else.
OrphanRequestsLocked((int)(&slot - g_slots));
if (unmapOwner && slot.liveSurface != nullptr) {
auto* owner = Sched::GetProcessByPid(slot.ownerPid);
if (owner != nullptr) {
Ipc::UnmapSurfaceForPid(slot.liveSurface, slot.ownerPid, owner->pml4Phys);
Ipc::UnmapSurfaceForPid(slot.liveSurface, slot.ownerPid, owner->pageTableRoot);
}
}
@@ -94,22 +101,67 @@ namespace WinServer {
ResetSlotLocked(slot);
}
static int SendEventLocked(int windowId, const montauk::abi::WinEvent* event) {
if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return -1;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.eventMailbox == nullptr) return -1;
// ---- Messaging state ----
int rc = (event->type == 1)
? Ipc::MailboxSendCoalescedMouse(slot.eventMailbox, 0, event, sizeof(*event))
: Ipc::MailboxSend(slot.eventMailbox, 0, event, sizeof(*event));
return rc > 0 ? 0 : -1;
static constexpr int MaxRequests = 64;
// One app may not hold more than this many queued requests, so a single
// misbehaving process cannot starve the others out of the queue.
static constexpr int MaxRequestsPerSender = 16;
static constexpr int MaxDragReaders = 8;
static montauk::abi::WinRequest g_requests[MaxRequests];
static int g_requestHead = 0; // oldest
static int g_requestCount = 0;
static uint32_t g_requestSerial = 0;
struct DragState {
bool active;
uint32_t serial;
int sourceWindow;
int sourcePid;
DataTransfer::Transfer transfer;
int readers[MaxDragReaders];
int readerCount;
};
static DragState g_drag;
static uint32_t g_dragSerial = 0;
static bool IsDragEvent(uint8_t type) {
return type >= montauk::abi::WIN_EVENT_DRAG_ENTER && type <= montauk::abi::WIN_EVENT_DRAG_END;
}
int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h,
static void GrantDragReaderLocked(uint32_t serial, int pid) {
if (!g_drag.active || g_drag.serial != serial || pid <= 0) return;
if (pid == g_drag.sourcePid) return;
for (int i = 0; i < g_drag.readerCount; i++) {
if (g_drag.readers[i] == pid) return;
}
if (g_drag.readerCount < MaxDragReaders) g_drag.readers[g_drag.readerCount++] = pid;
}
static int SendEventLocked(int windowId, const montauk::abi::WinEvent* event) {
if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return InvalidArgument;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.eventMailbox == nullptr) return InvalidArgument;
// Delivering a drag event is what lets the receiving app read the
// drag's data: it needs the formats to decide whether to accept.
if (IsDragEvent(event->type)) GrantDragReaderLocked(event->drag.serial, slot.ownerPid);
bool motion = event->type == montauk::abi::WIN_EVENT_MOUSE ||
event->type == montauk::abi::WIN_EVENT_DRAG_OVER;
int rc = motion
? Ipc::MailboxSendCoalescedMotion(slot.eventMailbox, 0, event, sizeof(*event))
: Ipc::MailboxSend(slot.eventMailbox, 0, event, sizeof(*event));
if (rc < 0) return GenericFailure;
return rc > 0 ? Success : Busy;
}
int Create(int ownerPid, uint64_t ownerRoot, const char* title, int w, int h,
uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard;
if (w <= 0 || h <= 0 || w > 16384 || h > 16384) return -1;
if (w <= 0 || h <= 0 || w > 16384 || h > 16384) return InvalidArgument;
int slotIdx = -1;
for (int i = 0; i < MaxWindows; i++) {
@@ -118,7 +170,7 @@ namespace WinServer {
break;
}
}
if (slotIdx < 0) return -1;
if (slotIdx < 0) return GenericFailure;
uint64_t bufSize = (uint64_t)w * (uint64_t)h * 4ULL;
Ipc::Surface* live = Ipc::CreateSurface(bufSize);
@@ -128,7 +180,7 @@ namespace WinServer {
if (live != nullptr) Ipc::ReleaseSurface(live);
if (snapshot != nullptr) Ipc::ReleaseSurface(snapshot);
if (mailbox != nullptr) Ipc::ReleaseMailbox(mailbox, false, false);
return -1;
return OutOfMemory;
}
Ipc::RetainSurface(live);
@@ -142,11 +194,11 @@ namespace WinServer {
FillSurfaceLocked(snapshot, w, h, InitialWindowPixel);
uint64_t userVa = 0;
if (Ipc::MapSurfaceForPid(live, ownerPid, ownerPml4, heapNext, userVa, false) != 0) {
if (Ipc::MapSurfaceForPid(live, ownerPid, ownerRoot, heapNext, userVa, false) != 0) {
Ipc::ReleaseMailbox(mailbox, true, true);
Ipc::ReleaseSurface(snapshot);
Ipc::ReleaseSurface(live);
return -1;
return GenericFailure;
}
WindowSlot& slot = g_slots[slotIdx];
@@ -176,40 +228,41 @@ namespace WinServer {
int Destroy(int windowId, int callerPid) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return -1;
if (windowId < 0 || windowId >= MaxWindows) return InvalidArgument;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
if (!slot.used) return InvalidArgument;
if (slot.ownerPid != callerPid) return PermissionDenied;
ReleaseSlotResourcesLocked(slot, true);
return 0;
return Success;
}
int Present(int windowId, int callerPid) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return -1;
if (windowId < 0 || windowId >= MaxWindows) return InvalidArgument;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid ||
slot.liveSurface == nullptr || slot.snapshotSurface == nullptr) {
return -1;
}
if (!slot.used || slot.liveSurface == nullptr || slot.snapshotSurface == nullptr)
return InvalidArgument;
if (slot.ownerPid != callerPid) return PermissionDenied;
if (Ipc::CopySurface(slot.snapshotSurface, slot.liveSurface) != 0) return -1;
if (Ipc::CopySurface(slot.snapshotSurface, slot.liveSurface) != 0) return GenericFailure;
slot.dirty = true;
return 0;
return Success;
}
int Poll(int windowId, int callerPid, montauk::abi::WinEvent* outEvent) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows || outEvent == nullptr) return -1;
if (windowId < 0 || windowId >= MaxWindows || outEvent == nullptr) return InvalidArgument;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid || slot.eventMailbox == nullptr) return -1;
if (!slot.used || slot.eventMailbox == nullptr) return InvalidArgument;
if (slot.ownerPid != callerPid) return PermissionDenied;
uint16_t len = sizeof(*outEvent);
int rc = Ipc::MailboxRecv(slot.eventMailbox, nullptr, outEvent, &len, true);
if (rc < 0) return -1;
if (rc < 0) return GenericFailure;
return rc > 0 ? 1 : 0;
}
@@ -234,7 +287,7 @@ namespace WinServer {
return count;
}
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext) {
uint64_t Map(int windowId, int callerPid, uint64_t callerRoot, uint64_t& heapNext) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return 0;
@@ -242,24 +295,24 @@ namespace WinServer {
if (!slot.used || slot.snapshotSurface == nullptr) return 0;
uint64_t outVa = 0;
if (Ipc::MapSurfaceForPid(slot.snapshotSurface, callerPid, callerPml4, heapNext, outVa, false) != 0) {
if (Ipc::MapSurfaceForPid(slot.snapshotSurface, callerPid, callerRoot, heapNext, outVa, false) != 0) {
return 0;
}
return outVa;
}
int Unmap(int windowId, int callerPid, uint64_t callerPml4) {
int Unmap(int windowId, int callerPid, uint64_t callerRoot) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) {
return UnmapRetiredSnapshotsLocked(windowId, callerPid, callerPml4);
return UnmapRetiredSnapshotsLocked(windowId, callerPid, callerRoot);
}
int result = UnmapRetiredSnapshotsLocked(windowId, callerPid, callerPml4);
int result = UnmapRetiredSnapshotsLocked(windowId, callerPid, callerRoot);
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.snapshotSurface == nullptr) return result;
int current = Ipc::UnmapSurfaceForPid(slot.snapshotSurface, callerPid, callerPml4);
if (current == 0) return 0;
int current = Ipc::UnmapSurfaceForPid(slot.snapshotSurface, callerPid, callerRoot);
if (current == 0) return Success;
return result;
}
@@ -268,17 +321,18 @@ namespace WinServer {
return SendEventLocked(windowId, event);
}
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
int Resize(int windowId, int callerPid, uint64_t ownerRoot, int newW, int newH,
uint64_t& heapNext, uint64_t& outVa) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return -1;
if (windowId < 0 || windowId >= MaxWindows) return InvalidArgument;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
if (newW <= 0 || newH <= 0 || newW > 16384 || newH > 16384) return -1;
if (!slot.used) return InvalidArgument;
if (slot.ownerPid != callerPid) return PermissionDenied;
if (newW <= 0 || newH <= 0 || newW > 16384 || newH > 16384) return InvalidArgument;
if (newW == slot.width && newH == slot.height) {
outVa = slot.ownerVa;
return 0;
return Success;
}
uint64_t bufSize = (uint64_t)newW * (uint64_t)newH * 4ULL;
@@ -287,17 +341,17 @@ namespace WinServer {
if (newLive == nullptr || newSnapshot == nullptr) {
if (newLive != nullptr) Ipc::ReleaseSurface(newLive);
if (newSnapshot != nullptr) Ipc::ReleaseSurface(newSnapshot);
return -1;
return OutOfMemory;
}
Ipc::RetainSurface(newLive);
Ipc::RetainSurface(newSnapshot);
uint64_t newOwnerVa = 0;
if (Ipc::MapSurfaceForPid(newLive, callerPid, ownerPml4, heapNext, newOwnerVa, false) != 0) {
if (Ipc::MapSurfaceForPid(newLive, callerPid, ownerRoot, heapNext, newOwnerVa, false) != 0) {
Ipc::ReleaseSurface(newSnapshot);
Ipc::ReleaseSurface(newLive);
return -1;
return GenericFailure;
}
if (slot.liveSurface != nullptr) {
@@ -312,7 +366,7 @@ namespace WinServer {
}
if (slot.liveSurface != nullptr) {
Ipc::UnmapSurfaceForPid(slot.liveSurface, callerPid, ownerPml4);
Ipc::UnmapSurfaceForPid(slot.liveSurface, callerPid, ownerRoot);
Ipc::ReleaseSurface(slot.liveSurface);
}
if (slot.snapshotSurface != nullptr) {
@@ -327,27 +381,29 @@ namespace WinServer {
slot.dirty = true;
outVa = newOwnerVa;
return 0;
return Success;
}
int SetCursor(int windowId, int callerPid, int cursor) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return -1;
if (windowId < 0 || windowId >= MaxWindows) return InvalidArgument;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
if (!slot.used) return InvalidArgument;
if (slot.ownerPid != callerPid) return PermissionDenied;
slot.cursor = (uint8_t)cursor;
return 0;
return Success;
}
int SetFlags(int windowId, int callerPid, uint32_t flags) {
WsGuard guard;
if (windowId < 0 || windowId >= MaxWindows) return -1;
if (windowId < 0 || windowId >= MaxWindows) return InvalidArgument;
WindowSlot& slot = g_slots[windowId];
if (!slot.used || slot.ownerPid != callerPid) return -1;
if (!slot.used) return InvalidArgument;
if (slot.ownerPid != callerPid) return PermissionDenied;
slot.flags = flags & montauk::abi::WIN_FLAG_FULLSCREEN;
return 0;
return Success;
}
int SetScale(int scale) {
@@ -364,19 +420,160 @@ namespace WinServer {
SendEventLocked(i, &ev);
}
}
return 0;
return Success;
}
int GetScale() {
return g_uiScale;
}
static void OrphanRequestsLocked(int windowId) {
for (int i = 0; i < g_requestCount; i++) {
montauk::abi::WinRequest& r = g_requests[(g_requestHead + i) % MaxRequests];
if (r.windowId == windowId) r.windowId = -1;
}
}
static void EndDragLocked() {
DataTransfer::Free(g_drag.transfer);
g_drag.active = false;
g_drag.readerCount = 0;
}
void CleanupProcess(int pid) {
WsGuard guard;
for (int i = 0; i < MaxWindows; i++) {
if (!g_slots[i].used || g_slots[i].ownerPid != pid) continue;
ReleaseSlotResourcesLocked(g_slots[i], false);
}
// Requests the process already posted are still delivered: they are
// messages in flight, and a process may well exit right after its
// last one ("open this file" from a dialog-only Files).
if (g_drag.active && g_drag.sourcePid == pid) EndDragLocked();
}
static int EnqueueRequestLocked(montauk::abi::WinRequest& request) {
if (g_requestCount >= MaxRequests) return WouldBlock;
request.serial = ++g_requestSerial;
if (request.serial == 0) request.serial = ++g_requestSerial;
g_requests[(g_requestHead + g_requestCount) % MaxRequests] = request;
g_requestCount++;
return Success;
}
static bool OwnsWindowLocked(int windowId, int pid) {
return windowId >= 0 && windowId < MaxWindows && g_slots[windowId].used &&
g_slots[windowId].ownerPid == pid;
}
int PostRequest(int windowId, int callerPid, const montauk::abi::WinRequest& request) {
using namespace montauk::abi;
if (request.type == 0 || request.type >= WIN_REQ_KERNEL_BASE) return InvalidArgument;
if (request.size > WIN_REQUEST_PAYLOAD_MAX) return InvalidArgument;
WsGuard guard;
if (!OwnsWindowLocked(windowId, callerPid)) return PermissionDenied;
int mine = 0;
for (int i = 0; i < g_requestCount; i++) {
if (g_requests[(g_requestHead + i) % MaxRequests].senderPid == callerPid) mine++;
}
if (mine >= MaxRequestsPerSender) return WouldBlock;
WinRequest copy = request;
copy.windowId = windowId;
copy.senderPid = callerPid;
// Bytes past `size` are not the sender's business to pass along.
for (uint32_t i = copy.size; i < WIN_REQUEST_PAYLOAD_MAX; i++) copy.payload[i] = 0;
return EnqueueRequestLocked(copy);
}
int PollRequest(montauk::abi::WinRequest* out) {
WsGuard guard;
while (g_requestCount > 0) {
montauk::abi::WinRequest& r = g_requests[g_requestHead];
g_requestHead = (g_requestHead + 1) % MaxRequests;
g_requestCount--;
// A destroyed window's requests arrive with windowId -1 (see
// OrphanRequestsLocked); anything else must still match.
if (r.windowId >= 0 && !OwnsWindowLocked(r.windowId, r.senderPid)) continue;
*out = r;
return 1;
}
return 0;
}
int DragBegin(int windowId, int callerPid, bool compositor,
const montauk::abi::WinDragBegin& info, DataTransfer::Transfer& transfer) {
using namespace montauk::abi;
WsGuard guard;
// The compositor drags from its own surface (desktop icons) with
// windowId -1; it already knows about such a drag, so no request.
bool fromSurface = compositor && windowId == -1;
if (!fromSurface && !OwnsWindowLocked(windowId, callerPid)) {
DataTransfer::Free(transfer);
return PermissionDenied;
}
uint32_t serial = ++g_dragSerial;
if (serial == 0) serial = ++g_dragSerial;
if (!fromSurface) {
WinRequest request = {};
request.type = WIN_REQ_DRAG_BEGIN;
request.size = sizeof(WinReqDragBegin);
request.windowId = windowId;
request.senderPid = callerPid;
WinReqDragBegin* payload = (WinReqDragBegin*)request.payload;
payload->serial = serial;
payload->info = info;
payload->info.label[sizeof(payload->info.label) - 1] = '\0';
int rc = EnqueueRequestLocked(request);
if (rc != Success) {
DataTransfer::Free(transfer);
return rc;
}
}
// A new drag replaces the previous one (only one pointer exists).
if (g_drag.active) EndDragLocked();
g_drag.active = true;
g_drag.serial = serial;
g_drag.sourceWindow = windowId;
g_drag.sourcePid = callerPid;
g_drag.transfer = transfer;
g_drag.readerCount = 0;
transfer.count = 0;
return (int)serial;
}
static bool DragReadableLocked(uint32_t serial, int callerPid, bool compositor) {
if (!g_drag.active || g_drag.serial != serial) return false;
if (compositor || callerPid == g_drag.sourcePid) return true;
for (int i = 0; i < g_drag.readerCount; i++) {
if (g_drag.readers[i] == callerPid) return true;
}
return false;
}
int DragFormats(uint32_t serial, int callerPid, bool compositor,
montauk::abi::TransferFormatInfo* out, uint32_t max) {
WsGuard guard;
if (!g_drag.active || g_drag.serial != serial) return NotFound;
if (!DragReadableLocked(serial, callerPid, compositor)) return PermissionDenied;
if (out == nullptr || max == 0) return (int)g_drag.transfer.count;
return (int)DataTransfer::ListFormats(g_drag.transfer, out, max);
}
int DragGetData(uint32_t serial, int callerPid, bool compositor, const char* mime,
void* buffer, uint32_t bufferLen, uint32_t* outLen) {
WsGuard guard;
if (outLen != nullptr) *outLen = 0;
if (!g_drag.active || g_drag.serial != serial) return NotFound;
if (!DragReadableLocked(serial, callerPid, compositor)) return PermissionDenied;
const DataTransfer::Format* format = DataTransfer::Find(g_drag.transfer, mime);
if (format == nullptr) return NotFound;
return DataTransfer::CopyOut(*format, buffer, bufferLen, outLen);
}
}
+27 -4
View File
@@ -8,6 +8,7 @@
#include "Syscall.hpp"
#include <cstdint>
#include <Ipc/Ipc.hpp>
#include "DataTransfer.hpp"
namespace WinServer {
@@ -27,16 +28,16 @@ namespace WinServer {
uint32_t flags;
};
int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h,
int Create(int ownerPid, uint64_t ownerRoot, const char* title, int w, int h,
uint64_t& heapNext, uint64_t& outVa);
int Destroy(int windowId, int callerPid);
int Present(int windowId, int callerPid);
int Poll(int windowId, int callerPid, montauk::abi::WinEvent* outEvent);
int Enumerate(montauk::abi::WinInfo* outArray, int maxCount);
uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext);
int Unmap(int windowId, int callerPid, uint64_t callerPml4);
uint64_t Map(int windowId, int callerPid, uint64_t callerRoot, uint64_t& heapNext);
int Unmap(int windowId, int callerPid, uint64_t callerRoot);
int SendEvent(int windowId, const montauk::abi::WinEvent* event);
int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH,
int Resize(int windowId, int callerPid, uint64_t ownerRoot, int newW, int newH,
uint64_t& heapNext, uint64_t& outVa);
void CleanupProcess(int pid);
int SetCursor(int windowId, int callerPid, int cursor);
@@ -44,4 +45,26 @@ namespace WinServer {
int SetScale(int scale);
int GetScale();
// ---- Messaging (see "Window server messaging" in Api/Syscall.hpp) ----
// Queue an app request about one of the caller's own windows.
int PostRequest(int windowId, int callerPid, const montauk::abi::WinRequest& request);
// Pop the oldest request (compositor only; the caller checks the cap).
// Returns 1 with *out filled, or 0 when the queue is empty.
int PollRequest(montauk::abi::WinRequest* out);
// Start a drag from the caller's window, or (compositor only, windowId
// -1) from the desktop surface itself. Takes ownership of `transfer` (it
// is emptied either way) and returns the new drag serial (> 0) or a
// negative StatusCodes value.
int DragBegin(int windowId, int callerPid, bool compositor,
const montauk::abi::WinDragBegin& info, DataTransfer::Transfer& transfer);
// Read the drag's formats or data. Allowed for the source process, the
// compositor, and processes the compositor has sent a drag event for this
// serial to.
int DragFormats(uint32_t serial, int callerPid, bool compositor,
montauk::abi::TransferFormatInfo* out, uint32_t max);
int DragGetData(uint32_t serial, int callerPid, bool compositor, const char* mime,
void* buffer, uint32_t bufferLen, uint32_t* outLen);
}
+56 -11
View File
@@ -2,7 +2,9 @@
* Window.hpp
* SYS_WINCREATE, SYS_WINDESTROY, SYS_WINPRESENT, SYS_WINPOLL,
* SYS_WINENUM, SYS_WINMAP, SYS_WINSENDEVENT, SYS_WINRESIZE,
* SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE syscalls
* SYS_WINSETCURSOR, SYS_WINSETFLAGS, SYS_WINSETSCALE, SYS_WINGETSCALE syscalls,
* and window server messaging: SYS_WIN_REQUEST, SYS_WIN_REQUEST_POLL,
* SYS_WIN_DRAG_BEGIN, SYS_WIN_DRAG_FORMATS, SYS_WIN_DRAG_GET_DATA
* Copyright (c) 2026 Daniel Hammer
*/
@@ -15,16 +17,16 @@
namespace montauk::abi {
static int Sys_WinCreate(const char* title, int w, int h, WinCreateResult* result) {
if (result == nullptr || title == nullptr) return -1;
if (result == nullptr || title == nullptr) return InvalidArgument;
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return -1;
if (proc == nullptr) return GenericFailure;
uint64_t outVa = 0;
int id = WinServer::Create(proc->pid, proc->pml4Phys, title, w, h,
int id = WinServer::Create(proc->pid, proc->pageTableRoot, title, w, h,
proc->heapNext, outVa);
result->id = id;
result->pixelVa = (id >= 0) ? outVa : 0;
return id >= 0 ? 0 : -1;
return id >= 0 ? Success : id;
}
static int Sys_WinDestroy(int windowId) {
@@ -36,7 +38,7 @@ namespace montauk::abi {
}
static int Sys_WinPoll(int windowId, WinEvent* outEvent) {
if (outEvent == nullptr) return -1;
if (outEvent == nullptr) return InvalidArgument;
return WinServer::Poll(windowId, Sched::GetCurrentPid(), outEvent);
}
@@ -45,20 +47,26 @@ namespace montauk::abi {
return WinServer::Enumerate(outArray, maxCount);
}
// Mapping another process's window and injecting input into it are the
// compositor's job alone. Left open, any app could read every window's
// pixels or click on any window -- including a prompt asking the user
// whether to grant that same app authority.
static uint64_t Sys_WinMap(int windowId) {
if (!Sched::HasCapability(CAP_COMPOSITOR)) return 0;
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return 0;
return WinServer::Map(windowId, proc->pid, proc->pml4Phys, proc->heapNext);
return WinServer::Map(windowId, proc->pid, proc->pageTableRoot, proc->heapNext);
}
static int Sys_WinUnmap(int windowId) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return -1;
return WinServer::Unmap(windowId, proc->pid, proc->pml4Phys);
if (proc == nullptr) return GenericFailure;
return WinServer::Unmap(windowId, proc->pid, proc->pageTableRoot);
}
static int Sys_WinSendEvent(int windowId, const WinEvent* event) {
if (event == nullptr) return -1;
if (!Sched::HasCapability(CAP_COMPOSITOR)) return CapabilityDenied;
if (event == nullptr) return InvalidArgument;
return WinServer::SendEvent(windowId, event);
}
@@ -66,7 +74,7 @@ namespace montauk::abi {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return 0;
uint64_t outVa = 0;
int r = WinServer::Resize(windowId, proc->pid, proc->pml4Phys, newW, newH,
int r = WinServer::Resize(windowId, proc->pid, proc->pageTableRoot, newW, newH,
proc->heapNext, outVa);
return (r == 0) ? outVa : 0;
}
@@ -86,4 +94,41 @@ namespace montauk::abi {
static int Sys_WinGetScale() {
return WinServer::GetScale();
}
// Any app may post requests, but only about its own windows (checked in
// WinServer); only the compositor may read them back.
static int Sys_WinRequest(int windowId, const WinRequest* request) {
if (request == nullptr) return InvalidArgument;
WinRequest copy = *request;
return WinServer::PostRequest(windowId, Sched::GetCurrentPid(), copy);
}
static int Sys_WinRequestPoll(WinRequest* out) {
if (!Sched::HasCapability(CAP_COMPOSITOR)) return CapabilityDenied;
if (out == nullptr) return InvalidArgument;
return WinServer::PollRequest(out);
}
static int Sys_WinDragBegin(int windowId, const WinDragBegin* info,
const TransferItem* items, uint32_t count) {
if (info == nullptr) return InvalidArgument;
WinDragBegin infoCopy = *info;
DataTransfer::Transfer transfer;
int rc = DataTransfer::BuildFromUser(items, count, transfer);
if (rc != Success) return rc;
return WinServer::DragBegin(windowId, Sched::GetCurrentPid(),
Sched::HasCapability(CAP_COMPOSITOR), infoCopy, transfer);
}
static int Sys_WinDragFormats(uint32_t serial, TransferFormatInfo* out, uint32_t max) {
return WinServer::DragFormats(serial, Sched::GetCurrentPid(),
Sched::HasCapability(CAP_COMPOSITOR), out, max);
}
static int Sys_WinDragGetData(uint32_t serial, const char* mime, void* buffer,
uint32_t bufferLen, uint32_t* outLen) {
return WinServer::DragGetData(serial, Sched::GetCurrentPid(),
Sched::HasCapability(CAP_COMPOSITOR), mime,
buffer, bufferLen, outLen);
}
};
+64
View File
@@ -0,0 +1,64 @@
/*
* Abi.hpp
* Architecture-specific parts of the user-mode ABI
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
// What the ELF loader and the process/thread setup code need to know about
// the user-mode binary interface of the target. Defined inline by
// Arch/<arch>/Abi.hpp.
namespace Arch {
// ELF e_machine value accepted for user programs and libraries, and a
// name for diagnostics.
// constexpr uint16_t ElfMachine;
// constexpr const char* ElfMachineName;
// Calling-convention attribute for UEFI runtime services:
// #define ARCH_EFIAPI
// How the loader must treat a dynamic relocation type.
enum class RelocationKind {
None, // no-op (type 0)
Relative, // *target = loadBias + addend
Symbol, // *target = symbol address + addend (64-bit absolute)
Unsupported,
};
inline RelocationKind ClassifyRelocation(uint32_t type);
// Geometry of one thread's TLS block, as offsets from its page-aligned
// base. The variant (I: TLS above the thread pointer, II: below it) is
// fixed by the architecture's ELF TLS ABI.
struct TlsLayout {
uint64_t templateOffset; // where .tdata/.tbss start
uint64_t tpOffset; // thread pointer; [TP] holds the self-pointer
uint64_t totalSize; // bytes to map, incl. the TCB
};
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign);
// Emit the user-mode code that calls SYS_EXIT(0). A process's entry
// function returns into it.
inline void WriteExitStub(uint8_t* page);
// Prepare the main thread's initial user stack so that returning from
// the entry function lands at `returnAddress`. `topPage` is the kernel
// mapping of the page just below `stackTop`. Returns the initial user SP.
inline uint64_t SetupUserEntryStack(uint8_t* topPage, uint64_t stackTop,
uint64_t returnAddress);
// Initial user SP for a sibling thread whose stack ends at `stackTop`
// (the thread trampoline never returns, so no return address is set).
inline uint64_t ThreadEntryStackPointer(uint64_t stackTop);
}
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/Abi.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/Abi.hpp>
#endif
+25
View File
@@ -0,0 +1,25 @@
/*
* Arch.hpp
* Target architecture selection
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
// Everything outside src/Arch/ is architecture-neutral and talks to the CPU
// only through the interfaces in src/Arch/*.hpp. Each supported architecture
// implements them under src/Arch/<arch>/; the build compiles only the
// directory matching ARCH. See docs/porting.md.
//
// Interface headers that need inline (per-arch) definitions include the
// matching implementation header at their end, selected below.
#if defined(__x86_64__)
#define MONTAUK_ARCH_X86_64 1
#define MONTAUK_ARCH_NAME "x86_64"
#elif defined(__aarch64__)
#define MONTAUK_ARCH_AARCH64 1
#define MONTAUK_ARCH_NAME "aarch64"
#else
#error "MontaukOS: unsupported target architecture (add one under src/Arch/)"
#endif
+49
View File
@@ -0,0 +1,49 @@
/*
* Context.hpp
* Architecture-neutral thread context interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
#include <Arch/Smp.hpp>
// The scheduler's view of a thread's CPU context: a kernel stack pointer
// saved by SwitchContext, an FPU/SIMD save area, and a page-table root.
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/Context.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/Context.hpp>
#endif
// The architecture provides (in Arch/<arch>/Context.hpp):
// constexpr uint64_t Arch::FpuStateSize, Arch::FpuStateAlign
namespace Arch {
// Reset an FPU/SIMD save area to the state a new thread starts with.
void InitFpuState(uint8_t* area);
// Build the initial kernel stack of a new thread so that the first
// SwitchContext to it "returns" into `entry`. Returns the saved SP.
uint64_t PrepareThreadStack(uint64_t kernelStackTop, void (*entry)());
// Save the running context (stack pointer to *oldSp, FPU state to
// oldFpu when non-null), switch to page-table root `newRoot` if it is
// not already active, and resume the context saved at newSp (restoring
// newFpu when non-null). Returns when the old context is resumed.
void SwitchContext(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
uint8_t* oldFpu, uint8_t* newFpu);
// Point this CPU at the thread about to run: the kernel stack used when
// it enters the kernel from user mode, and its TLS thread pointer.
void ActivateThread(Smp::CpuData* cpu, uint64_t kernelStackTop,
uint64_t threadPointer);
// Drop to user mode at `entry` with stack `userSp`; `arg` is the first
// argument register. Interrupts are enabled in user mode.
[[noreturn]] void EnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg);
}
+121
View File
@@ -0,0 +1,121 @@
/*
* Cpu.hpp
* Architecture-neutral CPU primitives
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
// Every function here is defined inline by the architecture
// (Arch/<arch>/Cpu.hpp), so the primitives cost what the raw instruction
// costs. Generic kernel code must use these instead of inline assembly.
namespace Arch {
// Saved interrupt-enable state, as returned by SaveAndDisableInterrupts.
// Opaque to generic code: only hand it back to RestoreInterrupts.
using IrqFlags = uint64_t;
// ---- Spinning and barriers ----
// Spin-loop hint (x86 PAUSE, ARM YIELD).
inline void Pause();
// Stop the compiler from caching memory across this point. Emits no
// instruction; use it in polling loops on device-written memory.
inline void CompilerBarrier() {
asm volatile("" ::: "memory");
}
// Order all earlier stores before all later stores, including stores to
// write-combining memory (framebuffers) that must become visible.
inline void StoreFence();
// Full read/write memory barrier.
inline void MemoryFence();
// Order this CPU's earlier writes to DMA memory (descriptors, buffers)
// before a later write the device acts on: the descriptor's valid/cycle
// bit, or a doorbell register. x86 keeps stores in order already.
inline void DmaWriteFence();
// Order the read of a device-written valid/cycle/status field before the
// reads of the data it guards.
inline void DmaReadFence();
// ---- Interrupt state ----
inline void EnableInterrupts();
inline void DisableInterrupts();
inline bool InterruptsEnabled();
// Disable interrupts and return the previous state.
inline IrqFlags SaveAndDisableInterrupts();
// Restore an interrupt state returned by SaveAndDisableInterrupts.
inline void RestoreInterrupts(IrqFlags flags);
// ---- Halting ----
// Wait for the next interrupt. Interrupt state is left as it was; with
// interrupts disabled this only returns on an NMI-class event.
inline void Halt();
// Called with interrupts disabled: atomically enable them and wait for
// an interrupt, then disable them again before returning. The atomicity
// closes the race where a wakeup IRQ lands between the readiness check
// and the wait, leaving the CPU asleep until the next timer deadline.
inline void HaltWithInterruptsDisabled();
// Disable interrupts and stop this CPU for good.
[[noreturn]] inline void HaltForever();
// ---- Time and delays ----
// A free-running, monotonically increasing cycle counter (x86 TSC, ARM
// CNTVCT). Its rate is architecture-defined; use it for entropy and
// relative timing, not wall-clock time.
inline uint64_t ReadTimestamp();
// Delay for at least about one microsecond. Works with interrupts
// disabled and before any clock is calibrated; errs long.
inline void IoDelay();
// ---- Caches and translation ----
// Write back and invalidate all CPU caches (before power state changes).
inline void WritebackInvalidateCaches();
// Cache maintenance for devices whose DMA does not snoop the CPU caches
// (ACPI _CCA = 0). Clean: write dirty lines back so the device reads
// what the CPU wrote. Invalidate: write back and discard, so the CPU
// then reads what the device wrote. No-ops where DMA is coherent (x86).
inline void CleanDataCache(const void* address, uint64_t size);
inline void InvalidateDataCache(const void* address, uint64_t size);
// Make instructions just written to [address, address+size) (any
// mapping of them) visible to instruction fetch on every CPU. A no-op
// where instruction fetch snoops data writes (x86).
inline void SyncInstructionCache(const void* address, uint64_t size);
// Invalidate this CPU's translation for one virtual page.
inline void FlushTlbPage(uint64_t virtualAddress);
// Invalidate this CPU's non-global translations.
inline void FlushTlbAll();
// Physical address of the active page-table root (x86 CR3, ARM TTBR0).
inline uint64_t ReadPageTableRoot();
// Switch to the page-table root at the given physical address.
inline void WritePageTableRoot(uint64_t rootPhys);
}
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/Cpu.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/Cpu.hpp>
#endif
@@ -1,14 +1,18 @@
/*
* CpuPower.hpp
* Intel CPU power management: HWP frequency scaling, C1E promotion,
* and the package thermal governor.
* Architecture-neutral CPU power/thermal management interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal {
// Frequency scaling, idle-state policy and the thermal governor are CPU
// specific (x86_64: Intel HWP, C1E promotion and a package-temperature
// governor driven through MSRs). An architecture without them implements
// these as no-ops and returns false from GetSnapshot.
namespace Arch {
namespace CpuPower {
// Snapshot of the power/thermal state for SYS_POWERINFO and logging.
@@ -24,6 +28,7 @@ namespace Hal {
uint32_t BaseMHz; // CPUID base frequency (0 = unknown)
uint32_t MaxMHz; // CPUID max turbo frequency
uint32_t EffMHz; // measured average active frequency (BSP)
uint8_t ApIdleHint; // idle state secondary CPUs enter (arch-defined)
};
// BSP: detect features, enable HWP with a balanced energy preference,
+81
View File
@@ -0,0 +1,81 @@
/*
* Interrupts.hpp
* Architecture-neutral hardware interrupt interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// Devices and kernel services see interrupts as numbered IRQ slots. The
// architecture maps each slot onto its interrupt controller (x86: an IDT
// vector fed by the I/O APIC, MSIs and local APIC IPIs; ARM: a GIC INTID)
// and calls the registered handler.
namespace Arch {
// IRQ handler function type. The parameter is the IRQ slot number.
// `fromUser` is derived directly from the interrupted context. Do not
// cache it in per-CPU data: an IRQ handler may context-switch and later
// resume on another CPU, making per-CPU saved-origin state stale.
using IrqHandler = void(*)(uint8_t irq, bool fromUser);
// Number of IRQ slots: 0-23 platform interrupt lines, 24-45 message-
// signalled interrupts (one slot per MSI-capable driver), 46-47 IPIs.
constexpr int IRQ_COUNT = 48;
// The architecture's periodic scheduler/timekeeping timer.
constexpr uint8_t IRQ_TIMER = 0;
// Platform interrupt lines with PC/ISA meaning. Only drivers for PC
// devices (PS/2, legacy PCI INTx through the I/O APIC) use these.
constexpr uint8_t IRQ_KEYBOARD = 1;
constexpr uint8_t IRQ_CASCADE = 2;
constexpr uint8_t IRQ_COM2 = 3;
constexpr uint8_t IRQ_COM1 = 4;
constexpr uint8_t IRQ_FLOPPY = 6;
constexpr uint8_t IRQ_RTC = 8;
constexpr uint8_t IRQ_MOUSE = 12;
constexpr uint8_t IRQ_ATA1 = 14;
constexpr uint8_t IRQ_ATA2 = 15;
// Inter-processor interrupts.
constexpr uint8_t IRQ_TLB_SHOOTDOWN = 46;
constexpr uint8_t IRQ_RESCHEDULE = 47;
// Register the handler for an IRQ slot.
void RegisterIrqHandler(uint8_t irq, IrqHandler handler);
// Route platform interrupt line `irq` to IRQ slot `irq` on the boot CPU,
// applying the firmware's polarity/trigger overrides, and unmask it.
void RouteLegacyIrq(uint8_t irq);
// Unmask platform interrupt line `irq` (already routed at boot).
void EnableLegacyIrq(uint8_t irq);
// Whether the platform interrupt lines are wired by the hardware (x86:
// ISA IRQs and I/O APIC pins, as the firmware programs them into PCI
// Interrupt Line registers). Where they are not (ARM), the PCI layer
// assigns lines to PCI INTx pins from ACPI _PRT with AssignPlatformLine.
bool PlatformLinesAreFixed();
// Make platform line `irq` (1-23) deliver global system interrupt `gsi`
// (ACPI numbering) with the given trigger mode and polarity, so that
// EnableLegacyIrq(irq) unmasks it. Only where !PlatformLinesAreFixed().
bool AssignPlatformLine(uint8_t irq, uint32_t gsi, bool levelTriggered, bool activeLow);
// A message-signalled interrupt targeting IRQ slot `irq` on the boot
// CPU: the device writes `data` to `address` to raise it. Program these
// into a PCI MSI capability or MSI-X table entry. Returns false when the
// platform cannot receive MSIs (drivers then fall back to INTx/polling).
struct MsiMessage {
uint64_t address;
uint32_t data;
};
bool ComposeMsi(uint8_t irq, MsiMessage& out);
// Raise IPI slot `irq` (IRQ_RESCHEDULE / IRQ_TLB_SHOOTDOWN) on the CPU
// with the given Smp index. Returns false if delivery did not complete.
bool SendIpi(int cpuIndex, uint8_t irq);
}
+58
View File
@@ -0,0 +1,58 @@
/*
* Platform.hpp
* Architecture hooks for kernel bring-up and power-off paths
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <ACPI/ACPI.hpp>
// kmain (Boot/Main.cpp) owns the boot sequence and calls into the
// architecture at these points, in this order.
namespace Arch {
// First thing after the boot console exists: CPU descriptor tables,
// FPU/SIMD enablement. No memory allocation is available yet.
void EarlyInitialize();
// The frame allocator and heap exist: install exception/trap vectors.
void InitializeTraps();
// The kernel page tables are live: program memory-type attributes that
// MapWC/MapMMIO rely on (x86 PAT).
void InitializeMemoryAttributes();
// ACPI tables are available: bring up interrupt controllers, the boot
// CPU's per-CPU data (Smp::InitBsp), idle-state and power management.
// Interrupts stay disabled; kmain enables them afterwards.
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader* xsdt);
// Platform devices that are not discovered on PCI (x86: the PS/2
// controller, keyboard and mouse). Called once the timer is running.
void InitializePlatformDevices();
// Right before the first user process is created: anything user-mode
// entry needs (x86: load the task register for ring-3 interrupt stacks).
void PrepareUserMode();
// Secondary CPUs are up and the first process is about to be spawned.
void LateInitialize();
// Report devices that belong to the platform rather than a bus (for
// SYS_DEVLIST): interrupt controllers (category 1), timers (2) and
// platform input devices (3).
using PlatformDeviceSink = void (*)(void* context, uint8_t category,
const char* name, const char* detail);
void ListPlatformDevices(PlatformDeviceSink sink, void* context);
// Reset the machine without firmware help (last resort after EFI
// ResetSystem). Never returns.
[[noreturn]] void Reboot();
// Switch the machine off where ACPI S5 does not apply (hardware-reduced
// ACPI: ARM's PSCI SYSTEM_OFF). Returns if the platform cannot.
void PowerOff();
}
+94
View File
@@ -0,0 +1,94 @@
/*
* Smp.hpp
* Architecture-neutral per-CPU data and SMP bring-up interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
#include <Boot/BootInfo.hpp>
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/PerCpu.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/PerCpu.hpp>
#endif
// The architecture provides (in Arch/<arch>/PerCpu.hpp):
// struct Arch::CpuEntryState -- fields kernel-entry assembly reads at a
// fixed offset (8) from the per-CPU base
// struct Arch::CpuState -- everything else the port keeps per CPU
// void* Arch::CurrentCpuLocal() -- this CPU's CpuData (fast path)
// void* Arch::TryCurrentCpuLocal() -- same, or nullptr before InitBsp
namespace Smp {
static constexpr int MaxCPUs = 64;
struct CpuData {
// === Fixed layout: read by low-level entry code ===
uint64_t selfPtr; // offset 0: pointer to self
Arch::CpuEntryState entry; // offset 8: arch kernel-entry fields
// === Generic fields ===
int32_t currentSlot; // process table slot, -1 = idle
int cpuIndex; // 0-based CPU index (0 = boot CPU)
uint64_t idleSavedSp; // idle context's stack pointer while a process runs
volatile bool started; // set by an AP once its init is complete
// When set, the scheduler tick must NOT pull this (idle) CPU away to
// run a ready process: its idle context is busy with long-running
// kernel work whose wall-clock-bounded waits would spuriously expire
// if the context only ran when nothing else is ready (observed: the
// deferred Bluetooth firmware download racing login-screen startup).
// Other CPUs schedule normally while this is held.
volatile bool reservedForKernelWork = false;
// Set by the BSP timer when an IRQ bottom half needs process-safe
// execution even though runnable user processes keep this CPU busy.
// Schedule() consumes it by yielding once to the idle kernel context.
bool kernelWorkPending = false;
// === Architecture-private state ===
Arch::CpuState arch;
};
static_assert(__builtin_offsetof(CpuData, selfPtr) == 0,
"CpuData: per-CPU base must point at selfPtr");
static_assert(__builtin_offsetof(CpuData, entry) == 8,
"CpuData: entry-assembly fields must start at offset 8");
// The current CPU's data.
inline CpuData* GetCurrentCpuData() {
return (CpuData*)Arch::CurrentCpuLocal();
}
// Exception paths may run before InitBsp() has installed the per-CPU
// base. This variant returns nullptr in that state instead of faulting
// recursively on a bogus base.
inline CpuData* TryGetCurrentCpuData() {
return (CpuData*)Arch::TryCurrentCpuLocal();
}
// Get CPU data by index
CpuData* GetCpuData(int index);
// Number of online CPUs
int GetCpuCount();
// Number of CPUs the firmware describes (available once interrupts are
// initialized; may exceed GetCpuCount if some failed to start).
// Implemented by the architecture.
int GetDetectedCpuCount();
// Initialize the boot CPU's per-CPU data (call before interrupts are
// enabled). Implemented by the architecture.
void InitBsp();
// Boot all secondary CPUs (call after all subsystems are ready).
// `smp` is the boot contract's SMP block; if FeatureSmp was not
// advertised the caller may still pass it (cpuCount<=1 is a no-op).
// Implemented by the architecture.
void BootAPs(const montauk::boot::SmpInfo& smp);
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Syscall.hpp
* Architecture-neutral system-call entry interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Arch.hpp>
#if MONTAUK_ARCH_X86_64
#include <Arch/x86_64/SyscallFrame.hpp>
#elif MONTAUK_ARCH_AARCH64
#include <Arch/aarch64/SyscallFrame.hpp>
#endif
// The architecture's system-call entry code saves the user context into an
// Arch::SyscallFrame on the thread's kernel stack (Arch/<arch>/
// SyscallFrame.hpp defines it, exposing at least `syscall_nr` and `arg1` ..
// `arg6`), enables interrupts, and calls
//
// extern "C" int64_t SyscallDispatch(Arch::SyscallFrame* frame);
//
// whose result it returns to user mode. Before returning it calls
//
// extern "C" void SchedExitIfKilled();
//
// so a kill that arrived during the syscall takes effect at a point where
// the syscall holds no locks.
namespace Arch {
// Install the system-call entry point on the calling CPU. Called on the
// boot CPU by montauk::abi::InitializeSyscalls and by every secondary
// CPU during its bring-up.
void InitializeSyscallEntry();
}
+32
View File
@@ -0,0 +1,32 @@
/*
* Timer.hpp
* Architecture-neutral per-CPU tick timer interface
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// The hardware behind Timekeeping (Timekeeping/Timer.hpp): a per-CPU timer
// that raises Arch::IRQ_TIMER periodically. Timekeeping registers the IRQ
// handler and owns all tick accounting; the architecture only calibrates
// and programs the hardware (x86: local APIC timer calibrated against the
// PIT; ARM: the generic timer).
namespace Arch::Timer {
// Calibrate the timer and start the calling (boot) CPU's periodic tick
// with the given period. Returns false if no usable timer was found.
bool InitializeBsp(uint32_t periodMs);
// (Re)start the calling CPU's periodic tick, reusing the boot CPU's
// calibration: secondary CPU bring-up, leaving idle, and the boot CPU
// after S3 resume (timer state does not survive suspend). Only called
// after InitializeBsp succeeded.
void StartPeriodic(uint32_t periodMs);
// Mask the calling CPU's tick (idle secondary CPUs rely on IPIs).
// Cheap when it is already masked.
void Stop();
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Abi.hpp
* aarch64 user-mode ABI details (implements Arch/Abi.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// UEFI on AArch64 uses the standard AAPCS64 calling convention.
#define ARCH_EFIAPI
namespace Arch {
constexpr uint16_t ElfMachine = 183; // EM_AARCH64
constexpr const char* ElfMachineName = "aarch64";
inline RelocationKind ClassifyRelocation(uint32_t type) {
switch (type) {
case 0: return RelocationKind::None;
case 1027: return RelocationKind::Relative; // R_AARCH64_RELATIVE
case 257: // R_AARCH64_ABS64
case 1025: // R_AARCH64_GLOB_DAT
case 1026: return RelocationKind::Symbol; // R_AARCH64_JUMP_SLOT
default: return RelocationKind::Unsupported;
}
}
// Variant I: TP points at a 16-byte TCB and the TLS block follows it,
// starting at the next p_align boundary.
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign) {
uint64_t align = pAlign ? pAlign : 1;
uint64_t templateOffset = (16 + align - 1) & ~(align - 1);
return {templateOffset, 0, templateOffset + memSize};
}
// System calls: number in x8, arguments in x0-x5, `svc #0`.
inline void WriteExitStub(uint8_t* page) {
uint32_t* code = (uint32_t*)page;
code[0] = 0xD2800000; // mov x0, #0 (exit code 0)
code[1] = 0xD2800008; // mov x8, #0 (SYS_EXIT = 0)
code[2] = 0xD4000001; // svc #0
}
// The return address travels in x30, not on the stack: EnterUserMode
// points x30 at the exit stub. The stack only needs 16-byte alignment.
inline uint64_t SetupUserEntryStack(uint8_t*, uint64_t stackTop, uint64_t) {
return stackTop & ~0xFULL;
}
inline uint64_t ThreadEntryStackPointer(uint64_t stackTop) {
return stackTop & ~0xFULL;
}
}
+26
View File
@@ -0,0 +1,26 @@
/*
* AcpiSleep.cpp
* aarch64: no ACPI S3 (implements ACPI/AcpiSleep.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <ACPI/AcpiSleep.hpp>
#include <Common/StatusCodes.hpp>
// Suspend on ARM goes through PSCI (SYSTEM_SUSPEND), not the FACS waking
// vector; until that exists, S3 is reported as unavailable.
namespace Hal::AcpiSleep {
void Initialize(ACPI::CommonSDTHeader*) {
}
bool IsS3Available() {
return false;
}
int Suspend() {
return -1;
}
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Bringup.hpp
* aarch64 per-CPU bring-up shared by the boot CPU and secondaries
* (private to the port)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
namespace Arch {
// System registers every CPU programs for itself: SP_EL1 as the stack,
// the exception vectors, SCTLR_EL1 policy bits, FP/SIMD access.
// Platform.cpp.
void InitializeCpuRegisters();
// Load the kernel's translation regime (MAIR, TCR, TTBR1 kernel half,
// TTBR0 kernel-context table) on the calling CPU. Paging::Init builds
// the tables and calls it on the boot CPU. Paging.cpp.
void ActivateKernelTranslation();
}
+144
View File
@@ -0,0 +1,144 @@
/*
Context.S
aarch64 context switch and first entry to EL0 (see Arch/Context.hpp)
Copyright (c) 2026 Daniel Hammer
*/
/* The kernel is built -mgeneral-regs-only; this file alone touches
the FP/SIMD registers, to save and restore user state. */
.arch_extension fp
.arch_extension simd
.text
/*
* void ArchContextSwitch(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
* uint8_t* oldFpu, uint8_t* newFpu)
* x0 = where to save the old SP x1 = SP to resume
* x2 = TTBR0 root for the new side x3/x4 = FP/SIMD save areas (may be 0)
*
* Frame on the kernel stack (Context.cpp's PrepareThreadStack builds the
* same shape): x19..x28, x29, x30 -- 96 bytes. The kernel is built with
* -mgeneral-regs-only, so q0-q31/FPCR/FPSR only ever hold user state.
*/
.global ArchContextSwitch
.type ArchContextSwitch, %function
ArchContextSwitch:
cbz x3, 1f
stp q0, q1, [x3, #0]
stp q2, q3, [x3, #32]
stp q4, q5, [x3, #64]
stp q6, q7, [x3, #96]
stp q8, q9, [x3, #128]
stp q10, q11, [x3, #160]
stp q12, q13, [x3, #192]
stp q14, q15, [x3, #224]
stp q16, q17, [x3, #256]
stp q18, q19, [x3, #288]
stp q20, q21, [x3, #320]
stp q22, q23, [x3, #352]
stp q24, q25, [x3, #384]
stp q26, q27, [x3, #416]
stp q28, q29, [x3, #448]
stp q30, q31, [x3, #480]
mrs x9, fpcr
mrs x10, fpsr
add x11, x3, #512
stp x9, x10, [x11]
1:
sub sp, sp, #96
stp x19, x20, [sp, #0]
stp x21, x22, [sp, #16]
stp x23, x24, [sp, #32]
stp x25, x26, [sp, #48]
stp x27, x28, [sp, #64]
stp x29, x30, [sp, #80]
mov x9, sp
str x9, [x0]
mov sp, x1
/* Switch the process half only when it differs (no ASIDs yet, so a
switch invalidates this CPU's TLB). */
mrs x9, ttbr0_el1
and x9, x9, #0x0000fffffffffffe
cmp x9, x2
b.eq 2f
dsb ishst
msr ttbr0_el1, x2
isb
tlbi vmalle1
dsb nsh
isb
2:
ldp x19, x20, [sp, #0]
ldp x21, x22, [sp, #16]
ldp x23, x24, [sp, #32]
ldp x25, x26, [sp, #48]
ldp x27, x28, [sp, #64]
ldp x29, x30, [sp, #80]
add sp, sp, #96
cbz x4, 3f
ldp q0, q1, [x4, #0]
ldp q2, q3, [x4, #32]
ldp q4, q5, [x4, #64]
ldp q6, q7, [x4, #96]
ldp q8, q9, [x4, #128]
ldp q10, q11, [x4, #160]
ldp q12, q13, [x4, #192]
ldp q14, q15, [x4, #224]
ldp q16, q17, [x4, #256]
ldp q18, q19, [x4, #288]
ldp q20, q21, [x4, #320]
ldp q22, q23, [x4, #352]
ldp q24, q25, [x4, #384]
ldp q26, q27, [x4, #416]
ldp q28, q29, [x4, #448]
ldp q30, q31, [x4, #480]
add x11, x4, #512
ldp x9, x10, [x11]
msr fpcr, x9
msr fpsr, x10
3:
ret
.size ArchContextSwitch, . - ArchContextSwitch
/*
* void ArchEnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg,
* uint64_t returnAddress, uint64_t kernelStackTop)
*
* Drops to EL0t with interrupts unmasked. The kernel stack is reset to its
* top: SP_EL1 is what the next exception from EL0 runs on.
*/
.global ArchEnterUserMode
.type ArchEnterUserMode, %function
ArchEnterUserMode:
msr daifset, #0xf
mov sp, x4
msr elr_el1, x0
msr sp_el0, x1
msr spsr_el1, xzr /* EL0t, DAIF clear */
mov x0, x2
mov x30, x3
mov x1, xzr
mov x2, xzr
mov x3, xzr
mov x4, xzr
mov x5, xzr
mov x6, xzr
mov x7, xzr
mov x8, xzr
mov x9, xzr
mov x10, xzr
mov x11, xzr
mov x12, xzr
mov x13, xzr
mov x14, xzr
mov x15, xzr
mov x16, xzr
mov x17, xzr
mov x18, xzr
mov x29, xzr
eret
.size ArchEnterUserMode, . - ArchEnterUserMode
+53
View File
@@ -0,0 +1,53 @@
/*
* Context.cpp
* aarch64 thread context (implements Arch/Context.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Context.hpp>
#include <Libraries/Memory.hpp>
#include <Sched/Scheduler.hpp>
// Context.S
extern "C" void ArchContextSwitch(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
uint8_t* oldFpu, uint8_t* newFpu);
extern "C" [[noreturn]] void ArchEnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg,
uint64_t returnAddress, uint64_t kernelStackTop);
namespace Arch {
void InitFpuState(uint8_t* area) {
// All-zero is the architectural reset state: round-to-nearest, no
// exceptions trapped, no flags raised.
memset(area, 0, FpuStateSize);
}
uint64_t PrepareThreadStack(uint64_t kernelStackTop, void (*entry)()) {
// The frame Context.S pops: x19..x28, x29, x30 (= return address).
uint64_t* sp = (uint64_t*)kernelStackTop - 12;
for (int i = 0; i < 11; i++) {
sp[i] = 0;
}
sp[11] = (uint64_t)entry;
return (uint64_t)sp;
}
void SwitchContext(uint64_t* oldSp, uint64_t newSp, uint64_t newRoot,
uint8_t* oldFpu, uint8_t* newFpu) {
ArchContextSwitch(oldSp, newSp, newRoot, oldFpu, newFpu);
}
void ActivateThread(Smp::CpuData* cpu, uint64_t kernelStackTop,
uint64_t threadPointer) {
cpu->entry.kernelStackTop = kernelStackTop;
asm volatile("msr tpidr_el0, %0" :: "r"(threadPointer));
}
void EnterUserMode(uint64_t entry, uint64_t userSp, uint64_t arg) {
// Returning from the entry function lands in the exit stub (see
// SetupUserEntryStack in Abi.hpp).
ArchEnterUserMode(entry, userSp, arg, Sched::ExitStubAddr,
Smp::GetCurrentCpuData()->entry.kernelStackTop);
}
}
+14
View File
@@ -0,0 +1,14 @@
/*
* Context.hpp
* aarch64 thread context constants (see Arch/Context.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// q0-q31 followed by FPCR and FPSR (Context.S).
constexpr uint64_t FpuStateSize = 32 * 16 + 16;
constexpr uint64_t FpuStateAlign = 16;
}
+170
View File
@@ -0,0 +1,170 @@
/*
* Cpu.hpp
* aarch64 implementation of the Arch/Cpu.hpp primitives
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// PSTATE.I in the DAIF view
static constexpr uint64_t DaifIrqMask = 1ULL << 7;
inline void Pause() {
asm volatile("yield" ::: "memory");
}
// Device-nGnRE / Normal-NC (write-combining) stores complete in order
// once a DSB has been issued.
inline void StoreFence() {
asm volatile("dsb st" ::: "memory");
}
inline void MemoryFence() {
asm volatile("dsb sy" ::: "memory");
}
// Devices are outer-shareable observers.
inline void DmaWriteFence() {
asm volatile("dmb oshst" ::: "memory");
}
inline void DmaReadFence() {
asm volatile("dmb oshld" ::: "memory");
}
inline void EnableInterrupts() {
asm volatile("msr daifclr, #2" ::: "memory");
}
inline void DisableInterrupts() {
asm volatile("msr daifset, #2" ::: "memory");
}
inline bool InterruptsEnabled() {
uint64_t daif;
asm volatile("mrs %0, daif" : "=r"(daif));
return (daif & DaifIrqMask) == 0;
}
inline IrqFlags SaveAndDisableInterrupts() {
uint64_t daif;
asm volatile("mrs %0, daif\n\tmsr daifset, #2" : "=r"(daif) :: "memory");
return daif;
}
inline void RestoreInterrupts(IrqFlags flags) {
asm volatile("msr daif, %0" :: "r"(flags) : "memory");
}
inline void Halt() {
asm volatile("wfi" ::: "memory");
}
// WFI wakes on a pending interrupt even while PSTATE.I masks it, so
// checking for work and then waiting with IRQs masked cannot lose a
// wakeup. Unmask briefly afterwards so the pending IRQ is taken.
inline void HaltWithInterruptsDisabled() {
asm volatile("wfi\n\tmsr daifclr, #2\n\tisb\n\tmsr daifset, #2" ::: "memory");
}
[[noreturn]] inline void HaltForever() {
for (;;) {
asm volatile("msr daifset, #0xf\n\twfi" ::: "memory");
}
}
inline uint64_t ReadTimestamp() {
uint64_t value;
asm volatile("isb\n\tmrs %0, cntvct_el0" : "=r"(value) :: "memory");
return value;
}
// The generic timer runs from boot at CNTFRQ_EL0 Hz, so it times short
// delays without calibration.
inline void IoDelay() {
uint64_t frequency;
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
uint64_t ticks = frequency / 1000000 + 1;
uint64_t start = ReadTimestamp();
while (ReadTimestamp() - start < ticks) {
Pause();
}
}
// ARMv8 has no "clean the whole cache" instruction: set/way maintenance
// is only safe from firmware, which PSCI performs on power transitions.
// Order outstanding accesses so the firmware call sees them.
inline void WritebackInvalidateCaches() {
asm volatile("dsb sy\n\tisb" ::: "memory");
}
// The TLBI operand is VA[55:12] in bits [43:0]; the bits above are RES0.
inline uint64_t DataCacheLineSize() {
uint64_t ctr;
asm volatile("mrs %0, ctr_el0" : "=r"(ctr));
return 4ULL << ((ctr >> 16) & 0xF); // DminLine
}
// To the point of coherency: where non-snooping device DMA looks.
inline void CleanDataCache(const void* address, uint64_t size) {
uint64_t line = DataCacheLineSize();
uint64_t end = (uint64_t)address + size;
for (uint64_t p = (uint64_t)address & ~(line - 1); p < end; p += line) {
asm volatile("dc cvac, %0" :: "r"(p) : "memory");
}
asm volatile("dsb sy" ::: "memory");
}
// Clean too: a partial line at either end may hold CPU data next to the
// buffer, which a plain invalidate would throw away.
inline void InvalidateDataCache(const void* address, uint64_t size) {
uint64_t line = DataCacheLineSize();
uint64_t end = (uint64_t)address + size;
for (uint64_t p = (uint64_t)address & ~(line - 1); p < end; p += line) {
asm volatile("dc civac, %0" :: "r"(p) : "memory");
}
asm volatile("dsb sy" ::: "memory");
}
// Clean the data cache to the point of unification, then invalidate
// the whole instruction cache: invalidating by address would only
// reach the alias it names on an aliasing (VIPT) instruction cache.
inline void SyncInstructionCache(const void* address, uint64_t size) {
uint64_t line = DataCacheLineSize();
uint64_t start = (uint64_t)address & ~(line - 1);
uint64_t end = (uint64_t)address + size;
for (uint64_t p = start; p < end; p += line) {
asm volatile("dc cvau, %0" :: "r"(p) : "memory");
}
asm volatile("dsb ish\n\tic ialluis\n\tdsb ish\n\tisb" ::: "memory");
}
inline void FlushTlbPage(uint64_t virtualAddress) {
asm volatile("dsb ishst\n\ttlbi vaae1is, %0\n\tdsb ish\n\tisb"
:: "r"((virtualAddress >> 12) & ((1ULL << 44) - 1)) : "memory");
}
inline void FlushTlbAll() {
asm volatile("dsb ishst\n\ttlbi vmalle1is\n\tdsb ish\n\tisb" ::: "memory");
}
// The process (lower) half of the address space is translated through
// TTBR0_EL1; the kernel half through TTBR1_EL1, which never changes.
static constexpr uint64_t TtbrAddressMask = 0x0000FFFFFFFFFFFEULL;
inline uint64_t ReadPageTableRoot() {
uint64_t ttbr;
asm volatile("mrs %0, ttbr0_el1" : "=r"(ttbr));
return ttbr & TtbrAddressMask;
}
// No ASIDs yet: every switch invalidates the old process's entries.
inline void WritePageTableRoot(uint64_t rootPhys) {
asm volatile("dsb ishst\n\tmsr ttbr0_el1, %0\n\tisb\n\ttlbi vmalle1\n\tdsb nsh\n\tisb"
:: "r"(rootPhys) : "memory");
}
}
+26
View File
@@ -0,0 +1,26 @@
/*
* CpuPower.cpp
* aarch64 CPU power management (implements Arch/CpuPower.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/CpuPower.hpp>
// No frequency scaling or thermal governor yet (on ARM these come from
// ACPI CPPC / SCMI rather than CPU registers).
namespace Arch::CpuPower {
void InitializeBsp() {}
void InitializeAp() {}
void ApplyPolicyIfChanged() {}
void ThermalTick(uint64_t) {}
void ServiceDeferredDiagnostics() {}
void ReapplyAfterWake() {}
bool GetSnapshot(Snapshot& out) {
out = {};
return false;
}
}
+184
View File
@@ -0,0 +1,184 @@
/*
* Exceptions.cpp
* aarch64 synchronous exceptions: faults, user crashes, kernel panics
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/aarch64/InterruptFrame.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Smp.hpp>
#include <Common/Panic.hpp>
#include <Common/TerminationCodes.hpp>
#include <CppLib/Stream.hpp>
#include <Memory/Paging.hpp>
#include <Sched/Scheduler.hpp>
#include <Sched/CrashReport.hpp>
#include <Api/Heap.hpp>
#include <Timekeeping/Timer.hpp>
using namespace Kt;
namespace {
// ESR_EL1.EC exception classes
constexpr uint64_t EcUnknown = 0x00;
constexpr uint64_t EcIllegalState = 0x0E;
constexpr uint64_t EcInstAbortLower = 0x20;
constexpr uint64_t EcInstAbortSame = 0x21;
constexpr uint64_t EcPcAlignment = 0x22;
constexpr uint64_t EcDataAbortLower = 0x24;
constexpr uint64_t EcDataAbortSame = 0x25;
constexpr uint64_t EcSpAlignment = 0x26;
constexpr uint64_t EcFpException = 0x2C;
constexpr uint64_t EcBreakpointLower = 0x30;
constexpr uint64_t EcBrk = 0x3C;
constexpr uint64_t UserSpaceEnd = 0x0000800000000000ULL;
uint64_t ExceptionClass(const Arch::PanicFrame* frame) {
return (frame->esr >> 26) & 0x3F;
}
bool IsAbort(uint64_t ec) {
return ec == EcInstAbortLower || ec == EcInstAbortSame
|| ec == EcDataAbortLower || ec == EcDataAbortSame;
}
// Fault status code (ISS[5:0]) of an abort: translation faults mean
// the page is not mapped, permission faults that it is.
bool IsTranslationFault(uint64_t esr) {
uint64_t fsc = esr & 0x3F;
return fsc >= 0x04 && fsc <= 0x07;
}
bool IsPermissionFault(uint64_t esr) {
uint64_t fsc = esr & 0x3F;
return fsc >= 0x0C && fsc <= 0x0F;
}
const char* ExceptionName(uint64_t ec) {
switch (ec) {
case EcUnknown: return "Undefined Instruction";
case EcIllegalState: return "Illegal Execution State";
case EcInstAbortLower:
case EcInstAbortSame: return "Instruction Abort";
case EcPcAlignment: return "PC Alignment Fault";
case EcDataAbortLower:
case EcDataAbortSame: return "Data Abort";
case EcSpAlignment: return "SP Alignment Fault";
case EcFpException: return "Floating-Point Exception";
case EcBrk: return "Breakpoint (BRK)";
default:
if (ec >= EcBreakpointLower && ec <= 0x35) return "Debug Exception";
return "Synchronous Exception";
}
}
TerminationCodes TerminationFromClass(uint64_t ec) {
if (ec == EcUnknown || ec == EcIllegalState) return TerminatedIllegalInstruction;
if (ec == EcBrk || (ec >= EcBreakpointLower && ec <= 0x35)) return TerminatedBreakpoint;
if (ec == EcFpException) return TerminatedArithmeticError;
return TerminatedAccessViolation;
}
template<size_t N>
void CopyName(char (&dst)[N], const char* src) {
size_t i = 0;
for (; i + 1 < N && src[i]; i++) dst[i] = src[i];
dst[i] = '\0';
}
// Kill the faulting process with a crash report. Never returns.
[[noreturn]] void TerminateUserProcess(Arch::PanicFrame* frame, uint64_t ec) {
// Teardown can wait on devices and other CPUs; keep the clock running.
Arch::EnableInterrupts();
auto* proc = Sched::GetCurrentProcessPtr();
const char* name = ExceptionName(ec);
KernelLogStream(ERROR, "Exception") << name << " in process \"" << proc->name
<< "\" (pid " << proc->pid << ", ESR 0x" << base::hex << frame->esr
<< ", PC 0x" << frame->elr << ") - process terminated";
CrashReport::Report rep{};
rep.valid = true;
rep.pid = proc->pid;
CopyName(rep.processName, proc->name);
rep.exceptionVector = (uint8_t)ec;
CopyName(rep.exceptionName, name);
rep.instructionPointer = frame->elr;
rep.flags = frame->spsr;
rep.stackPointer = frame->sp;
constexpr uint64_t snapshotBytes = CrashReport::StackSnapshotWords * sizeof(uint64_t);
if (Memory::VMM::Paging::IsUserRangeAccessible(proc->pageTableRoot, frame->sp,
snapshotBytes, false)) {
auto* userStack = (const volatile uint64_t*)frame->sp;
for (int word = 0; word < CrashReport::StackSnapshotWords; word++)
rep.stackWords[word] = userStack[word];
rep.stackWordCount = CrashReport::StackSnapshotWords;
}
if (IsAbort(ec)) {
rep.faultingAddress = frame->far;
rep.pfPresent = IsPermissionFault(frame->esr);
rep.pfWrite = ec == EcDataAbortLower && (frame->esr & (1ULL << 6)) != 0;
rep.pfUser = 1;
rep.pfInstructionFetch = ec == EcInstAbortLower;
}
rep.timestampTick = Timekeeping::GetTicks();
CrashReport::AddReport(rep);
Sched::SpawnCrashPad(proc->pid);
// As on x86: a sibling thread's fault must end the whole process.
int code = TerminationFromClass(ec);
if (Sched::GetCurrentThreadPtr() != proc) {
Sched::KillProcess(proc->pid, code);
} else {
Sched::SetProcessExitCode(proc->pid, code);
}
Sched::ExitProcess();
__builtin_unreachable();
}
// A not-present user page: anonymous memory mapped lazily or the user
// stack growing. Also reached from the kernel touching user buffers.
bool TryResolveUserFault(const Arch::PanicFrame* frame, uint64_t ec) {
if (!IsAbort(ec) || !IsTranslationFault(frame->esr)) return false;
if (frame->far >= UserSpaceEnd) return false;
auto* cpu = Smp::TryGetCurrentCpuData();
if (cpu == nullptr || cpu->currentSlot < 0 || Sched::GetCurrentPid() < 0) return false;
bool isData = ec == EcDataAbortLower || ec == EcDataAbortSame;
Memory::VMM::PageFault fault{false, isData && (frame->esr & (1ULL << 6)) != 0, !isData};
return montauk::abi::TryHandleAnonymousPageFault(frame->far, fault)
|| Sched::TryGrowUserStack(frame->far);
}
}
extern "C" void ArchSyncException(Arch::PanicFrame* frame, bool fromUser) {
uint64_t ec = ExceptionClass(frame);
if (TryResolveUserFault(frame, ec)) {
return;
}
if (fromUser && Sched::GetCurrentPid() >= 0) {
TerminateUserProcess(frame, ec);
}
Panic(ExceptionName(ec), frame);
}
// FIQs are never routed, SErrors are asynchronous external aborts, and the
// kernel neither runs on SP_EL0 nor supports AArch32 user code.
extern "C" void ArchUnexpectedException(Arch::PanicFrame* frame, uint64_t kind) {
static const char* const names[] = {
"Exception on SP_EL0", "Unexpected FIQ", "SError (asynchronous abort)",
"AArch32 exception",
};
Panic(kind < 4 ? names[kind] : "Unexpected exception", frame);
}
+40
View File
@@ -0,0 +1,40 @@
/*
* Gic.hpp
* aarch64 GICv2 interrupt controller (private to the port)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <ACPI/ACPI.hpp>
namespace Arch::Gic {
// Bring up the distributor and the calling CPU's interface from the
// ACPI MADT. Returns false when no usable GIC is described.
bool Initialize(Hal::ACPI::CommonSDTHeader* xsdt);
bool IsInitialized();
// The XSDT Initialize was given (for the other ACPI tables the port reads).
Hal::ACPI::CommonSDTHeader* CurrentXsdt();
// Per-CPU part of Initialize, for secondary CPUs.
void InitializeCpuInterface();
// Map a private peripheral interrupt (16-31) onto an IRQ slot and enable
// it on the calling CPU.
void RoutePpi(uint32_t intid, uint8_t irq, bool edgeTriggered);
// Called for an IRQ slot after the interrupt is acknowledged and before
// it is completed (EOI): level-triggered sources deassert here.
using AckHook = void (*)();
void SetAckHook(uint8_t irq, AckHook hook);
// The calling CPU's target bit, as the distributor sees it.
uint8_t CurrentTargetMask();
// Find an ACPI table in the XSDT by signature.
Hal::ACPI::CommonSDTHeader* FindTable(Hal::ACPI::CommonSDTHeader* xsdt, const char* signature);
}
@@ -0,0 +1,24 @@
/*
* InterruptFrame.hpp
* aarch64 exception frame
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// What the exception vectors (Vectors.S) save before calling into C++.
struct PanicFrame {
uint64_t x[31]; // x0-x30
uint64_t sp; // interrupted SP (SP_EL0 for EL0, else SP_EL1)
uint64_t elr; // ELR_EL1: faulting/preferred-return PC
uint64_t spsr; // SPSR_EL1
uint64_t esr; // ESR_EL1: exception syndrome
uint64_t far; // FAR_EL1: faulting address
};
static_assert(sizeof(PanicFrame) == 288, "must match Vectors.S");
}
+420
View File
@@ -0,0 +1,420 @@
/*
* Interrupts.cpp
* aarch64 GICv2 driver and IRQ slot dispatch (implements Arch/Interrupts.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Interrupts.hpp>
#include <Arch/Smp.hpp>
#include <Arch/aarch64/Gic.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
// IRQ slots map onto GIC interrupt IDs (INTIDs):
// IRQ_TIMER the generic timer's PPI (Timer.cpp routes it)
// 24-45 (MSI slots) SPIs of the GICv2m MSI frame, one per slot
// IRQ_TLB_SHOOTDOWN,
// IRQ_RESCHEDULE SGIs 0 and 1
// 1-23 (platform lines) SPIs the PCI layer assigns to INTx pins from
// ACPI _PRT (AssignPlatformLine)
using namespace Kt;
namespace Arch {
// Distributor registers
static constexpr uint32_t GICD_CTLR = 0x000;
static constexpr uint32_t GICD_TYPER = 0x004;
static constexpr uint32_t GICD_ISENABLER = 0x100;
static constexpr uint32_t GICD_ICENABLER = 0x180;
static constexpr uint32_t GICD_ICPENDR = 0x280;
static constexpr uint32_t GICD_ICACTIVER = 0x380;
static constexpr uint32_t GICD_IPRIORITYR = 0x400;
static constexpr uint32_t GICD_ITARGETSR = 0x800;
static constexpr uint32_t GICD_ICFGR = 0xC00;
static constexpr uint32_t GICD_SGIR = 0xF00;
// CPU interface registers
static constexpr uint32_t GICC_CTLR = 0x00;
static constexpr uint32_t GICC_PMR = 0x04;
static constexpr uint32_t GICC_BPR = 0x08;
static constexpr uint32_t GICC_IAR = 0x0C;
static constexpr uint32_t GICC_EOIR = 0x10;
// GICv2m MSI frame registers
static constexpr uint32_t V2M_MSI_TYPER = 0x008;
static constexpr uint32_t V2M_MSI_SETSPI_NS = 0x040;
static constexpr uint32_t SpuriousIntid = 1020;
static constexpr uint32_t MaxIntids = 1020;
static constexpr uint8_t DefaultPriority = 0xA0;
static constexpr uint8_t NoSlot = 0xFF;
static constexpr uint8_t FirstMsiSlot = 24;
static constexpr uint8_t LastMsiSlot = 45;
// MADT interrupt-controller structures (ACPI 6.x)
static constexpr uint8_t MadtGicc = 0x0B;
static constexpr uint8_t MadtGicd = 0x0C;
static constexpr uint8_t MadtGicMsiFrame = 0x0D;
struct MadtEntry {
uint8_t Type;
uint8_t Length;
} __attribute__((packed));
struct MadtGiccEntry {
MadtEntry Header;
uint16_t Reserved;
uint32_t CpuInterfaceNumber;
uint32_t AcpiProcessorUid;
uint32_t Flags;
uint32_t ParkingProtocolVersion;
uint32_t PerformanceInterruptGsiv;
uint64_t ParkedAddress;
uint64_t PhysicalBaseAddress; // GICC
uint64_t Gicv;
uint64_t Gich;
uint32_t VgicMaintenanceInterrupt;
uint64_t GicrBaseAddress;
uint64_t Mpidr;
} __attribute__((packed));
struct MadtGicdEntry {
MadtEntry Header;
uint16_t Reserved;
uint32_t GicId;
uint64_t PhysicalBaseAddress;
uint32_t SystemVectorBase;
uint8_t GicVersion;
} __attribute__((packed));
struct MadtMsiFrameEntry {
MadtEntry Header;
uint16_t Reserved;
uint32_t FrameId;
uint64_t PhysicalBaseAddress;
uint32_t Flags; // bit 0: SpiCount/SpiBase override TYPER
uint16_t SpiCount;
uint16_t SpiBase;
} __attribute__((packed));
static constexpr uint32_t MadtHeaderSize = 44; // SDT header + LAPIC address + flags
static IrqHandler g_irqHandlers[IRQ_COUNT] = {};
static Gic::AckHook g_ackHooks[IRQ_COUNT] = {};
static uint8_t g_intidToSlot[MaxIntids];
static uint32_t g_slotToIntid[IRQ_COUNT];
static volatile uint8_t* g_gicd = nullptr;
static volatile uint8_t* g_gicc = nullptr;
static uint32_t g_intidCount = 0;
static Hal::ACPI::CommonSDTHeader* g_xsdt = nullptr;
struct PlatformLine {
uint32_t gsi; // 0: unassigned
bool edgeTriggered;
};
static PlatformLine g_platformLines[24] = {};
static uint64_t g_msiFramePhys = 0;
static uint32_t g_msiSpiBase = 0;
static uint32_t g_msiSpiCount = 0;
static inline uint32_t GicdRead(uint32_t reg) {
return *(volatile uint32_t*)(g_gicd + reg);
}
static inline void GicdWrite(uint32_t reg, uint32_t value) {
*(volatile uint32_t*)(g_gicd + reg) = value;
}
static inline void GicdWrite8(uint32_t reg, uint8_t value) {
*(volatile uint8_t*)(g_gicd + reg) = value;
}
static inline uint8_t GicdRead8(uint32_t reg) {
return *(volatile uint8_t*)(g_gicd + reg);
}
static inline uint32_t GiccRead(uint32_t reg) {
return *(volatile uint32_t*)(g_gicc + reg);
}
static inline void GiccWrite(uint32_t reg, uint32_t value) {
*(volatile uint32_t*)(g_gicc + reg) = value;
}
static volatile uint8_t* MapRegisters(uint64_t phys, uint64_t size) {
for (uint64_t offset = 0; offset < size; offset += 0x1000) {
Memory::VMM::g_paging->MapMMIO(phys + offset, Memory::HHDM(phys + offset));
}
return (volatile uint8_t*)Memory::HHDM(phys);
}
static void SetConfig(uint32_t intid, bool edgeTriggered) {
uint32_t reg = GICD_ICFGR + (intid / 16) * 4;
uint32_t shift = (intid % 16) * 2 + 1;
uint32_t value = GicdRead(reg);
if (edgeTriggered) value |= 1u << shift;
else value &= ~(1u << shift);
GicdWrite(reg, value);
}
static void EnableIntid(uint32_t intid) {
GicdWrite(GICD_ISENABLER + (intid / 32) * 4, 1u << (intid % 32));
}
static void Bind(uint32_t intid, uint8_t irq) {
if (intid >= MaxIntids || irq >= IRQ_COUNT) return;
g_intidToSlot[intid] = irq;
g_slotToIntid[irq] = intid;
}
// Route a shared peripheral interrupt to the boot CPU and unmask it.
static void RouteSpi(uint32_t intid, uint8_t irq, bool edgeTriggered) {
if (intid < 32 || intid >= g_intidCount) return;
Bind(intid, irq);
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
GicdWrite8(GICD_ITARGETSR + intid, Smp::GetCpuData(0)->arch.gicTargetMask);
SetConfig(intid, edgeTriggered);
EnableIntid(intid);
}
namespace Gic {
Hal::ACPI::CommonSDTHeader* FindTable(Hal::ACPI::CommonSDTHeader* xsdt, const char* signature) {
if (xsdt == nullptr) return nullptr;
uint32_t count = (xsdt->Length - sizeof(Hal::ACPI::CommonSDTHeader)) / 8;
auto* entries = (uint64_t*)((uint64_t)xsdt + sizeof(Hal::ACPI::CommonSDTHeader));
for (uint32_t i = 0; i < count; i++) {
auto* header = (Hal::ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
if (header->Signature[0] == signature[0] && header->Signature[1] == signature[1]
&& header->Signature[2] == signature[2] && header->Signature[3] == signature[3]) {
return header;
}
}
return nullptr;
}
// ITARGETSR0-7 are banked: each byte reads back the reading CPU's
// own interface bit.
uint8_t CurrentTargetMask() {
uint32_t value = GicdRead(GICD_ITARGETSR);
return (uint8_t)(value | value >> 8 | value >> 16 | value >> 24);
}
void InitializeCpuInterface() {
// SGIs and PPIs are banked per CPU: disable the PPIs until a
// driver routes one, keep the SGIs (IPIs) enabled.
GicdWrite(GICD_ICENABLER, 0xFFFF0000u);
GicdWrite(GICD_ISENABLER, 0x0000FFFFu);
GicdWrite(GICD_ICPENDR, 0xFFFFFFFFu);
for (uint32_t intid = 0; intid < 32; intid++) {
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
}
GiccWrite(GICC_PMR, 0xF0);
GiccWrite(GICC_BPR, 0);
GiccWrite(GICC_CTLR, 1);
auto* cpu = Smp::GetCurrentCpuData();
cpu->arch.gicTargetMask = CurrentTargetMask();
}
void RoutePpi(uint32_t intid, uint8_t irq, bool edgeTriggered) {
if (intid < 16 || intid >= 32) return;
Bind(intid, irq);
SetConfig(intid, edgeTriggered);
EnableIntid(intid);
}
void SetAckHook(uint8_t irq, AckHook hook) {
if (irq < IRQ_COUNT) g_ackHooks[irq] = hook;
}
bool IsInitialized() {
return g_gicc != nullptr;
}
Hal::ACPI::CommonSDTHeader* CurrentXsdt() {
return g_xsdt;
}
bool Initialize(Hal::ACPI::CommonSDTHeader* xsdt) {
g_xsdt = xsdt;
auto* madt = FindTable(xsdt, "APIC");
if (madt == nullptr) {
KernelLogStream(ERROR, "GIC") << "No MADT";
return false;
}
uint64_t gicdPhys = 0, giccPhys = 0;
uint8_t version = 0;
auto* cursor = (uint8_t*)madt + MadtHeaderSize;
auto* end = (uint8_t*)madt + madt->Length;
while (cursor + sizeof(MadtEntry) <= end) {
auto* entry = (MadtEntry*)cursor;
if (entry->Length < sizeof(MadtEntry)) break;
if (entry->Type == MadtGicd) {
auto* gicd = (MadtGicdEntry*)entry;
gicdPhys = gicd->PhysicalBaseAddress;
version = gicd->GicVersion;
} else if (entry->Type == MadtGicc && giccPhys == 0) {
giccPhys = ((MadtGiccEntry*)entry)->PhysicalBaseAddress;
} else if (entry->Type == MadtGicMsiFrame && g_msiFramePhys == 0) {
auto* frame = (MadtMsiFrameEntry*)entry;
g_msiFramePhys = frame->PhysicalBaseAddress;
if (frame->Flags & 1) {
g_msiSpiBase = frame->SpiBase;
g_msiSpiCount = frame->SpiCount;
}
}
cursor += entry->Length;
}
// Version 0 means "infer from the hardware"; 1 and 2 share a
// programming model.
if (gicdPhys == 0 || giccPhys == 0 || version > 2) {
KernelLogStream(ERROR, "GIC") << "No GICv2 in the MADT (version "
<< base::dec << (uint64_t)version << ")";
return false;
}
for (uint32_t i = 0; i < MaxIntids; i++) g_intidToSlot[i] = NoSlot;
for (int i = 0; i < IRQ_COUNT; i++) g_slotToIntid[i] = SpuriousIntid;
Bind(0, IRQ_TLB_SHOOTDOWN);
Bind(1, IRQ_RESCHEDULE);
g_gicd = MapRegisters(gicdPhys, 0x1000);
g_gicc = MapRegisters(giccPhys, 0x2000);
g_intidCount = ((GicdRead(GICD_TYPER) & 0x1F) + 1) * 32;
if (g_intidCount > MaxIntids) g_intidCount = MaxIntids;
// Distributor: everything masked, idle, level-triggered and
// aimed at no CPU until RouteSpi claims it.
GicdWrite(GICD_CTLR, 0);
for (uint32_t intid = 32; intid < g_intidCount; intid += 32) {
GicdWrite(GICD_ICENABLER + (intid / 32) * 4, 0xFFFFFFFFu);
GicdWrite(GICD_ICPENDR + (intid / 32) * 4, 0xFFFFFFFFu);
GicdWrite(GICD_ICACTIVER + (intid / 32) * 4, 0xFFFFFFFFu);
}
for (uint32_t intid = 32; intid < g_intidCount; intid++) {
GicdWrite8(GICD_IPRIORITYR + intid, DefaultPriority);
GicdWrite8(GICD_ITARGETSR + intid, 0);
}
for (uint32_t intid = 32; intid < g_intidCount; intid += 16) {
GicdWrite(GICD_ICFGR + (intid / 16) * 4, 0);
}
GicdWrite(GICD_CTLR, 1);
InitializeCpuInterface();
if (g_msiFramePhys != 0) {
volatile uint8_t* frame = MapRegisters(g_msiFramePhys, 0x1000);
if (g_msiSpiCount == 0) {
uint32_t typer = *(volatile uint32_t*)(frame + V2M_MSI_TYPER);
g_msiSpiBase = (typer >> 16) & 0x3FF;
g_msiSpiCount = typer & 0x3FF;
}
}
KernelLogStream(OK, "GIC") << "GICv2: distributor 0x" << base::hex << gicdPhys
<< ", CPU interface 0x" << giccPhys << ", " << base::dec
<< (uint64_t)g_intidCount << " interrupt IDs";
if (g_msiSpiCount != 0) {
KernelLogStream(OK, "GIC") << "GICv2m MSI frame 0x" << base::hex << g_msiFramePhys
<< ": SPIs " << base::dec << (uint64_t)g_msiSpiBase << "-"
<< (uint64_t)(g_msiSpiBase + g_msiSpiCount - 1);
}
return true;
}
}
void RegisterIrqHandler(uint8_t irq, IrqHandler handler) {
if (irq >= IRQ_COUNT) return;
g_irqHandlers[irq] = handler;
KernelLogStream(DEBUG, "IRQ") << "Registered handler for IRQ "
<< base::dec << (uint64_t)irq;
}
bool PlatformLinesAreFixed() {
return false;
}
// ACPI GSIs are GIC INTIDs. The GIC knows only active-high levels and
// rising edges; an active-low line is inverted on its way in.
bool AssignPlatformLine(uint8_t irq, uint32_t gsi, bool levelTriggered, bool) {
if (irq == 0 || irq >= 24 || gsi < 32 || gsi >= g_intidCount) return false;
g_platformLines[irq] = PlatformLine{ gsi, !levelTriggered };
return true;
}
void RouteLegacyIrq(uint8_t irq) {
if (irq >= 24 || g_platformLines[irq].gsi == 0) {
KernelLogStream(WARNING, "IRQ") << "aarch64: platform line "
<< base::dec << (uint64_t)irq << " is not assigned";
return;
}
RouteSpi(g_platformLines[irq].gsi, irq, g_platformLines[irq].edgeTriggered);
}
void EnableLegacyIrq(uint8_t irq) {
RouteLegacyIrq(irq);
}
bool ComposeMsi(uint8_t irq, MsiMessage& out) {
if (g_gicd == nullptr || irq < FirstMsiSlot || irq > LastMsiSlot) return false;
uint32_t index = irq - FirstMsiSlot;
if (index >= g_msiSpiCount) return false;
uint32_t spi = g_msiSpiBase + index;
if (g_slotToIntid[irq] != spi) {
RouteSpi(spi, irq, true);
}
out = MsiMessage{ g_msiFramePhys + V2M_MSI_SETSPI_NS, spi };
return true;
}
bool SendIpi(int cpuIndex, uint8_t irq) {
if (g_gicd == nullptr) return false;
auto* cpu = Smp::GetCpuData(cpuIndex);
if (cpu == nullptr || cpu->arch.gicTargetMask == 0) return false;
uint32_t sgi = g_slotToIntid[irq];
if (sgi >= 16) return false;
// Make the IPI's memory effects visible before it is raised.
asm volatile("dsb ishst" ::: "memory");
GicdWrite(GICD_SGIR, ((uint32_t)cpu->arch.gicTargetMask << 16) | sgi);
return true;
}
}
// IRQ exception vector (Vectors.S). As on x86, the interrupt is completed
// BEFORE the handler runs: the handler may context-switch away and not
// come back here for a long time, and an un-EOId interrupt would hold this
// CPU's running priority up (no further ticks). Level-triggered sources
// get the chance to drop their line first through an ack hook.
extern "C" void ArchIrqException(void*, bool fromUser) {
using namespace Arch;
uint32_t iar = GiccRead(GICC_IAR);
uint32_t intid = iar & 0x3FF;
if (intid >= SpuriousIntid) return;
uint8_t irq = g_intidToSlot[intid];
if (irq != NoSlot && g_ackHooks[irq] != nullptr) {
g_ackHooks[irq]();
}
GiccWrite(GICC_EOIR, iar);
if (irq != NoSlot && g_irqHandlers[irq] != nullptr) {
g_irqHandlers[irq](irq, fromUser);
}
}
+28
View File
@@ -0,0 +1,28 @@
/*
* IoPort.hpp
* aarch64 port I/O (implements Io/IoPort.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <Arch/Cpu.hpp>
// ARM has no I/O address space. Until a PCI host bridge driver maps its I/O
// aperture, port accesses behave like accesses to an absent device.
namespace Io {
static constexpr bool HasPortIo = false;
inline void Out8(uint8_t, uint16_t) {}
inline void Out16(uint16_t, uint16_t) {}
inline void Out32(uint32_t, uint16_t) {}
inline uint8_t In8(uint16_t) { return 0xFF; }
inline uint16_t In16(uint16_t) { return 0xFFFF; }
inline uint32_t In32(uint16_t) { return 0xFFFFFFFF; }
inline void IoPortWait() {
Arch::IoDelay();
}
};
+528
View File
@@ -0,0 +1,528 @@
/*
* Paging.cpp
* aarch64 virtual memory (implements Memory/Paging.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Memory/Paging.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp>
#include <Arch/Cpu.hpp>
#include <Common/Panic.hpp>
#include <CppLib/Spinlock.hpp>
#include <Arch/aarch64/Bringup.hpp>
// 4 KiB granule, 48-bit virtual addresses, four levels (0-3). The kernel
// half is translated through TTBR1_EL1 (g_kernelHalfRoot), which never
// changes after Init. The "address space roots" the generic kernel switches
// between are TTBR0_EL1 tables: a process's root holds only its own lower
// half, and KernelRoot() is the TTBR0 table of kernel contexts, empty apart
// from the EFI runtime identity mappings (so null pointers fault).
//
// The HHDM covers only what the memory map lists as RAM or firmware data:
// unlike on x86, the holes between are device space on ARM machines, and a
// cacheable mapping of device registers invites speculative accesses.
namespace Memory::VMM {
Paging* g_paging = nullptr;
static std::uint64_t g_hhdmLimit = 0;
static std::uint64_t g_kernelHalfRoot = 0;
std::uint64_t Paging::HhdmLimit() { return g_hhdmLimit; }
// Translation table descriptor bits (VMSAv8-64, stage 1)
static constexpr uint64_t DescValid = 1ULL << 0;
static constexpr uint64_t DescTable = 1ULL << 1; // table (levels 0-2) or page (level 3)
static constexpr uint64_t DescAttrShift = 2; // AttrIndx[2:0], into MAIR_EL1
static constexpr uint64_t DescAttrMask = 7ULL << DescAttrShift;
static constexpr uint64_t DescApUser = 1ULL << 6; // AP[1]: EL0 may access
static constexpr uint64_t DescApReadOnly = 1ULL << 7; // AP[2]
static constexpr uint64_t DescShInner = 3ULL << 8;
static constexpr uint64_t DescAf = 1ULL << 10; // access flag (no AF faults)
static constexpr uint64_t DescNotGlobal = 1ULL << 11;
static constexpr uint64_t DescPxn = 1ULL << 53;
static constexpr uint64_t DescUxn = 1ULL << 54;
static constexpr uint64_t DescAddrMask = 0x0000FFFFFFFFF000ULL;
// MAIR_EL1 attribute indices
static constexpr uint64_t AttrNormal = 0; // write-back, read/write-allocate
static constexpr uint64_t AttrNormalNc = 1; // non-cacheable (write-combining)
static constexpr uint64_t AttrDevice = 2; // Device-nGnRE
static constexpr uint64_t MairValue = 0xFFULL << (8 * AttrNormal)
| 0x44ULL << (8 * AttrNormalNc)
| 0x04ULL << (8 * AttrDevice);
static constexpr uint64_t KernelHalfBase = 0xFFFF000000000000ULL;
static constexpr uint64_t UserSpaceEnd = 0x0000800000000000ULL;
// EFI memory types whose runtime regions are device registers
static constexpr uint32_t EfiMemoryMappedIo = 11;
static constexpr uint32_t EfiMemoryMappedIoPortSpace = 12;
static kcp::Mutex pagingLock;
void LockUserPaging() { pagingLock.Acquire(); }
void UnlockUserPaging() { pagingLock.Release(); }
void ActivateKernelAddressSpace() {
Arch::WritePageTableRoot(g_paging->KernelRoot());
}
void FlushTLB() {
Arch::FlushTlbAll();
}
using Descriptor = uint64_t;
static inline volatile Descriptor* TableAt(uint64_t phys) {
return (volatile Descriptor*)Memory::HHDM(phys);
}
static inline uint64_t Index(uint64_t va, int level) {
return (va >> (39 - 9 * level)) & 0x1FF;
}
// Order descriptor writes before the table walker (and later
// instructions) can observe them.
static inline void PublishTables() {
asm volatile("dsb ishst\n\tisb" ::: "memory");
}
static inline uint64_t ReadTtbr1() {
uint64_t ttbr;
asm volatile("mrs %0, ttbr1_el1" : "=r"(ttbr));
return ttbr & Arch::TtbrAddressMask;
}
// The level-3 descriptor for `va`, creating intermediate tables.
// Returns nullptr when out of memory or when a block mapping covers
// `va` (only the bootloader's tables contain blocks).
static volatile Descriptor* WalkCreate(uint64_t root, uint64_t va) {
uint64_t table = root;
for (int level = 0; level < 3; level++) {
volatile Descriptor* entry = &TableAt(table)[Index(va, level)];
Descriptor desc = *entry;
if ((desc & DescValid) == 0) {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) return nullptr;
table = Memory::SubHHDM(page);
// The zeroed table must be visible before the walker can
// reach it.
asm volatile("dsb ishst" ::: "memory");
*entry = table | DescTable | DescValid;
} else if ((desc & DescTable) == 0) {
return nullptr;
} else {
table = desc & DescAddrMask;
}
}
return &TableAt(table)[Index(va, 3)];
}
// The level-3 descriptor for `va`, or nullptr if a table is missing.
static volatile Descriptor* WalkExisting(uint64_t root, uint64_t va) {
uint64_t table = root;
for (int level = 0; level < 3; level++) {
Descriptor desc = TableAt(table)[Index(va, level)];
if ((desc & (DescValid | DescTable)) != (DescValid | DescTable)) return nullptr;
table = desc & DescAddrMask;
}
return &TableAt(table)[Index(va, 3)];
}
// Install a leaf. Replacing a live translation (other address or
// attributes) needs break-before-make: invalidate, flush, then write.
static void SetLeaf(volatile Descriptor* entry, Descriptor desc, uint64_t va) {
Descriptor old = *entry;
if (old == desc) return;
if (old & DescValid) {
*entry = 0;
Arch::FlushTlbPage(va);
}
*entry = desc;
PublishTables();
}
static Descriptor KernelLeaf(uint64_t phys, uint64_t attr) {
Descriptor desc = (phys & DescAddrMask) | DescTable | DescValid | DescAf | DescShInner
| (attr << DescAttrShift) | DescUxn;
// Kernel memory stays executable, as on x86; device memory never is.
if (attr != AttrNormal) desc |= DescPxn;
return desc;
}
static Descriptor UserLeaf(uint64_t phys, uint64_t attr, bool writable, bool executable) {
Descriptor desc = (phys & DescAddrMask) | DescTable | DescValid | DescAf | DescShInner
| DescNotGlobal | (attr << DescAttrShift) | DescApUser | DescPxn;
if (!writable) desc |= DescApReadOnly;
if (!executable) desc |= DescUxn;
return desc;
}
static inline bool IsUserLeaf(Descriptor desc) {
return (desc & (DescValid | DescTable)) == (DescValid | DescTable) && (desc & DescApUser);
}
// Translate through a table tree that may contain block mappings (the
// bootloader's). Returns 0 if unmapped.
static uint64_t Translate(uint64_t root, uint64_t va) {
uint64_t table = root;
for (int level = 0; level <= 3; level++) {
Descriptor desc = TableAt(table)[Index(va, level)];
if ((desc & DescValid) == 0) return 0;
uint64_t span = 1ULL << (39 - 9 * level);
bool leaf = level == 3 || (desc & DescTable) == 0;
if (leaf) {
if (level == 0) return 0; // no level-0 blocks with 4 KiB granules
return ((desc & DescAddrMask) & ~(span - 1)) | (va & (span - 1));
}
table = desc & DescAddrMask;
}
return 0;
}
void Paging::MapEfiRuntime(const montauk::boot::EfiInfo& efi) {
if (efi.memoryMap == nullptr || efi.descriptorSize == 0) return;
struct EfiMemDesc {
uint32_t Type;
uint64_t PhysicalStart;
uint64_t VirtualStart;
uint64_t NumberOfPages;
uint64_t Attribute;
};
static constexpr uint64_t EFI_MEMORY_RUNTIME = 0x8000000000000000ULL;
auto* base = (uint8_t*)efi.memoryMap;
uint64_t count = efi.memoryMapSize / efi.descriptorSize;
for (uint64_t i = 0; i < count; i++) {
auto* desc = (EfiMemDesc*)(base + i * efi.descriptorSize);
if ((desc->Attribute & EFI_MEMORY_RUNTIME) == 0) continue;
bool device = desc->Type == EfiMemoryMappedIo || desc->Type == EfiMemoryMappedIoPortSpace;
for (uint64_t p = 0; p < desc->NumberOfPages; p++) {
uint64_t addr = desc->PhysicalStart + p * 0x1000;
// Firmware code runs at its HHDM alias (Efi::Init calls the
// services there) and reaches its data at physical addresses.
// Runtime regions are reserved memory, outside the HHDM.
if (device) {
MapMMIO(addr, addr);
MapMMIO(addr, Memory::HHDM(addr));
} else {
Map(addr, addr);
Map(addr, Memory::HHDM(addr));
}
}
}
FlushTLB();
}
}
namespace Arch {
// The new tables map the code running here at the same address as the
// bootloader's did, so the switch is a register write followed by a
// full invalidate.
void ActivateKernelTranslation() {
using namespace Memory::VMM;
// 48-bit VAs in both halves, 4 KiB granules, write-back walks of
// inner-shareable tables; the physical size is what the CPU has.
uint64_t mmfr0;
asm volatile("mrs %0, id_aa64mmfr0_el1" : "=r"(mmfr0));
uint64_t ips = mmfr0 & 0xF;
if (ips > 5) ips = 5; // 52-bit output needs LPA2 descriptors
uint64_t tcr = 16ULL // T0SZ
| 1ULL << 8 | 1ULL << 10 // IRGN0, ORGN0: write-back
| 3ULL << 12 // SH0: inner shareable
| 0ULL << 14 // TG0: 4 KiB
| 16ULL << 16 // T1SZ
| 1ULL << 24 | 1ULL << 26 // IRGN1, ORGN1
| 3ULL << 28 // SH1
| 2ULL << 30 // TG1: 4 KiB
| ips << 32; // IPS
asm volatile(
"dsb ish\n\t"
"msr mair_el1, %0\n\t"
"msr tcr_el1, %1\n\t"
"isb\n\t"
"msr ttbr1_el1, %2\n\t"
"msr ttbr0_el1, %3\n\t"
"isb\n\t"
"tlbi vmalle1\n\t"
"dsb nsh\n\t"
"isb"
:: "r"(MairValue), "r"(tcr), "r"(g_kernelHalfRoot), "r"(g_paging->KernelRoot()) : "memory");
}
}
namespace Memory::VMM {
Paging::Paging() {
rootPhys = Memory::SubHHDM(Memory::g_pfa->AllocateZeroed());
g_kernelHalfRoot = Memory::SubHHDM(Memory::g_pfa->AllocateZeroed());
}
// Kernel mappings of lower-half addresses (EFI identity mappings) go to
// the kernel contexts' TTBR0 table.
static uint64_t KernelRootFor(uint64_t kernelContextRoot, uint64_t va) {
return va >= KernelHalfBase ? g_kernelHalfRoot : kernelContextRoot;
}
void Paging::Init(std::uint64_t kernelBaseVirt, std::uint64_t kernelSize, const montauk::boot::MemoryMap& memMap,
const montauk::boot::Framebuffer& framebuffer) {
if (Memory::HHDMBase < KernelHalfBase) {
Panic("aarch64: HHDM outside the 48-bit kernel half", nullptr);
}
uint64_t bootTtbr1 = ReadTtbr1();
uint64_t bootTcr, bootMair;
asm volatile("mrs %0, tcr_el1" : "=r"(bootTcr));
asm volatile("mrs %0, mair_el1" : "=r"(bootMair));
Kt::KernelLogStream(Kt::DEBUG, "VMM") << "Bootloader TCR 0x" << base::hex << bootTcr
<< ", MAIR 0x" << bootMair << ", TTBR1 0x" << bootTtbr1;
// The kernel image, at the physical pages the bootloader loaded it to.
for (uint64_t va = kernelBaseVirt; va < kernelBaseVirt + kernelSize; va += 0x1000) {
uint64_t phys = Translate(bootTtbr1, va);
if (phys == 0) Panic("aarch64: kernel page missing from the bootloader's tables", nullptr);
Map(phys, va);
}
// HHDM: every region that is real backing store (see the top of
// this file). The limit is published for the AML interpreter.
uint64_t maxPhysAddr = 0;
for (size_t i = 0; i < memMap.count; i++) {
const auto& region = memMap.regions[i];
if (region.kind == montauk::boot::MemoryKind::Reserved
|| region.kind == montauk::boot::MemoryKind::Unknown) {
continue;
}
uint64_t start = region.base & ~0xFFFULL;
uint64_t end = (region.base + region.length + 0xFFF) & ~0xFFFULL;
bool framebufferRegion = region.kind == montauk::boot::MemoryKind::Framebuffer;
for (uint64_t phys = start; phys < end; phys += 0x1000) {
if (framebufferRegion) MapWC(phys, Memory::HHDM(phys));
else Map(phys, Memory::HHDM(phys));
}
if (end > maxPhysAddr) maxPhysAddr = end;
}
g_hhdmLimit = maxPhysAddr;
// The framebuffer may not be listed in the memory map at all.
if (framebuffer.address != 0) {
uint64_t fbPhys = Memory::SubHHDM(framebuffer.address) & ~0xFFFULL;
uint64_t fbPages = (framebuffer.height * framebuffer.pitch + 0xFFF) / 0x1000;
for (uint64_t p = 0; p < fbPages; p++) {
uint64_t phys = fbPhys + p * 0x1000;
MapWC(phys, Memory::HHDM(phys));
}
}
// The bootloader wrote the image through the data cache. Push it to
// the point of unification and drop every instruction-cache line
// before the kernel relies on fetching code it has not run yet: a
// line still dirty in L1D is invisible to instruction fetch, which
// then reads stale DRAM (an "unknown reason" exception on a valid
// instruction).
Arch::SyncInstructionCache((const void*)kernelBaseVirt, kernelSize);
Arch::ActivateKernelTranslation();
Kt::KernelLogStream(Kt::OK, "VMM") << "Switched to kernel translation tables (HHDM limit 0x"
<< base::hex << g_hhdmLimit << ")";
}
static void MapKernelPage(uint64_t root, uint64_t phys, uint64_t va, uint64_t attr, const char* what) {
if (va % 0x1000 != 0 || phys % 0x1000 != 0) {
Panic(what, nullptr);
}
volatile Descriptor* entry = WalkCreate(KernelRootFor(root, va), va);
if (entry == nullptr) {
Panic("OOM in Paging (kernel page table allocation)", nullptr);
}
SetLeaf(entry, KernelLeaf(phys, attr), va);
}
void Paging::Map(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrNormal,
"Value that isn't page-aligned passed as address to Paging::Map!");
}
void Paging::MapMMIO(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrDevice,
"Value that isn't page-aligned passed as address to Paging::MapMMIO!");
}
void Paging::MapWC(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
MapKernelPage(rootPhys, physicalAddress, virtualAddress, AttrNormalNc,
"Value that isn't page-aligned passed as address to Paging::MapWC!");
}
void Paging::MapUser(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) {
Panic("Value that isn't page-aligned passed as address to Paging::MapUser!", nullptr);
}
volatile Descriptor* entry = WalkCreate(rootPhys, virtualAddress);
if (entry == nullptr) {
Panic("OOM in Paging::MapUser (page table allocation)", nullptr);
}
SetLeaf(entry, UserLeaf(physicalAddress, AttrNormal, true, true), virtualAddress);
}
std::uint64_t Paging::GetPhysAddr(std::uint64_t root, std::uint64_t virtualAddress) {
if (virtualAddress >= KernelHalfBase) {
root = g_kernelHalfRoot;
}
return Translate(root, virtualAddress);
}
std::uint64_t Paging::GetPhysAddr(std::uint64_t virtualAddress) {
return GetPhysAddr(rootPhys, virtualAddress);
}
// The kernel half lives in TTBR1, so a new address space starts empty.
std::uint64_t Paging::CreateUserAddressSpace() {
void* page = Memory::g_pfa->AllocateZeroed();
if (page == nullptr) return 0;
return Memory::SubHHDM(page);
}
bool Paging::MapUserIn(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
return MapUserInPermissions(root, physicalAddress, virtualAddress, true, true);
}
static bool MapUserLeaf(uint64_t root, uint64_t phys, uint64_t va, Descriptor desc, const char* what) {
pagingLock.Acquire();
if (va % 0x1000 != 0 || phys % 0x1000 != 0) {
pagingLock.Release();
Panic(what, nullptr);
}
volatile Descriptor* entry = WalkCreate(root, va);
if (entry == nullptr) {
pagingLock.Release();
return false;
}
SetLeaf(entry, desc, va);
pagingLock.Release();
return true;
}
bool Paging::MapUserInPermissions(std::uint64_t root, std::uint64_t physicalAddress,
std::uint64_t virtualAddress, bool writable, bool executable) {
return MapUserLeaf(root, physicalAddress, virtualAddress,
UserLeaf(physicalAddress, AttrNormal, writable, executable),
"Non-aligned address in Paging::MapUserIn!");
}
bool Paging::MapUserInWC(std::uint64_t root, std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
return MapUserLeaf(root, physicalAddress, virtualAddress,
UserLeaf(physicalAddress, AttrNormalNc, true, false),
"Non-aligned address in Paging::MapUserInWC!");
}
// Permission changes need no break-before-make, only the TLB flush.
bool Paging::ProtectUserIn(std::uint64_t root, std::uint64_t virtualAddress,
bool writable, bool executable) {
if ((virtualAddress & 0xFFFULL) != 0) return false;
pagingLock.Acquire();
volatile Descriptor* entry = WalkExisting(root, virtualAddress);
if (entry == nullptr || !IsUserLeaf(*entry)) {
pagingLock.Release();
return false;
}
Descriptor desc = *entry;
if (writable) desc &= ~DescApReadOnly;
else desc |= DescApReadOnly;
if (executable) desc &= ~DescUxn;
else desc |= DescUxn;
*entry = desc;
Arch::FlushTlbPage(virtualAddress);
pagingLock.Release();
return true;
}
void Paging::UnmapUserIn(std::uint64_t root, std::uint64_t virtualAddress) {
pagingLock.Acquire();
volatile Descriptor* entry = WalkExisting(root, virtualAddress);
if (entry != nullptr && (*entry & DescValid)) {
*entry = 0;
Arch::FlushTlbPage(virtualAddress);
}
pagingLock.Release();
}
void Paging::FreeUserHalf(std::uint64_t root) {
// User space is the lower 128 TiB: level-0 entries 0-255.
for (int i0 = 0; i0 < 256; i0++) {
Descriptor d0 = TableAt(root)[i0];
if ((d0 & DescValid) == 0) continue;
uint64_t l1 = d0 & DescAddrMask;
for (int i1 = 0; i1 < 512; i1++) {
Descriptor d1 = TableAt(l1)[i1];
if ((d1 & DescValid) == 0) continue;
uint64_t l2 = d1 & DescAddrMask;
for (int i2 = 0; i2 < 512; i2++) {
Descriptor d2 = TableAt(l2)[i2];
if ((d2 & DescValid) == 0) continue;
uint64_t l3 = d2 & DescAddrMask;
for (int i3 = 0; i3 < 512; i3++) {
Descriptor leaf = TableAt(l3)[i3];
if ((leaf & DescValid) == 0) continue;
// Write-combining/device pages are not the process's.
if (((leaf & DescAttrMask) >> DescAttrShift) != AttrNormal) continue;
uint64_t pagePhys = leaf & DescAddrMask;
if (pagePhys != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(pagePhys));
}
}
Memory::g_pfa->Free((void*)Memory::HHDM(l3));
}
Memory::g_pfa->Free((void*)Memory::HHDM(l2));
}
Memory::g_pfa->Free((void*)Memory::HHDM(l1));
}
}
bool Paging::IsUserRangeAccessible(std::uint64_t root, std::uint64_t virtualAddress,
std::uint64_t size, bool requireWrite) {
if (size == 0) return true;
if (virtualAddress >= UserSpaceEnd) return false;
if (virtualAddress + size < virtualAddress) return false;
if (virtualAddress + size > UserSpaceEnd) return false;
uint64_t page = virtualAddress & ~0xFFFULL;
uint64_t lastPage = (virtualAddress + size - 1) & ~0xFFFULL;
for (;;) {
volatile Descriptor* entry = WalkExisting(root, page);
if (entry == nullptr) return false;
Descriptor desc = *entry;
if (!IsUserLeaf(desc)) return false;
if (requireWrite && (desc & DescApReadOnly)) return false;
if (page == lastPage) break;
page += 0x1000;
}
return true;
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* PanicFrame.cpp
* aarch64 CPU state for the panic screen
* Copyright (c) 2026 Daniel Hammer
*/
#include <Common/Panic.hpp>
#include <Arch/aarch64/InterruptFrame.hpp>
#include <CppLib/BoxUI.hpp>
static constexpr int BoxWidth = PanicBoxWidth;
namespace Arch {
void PrintPanicFrame(PanicFrame* frame) {
PrintBoxedSeparator(kerr, BoxWidth);
PrintBoxedLine(kerr, "CPU State:", BoxWidth, true);
PrintBoxedHex(kerr, "Exception Class (ESR)", frame->esr >> 26, BoxWidth);
PrintBoxedHex(kerr, "Syndrome (ESR)", frame->esr, BoxWidth);
PrintBoxedHex(kerr, "Fault Address (FAR)", frame->far, BoxWidth);
PrintBoxedSeparator(kerr, BoxWidth);
PrintBoxedLine(kerr, "Registers:", BoxWidth, true);
PrintBoxedHex(kerr, "Program Counter (ELR)", frame->elr, BoxWidth);
PrintBoxedHex(kerr, "PSTATE (SPSR)", frame->spsr, BoxWidth);
PrintBoxedHex(kerr, "Stack Pointer", frame->sp, BoxWidth);
PrintBoxedHex(kerr, "Link Register (x30)", frame->x[30], BoxWidth);
// An "unknown reason" exception on a kernel PC: show the word the
// data side sees there. A valid instruction means instruction fetch
// saw something else (cache maintenance); garbage means the code
// itself was overwritten. The fetch succeeded, so the page is mapped.
if ((frame->esr >> 26) == 0 && frame->elr >= 0xFFFF000000000000ULL && (frame->elr & 3) == 0) {
PrintBoxedHex(kerr, "Instruction at PC", *(volatile uint32_t*)frame->elr, BoxWidth);
}
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* PerCpu.hpp
* aarch64 per-CPU state (see Arch/Smp.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// Read by the exception vectors through TPIDR_EL1.
struct CpuEntryState {
uint64_t kernelStackTop; // SP_EL1 to use on entry from EL0
uint64_t scratch; // vector-private scratch slot
};
struct CpuState {
uint64_t mpidr; // MPIDR_EL1 affinity of this CPU
uint8_t gicTargetMask; // GICv2 CPU-interface bit (SGI/SPI targets)
uint64_t timerPeriod; // tick period in counter ticks, 0 = stopped
};
// TPIDR_EL1 holds the CpuData pointer (zeroed in EarlyInitialize until
// Smp::InitBsp installs it).
inline void* CurrentCpuLocal() {
void* ptr;
asm volatile("mrs %0, tpidr_el1" : "=r"(ptr));
return ptr;
}
inline void* TryCurrentCpuLocal() {
return CurrentCpuLocal();
}
}
+140
View File
@@ -0,0 +1,140 @@
/*
* Platform.cpp
* aarch64 bring-up hooks (implements Arch/Platform.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Platform.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Smp.hpp>
#include <Arch/aarch64/Gic.hpp>
#include <Arch/aarch64/Bringup.hpp>
#include <ACPI/FADT.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
using namespace Kt;
extern "C" char ArchExceptionVectors[]; // Vectors.S
namespace Arch {
// SCTLR_EL1.WXN: writable memory is execute-never
static constexpr uint64_t SctlrWxn = 1ULL << 19;
// SCTLR_EL1.UCT/UCI: EL0 may read CTR_EL0 and clean/invalidate caches
// by address (what __builtin___clear_cache needs for generated code).
static constexpr uint64_t SctlrUct = 1ULL << 15;
static constexpr uint64_t SctlrUci = 1ULL << 26;
// PSCI 0.2+ function IDs (SMC32 calling convention)
static constexpr uint64_t PsciSystemOff = 0x84000008;
static constexpr uint64_t PsciSystemReset = 0x84000009;
// How the firmware takes PSCI calls, from the FADT's ARM_BOOT_ARCH.
enum class PsciConduit { None, Smc, Hvc };
static PsciConduit g_psci = PsciConduit::None;
static uint64_t PsciCall(uint64_t function) {
register uint64_t x0 asm("x0") = function;
register uint64_t x1 asm("x1") = 0;
register uint64_t x2 asm("x2") = 0;
register uint64_t x3 asm("x3") = 0;
if (g_psci == PsciConduit::Hvc) {
asm volatile("hvc #0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3) :: "memory");
} else if (g_psci == PsciConduit::Smc) {
asm volatile("smc #0" : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3) :: "memory");
}
return x0;
}
static void InitializePsci(Hal::ACPI::CommonSDTHeader* xsdt) {
Hal::FADT::ParsedFADT fadt{};
if (!Hal::FADT::Parse(xsdt, fadt) || !fadt.Valid) return;
if (!(fadt.ArmBootArchFlags & Hal::FADT::ArmBootPsciCompliant)) {
KernelLogStream(WARNING, "PSCI") << "Firmware is not PSCI compliant: no reset or power-off";
return;
}
g_psci = (fadt.ArmBootArchFlags & Hal::FADT::ArmBootPsciUseHvc) ? PsciConduit::Hvc : PsciConduit::Smc;
KernelLogStream(OK, "PSCI") << "PSCI through " << (g_psci == PsciConduit::Hvc ? "HVC" : "SMC");
}
void InitializeCpuRegisters() {
// Run on SP_EL1 so exceptions taken from the kernel arrive on the
// stack it is already using (Vectors.S has no SP_EL0 handlers).
asm volatile("mov x9, sp\n\tmsr spsel, #1\n\tmov sp, x9" ::: "x9", "memory");
// The vectors need no allocation, so take over from the
// bootloader's right away.
asm volatile("msr vbar_el1, %0\n\tisb" :: "r"(ArchExceptionVectors) : "memory");
// Kernel mappings are RWX (Paging.cpp), so writable must not
// imply execute-never; user code may maintain its own caches.
uint64_t sctlr;
asm volatile("mrs %0, sctlr_el1" : "=r"(sctlr));
sctlr &= ~SctlrWxn;
sctlr |= SctlrUct | SctlrUci;
asm volatile("msr sctlr_el1, %0\n\tisb" :: "r"(sctlr));
// CPACR_EL1.FPEN = 0b11: FP/SIMD usable at EL0 and EL1 (the kernel
// itself is built -mgeneral-regs-only; Context.S saves user state).
uint64_t cpacr;
asm volatile("mrs %0, cpacr_el1" : "=r"(cpacr));
cpacr |= 3ULL << 20;
asm volatile("msr cpacr_el1, %0\n\tisb" :: "r"(cpacr));
}
void EarlyInitialize() {
// No per-CPU data yet: Smp::TryGetCurrentCpuData must see null.
asm volatile("msr tpidr_el1, xzr");
InitializeCpuRegisters();
}
// Installed by EarlyInitialize.
void InitializeTraps() {
KernelLogStream(OK, "Arch") << "Installed exception vectors at 0x" << base::hex
<< (uint64_t)ArchExceptionVectors;
}
// MAIR_EL1 attribute indices are chosen together with the page-table
// format (Paging.cpp), so there is nothing to program separately.
void InitializeMemoryAttributes() {
}
void InitializeInterrupts(Hal::ACPI::CommonSDTHeader* xsdt) {
Smp::InitBsp();
if (!Gic::Initialize(xsdt)) {
KernelLogStream(ERROR, "Arch") << "aarch64: no interrupt controller, running without IRQs";
}
InitializePsci(xsdt);
}
void InitializePlatformDevices() {
}
// EL0 entry needs no global state beyond Arch::ActivateThread.
void PrepareUserMode() {
}
void LateInitialize() {
}
void ListPlatformDevices(PlatformDeviceSink sink, void* context) {
if (Gic::IsInitialized()) {
sink(context, 1, "GICv2", "ARM Generic Interrupt Controller");
sink(context, 2, "Generic Timer", "ARM architected timer (EL1 virtual)");
}
}
void Reboot() {
PsciCall(PsciSystemReset);
HaltForever();
}
void PowerOff() {
if (g_psci != PsciConduit::None) {
PsciCall(PsciSystemOff);
}
}
}
+135
View File
@@ -0,0 +1,135 @@
/*
* Smp.cpp
* aarch64 per-CPU data and secondary CPU start-up (implements the arch half of Arch/Smp.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Smp.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Syscall.hpp>
#include <Arch/aarch64/Bringup.hpp>
#include <Arch/aarch64/Gic.hpp>
#include <Boot/BootInfo.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Timekeeping/Timer.hpp>
using namespace Kt;
namespace Smp {
static CpuData g_cpus[MaxCPUs];
static int g_cpuCount = 0;
static int g_detectedCpuCount = 1;
CpuData* GetCpuData(int index) {
if (index < 0 || index >= g_cpuCount) return nullptr;
return &g_cpus[index];
}
int GetCpuCount() {
return g_cpuCount;
}
int GetDetectedCpuCount() {
return g_detectedCpuCount;
}
void InitBsp() {
memset(g_cpus, 0, sizeof(g_cpus));
CpuData& bsp = g_cpus[0];
bsp.selfPtr = (uint64_t)&bsp;
bsp.cpuIndex = 0;
bsp.currentSlot = -1;
bsp.started = true;
asm volatile("mrs %0, mpidr_el1" : "=r"(bsp.arch.mpidr));
asm volatile("msr tpidr_el1, %0" :: "r"(&bsp));
g_cpuCount = 1;
KernelLogStream(OK, "SMP") << "BSP initialized (MPIDR " << base::hex
<< bsp.arch.mpidr << ")";
}
// A secondary CPU, woken by the bootloader on its own stack and still
// on the bootloader's translation tables. The kernel stashed this CPU's
// CpuData in extraArgument.
static void ApEntry(montauk::boot::BootCpu* bootCpu) {
CpuData* cpu = (CpuData*)bootCpu->extraArgument;
Arch::InitializeCpuRegisters();
Arch::ActivateKernelTranslation();
asm volatile("msr tpidr_el1, %0" :: "r"(cpu));
Arch::Gic::InitializeCpuInterface();
Arch::InitializeSyscallEntry();
Timekeeping::TimerInitializeAP();
__atomic_store_n(&cpu->started, true, __ATOMIC_RELEASE);
Arch::EnableInterrupts();
// Runnable work arrives as a reschedule IPI; keep the periodic tick
// masked while idle.
for (;;) {
Timekeeping::TimerEnterApIdle();
Timekeeping::ServiceDeferredWork();
Arch::DisableInterrupts();
Arch::HaltWithInterruptsDisabled();
Arch::EnableInterrupts();
}
}
void BootAPs(const montauk::boot::SmpInfo& smp) {
if (smp.cpus == nullptr || smp.startCpu == nullptr || smp.cpuCount <= 1) {
KernelLogStream(INFO, "SMP") << "Single CPU system - no secondary CPUs to start";
return;
}
g_detectedCpuCount = (int)smp.cpuCount;
if (!Arch::Gic::IsInitialized()) {
KernelLogStream(WARNING, "SMP") << "No interrupt controller: secondary CPUs left parked";
return;
}
int apIndex = 1;
for (uint64_t i = 0; i < smp.cpuCount && apIndex < MaxCPUs; i++) {
montauk::boot::BootCpu& info = smp.cpus[i];
if (info.hardwareId == smp.bspHardwareId) continue;
CpuData& ap = g_cpus[apIndex];
ap.selfPtr = (uint64_t)&ap;
ap.cpuIndex = apIndex;
ap.currentSlot = -1;
ap.started = false;
ap.arch.mpidr = info.hardwareId;
info.extraArgument = (uint64_t)&ap;
// Publish the CpuData before the CPU that reads it is woken.
asm volatile("dsb ish" ::: "memory");
smp.startCpu(&info, ApEntry);
apIndex++;
}
g_cpuCount = apIndex;
int online = 1;
for (int i = 1; i < g_cpuCount; i++) {
uint64_t start = Arch::ReadTimestamp();
uint64_t frequency;
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
while (!__atomic_load_n(&g_cpus[i].started, __ATOMIC_ACQUIRE) &&
Arch::ReadTimestamp() - start < frequency) {
Arch::Pause();
}
if (g_cpus[i].started) {
online++;
} else {
KernelLogStream(ERROR, "SMP") << "CPU " << base::dec << (uint64_t)i
<< " (MPIDR " << base::hex << g_cpus[i].arch.mpidr << ") failed to start";
}
}
KernelLogStream(OK, "SMP") << base::dec << (uint64_t)online << " CPU(s) online";
}
}
+16
View File
@@ -0,0 +1,16 @@
/*
* Syscall.cpp
* aarch64 system-call entry (implements Arch/Syscall.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Syscall.hpp>
namespace Arch {
// `svc #0` arrives through the EL0 synchronous vector installed by
// InitializeTraps; there is no per-CPU register to program.
void InitializeSyscallEntry() {
}
}
+25
View File
@@ -0,0 +1,25 @@
/*
* SyscallFrame.hpp
* aarch64 system-call frame (see Arch/Syscall.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Arch {
// The exception frame Vectors.S saves for `svc #0` (the same layout as
// Arch::PanicFrame): x0-x5 carry the arguments and x8 the system-call
// number; the result is written back to x0.
struct SyscallFrame {
uint64_t arg1, arg2, arg3, arg4, arg5, arg6; // x0-x5
uint64_t x6, x7;
uint64_t syscall_nr; // x8
uint64_t x9_x30[22];
uint64_t sp_el0, elr, spsr, esr, far;
};
static_assert(sizeof(SyscallFrame) == 288, "must match Vectors.S");
}
+116
View File
@@ -0,0 +1,116 @@
/*
* Timer.cpp
* aarch64 tick timer: the EL1 virtual generic timer (implements Arch/Timer.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#include <Arch/Timer.hpp>
#include <Arch/Interrupts.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/Smp.hpp>
#include <Arch/aarch64/Gic.hpp>
#include <ACPI/ACPI.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
// The generic timer counts at CNTFRQ_EL0 on every CPU, so it needs no
// calibration. Each CPU's virtual timer raises a PPI (the GTDT names it,
// normally INTID 27); the tick is re-armed from the interrupt's ack hook,
// which also drops the level-triggered line before the EOI.
using namespace Kt;
namespace Arch::Timer {
// CNTV_CTL_EL0
static constexpr uint64_t CtlEnable = 1ULL << 0;
static constexpr uint64_t CtlMask = 1ULL << 1;
// GTDT timer flags
static constexpr uint32_t GtdtEdgeTriggered = 1u << 0;
static constexpr uint32_t DefaultVirtualTimerIntid = 27;
struct Gtdt {
Hal::ACPI::CommonSDTHeader Header;
uint64_t CntControlBase;
uint32_t Reserved;
uint32_t SecureEl1Gsiv;
uint32_t SecureEl1Flags;
uint32_t NonSecureEl1Gsiv;
uint32_t NonSecureEl1Flags;
uint32_t VirtualTimerGsiv;
uint32_t VirtualTimerFlags;
} __attribute__((packed));
static uint64_t g_ticksPerMs = 0;
static uint32_t g_intid = DefaultVirtualTimerIntid;
static bool g_edgeTriggered = false;
static inline void WriteCtl(uint64_t value) {
asm volatile("msr cntv_ctl_el0, %0\n\tisb" :: "r"(value) : "memory");
}
static inline uint64_t ReadCval() {
uint64_t value;
asm volatile("mrs %0, cntv_cval_el0" : "=r"(value));
return value;
}
static inline void WriteCval(uint64_t value) {
asm volatile("msr cntv_cval_el0, %0\n\tisb" :: "r"(value) : "memory");
}
// Advance the deadline by one period from the previous one (no drift),
// skipping ticks the CPU missed rather than replaying them.
static void Rearm() {
uint64_t period = Smp::GetCurrentCpuData()->arch.timerPeriod;
if (period == 0) {
WriteCtl(CtlMask);
return;
}
uint64_t now = ReadTimestamp();
uint64_t next = ReadCval() + period;
if ((int64_t)(next - now) <= 0) next = now + period;
WriteCval(next);
}
static void ReadGtdt() {
auto* gtdt = (Gtdt*)Gic::FindTable(Gic::CurrentXsdt(), "GTDT");
if (gtdt == nullptr || gtdt->Header.Length < sizeof(Gtdt)) return;
if (gtdt->VirtualTimerGsiv >= 16 && gtdt->VirtualTimerGsiv < 32) {
g_intid = gtdt->VirtualTimerGsiv;
g_edgeTriggered = (gtdt->VirtualTimerFlags & GtdtEdgeTriggered) != 0;
}
}
bool InitializeBsp(uint32_t periodMs) {
uint64_t frequency;
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
if (frequency < 1000 || !Gic::IsInitialized()) return false;
g_ticksPerMs = frequency / 1000;
ReadGtdt();
Gic::SetAckHook(IRQ_TIMER, Rearm);
StartPeriodic(periodMs);
KernelLogStream(OK, "Timer") << "Generic timer: " << base::dec << frequency
<< " Hz counter, virtual timer INTID " << (uint64_t)g_intid;
return true;
}
// PPIs are banked: each CPU enables its own timer interrupt.
void StartPeriodic(uint32_t periodMs) {
Gic::RoutePpi(g_intid, IRQ_TIMER, g_edgeTriggered);
uint64_t period = g_ticksPerMs * periodMs;
Smp::GetCurrentCpuData()->arch.timerPeriod = period;
WriteCval(ReadTimestamp() + period);
WriteCtl(CtlEnable);
}
void Stop() {
Smp::GetCurrentCpuData()->arch.timerPeriod = 0;
WriteCtl(CtlMask);
}
}
+201
View File
@@ -0,0 +1,201 @@
/*
Vectors.S
aarch64 exception vector table (VBAR_EL1)
Copyright (c) 2026 Daniel Hammer
*/
/*
* Every exception saves the same 288-byte frame on SP_EL1, laid out as
* Arch::PanicFrame (InterruptFrame.hpp) and Arch::SyscallFrame:
*
* 0 x0 .. x30 (x30 at 240)
* 248 interrupted SP (SP_EL0 from EL0, the pre-exception SP_EL1 from EL1)
* 256 ELR_EL1
* 264 SPSR_EL1
* 272 ESR_EL1
* 280 FAR_EL1
*
* The kernel runs on SP_EL1 (SPSel = 1, see Platform.cpp). From EL0,
* SP_EL1 is the top of the thread's kernel stack: ArchEnterUserMode sets
* it before the first ERET and every return to EL0 restores it.
*/
#define FRAME_SIZE 288
#define FRAME_SP 248
#define FRAME_ELR 256
#define FRAME_SPSR 264
#define FRAME_ESR 272
#define FRAME_FAR 280
.text
.macro SAVE_FRAME el
sub sp, sp, #FRAME_SIZE
stp x0, x1, [sp, #0]
stp x2, x3, [sp, #16]
stp x4, x5, [sp, #32]
stp x6, x7, [sp, #48]
stp x8, x9, [sp, #64]
stp x10, x11, [sp, #80]
stp x12, x13, [sp, #96]
stp x14, x15, [sp, #112]
stp x16, x17, [sp, #128]
stp x18, x19, [sp, #144]
stp x20, x21, [sp, #160]
stp x22, x23, [sp, #176]
stp x24, x25, [sp, #192]
stp x26, x27, [sp, #208]
stp x28, x29, [sp, #224]
.if \el == 0
mrs x21, sp_el0
.else
add x21, sp, #FRAME_SIZE
.endif
stp x30, x21, [sp, #240]
mrs x22, elr_el1
mrs x23, spsr_el1
stp x22, x23, [sp, #FRAME_ELR]
mrs x24, esr_el1
mrs x25, far_el1
stp x24, x25, [sp, #FRAME_ESR]
.endm
/* Interrupts are masked again before ELR/SPSR are reloaded: an IRQ taken
in between would overwrite them. */
.macro RESTORE_FRAME el
msr daifset, #2
ldp x22, x23, [sp, #FRAME_ELR]
msr elr_el1, x22
msr spsr_el1, x23
.if \el == 0
ldr x21, [sp, #FRAME_SP]
msr sp_el0, x21
.endif
ldp x0, x1, [sp, #0]
ldp x2, x3, [sp, #16]
ldp x4, x5, [sp, #32]
ldp x6, x7, [sp, #48]
ldp x8, x9, [sp, #64]
ldp x10, x11, [sp, #80]
ldp x12, x13, [sp, #96]
ldp x14, x15, [sp, #112]
ldp x16, x17, [sp, #128]
ldp x18, x19, [sp, #144]
ldp x20, x21, [sp, #160]
ldp x22, x23, [sp, #176]
ldp x24, x25, [sp, #192]
ldp x26, x27, [sp, #208]
ldp x28, x29, [sp, #224]
ldr x30, [sp, #240]
add sp, sp, #FRAME_SIZE
eret
.endm
/* Each vector slot is 128 bytes; the handlers live outside the table. */
.macro VECTOR target
.balign 0x80
b \target
.endm
.balign 0x800
.global ArchExceptionVectors
ArchExceptionVectors:
/* Current EL with SP_EL0: the kernel never runs on SP_EL0. */
VECTOR el1t_unexpected
VECTOR el1t_unexpected
VECTOR el1t_unexpected
VECTOR el1t_unexpected
/* Current EL with SP_ELx */
VECTOR el1h_sync
VECTOR el1h_irq
VECTOR el1h_fiq
VECTOR el1h_serror
/* Lower EL, AArch64 */
VECTOR el0_sync
VECTOR el0_irq
VECTOR el0_fiq
VECTOR el0_serror
/* Lower EL, AArch32: not supported */
VECTOR el0_aarch32
VECTOR el0_aarch32
VECTOR el0_aarch32
VECTOR el0_aarch32
/* ---- Exceptions from the kernel ---- */
el1t_unexpected:
SAVE_FRAME 1
mov x0, sp
mov x1, #0
bl ArchUnexpectedException
el1h_sync:
SAVE_FRAME 1
mov x0, sp
mov x1, #0
bl ArchSyncException
RESTORE_FRAME 1
el1h_irq:
SAVE_FRAME 1
mov x0, sp
mov x1, #0
bl ArchIrqException
RESTORE_FRAME 1
el1h_fiq:
SAVE_FRAME 1
mov x0, sp
mov x1, #1
bl ArchUnexpectedException
el1h_serror:
SAVE_FRAME 1
mov x0, sp
mov x1, #2
bl ArchUnexpectedException
/* ---- Exceptions from user mode ---- */
el0_sync:
SAVE_FRAME 0
lsr x9, x24, #26 /* ESR_EL1.EC */
cmp x9, #0x15 /* SVC from AArch64 */
b.ne 1f
msr daifclr, #2
mov x0, sp
bl SyscallDispatch
str x0, [sp, #0]
bl SchedExitIfKilled
RESTORE_FRAME 0
1: mov x0, sp
mov x1, #1
bl ArchSyncException
RESTORE_FRAME 0
el0_irq:
SAVE_FRAME 0
mov x0, sp
mov x1, #1
bl ArchIrqException
RESTORE_FRAME 0
el0_fiq:
SAVE_FRAME 0
mov x0, sp
mov x1, #1
bl ArchUnexpectedException
el0_serror:
SAVE_FRAME 0
mov x0, sp
mov x1, #2
bl ArchUnexpectedException
el0_aarch32:
SAVE_FRAME 0
mov x0, sp
mov x1, #3
bl ArchUnexpectedException
+62
View File
@@ -0,0 +1,62 @@
/*
* Abi.hpp
* x86_64 user-mode ABI details (implements Arch/Abi.hpp)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
// UEFI uses the Microsoft x64 calling convention.
#define ARCH_EFIAPI __attribute__((__ms_abi__))
namespace Arch {
constexpr uint16_t ElfMachine = 62; // EM_X86_64
constexpr const char* ElfMachineName = "x86_64";
inline RelocationKind ClassifyRelocation(uint32_t type) {
switch (type) {
case 0: return RelocationKind::None;
case 8: return RelocationKind::Relative; // R_X86_64_RELATIVE
case 1: // R_X86_64_64
case 6: // R_X86_64_GLOB_DAT
case 7: return RelocationKind::Symbol; // R_X86_64_JUMP_SLOT
default: return RelocationKind::Unsupported;
}
}
// Variant II. The linker addresses TLS variables at
// TP - align_up(memSize, p_align) + offset, so the template must start
// exactly that far below TP; rounding p_align up first (as an older
// version did) shifts every variable away from its initial value.
// TP itself is kept at least 16-byte aligned, which the gap between
// base and the template absorbs. The 16 bytes above TP are the TCB.
inline TlsLayout ComputeTlsLayout(uint64_t memSize, uint64_t pAlign) {
uint64_t align = pAlign ? pAlign : 1;
uint64_t block = (memSize + align - 1) & ~(align - 1);
uint64_t tpAlign = align < 16 ? 16 : align;
uint64_t tp = (block + tpAlign - 1) & ~(tpAlign - 1);
return {tp - block, tp, tp + 16};
}
inline void WriteExitStub(uint8_t* stub) {
stub[0] = 0x31; stub[1] = 0xFF; // xor edi, edi (exit code 0)
stub[2] = 0x31; stub[3] = 0xC0; // xor eax, eax (SYS_EXIT = 0)
stub[4] = 0x0F; stub[5] = 0x05; // syscall
}
// The entry function is entered as if CALLed: the return address sits
// at [RSP], with RSP = 16n - 8.
inline uint64_t SetupUserEntryStack(uint8_t* topPage, uint64_t stackTop,
uint64_t returnAddress) {
*(uint64_t*)(topPage + 0xFF8) = returnAddress;
return stackTop - 8;
}
// SystemV alignment as if CALLed (16n - 8); the slot is never read.
inline uint64_t ThreadEntryStackPointer(uint64_t stackTop) {
return (stackTop & ~0xFULL) - 8;
}
}
@@ -4,8 +4,10 @@
* Copyright (c) 2026 Daniel Hammer
*/
#include "AcpiSleep.hpp"
#include "AcpiEvents.hpp"
#include <ACPI/AcpiSleep.hpp>
#include <ACPI/AcpiEvents.hpp>
#include <Arch/Cpu.hpp>
#include <Arch/x86_64/Acpi/S3State.hpp>
#include <ACPI/FADT.hpp>
#include <ACPI/AML/AmlParser.hpp>
#include <ACPI/AML/AmlInterpreter.hpp>
@@ -14,18 +16,19 @@
#include <CppLib/Stream.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <Hal/GDT.hpp>
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/IoApic.hpp>
#include <Hal/Apic/Pic.hpp>
#include <Timekeeping/ApicTimer.hpp>
#include <Hal/CpuPower.hpp>
#include <Arch/x86_64/GDT.hpp>
#include <Arch/x86_64/Apic/Apic.hpp>
#include <Arch/x86_64/Apic/IoApic.hpp>
#include <Arch/x86_64/Apic/Pic.hpp>
#include <Timekeeping/Timer.hpp>
#include <Arch/CpuPower.hpp>
#include <Drivers/Graphics/IntelGPU.hpp>
#include <Drivers/Graphics/DgpuPower.hpp>
#include <Drivers/PS2/PS2Controller.hpp>
#include <Graphics/Framebuffer.hpp>
#include <Libraries/Memory.hpp>
#include <Hal/MSR.hpp>
#include <Hal/SmpBoot.hpp>
#include <Arch/x86_64/MSR.hpp>
#include <Arch/Smp.hpp>
#include <Api/Syscall.hpp>
using namespace Kt;
@@ -128,9 +131,9 @@ namespace Hal {
const char* stepNames[] = {
"?", "GDT", "TSS", "Syscalls", "PAT", "PIC",
"LocalAPIC", "IoAPIC", "APICTimer", "PM1Clear",
"WAK", "PS2", "IntelGPU", "sti"
"WAK", "PS2", "IntelGPU", "dGPU", "sti"
};
const char* stepName = reinitStep <= 0x0D ? stepNames[reinitStep] : "?";
const char* stepName = reinitStep <= 0x0E ? stepNames[reinitStep] : "?";
KernelLogStream(WARNING, "S3") << "Reinit stopped after step 0x"
<< base::hex << (uint64_t)reinitStep << " (" << stepName
<< ") - next step crashed/hung";
@@ -185,7 +188,7 @@ namespace Hal {
<< base::hex << (uint64_t)g_s3SlpTypA
<< " SLP_TYPb=" << base::hex << (uint64_t)g_s3SlpTypB << ")";
KernelLogStream(INFO, "S3") << "Kernel PML4 phys = " << base::hex
<< (uint64_t)Memory::VMM::g_paging->PML4;
<< Memory::VMM::g_paging->KernelRoot();
KernelLogStream(INFO, "S3") << "FACS at phys " << base::hex
<< (uint64_t)fadt.FacsAddress << " version=" << base::dec
<< (uint64_t)g_facs->Version << " flags=" << base::hex
@@ -313,7 +316,7 @@ namespace Hal {
// shadow copy of the PML4 at a fixed low-memory address.
// Only the PML4 page itself needs to be below 4GB — its entries
// contain full 52-bit physical addresses for sub-tables.
uint64_t kernelPml4Phys = (uint64_t)Memory::VMM::g_paging->PML4;
uint64_t kernelPml4Phys = Memory::VMM::g_paging->KernelRoot();
uint8_t* kernelPml4Virt = (uint8_t*)Memory::HHDM(kernelPml4Phys);
uint8_t* shadowPml4Virt = (uint8_t*)Memory::HHDM((uint64_t)SHADOW_PML4_PHYS);
memcpy(shadowPml4Virt, kernelPml4Virt, 4096);
@@ -438,7 +441,7 @@ namespace Hal {
Hal::InitializePAT();
// HWP enable and POWER_CTL reset across S3; re-apply them and
// bump the policy epoch so APs re-apply on their next tick.
Hal::CpuPower::ReapplyAfterWake();
Arch::CpuPower::ReapplyAfterWake();
reinitProgress(0x05); // PIC
Hal::DisableLegacyPic();
reinitProgress(0x06); // Local APIC
@@ -446,7 +449,7 @@ namespace Hal {
reinitProgress(0x07); // IO APIC
Hal::IoApic::Reinitialize();
reinitProgress(0x08); // APIC Timer
Timekeeping::ApicTimerReinitialize();
Timekeeping::TimerReinitialize();
reinitProgress(0x09); // PM1 clear
ClearPM1Status();
reinitProgress(0x0A); // _WAK
@@ -459,8 +462,10 @@ namespace Hal {
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
Drivers::Graphics::IntelGPU::Reinitialize();
}
reinitProgress(0x0D); // sti
asm volatile("sti");
reinitProgress(0x0D); // discrete GPU back off
Drivers::Graphics::DgpuPower::Resume();
reinitProgress(0x0E); // sti
Arch::EnableInterrupts();
KernelLogStream(OK, "S3") << "Resumed from S3 suspend";
@@ -514,7 +519,7 @@ namespace Hal {
Io::Out8(0x00, 0x71);
// 3. Disable interrupts
asm volatile("cli");
Arch::DisableInterrupts();
// 4. Identity-map the trampoline page so the CPU can execute it
// after paging is re-enabled with our CR3 during wake.
@@ -522,7 +527,7 @@ namespace Hal {
Memory::VMM::g_paging->Map(TRAMPOLINE_PHYS, TRAMPOLINE_PHYS);
// 5. Flush all caches
asm volatile("wbinvd");
Arch::WritebackInvalidateCaches();
// 6. Set the waking vector in FACS
SetWakingVector();
@@ -545,7 +550,7 @@ namespace Hal {
}
// 10. Flush caches again
asm volatile("wbinvd");
Arch::WritebackInvalidateCaches();
// 11. Assert SLP_EN — this triggers S3 entry
Io::Out16(pm1a | PM1_SLP_EN, (uint16_t)g_pm1aControlBlock);
@@ -560,7 +565,7 @@ namespace Hal {
// on QEMU/KVM the vCPU must reach HLT before QEMU finalizes S3.
// On resume, firmware jumps to our waking vector.
for (;;) {
asm volatile("hlt");
Arch::Halt();
// If we wake from HLT (spurious or real wake), check WAK_STS
if (g_pm1aEventBlock != 0) {
uint16_t sts = Io::In16((uint16_t)g_pm1aEventBlock);
@@ -571,7 +576,7 @@ namespace Hal {
// If we get here, either S3 failed or we woke immediately.
// The normal wake path goes through AcpiResumeLongMode instead.
// Re-enable interrupts as a safety measure.
asm volatile("sti");
Arch::EnableInterrupts();
KernelLogStream(WARNING, "S3") << "S3 entry may have failed (fell through)";
return 0;
@@ -5,10 +5,11 @@
*/
#include "CpuIdle.hpp"
#include <Arch/Cpu.hpp>
#include <ACPI/FADT.hpp>
#include <ACPI/AML/AmlInterpreter.hpp>
#include <Hal/Cpu.hpp>
#include <Hal/SmpBoot.hpp>
#include <Arch/x86_64/CpuFeatures.hpp>
#include <Arch/Smp.hpp>
#include <Io/IoPort.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
@@ -249,13 +250,17 @@ namespace Hal {
}
static bool ParseCstObject(const char* path, const AML::Object& obj) {
if (obj.Type != AML::ObjectType::Package || obj.Buffer.Length < 2) {
if (obj.Type != AML::ObjectType::Package || !obj.Package.Data ||
obj.Package.Length < 1) {
return false;
}
uint32_t pos = 1; // package element count byte
// Package.Data starts at the first element (the state count).
const uint8_t* data = obj.Package.Data;
uint32_t length = obj.Package.Length;
uint32_t pos = 0;
uint64_t declaredStateCount = 0;
if (!DecodeInteger(obj.Buffer.Data, obj.Buffer.Length, pos, declaredStateCount)) {
if (!DecodeInteger(data, length, pos, declaredStateCount)) {
return false;
}
@@ -263,7 +268,7 @@ namespace Hal {
for (uint64_t i = 0; i < declaredStateCount && parsedCount < MaxIdleStates; i++) {
const uint8_t* nestedData = nullptr;
uint32_t nestedLength = 0;
if (!DecodePackageObject(obj.Buffer.Data, obj.Buffer.Length, pos, nestedData, nestedLength)) {
if (!DecodePackageObject(data, length, pos, nestedData, nestedLength)) {
break;
}
@@ -310,7 +315,10 @@ namespace Hal {
auto& ns = interp.GetNamespace();
char path[256];
for (int32_t nodeIndex = 0; nodeIndex < ns.NodeCount(); nodeIndex++) {
// DSDT processors only, for the same reason as the _CST scan.
int32_t limit = interp.DsdtNodeCount();
if (limit == 0) limit = ns.NodeCount();
for (int32_t nodeIndex = 0; nodeIndex < limit; nodeIndex++) {
auto* node = ns.GetNode(nodeIndex);
if (node == nullptr || node->Obj.Type != AML::ObjectType::Processor) {
continue;
@@ -473,7 +481,7 @@ namespace Hal {
if (hasMwait) {
Hal::IdleWaitWithInterruptsDisabled(addr, CurrentIdleHint());
} else {
Hal::HaltWithInterruptsDisabled();
Arch::HaltWithInterruptsDisabled();
}
}
@@ -522,7 +530,13 @@ namespace Hal {
auto& ns = interp.GetNamespace();
char path[256];
for (int32_t nodeIndex = 0; nodeIndex < ns.NodeCount(); nodeIndex++) {
// DSDT objects only. SSDTs are loaded too now (for device power
// methods), and many carry processor _CST tables; those C-state
// lists have never been exercised against the deep-idle timer
// behaviour on real laptops, so they stay out until they have.
int32_t limit = interp.DsdtNodeCount();
if (limit == 0) limit = ns.NodeCount();
for (int32_t nodeIndex = 0; nodeIndex < limit; nodeIndex++) {
auto* node = ns.GetNode(nodeIndex);
if (node == nullptr) continue;
if (!IsName(node->Name, "_CST")) continue;
File renamed without changes.
+30
View File
@@ -0,0 +1,30 @@
/*
* S3State.hpp
* CPU state saved across S3 by S3Wake.asm
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Hal {
namespace AcpiSleep {
// ============================================================================
// Layout must match S3Wake.asm offsets exactly.
// No FPU/SSE state — the kernel is compiled with -mno-sse.
struct CpuState {
uint64_t Rax, Rbx, Rcx, Rdx; // 0x00 - 0x18
uint64_t Rsi, Rdi, Rbp, Rsp; // 0x20 - 0x38
uint64_t R8, R9, R10, R11; // 0x40 - 0x58
uint64_t R12, R13, R14, R15; // 0x60 - 0x78
uint64_t Rflags; // 0x80
uint64_t Rip; // 0x88
uint64_t Cr3; // 0x90
uint64_t Cr0, Cr4; // 0x98, 0xA0
uint8_t GdtPtr[10]; // 0xA8
uint8_t IdtPtr[10]; // 0xB2
} __attribute__((aligned(16)));
};
};
File renamed without changes.
Loaded 100 of 591 files, more files were not shown because too many files have changed in this diff. Show more