Compare commits
61
Commits
5b90869101
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51a2b3a932 | ||
|
|
8b4a938164 | ||
|
|
be84506fdf | ||
|
|
559c3e2875 | ||
|
|
36291cd1cc | ||
|
|
b781cf8c97 | ||
|
|
7e036c94e7 | ||
|
|
1d00c1c016 | ||
|
|
3310b3d6de | ||
|
|
93d8becce3 | ||
|
|
ffee9002a6 | ||
|
|
808e1d66fd | ||
|
|
8c09ccef66 | ||
|
|
8b6379fb7e | ||
|
|
aae4db01ad | ||
|
|
1bae77ec84 | ||
|
|
dab7d5427b | ||
|
|
0a20385e12 | ||
|
|
8ae19258b1 | ||
|
|
8de98f3ab3 | ||
|
|
d3e49a925d | ||
|
|
07191b1286 | ||
|
|
e047c1e0c0 | ||
|
|
23cc56b933 | ||
|
|
3c2eefe7f4 | ||
|
|
14c7099e3b | ||
|
|
e87bd358a6 | ||
|
|
9e66c3b249 | ||
|
|
31edff3da4 | ||
|
|
f3d475a0d5 | ||
|
|
278b208258 | ||
|
|
f8b8f9610f | ||
|
|
a81298fb24 | ||
|
|
abfd9de8f6 | ||
|
|
60ca9c78c7 | ||
|
|
2aa6360b3d | ||
|
|
32660ddd1a | ||
|
|
a51ee3339f | ||
|
|
f9e21e0f13 | ||
|
|
7979acc2eb | ||
|
|
ef1e69ae83 | ||
|
|
c91de69189 | ||
|
|
41de95b277 | ||
|
|
24f88ec4c6 | ||
|
|
7f12e1a150 | ||
|
|
63b4bd51e8 | ||
|
|
2e3055f5f8 | ||
|
|
5e4e8902c2 | ||
|
|
8a3ffb2be1 | ||
|
|
93894b8fe5 | ||
|
|
2eee6696ef | ||
|
|
ae5e84592d | ||
|
|
c54181103f | ||
|
|
95e8a739d4 | ||
|
|
75e85f24ef | ||
|
|
80b5beb17d | ||
|
|
420a7c996c | ||
|
|
36cbee1415 | ||
|
|
f95bce01d1 | ||
|
|
e641500b40 | ||
|
|
0a18b2cf2d |
No files matched your search
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
-117
@@ -1,117 +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.
|
||||
|
||||
## 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).
|
||||
|
||||
The `power` program (`programs/src/power`) prints it:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 = .;
|
||||
}
|
||||
+2869
-771
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include "AmlNamespace.hpp"
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
@@ -96,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;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -103,7 +133,29 @@ 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
|
||||
// legitimately deep -- every If/Else/While body is another level, so a
|
||||
// long firmware method reaches well past a handful. The real protection
|
||||
// against running off the stack is HaveStackHeadroom(), which measures
|
||||
// actual usage; this only catches a decode that recurses without
|
||||
// consuming input.
|
||||
static constexpr int MaxExprDepth = 48;
|
||||
|
||||
// How much stack the interpreter may consume before it gives up. Note
|
||||
// this is an ALLOWANCE, not a reserve: frames here are expensive (a
|
||||
// name-resolving EvalTerm frame carries a 256-byte path buffer plus
|
||||
// 272-byte Object temporaries, so ~800 bytes each), and a real
|
||||
// firmware _BIF nests far enough to need tens of them. Sized against
|
||||
// Sched::StackPages -- keep well under it, since the syscall path below
|
||||
// has already spent some of that stack.
|
||||
static constexpr uint64_t StackBudget = 32768;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -114,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.
|
||||
@@ -125,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);
|
||||
|
||||
@@ -138,18 +214,67 @@ 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.
|
||||
void SetTrace(int budget) { m_traceBudget = budget; }
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Packages
|
||||
|
||||
// ============================================================================
|
||||
// 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. 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.
|
||||
int GetPackageCount(const Object& package);
|
||||
|
||||
// Give a package stored in a namespace node real per-element
|
||||
// storage so Index() can be written through. Returns the store
|
||||
// index, or -1 if the node does not hold a package.
|
||||
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);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -158,8 +283,21 @@ namespace Hal {
|
||||
// ============================================================================
|
||||
// Read a NameString from AML and produce an absolute path.
|
||||
// Advances *pos past the name.
|
||||
// Produces the absolute path (rebased onto scopeNode for a
|
||||
// relative name). outRelative, when given, receives the name as it
|
||||
// actually appeared in the AML, and *outSearchable says whether the
|
||||
// ACPI search rule applies to it -- true only when the name carried
|
||||
// no root or parent prefix. Resolution must fall back to
|
||||
// Namespace::SearchUpward() in that case: the absolute path is a
|
||||
// guess at one scope, and the object usually lives in a parent.
|
||||
int ReadNameString(const uint8_t* aml, uint32_t* pos, int32_t scopeNode,
|
||||
char* outPath, int maxLen);
|
||||
char* outPath, int maxLen,
|
||||
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);
|
||||
@@ -170,6 +308,12 @@ 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. 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) const;
|
||||
uint64_t DecodeInteger(const uint8_t* aml, uint32_t* pos);
|
||||
|
||||
// ============================================================================
|
||||
@@ -187,7 +331,28 @@ namespace Hal {
|
||||
Object ReturnValue;
|
||||
bool Returned;
|
||||
bool Broken;
|
||||
int Depth;
|
||||
bool Continued;
|
||||
bool Aborted; // undecodable AML: unwind the method
|
||||
int Depth; // method-call nesting
|
||||
int ExprDepth; // expression/block nesting
|
||||
};
|
||||
|
||||
// Increments a depth counter for the duration of a scope; pairs
|
||||
// with the MaxExprDepth check at each recursive entry point.
|
||||
struct DepthGuard {
|
||||
int& Value;
|
||||
explicit DepthGuard(int& v) : Value(v) { Value++; }
|
||||
~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);
|
||||
@@ -196,35 +361,140 @@ namespace Hal {
|
||||
// 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.
|
||||
bool EvalTarget(ExecContext& ctx, uint32_t* pos,
|
||||
int32_t& nodeIndex, bool& isLocal, int& localIdx,
|
||||
bool& isArg, int& argIdx);
|
||||
// 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);
|
||||
// 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);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// State
|
||||
|
||||
// ============================================================================
|
||||
// ExecContext is ~4.4 KB (sixteen Objects, each carrying a
|
||||
// 256-byte buffer inline). One per nested method call on a 16 KiB
|
||||
// kernel stack overflows after three levels, so they come from a
|
||||
// pool instead. The pool is only safe because evaluation is
|
||||
// serialized -- see m_evalLock.
|
||||
// Stack-exhaustion check; `frame` is the address of a local in
|
||||
// the calling frame.
|
||||
bool HaveStackHeadroom(const void* frame) const;
|
||||
|
||||
// One-shot warning when the nesting backstop trips.
|
||||
static void ReportDepthLimit();
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -7,10 +7,118 @@
|
||||
#include "AmlNamespace.hpp"
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Memory/Heap.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace AML {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Package element store
|
||||
|
||||
// ============================================================================
|
||||
namespace PackageStore {
|
||||
|
||||
struct Entry {
|
||||
Object* Elements;
|
||||
int Count;
|
||||
};
|
||||
|
||||
static Entry s_persistent[MaxPersistent];
|
||||
static Entry s_transient[MaxTransient];
|
||||
static int s_persistentUsed = 0;
|
||||
static int s_transientNext = 0;
|
||||
|
||||
// Indices handed out are 1-based so that a zeroed Object (nodes are
|
||||
// memset before Clear() runs, and callers zero-init freely) never
|
||||
// looks like a reference to entry 0.
|
||||
static constexpr int32_t TransientBase = MaxPersistent;
|
||||
|
||||
static Object* AllocElements(int count) {
|
||||
auto* mem = (Object*)Memory::g_heap->Request(sizeof(Object) * count);
|
||||
if (!mem) return nullptr;
|
||||
for (int i = 0; i < count; i++)
|
||||
mem[i] = Object{};
|
||||
return mem;
|
||||
}
|
||||
|
||||
int32_t AllocPersistent(int count) {
|
||||
if (count <= 0 || count > MaxPackageElements) return -1;
|
||||
if (s_persistentUsed >= MaxPersistent) return -1;
|
||||
|
||||
Entry& e = s_persistent[s_persistentUsed];
|
||||
e.Elements = AllocElements(count);
|
||||
if (!e.Elements) return -1;
|
||||
e.Count = count;
|
||||
return (int32_t)(s_persistentUsed++) + 1;
|
||||
}
|
||||
|
||||
int32_t AllocTransient(int count) {
|
||||
if (count <= 0 || count > MaxPackageElements) return -1;
|
||||
|
||||
int slot = s_transientNext;
|
||||
if (slot >= MaxTransient) {
|
||||
// The ring is only rewound between top-level evaluations, so
|
||||
// a method that builds more packages than this reuses the
|
||||
// oldest slots rather than failing outright.
|
||||
slot = 0;
|
||||
s_transientNext = 0;
|
||||
}
|
||||
s_transientNext++;
|
||||
|
||||
Entry& e = s_transient[slot];
|
||||
// Element arrays are sized on first use and reused afterwards;
|
||||
// a larger request reallocates.
|
||||
if (e.Elements == nullptr || e.Count < count) {
|
||||
Object* mem = AllocElements(count);
|
||||
if (!mem) return -1;
|
||||
if (e.Elements) Memory::g_heap->Free(e.Elements);
|
||||
e.Elements = mem;
|
||||
} else {
|
||||
for (int i = 0; i < count; i++)
|
||||
e.Elements[i] = Object{};
|
||||
}
|
||||
e.Count = count;
|
||||
return TransientBase + slot + 1;
|
||||
}
|
||||
|
||||
void ResetTransient() {
|
||||
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;
|
||||
|
||||
if (i < MaxPersistent) {
|
||||
if (i >= s_persistentUsed) return nullptr;
|
||||
return s_persistent[i].Elements ? &s_persistent[i] : nullptr;
|
||||
}
|
||||
|
||||
i -= TransientBase;
|
||||
if (i < 0 || i >= MaxTransient) return nullptr;
|
||||
return s_transient[i].Elements ? &s_transient[i] : nullptr;
|
||||
}
|
||||
|
||||
Object* Element(int32_t index, int element) {
|
||||
Entry* e = Lookup(index);
|
||||
if (!e || element < 0 || element >= e->Count) return nullptr;
|
||||
return &e->Elements[element];
|
||||
}
|
||||
|
||||
int Count(int32_t index) {
|
||||
Entry* e = Lookup(index);
|
||||
return e ? e->Count : 0;
|
||||
}
|
||||
};
|
||||
|
||||
Namespace::Namespace() : m_chunkCount(0), m_nodeCount(0) {
|
||||
for (int i = 0; i < MaxChunks; i++)
|
||||
m_chunks[i] = nullptr;
|
||||
@@ -30,7 +138,17 @@ namespace Hal {
|
||||
}
|
||||
|
||||
bool Namespace::AllocChunk() {
|
||||
if (m_chunkCount >= MaxChunks) return false;
|
||||
if (m_chunkCount >= MaxChunks) {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
KernelLogStream(WARNING, "AML")
|
||||
<< "Namespace node pool full at " << base::dec
|
||||
<< (uint64_t)(MaxChunks * NodesPerChunk)
|
||||
<< " nodes; part of the namespace will be missing";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* chunk = (NamespaceNode*)Memory::g_heap->Request(
|
||||
sizeof(NamespaceNode) * NodesPerChunk);
|
||||
@@ -45,6 +163,41 @@ namespace Hal {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Namespace::AppendChild(NamespaceNode* parent, int32_t child) {
|
||||
if (parent->ChildCount >= parent->ChildCapacity) {
|
||||
int32_t newCap = parent->ChildCapacity ? parent->ChildCapacity * 2
|
||||
: InitialChildren;
|
||||
|
||||
auto* grown = (int32_t*)Memory::g_heap->Request(sizeof(int32_t) * newCap);
|
||||
if (!grown) {
|
||||
// Losing a child silently costs whole devices, so say it
|
||||
// once rather than leaving a hole nobody can see.
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
KernelLogStream(WARNING, "AML")
|
||||
<< "Out of heap growing a namespace node's child list;"
|
||||
<< " part of the namespace will be missing";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < parent->ChildCount; i++)
|
||||
grown[i] = parent->ChildIndices[i];
|
||||
for (int32_t i = parent->ChildCount; i < newCap; i++)
|
||||
grown[i] = -1;
|
||||
|
||||
if (parent->ChildIndices)
|
||||
Memory::g_heap->Free(parent->ChildIndices);
|
||||
|
||||
parent->ChildIndices = grown;
|
||||
parent->ChildCapacity = newCap;
|
||||
}
|
||||
|
||||
parent->ChildIndices[parent->ChildCount++] = child;
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t Namespace::AllocNode() {
|
||||
// Check if we need a new chunk
|
||||
int32_t capacity = m_chunkCount * NodesPerChunk;
|
||||
@@ -144,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];
|
||||
@@ -165,11 +324,7 @@ namespace Hal {
|
||||
// Add to parent's children
|
||||
auto* parent = GetNode(current);
|
||||
if (!parent) return -1;
|
||||
if (parent->ChildCount < MaxChildren) {
|
||||
parent->ChildIndices[parent->ChildCount++] = child;
|
||||
} else {
|
||||
return -1; // too many children
|
||||
}
|
||||
if (!AppendChild(parent, child)) return -1;
|
||||
}
|
||||
current = child;
|
||||
}
|
||||
@@ -188,6 +343,51 @@ namespace Hal {
|
||||
return current;
|
||||
}
|
||||
|
||||
int32_t Namespace::SearchUpward(const char* relative, int32_t scopeNodeIndex) const {
|
||||
char segments[MaxPathDepth][MaxNameSegLen + 1];
|
||||
int segCount = 0;
|
||||
|
||||
// Split on '.', padding each segment to four characters the way
|
||||
// node names are stored.
|
||||
int i = 0;
|
||||
while (relative[i] && segCount < MaxPathDepth) {
|
||||
char seg[MaxNameSegLen + 1];
|
||||
int n = 0;
|
||||
while (relative[i] && relative[i] != '.' && n < MaxNameSegLen)
|
||||
seg[n++] = relative[i++];
|
||||
seg[n] = '\0';
|
||||
while (relative[i] && relative[i] != '.') i++; // overlong segment
|
||||
if (relative[i] == '.') i++;
|
||||
|
||||
PadSegment(seg, segments[segCount]);
|
||||
segCount++;
|
||||
}
|
||||
|
||||
if (segCount == 0) return -1;
|
||||
|
||||
// Walk up from the current scope looking for the first segment.
|
||||
int32_t scope = scopeNodeIndex;
|
||||
while (scope >= 0) {
|
||||
int32_t found = FindChildByName(scope, segments[0]);
|
||||
if (found >= 0) {
|
||||
// Descend the rest. A scope that matches the first segment
|
||||
// but not the remainder does not win -- keep walking up.
|
||||
int32_t cur = found;
|
||||
bool ok = true;
|
||||
for (int k = 1; k < segCount; k++) {
|
||||
cur = FindChildByName(cur, segments[k]);
|
||||
if (cur < 0) { ok = false; break; }
|
||||
}
|
||||
if (ok) return cur;
|
||||
}
|
||||
|
||||
auto* node = GetNode(scope);
|
||||
scope = node ? node->ParentIndex : -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t Namespace::ResolveName(const char* name, int32_t scopeNodeIndex) const {
|
||||
// If it starts with '\', it's absolute
|
||||
if (name[0] == '\\')
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -55,10 +65,15 @@ namespace Hal {
|
||||
// ============================================================================
|
||||
static constexpr int MaxNameSegLen = 4;
|
||||
static constexpr int MaxPathDepth = 16;
|
||||
static constexpr int MaxChildren = 64;
|
||||
// Children are held in a heap array that grows on demand, starting at
|
||||
// this many entries. A fixed ceiling here is not survivable: an EC or
|
||||
// GPE scope in a modern laptop DSDT carries well over a hundred named
|
||||
// objects, and dropping the overflow loses whole devices (the battery
|
||||
// among them) with no way to tell from the outside.
|
||||
static constexpr int InitialChildren = 8;
|
||||
static constexpr int MaxStringLen = 64;
|
||||
static constexpr int MaxBufferLen = 256;
|
||||
static constexpr int MaxPackageElements = 16;
|
||||
static constexpr int MaxPackageElements = 64;
|
||||
static constexpr int MaxMethodArgs = 7;
|
||||
static constexpr int MaxMethodLocals = 8;
|
||||
|
||||
@@ -76,6 +91,14 @@ namespace Hal {
|
||||
struct Object {
|
||||
ObjectType Type = ObjectType::None;
|
||||
|
||||
// Packages keep their raw AML bytes in Buffer (constant elements can
|
||||
// be decoded straight from there), but a package whose elements are
|
||||
// computed or written through Index() needs real per-element
|
||||
// storage. PkgIndex names that storage once it is materialized;
|
||||
// -1 means "not materialized yet". It lives outside the union so a
|
||||
// package can carry both representations at once.
|
||||
int32_t PkgIndex = -1;
|
||||
|
||||
union {
|
||||
uint64_t Integer;
|
||||
|
||||
@@ -89,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;
|
||||
|
||||
@@ -103,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;
|
||||
@@ -116,7 +173,51 @@ namespace Hal {
|
||||
} Processor;
|
||||
};
|
||||
|
||||
Object() : Type(ObjectType::None), Integer(0) {}
|
||||
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
|
||||
|
||||
// ============================================================================
|
||||
// Element arrays for materialized packages, kept outside Object so the
|
||||
// tagged union stays small. Entries are split in two: a persistent
|
||||
// region for packages declared with Name() (materialized once and kept
|
||||
// for the life of the namespace) and a transient ring for packages
|
||||
// built inside a method, which is rewound at each top-level evaluation
|
||||
// so repeated calls -- polling _BST, say -- cannot exhaust it.
|
||||
namespace PackageStore {
|
||||
static constexpr int MaxPersistent = 32;
|
||||
static constexpr int MaxTransient = 32;
|
||||
|
||||
// Allocate an element array of `count` objects. Returns -1 when the
|
||||
// store is full or the count is out of range.
|
||||
int32_t AllocPersistent(int count);
|
||||
int32_t AllocTransient(int count);
|
||||
|
||||
// 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);
|
||||
int Count(int32_t index);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -129,16 +230,19 @@ namespace Hal {
|
||||
char Name[MaxNameSegLen + 1]; // null-terminated 4-char segment
|
||||
Object Obj;
|
||||
int32_t ParentIndex; // -1 for root
|
||||
int32_t ChildIndices[MaxChildren];
|
||||
int32_t* ChildIndices; // heap array, ChildCapacity long
|
||||
int32_t ChildCount;
|
||||
int32_t ChildCapacity;
|
||||
|
||||
// Only ever called on a freshly allocated (zeroed) node, so there is
|
||||
// no child array to release here.
|
||||
void Clear() {
|
||||
Name[0] = 0;
|
||||
Obj = Object{};
|
||||
ParentIndex = -1;
|
||||
ChildIndices = nullptr;
|
||||
ChildCount = 0;
|
||||
for (int i = 0; i < MaxChildren; i++)
|
||||
ChildIndices[i] = -1;
|
||||
ChildCapacity = 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -166,6 +270,18 @@ namespace Hal {
|
||||
// 3. Root scope
|
||||
int32_t ResolveName(const char* name, int32_t scopeNodeIndex) const;
|
||||
|
||||
// The ACPI name search rule: for a name written WITHOUT a root or
|
||||
// parent prefix, search the current scope for its first segment,
|
||||
// then each parent scope up to the root, and descend the remaining
|
||||
// segments from wherever it matched. `relative` is the name as it
|
||||
// appeared in the AML ("PBIF", "BAT0.PBIF"), never a path already
|
||||
// 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;
|
||||
@@ -217,6 +333,11 @@ namespace Hal {
|
||||
private:
|
||||
int32_t AllocNode();
|
||||
bool AllocChunk();
|
||||
|
||||
// Append a child index to a parent, growing its array as needed.
|
||||
// Returns false only when the heap is exhausted.
|
||||
bool AppendChild(NamespaceNode* parent, int32_t child);
|
||||
|
||||
void EnsureRoot();
|
||||
int32_t FindChildByName(int32_t parentIndex, const char* seg) const;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
};
|
||||
};
|
||||
@@ -319,6 +319,58 @@ namespace Hal {
|
||||
|
||||
// EnumerateAll
|
||||
|
||||
// ============================================================================
|
||||
// Fill one DeviceInfo from a namespace node.
|
||||
static void DescribeDevice(AML::Namespace& ns, int32_t idx, DeviceInfo& dev) {
|
||||
dev.NodeIndex = idx;
|
||||
ns.GetNodePath(idx, dev.Path, 128);
|
||||
|
||||
dev.Status = EvaluateSta(idx);
|
||||
dev.IsPresent = (dev.Status & STA_PRESENT) && (dev.Status & STA_FUNCTIONAL);
|
||||
|
||||
if (!EvaluateHid(idx, dev.HardwareId, 16))
|
||||
dev.HardwareId[0] = '\0';
|
||||
if (!EvaluateUid(idx, dev.UniqueId, 16))
|
||||
dev.UniqueId[0] = '\0';
|
||||
|
||||
dev.Address = EvaluateAdr(idx);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// FindDeviceByHid
|
||||
|
||||
// ============================================================================
|
||||
bool FindDeviceByHid(const char* hid, DeviceInfo& out) {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return false;
|
||||
|
||||
auto& ns = interp.GetNamespace();
|
||||
int32_t found = -1;
|
||||
|
||||
// Only _HID is evaluated per candidate; the full description is
|
||||
// built once, for the match. WalkDescendants has no early exit, so
|
||||
// the first match wins and later ones are ignored.
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
|
||||
[&](int32_t idx, [[maybe_unused]] const AML::NamespaceNode* node) {
|
||||
if (found >= 0) return;
|
||||
|
||||
char candidate[16];
|
||||
if (!EvaluateHid(idx, candidate, 16)) return;
|
||||
if (StrEqual(candidate, hid)) found = idx;
|
||||
});
|
||||
|
||||
if (found < 0) return false;
|
||||
|
||||
out = DeviceInfo{};
|
||||
DescribeDevice(ns, found, out);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// EnumerateAll
|
||||
|
||||
// ============================================================================
|
||||
void EnumerateAll(DeviceList& result) {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
@@ -326,39 +378,32 @@ namespace Hal {
|
||||
|
||||
auto& ns = interp.GetNamespace();
|
||||
result.Count = 0;
|
||||
result.Truncated = false;
|
||||
|
||||
// Walk all namespace nodes looking for Device objects
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
|
||||
[&](int32_t idx, [[maybe_unused]] const AML::NamespaceNode* node) {
|
||||
if (result.Count >= MaxDevices) return;
|
||||
|
||||
auto& dev = result.Devices[result.Count];
|
||||
dev.NodeIndex = idx;
|
||||
|
||||
// Get the path
|
||||
ns.GetNodePath(idx, dev.Path, 128);
|
||||
|
||||
// Evaluate _STA
|
||||
dev.Status = EvaluateSta(idx);
|
||||
dev.IsPresent = (dev.Status & STA_PRESENT) && (dev.Status & STA_FUNCTIONAL);
|
||||
|
||||
// Evaluate _HID
|
||||
if (!EvaluateHid(idx, dev.HardwareId, 16))
|
||||
dev.HardwareId[0] = '\0';
|
||||
|
||||
// Evaluate _UID
|
||||
if (!EvaluateUid(idx, dev.UniqueId, 16))
|
||||
dev.UniqueId[0] = '\0';
|
||||
|
||||
// Evaluate _ADR
|
||||
dev.Address = EvaluateAdr(idx);
|
||||
if (result.Count >= MaxDevices) {
|
||||
result.Truncated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
DescribeDevice(ns, idx, result.Devices[result.Count]);
|
||||
result.Count++;
|
||||
});
|
||||
|
||||
KernelLogStream(OK, "ACPI") << "Enumerated " << base::dec
|
||||
<< (uint64_t)result.Count << " ACPI devices";
|
||||
|
||||
// The list is a fixed-size stack object, so a large namespace runs
|
||||
// past it. Say so: a device missing from the log below may simply
|
||||
// be one that did not fit, and looking it up by HID still works.
|
||||
if (result.Truncated) {
|
||||
KernelLogStream(WARNING, "ACPI") << "Device list truncated at "
|
||||
<< base::dec << (uint64_t)MaxDevices
|
||||
<< "; use FindDeviceByHid to reach the rest";
|
||||
}
|
||||
|
||||
// Log discovered devices
|
||||
for (int i = 0; i < result.Count; i++) {
|
||||
auto& dev = result.Devices[i];
|
||||
@@ -408,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
|
||||
|
||||
@@ -45,8 +45,9 @@ namespace Hal {
|
||||
struct DeviceList {
|
||||
DeviceInfo Devices[MaxDevices];
|
||||
int Count;
|
||||
bool Truncated; // the namespace held more than MaxDevices
|
||||
|
||||
DeviceList() : Count(0) {}
|
||||
DeviceList() : Count(0), Truncated(false) {}
|
||||
|
||||
// Find a device by HID. Returns nullptr if not found.
|
||||
const DeviceInfo* FindByHid(const char* hid) const;
|
||||
@@ -64,6 +65,12 @@ namespace Hal {
|
||||
// The interpreter must be initialized first (LoadTable called).
|
||||
void EnumerateAll(DeviceList& result);
|
||||
|
||||
// Find the first device with the given _HID, walking the namespace
|
||||
// directly. Unlike EnumerateAll this has no device-count ceiling and
|
||||
// needs no multi-kilobyte list on the caller's stack, so it is the way
|
||||
// to reach a specific device on a machine whose namespace is large.
|
||||
bool FindDeviceByHid(const char* hid, DeviceInfo& out);
|
||||
|
||||
// Evaluate _STA for a device node. Returns STA_DEFAULT if no _STA exists.
|
||||
uint32_t EvaluateSta(int32_t deviceNodeIndex);
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,422 @@
|
||||
/*
|
||||
* Battery.cpp
|
||||
* ACPI Control Method Battery: _STA / _BIF / _BIX / _BST, plus _PSR on the
|
||||
* AC adapter
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Battery.hpp"
|
||||
#include "AcpiDevices.hpp"
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Timekeeping/Timer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace Battery {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// State
|
||||
|
||||
// ============================================================================
|
||||
// ACPI reports an unreadable field as all-ones rather than zero.
|
||||
static constexpr uint32_t Unknown32 = 0xFFFFFFFFu;
|
||||
|
||||
// The EC transactions behind _BST take a few milliseconds, and the
|
||||
// value only moves on a scale of minutes, so a short cache keeps a
|
||||
// one-second panel repaint from becoming a bus workload.
|
||||
static constexpr uint64_t CacheMs = 2000;
|
||||
|
||||
static bool g_initialized = false;
|
||||
static bool g_haveBattery = false;
|
||||
static bool g_haveAdapter = false;
|
||||
static char g_batteryPath[128];
|
||||
static char g_adapterPath[128];
|
||||
static int32_t g_batteryNode = -1;
|
||||
|
||||
static kcp::Mutex g_lock;
|
||||
|
||||
// Set for the boot-path probe only, so the per-method tracing does not
|
||||
// repeat on every poll once the system is up.
|
||||
static bool g_tracing = false;
|
||||
|
||||
static bool Refresh(Status& out);
|
||||
static void Probe();
|
||||
static Status g_cached{};
|
||||
static bool g_cacheValid = false;
|
||||
static uint64_t g_cacheStamp = 0;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Path helpers
|
||||
|
||||
// ============================================================================
|
||||
static void AppendMethod(const char* devicePath, const char* method, char* out, int maxLen) {
|
||||
int i = 0;
|
||||
while (devicePath[i] && i < maxLen - 6) {
|
||||
out[i] = devicePath[i];
|
||||
i++;
|
||||
}
|
||||
// The root scope is written "\" with no separator before the first
|
||||
// segment; every other path takes a dot.
|
||||
if (!(i == 1 && out[0] == '\\'))
|
||||
out[i++] = '.';
|
||||
for (int j = 0; j < 4 && i < maxLen - 1; j++)
|
||||
out[i++] = method[j];
|
||||
out[i] = '\0';
|
||||
}
|
||||
|
||||
static void CopyPath(char* dst, const char* src, int maxLen) {
|
||||
int i = 0;
|
||||
while (src[i] && i < maxLen - 1) {
|
||||
dst[i] = src[i];
|
||||
i++;
|
||||
}
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Initialize
|
||||
|
||||
// ============================================================================
|
||||
void Initialize() {
|
||||
if (g_initialized) return;
|
||||
g_initialized = true;
|
||||
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return;
|
||||
|
||||
// Searched by HID rather than off an enumerated list: a laptop
|
||||
// namespace runs well past the list's fixed size, and the battery
|
||||
// sits deep under the EC where it would be cut off.
|
||||
AcpiDevices::DeviceInfo bat{};
|
||||
if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_BATTERY, bat)) {
|
||||
CopyPath(g_batteryPath, bat.Path, sizeof(g_batteryPath));
|
||||
g_batteryNode = bat.NodeIndex;
|
||||
g_haveBattery = true;
|
||||
}
|
||||
|
||||
AcpiDevices::DeviceInfo ac{};
|
||||
if (AcpiDevices::FindDeviceByHid(AcpiDevices::HID_AC_ADAPTER, ac)) {
|
||||
CopyPath(g_adapterPath, ac.Path, sizeof(g_adapterPath));
|
||||
g_haveAdapter = true;
|
||||
}
|
||||
|
||||
if (!g_haveBattery) {
|
||||
// Distinguish "this machine has no battery" from "the namespace
|
||||
// walk found nothing at all", which would point at the walk
|
||||
// rather than the hardware.
|
||||
auto& ns = interp.GetNamespace();
|
||||
int deviceCount = 0;
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Device,
|
||||
[&](int32_t, const AML::NamespaceNode*) { deviceCount++; });
|
||||
|
||||
KernelLogStream(INFO, "Battery") << "No ACPI battery device (PNP0C0A) among "
|
||||
<< base::dec << (uint64_t)deviceCount << " ACPI devices";
|
||||
return;
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "Battery") << "ACPI battery at " << g_batteryPath
|
||||
<< (g_haveAdapter ? " (AC adapter present)" : " (no AC adapter device)");
|
||||
|
||||
Probe();
|
||||
}
|
||||
|
||||
// Evaluate the battery methods once, here on the boot path, announcing
|
||||
// each before it runs. These are the first firmware methods this kernel
|
||||
// executes that reach the embedded controller, and if one of them takes
|
||||
// the machine down the last line printed says which -- far better than
|
||||
// discovering it when the desktop makes its first query.
|
||||
static void Probe() {
|
||||
KernelLogStream(INFO, "Battery") << "Probing battery methods";
|
||||
|
||||
g_tracing = true;
|
||||
Status s{};
|
||||
bool ok = Refresh(s);
|
||||
g_tracing = false;
|
||||
|
||||
if (!ok) {
|
||||
KernelLogStream(WARNING, "Battery") << "Probe failed; battery status unavailable";
|
||||
g_haveBattery = false;
|
||||
return;
|
||||
}
|
||||
|
||||
g_cached = s;
|
||||
g_cacheStamp = 0; // let the first real query refresh
|
||||
g_cacheValid = true;
|
||||
|
||||
if (!s.Present) {
|
||||
KernelLogStream(INFO, "Battery") << "Battery slot empty"
|
||||
<< (s.AcOnline ? " (on AC)" : "");
|
||||
return;
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "Battery") << "Battery at " << base::dec
|
||||
<< (uint64_t)s.Percent << "% ("
|
||||
<< (s.Charging ? "charging" : (s.Discharging ? "discharging" : "idle"))
|
||||
<< ", " << (uint64_t)s.RemainingCapacity << " of "
|
||||
<< (uint64_t)s.FullCapacity << (s.PowerUnitMilliAmps ? " mAh)" : " mWh)");
|
||||
}
|
||||
|
||||
bool IsPresent() {
|
||||
return g_haveBattery;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Method evaluation
|
||||
|
||||
// ============================================================================
|
||||
static const char* TypeName(AML::ObjectType t) {
|
||||
switch (t) {
|
||||
case AML::ObjectType::None: return "none";
|
||||
case AML::ObjectType::Integer: return "integer";
|
||||
case AML::ObjectType::String: return "string";
|
||||
case AML::ObjectType::Buffer: return "buffer";
|
||||
case AML::ObjectType::Package: return "package";
|
||||
case AML::ObjectType::Device: return "device";
|
||||
case AML::ObjectType::Method: return "method";
|
||||
case AML::ObjectType::OperationRegion: return "opregion";
|
||||
case AML::ObjectType::Field: return "field";
|
||||
default: return "other";
|
||||
}
|
||||
}
|
||||
|
||||
static bool EvalPackage(const char* devicePath, const char* method, AML::Object& out) {
|
||||
char path[160];
|
||||
AppendMethod(devicePath, method, path, sizeof(path));
|
||||
|
||||
auto& interp = AML::GetInterpreter();
|
||||
|
||||
// Separate "the method is not there" from "it ran and gave us
|
||||
// something that is not a package": those need opposite fixes, and
|
||||
// the failure looks identical from the caller.
|
||||
int32_t node = interp.GetNamespace().FindNode(path);
|
||||
if (node < 0) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " " << path << ": absent";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Trace the opcodes of the one method whose behaviour we cannot
|
||||
// otherwise explain: it runs to completion, trips no limit, and
|
||||
// still yields no value.
|
||||
bool traceThis = g_tracing && method[1] == 'B';
|
||||
if (traceThis) interp.SetTrace(120);
|
||||
|
||||
bool ok = interp.EvaluateObject(path, out);
|
||||
|
||||
if (traceThis) interp.SetTrace(0);
|
||||
|
||||
if (g_tracing) {
|
||||
KernelLogStream(INFO, "Battery") << " " << path << ": "
|
||||
<< (ok ? "ok" : "FAILED") << ", returned " << TypeName(out.Type)
|
||||
<< " with " << base::dec
|
||||
<< (uint64_t)interp.GetPackageCount(out) << " element(s)";
|
||||
}
|
||||
|
||||
return ok && out.Type == AML::ObjectType::Package;
|
||||
}
|
||||
|
||||
static uint32_t ElementInteger(const AML::Object& pkg, int index) {
|
||||
AML::Object elem{};
|
||||
if (!AML::GetInterpreter().GetPackageElement(pkg, index, elem)) return Unknown32;
|
||||
if (elem.Type != AML::ObjectType::Integer) return Unknown32;
|
||||
return (uint32_t)elem.Integer;
|
||||
}
|
||||
|
||||
// _BIX supersedes _BIF and prepends a Revision field, shifting every
|
||||
// shared field by one. Both are read into the same three numbers.
|
||||
struct BatteryInfo {
|
||||
bool Valid;
|
||||
bool PowerUnitMilliAmps;
|
||||
uint32_t DesignCapacity;
|
||||
uint32_t FullCapacity;
|
||||
};
|
||||
|
||||
static bool ReadInfo(BatteryInfo& info) {
|
||||
info = BatteryInfo{};
|
||||
|
||||
AML::Object pkg{};
|
||||
int base = 0;
|
||||
|
||||
if (EvalPackage(g_batteryPath, "_BIX", pkg)) {
|
||||
// _BIX revision 0 starts with Revision; later revisions keep it
|
||||
// in the same place, so the offset is fixed at one.
|
||||
base = 1;
|
||||
} else if (EvalPackage(g_batteryPath, "_BIF", pkg)) {
|
||||
base = 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t unit = ElementInteger(pkg, base + 0);
|
||||
uint32_t design = ElementInteger(pkg, base + 1);
|
||||
uint32_t full = ElementInteger(pkg, base + 2);
|
||||
|
||||
info.PowerUnitMilliAmps = (unit != Unknown32) && (unit & 1);
|
||||
info.DesignCapacity = (design == Unknown32) ? 0 : design;
|
||||
info.FullCapacity = (full == Unknown32) ? 0 : full;
|
||||
info.Valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool AcOnline() {
|
||||
if (!g_haveAdapter) return false;
|
||||
|
||||
char path[160];
|
||||
AppendMethod(g_adapterPath, "_PSR", path, sizeof(path));
|
||||
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " evaluating " << path;
|
||||
|
||||
AML::Object result{};
|
||||
if (!AML::GetInterpreter().EvaluateObject(path, result)) return false;
|
||||
return result.Type == AML::ObjectType::Integer && result.Integer != 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// GetStatus
|
||||
|
||||
// ============================================================================
|
||||
static bool Refresh(Status& out) {
|
||||
out = Status{};
|
||||
out.MinutesRemaining = -1;
|
||||
out.HasAcAdapter = g_haveAdapter;
|
||||
|
||||
// _STA bit 4 says whether a battery is actually in the slot. A
|
||||
// laptop with the pack removed still has the device.
|
||||
uint32_t sta = (g_batteryNode >= 0)
|
||||
? AcpiDevices::EvaluateSta(g_batteryNode)
|
||||
: AcpiDevices::STA_DEFAULT;
|
||||
|
||||
// Bit 4 is the one that decides everything below, and firmware
|
||||
// clears it for reasons that have nothing to do with the pack
|
||||
// (an EC that has not been told its region handler is live, most
|
||||
// often), so log the raw value rather than just the verdict.
|
||||
if (g_tracing) {
|
||||
KernelLogStream(INFO, "Battery") << " _STA = " << base::hex
|
||||
<< (uint64_t)sta << (((sta & AcpiDevices::STA_BATTERY) != 0)
|
||||
? " (battery present)" : " (battery bit clear)");
|
||||
}
|
||||
|
||||
out.AcOnline = AcOnline();
|
||||
|
||||
// _STA bit 4 is the spec's answer for "is a pack installed", but
|
||||
// it is only as good as the firmware path behind it. Read the
|
||||
// battery methods either way and let the capacity data have the
|
||||
// final say -- see the staSaysAbsent handling below.
|
||||
bool staSaysAbsent = (sta & AcpiDevices::STA_BATTERY) == 0;
|
||||
|
||||
BatteryInfo info{};
|
||||
if (!ReadInfo(info)) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " no _BIX/_BIF package";
|
||||
return true;
|
||||
}
|
||||
|
||||
AML::Object bst{};
|
||||
if (!EvalPackage(g_batteryPath, "_BST", bst)) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery") << " no _BST package";
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t state = ElementInteger(bst, 0);
|
||||
uint32_t rate = ElementInteger(bst, 1);
|
||||
uint32_t remaining = ElementInteger(bst, 2);
|
||||
uint32_t voltage = ElementInteger(bst, 3);
|
||||
|
||||
if (g_tracing) {
|
||||
KernelLogStream(INFO, "Battery") << " _BIF/_BIX: design="
|
||||
<< base::dec << (uint64_t)info.DesignCapacity
|
||||
<< " lastFull=" << (uint64_t)info.FullCapacity
|
||||
<< (info.PowerUnitMilliAmps ? " (mAh)" : " (mWh)");
|
||||
KernelLogStream(INFO, "Battery") << " _BST: state=" << base::hex
|
||||
<< (uint64_t)state << " rate=" << base::dec << (uint64_t)rate
|
||||
<< " remaining=" << (uint64_t)remaining
|
||||
<< " voltage=" << (uint64_t)voltage << "mV";
|
||||
}
|
||||
|
||||
// Trust real capacity data over a status bit that says otherwise:
|
||||
// firmware that reports a pack's charge is describing a pack that
|
||||
// is there.
|
||||
if (staSaysAbsent) {
|
||||
bool looksReal = info.FullCapacity > 0 && info.FullCapacity != Unknown32
|
||||
&& remaining != Unknown32;
|
||||
if (!looksReal) {
|
||||
if (g_tracing)
|
||||
KernelLogStream(INFO, "Battery")
|
||||
<< " _STA says absent and the methods agree";
|
||||
return true;
|
||||
}
|
||||
if (g_tracing)
|
||||
KernelLogStream(WARNING, "Battery")
|
||||
<< " _STA says absent but _BST reports charge; trusting _BST";
|
||||
}
|
||||
|
||||
if (state == Unknown32) state = 0;
|
||||
|
||||
out.Present = true;
|
||||
out.Charging = (state & StateCharging) != 0;
|
||||
out.Discharging = (state & StateDischarging) != 0;
|
||||
out.Critical = (state & StateCritical) != 0;
|
||||
|
||||
out.PowerUnitMilliAmps = info.PowerUnitMilliAmps;
|
||||
out.DesignCapacity = info.DesignCapacity;
|
||||
out.FullCapacity = info.FullCapacity;
|
||||
out.RemainingCapacity = (remaining == Unknown32) ? 0 : remaining;
|
||||
out.PresentRate = (rate == Unknown32) ? 0 : rate;
|
||||
out.VoltageMv = (voltage == Unknown32) ? 0 : voltage;
|
||||
|
||||
// Some firmware leaves last-full at zero or below the current
|
||||
// reading after a swap; fall back to the design capacity, and
|
||||
// clamp so the panel can never show 143%.
|
||||
uint32_t full = out.FullCapacity;
|
||||
if (full == 0) full = out.DesignCapacity;
|
||||
|
||||
if (full > 0 && out.RemainingCapacity > 0) {
|
||||
uint64_t pct = ((uint64_t)out.RemainingCapacity * 100) / full;
|
||||
out.Percent = (pct > 100) ? 100 : (uint8_t)pct;
|
||||
}
|
||||
|
||||
if (out.PresentRate > 0 && out.Discharging) {
|
||||
out.MinutesRemaining =
|
||||
(int32_t)(((uint64_t)out.RemainingCapacity * 60) / out.PresentRate);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetStatus(Status& out) {
|
||||
if (!g_haveBattery) return false;
|
||||
|
||||
g_lock.Acquire();
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_cacheValid && now - g_cacheStamp < CacheMs) {
|
||||
out = g_cached;
|
||||
g_lock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
Status fresh{};
|
||||
bool ok = Refresh(fresh);
|
||||
if (ok) {
|
||||
g_cached = fresh;
|
||||
g_cacheStamp = now;
|
||||
g_cacheValid = true;
|
||||
}
|
||||
|
||||
out = ok ? fresh : g_cached;
|
||||
g_lock.Release();
|
||||
return ok || g_cacheValid;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Battery.hpp
|
||||
* ACPI Control Method Battery (PNP0C0A) and AC adapter (ACPI0003)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
namespace Battery {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Battery state
|
||||
|
||||
// ============================================================================
|
||||
// _BST battery state bits.
|
||||
static constexpr uint32_t StateDischarging = (1 << 0);
|
||||
static constexpr uint32_t StateCharging = (1 << 1);
|
||||
static constexpr uint32_t StateCritical = (1 << 2);
|
||||
|
||||
struct Status {
|
||||
bool Present; // a battery slot exists and reports a battery in it
|
||||
bool Charging;
|
||||
bool Discharging;
|
||||
bool Critical;
|
||||
bool AcOnline; // AC adapter connected (false when no adapter device)
|
||||
bool HasAcAdapter; // an ACPI0003 device exists
|
||||
|
||||
uint8_t Percent; // 0..100, clamped; 0 when capacity is unknown
|
||||
uint32_t RemainingCapacity; // mWh or mAh, per PowerUnitMilliAmps
|
||||
uint32_t FullCapacity; // last full charge capacity, same unit
|
||||
uint32_t DesignCapacity;
|
||||
uint32_t PresentRate; // mW or mA, 0 when unknown
|
||||
uint32_t VoltageMv; // present voltage in mV, 0 when unknown
|
||||
bool PowerUnitMilliAmps; // capacities are mAh/mA rather than mWh/mW
|
||||
|
||||
// Minutes of runtime left at the present rate, or -1 when the rate
|
||||
// is unknown/zero (which includes every charging battery).
|
||||
int32_t MinutesRemaining;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// API
|
||||
|
||||
// ============================================================================
|
||||
// Discover battery and AC adapter devices in the ACPI namespace. Safe to
|
||||
// call more than once; only the first call does work.
|
||||
void Initialize();
|
||||
|
||||
// True when at least one battery device was found, whether or not a
|
||||
// battery is currently installed in it.
|
||||
bool IsPresent();
|
||||
|
||||
// Read the current status. Results are cached briefly so that polling
|
||||
// from a panel indicator does not hammer the embedded controller.
|
||||
// Returns false when the machine has no battery device at all.
|
||||
bool GetStatus(Status& out);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* EmbeddedController.cpp
|
||||
* ACPI Embedded Controller transport (polled, no burst mode)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "EmbeddedController.hpp"
|
||||
#include "ACPI.hpp"
|
||||
#include "AcpiDevices.hpp"
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Hal {
|
||||
namespace EmbeddedController {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// EC register protocol
|
||||
|
||||
// ============================================================================
|
||||
// EC_SC (status, read from the command port)
|
||||
static constexpr uint8_t SC_OBF = (1 << 0); // output buffer full: data is readable
|
||||
static constexpr uint8_t SC_IBF = (1 << 1); // input buffer full: EC has not consumed our byte
|
||||
|
||||
// EC_SC commands (written to the command port)
|
||||
static constexpr uint8_t CMD_READ = 0x80;
|
||||
static constexpr uint8_t CMD_WRITE = 0x81;
|
||||
|
||||
// The EC is a slow microcontroller; a byte transaction is typically a
|
||||
// few hundred microseconds but firmware is allowed to take much longer.
|
||||
// One "tick" is a port-wait (~1us), so this caps a wait near 500ms.
|
||||
static constexpr uint32_t PollTicks = 500000;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// State
|
||||
|
||||
// ============================================================================
|
||||
static uint16_t g_dataPort = 0;
|
||||
static uint16_t g_cmdPort = 0;
|
||||
static bool g_available = false;
|
||||
static bool g_initialized = false;
|
||||
// A Mutex, not a Spinlock: EC transactions run only from process and
|
||||
// syscall context, and can spin for milliseconds waiting on the
|
||||
// controller. A Spinlock would hold interrupts off for all of it.
|
||||
static kcp::Mutex g_lock;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// ECDT
|
||||
|
||||
// ============================================================================
|
||||
// The ECDT gives the EC's ports without needing the AML namespace. It is
|
||||
// optional, and some firmware ships one with zeroed ports, so treat a
|
||||
// zero port as "not described here" and fall through to the namespace.
|
||||
struct EcdtTable {
|
||||
ACPI::CommonSDTHeader Header;
|
||||
uint8_t ControlGas[12];
|
||||
uint8_t DataGas[12];
|
||||
uint32_t Uid;
|
||||
uint8_t GpeBit;
|
||||
// followed by a null-terminated namespace path
|
||||
} __attribute__((packed));
|
||||
|
||||
static bool SignatureMatch(const char* sig, const char* target) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (sig[i] != target[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decode a Generic Address Structure that must describe a 16-bit I/O port.
|
||||
static uint16_t GasIoPort(const uint8_t gas[12]) {
|
||||
if (gas[0] != 1) return 0; // AddressSpaceId 1 == SystemIO
|
||||
|
||||
uint64_t address = 0;
|
||||
for (int i = 0; i < 8; i++)
|
||||
address |= (uint64_t)gas[4 + i] << (i * 8);
|
||||
return address <= 0xFFFF ? (uint16_t)address : 0;
|
||||
}
|
||||
|
||||
static bool FromEcdt(ACPI::CommonSDTHeader* xsdt) {
|
||||
if (xsdt == nullptr) return false;
|
||||
|
||||
uint32_t entryCount = (xsdt->Length - sizeof(ACPI::CommonSDTHeader)) / 8;
|
||||
uint64_t* entries = (uint64_t*)((uint64_t)xsdt + sizeof(ACPI::CommonSDTHeader));
|
||||
|
||||
for (uint32_t i = 0; i < entryCount; i++) {
|
||||
auto* header = (ACPI::CommonSDTHeader*)Memory::HHDM(entries[i]);
|
||||
if (!SignatureMatch(header->Signature, "ECDT")) continue;
|
||||
if (header->Length < sizeof(EcdtTable)) continue;
|
||||
|
||||
auto* ecdt = (EcdtTable*)header;
|
||||
uint16_t cmd = GasIoPort(ecdt->ControlGas);
|
||||
uint16_t data = GasIoPort(ecdt->DataGas);
|
||||
if (cmd == 0 || data == 0) continue;
|
||||
|
||||
g_cmdPort = cmd;
|
||||
g_dataPort = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Namespace fallback
|
||||
|
||||
// ============================================================================
|
||||
// _CRS on an EC device lists two I/O resources in a fixed order: data
|
||||
// register first, then command/status. Anything else is not an EC we
|
||||
// know how to drive.
|
||||
static bool FromNamespace() {
|
||||
if (!AML::GetInterpreter().IsInitialized()) return false;
|
||||
|
||||
AcpiDevices::DeviceInfo ec{};
|
||||
if (!AcpiDevices::FindDeviceByHid(AcpiDevices::HID_EC, ec)) return false;
|
||||
|
||||
AML::ResourceList crs{};
|
||||
if (!AcpiDevices::EvaluateCrs(ec.NodeIndex, crs)) return false;
|
||||
|
||||
uint16_t ports[2] = {0, 0};
|
||||
int found = 0;
|
||||
for (int i = 0; i < crs.Count && found < 2; i++) {
|
||||
if (crs.Resources[i].Type == AML::ResourceType::IoPort)
|
||||
ports[found++] = crs.Resources[i].IoPort.Base;
|
||||
else if (crs.Resources[i].Type == AML::ResourceType::FixedIoPort)
|
||||
ports[found++] = crs.Resources[i].FixedIoPort.Base;
|
||||
}
|
||||
|
||||
if (found < 2 || ports[0] == 0 || ports[1] == 0) return false;
|
||||
|
||||
g_dataPort = ports[0];
|
||||
g_cmdPort = ports[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Initialize
|
||||
|
||||
// ============================================================================
|
||||
void Initialize(void* xsdt) {
|
||||
if (g_initialized) return;
|
||||
g_initialized = true;
|
||||
|
||||
if (!FromEcdt((ACPI::CommonSDTHeader*)xsdt) && !FromNamespace()) {
|
||||
KernelLogStream(INFO, "EC") << "No ACPI Embedded Controller present";
|
||||
return;
|
||||
}
|
||||
|
||||
g_available = true;
|
||||
KernelLogStream(OK, "EC") << "Embedded Controller at cmd=" << base::hex
|
||||
<< (uint64_t)g_cmdPort << " data=" << (uint64_t)g_dataPort;
|
||||
|
||||
RunRegMethods();
|
||||
|
||||
KernelLogStream(INFO, "EC") << "EC space dump:";
|
||||
DumpSpace(0x00, 32);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// _REG
|
||||
|
||||
// ============================================================================
|
||||
void RunRegMethods() {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
if (!interp.IsInitialized()) return;
|
||||
|
||||
auto& ns = interp.GetNamespace();
|
||||
|
||||
// Collect first, evaluate second. Executing AML can create namespace
|
||||
// nodes, which reallocates a parent's child array -- walking the
|
||||
// tree while methods run would be iterating a freed array.
|
||||
static constexpr int MaxRegMethods = 64;
|
||||
int32_t nodes[MaxRegMethods];
|
||||
int count = 0;
|
||||
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Method,
|
||||
[&](int32_t idx, const AML::NamespaceNode* node) {
|
||||
if (count >= MaxRegMethods) return;
|
||||
if (node->Name[0] == '_' && node->Name[1] == 'R' &&
|
||||
node->Name[2] == 'E' && node->Name[3] == 'G')
|
||||
nodes[count++] = idx;
|
||||
});
|
||||
|
||||
// Arg0 = region space (3, EmbeddedControl), Arg1 = 1 (handler is
|
||||
// connected). A _REG that handles some other space checks Arg0 and
|
||||
// ignores the call, so this is safe to issue namespace-wide.
|
||||
AML::Object args[2]{};
|
||||
args[0].Type = AML::ObjectType::Integer;
|
||||
args[0].Integer = (uint64_t)AML::RegionSpace::EmbeddedControl;
|
||||
args[1].Type = AML::ObjectType::Integer;
|
||||
args[1].Integer = 1;
|
||||
|
||||
int ran = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
char path[256];
|
||||
ns.GetNodePath(nodes[i], path, sizeof(path));
|
||||
|
||||
AML::Object result{};
|
||||
bool ok = interp.EvaluateMethod(path, args, 2, result);
|
||||
if (ok) ran++;
|
||||
|
||||
KernelLogStream(INFO, "EC") << " _REG " << path
|
||||
<< (ok ? " ok" : " FAILED");
|
||||
}
|
||||
|
||||
KernelLogStream(OK, "EC") << "Ran " << base::dec << (uint64_t)ran
|
||||
<< " of " << (uint64_t)count << " _REG method(s)";
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Diagnostics
|
||||
|
||||
// ============================================================================
|
||||
void DumpSpace(uint8_t base_, int bytes) {
|
||||
if (!g_available) return;
|
||||
|
||||
// Whether the transport works at all is not otherwise observable:
|
||||
// a failed read and a genuinely zero register look identical by the
|
||||
// time firmware AML has turned them into a status value. All-00 or
|
||||
// all-FF here means the handshake is wrong, not that the battery is
|
||||
// missing.
|
||||
for (int row = 0; row < bytes; row += 16) {
|
||||
char line[64];
|
||||
int n = 0;
|
||||
bool anyFailed = false;
|
||||
|
||||
for (int i = 0; i < 16 && (row + i) < bytes; i++) {
|
||||
uint8_t v = 0;
|
||||
if (!Read((uint8_t)(base_ + row + i), v)) anyFailed = true;
|
||||
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
line[n++] = hex[(v >> 4) & 0xF];
|
||||
line[n++] = hex[v & 0xF];
|
||||
line[n++] = ' ';
|
||||
}
|
||||
line[n] = '\0';
|
||||
|
||||
KernelLogStream(INFO, "EC") << " " << base::hex
|
||||
<< (uint64_t)(base_ + row) << ": " << line
|
||||
<< (anyFailed ? " (read timeout)" : "");
|
||||
}
|
||||
}
|
||||
|
||||
bool IsAvailable() {
|
||||
return g_available;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Transport
|
||||
|
||||
// ============================================================================
|
||||
// Spin until the named status bit reaches the wanted state.
|
||||
static bool WaitStatus(uint8_t mask, bool wantSet) {
|
||||
for (uint32_t i = 0; i < PollTicks; i++) {
|
||||
uint8_t sc = Io::In8(g_cmdPort);
|
||||
if (((sc & mask) != 0) == wantSet) return true;
|
||||
Io::IoPortWait();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Drain a byte the EC left in the output buffer from an earlier aborted
|
||||
// transaction. Without this the next read returns the stale value.
|
||||
static void DrainOutput() {
|
||||
if (Io::In8(g_cmdPort) & SC_OBF)
|
||||
(void)Io::In8(g_dataPort);
|
||||
}
|
||||
|
||||
bool Read(uint8_t address, uint8_t& value) {
|
||||
if (!g_available) return false;
|
||||
|
||||
g_lock.Acquire();
|
||||
DrainOutput();
|
||||
|
||||
bool ok = false;
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(CMD_READ, g_cmdPort);
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(address, g_dataPort);
|
||||
if (WaitStatus(SC_OBF, true)) {
|
||||
value = Io::In8(g_dataPort);
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_lock.Release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool Write(uint8_t address, uint8_t value) {
|
||||
if (!g_available) return false;
|
||||
|
||||
g_lock.Acquire();
|
||||
DrainOutput();
|
||||
|
||||
bool ok = false;
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(CMD_WRITE, g_cmdPort);
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(address, g_dataPort);
|
||||
if (WaitStatus(SC_IBF, false)) {
|
||||
Io::Out8(value, g_dataPort);
|
||||
ok = WaitStatus(SC_IBF, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_lock.Release();
|
||||
return ok;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* EmbeddedController.hpp
|
||||
* ACPI Embedded Controller (PNP0C09) transport
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Hal {
|
||||
namespace EmbeddedController {
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Bring-up
|
||||
|
||||
// ============================================================================
|
||||
// Locate the EC in the ACPI namespace and latch its command/data ports.
|
||||
// Prefers the ECDT table when present (it is valid before the namespace
|
||||
// is loaded); otherwise walks the namespace for PNP0C09 and reads _CRS.
|
||||
// Safe to call more than once; only the first call does work.
|
||||
void Initialize(void* xsdt);
|
||||
|
||||
// True once a usable command/data port pair has been found.
|
||||
bool IsAvailable();
|
||||
|
||||
// Run the namespace's _REG(EmbeddedControl, 1) methods, telling firmware
|
||||
// that EC region access is live. ACPI requires this once a handler for
|
||||
// an address space is installed, and firmware leans on it hard: EC
|
||||
// methods are typically guarded by a flag that _REG sets, and until it
|
||||
// is set they return safe do-nothing answers (a battery reports itself
|
||||
// absent, a thermal zone reports no temperature). Called by Initialize.
|
||||
void RunRegMethods();
|
||||
|
||||
// Log `bytes` bytes of EC address space starting at `base_`. Purely a
|
||||
// diagnostic: it is the only way to tell a broken handshake from a
|
||||
// register that genuinely reads zero.
|
||||
void DumpSpace(uint8_t base_, int bytes);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Transport
|
||||
|
||||
// ============================================================================
|
||||
// Read/write one byte of the EC's 256-byte address space. Both return
|
||||
// false on timeout or when no EC is present, leaving *value untouched.
|
||||
bool Read(uint8_t address, uint8_t& value);
|
||||
bool Write(uint8_t address, uint8_t value);
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 184
|
||||
#define MONTAUK_BUILD_NUMBER 123
|
||||
@@ -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;
|
||||
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) {
|
||||
int rc = DataTransfer::CopyOut(*text, buffer, bufferLen, outLen);
|
||||
g_clipboardLock.Release();
|
||||
return -1;
|
||||
return rc;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < slot->len; i++)
|
||||
buffer[i] = slot->text[i];
|
||||
g_clipboardLock.Release();
|
||||
return 0;
|
||||
}
|
||||
static int Sys_ClipboardFormats(TransferFormatInfo* out, uint32_t max, uint64_t* outSerial) {
|
||||
if (outSerial != nullptr) *outSerial = 0;
|
||||
|
||||
static int Sys_ClipboardClear() {
|
||||
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 = ClipboardEnsureSlotLocked(user);
|
||||
if (slot == nullptr) {
|
||||
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 -1;
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
oldText = slot->text;
|
||||
slot->text = nullptr;
|
||||
slot->len = 0;
|
||||
slot->serial = ClipboardNextSerial(slot->serial);
|
||||
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 = 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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]++;
|
||||
|
||||
@@ -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
@@ -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, ©);
|
||||
}
|
||||
@@ -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, ©, 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;
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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 ----
|
||||
|
||||
+46
-19
@@ -8,12 +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 <Hal/CpuPower.hpp>
|
||||
#include <ACPI/Battery.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>
|
||||
@@ -48,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() {
|
||||
@@ -74,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();
|
||||
}
|
||||
@@ -95,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;
|
||||
@@ -111,7 +112,33 @@ 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
|
||||
// no ACPI battery device, which is how a desktop-class caller learns not to
|
||||
// draw an indicator at all.
|
||||
static int64_t Sys_BatteryInfo(BatteryInfo* out) {
|
||||
Hal::Battery::Status s{};
|
||||
if (!Hal::Battery::GetStatus(s)) {
|
||||
return NoDevice;
|
||||
}
|
||||
|
||||
out->present = s.Present ? 1 : 0;
|
||||
out->charging = s.Charging ? 1 : 0;
|
||||
out->discharging = s.Discharging ? 1 : 0;
|
||||
out->critical = s.Critical ? 1 : 0;
|
||||
out->acOnline = s.AcOnline ? 1 : 0;
|
||||
out->hasAcAdapter = s.HasAcAdapter ? 1 : 0;
|
||||
out->percent = s.Percent;
|
||||
out->powerUnitMilliAmps = s.PowerUnitMilliAmps ? 1 : 0;
|
||||
out->remainingCapacity = s.RemainingCapacity;
|
||||
out->fullCapacity = s.FullCapacity;
|
||||
out->designCapacity = s.DesignCapacity;
|
||||
out->presentRate = s.PresentRate;
|
||||
out->voltageMv = s.VoltageMv;
|
||||
out->minutesRemaining = s.MinutesRemaining;
|
||||
return Success;
|
||||
}
|
||||
};
|
||||
+65
-30
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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, ¶ms->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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+285
-211
@@ -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,52 +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 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);
|
||||
@@ -555,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,
|
||||
@@ -642,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);
|
||||
@@ -664,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'
|
||||
@@ -698,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)";
|
||||
}
|
||||
|
||||
}
|
||||
+360
-24
@@ -7,6 +7,8 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <Common/StatusCodes.hpp>
|
||||
#include <Arch/Syscall.hpp>
|
||||
|
||||
namespace montauk::abi {
|
||||
|
||||
@@ -343,6 +345,69 @@ namespace montauk::abi {
|
||||
touch in full (image decode, heap slabs). */
|
||||
static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure
|
||||
|
||||
/* 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;
|
||||
@@ -363,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 |
|
||||
@@ -375,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
|
||||
@@ -537,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;
|
||||
@@ -553,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;
|
||||
@@ -586,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;
|
||||
@@ -616,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;
|
||||
@@ -632,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
|
||||
@@ -673,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;
|
||||
@@ -692,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;
|
||||
@@ -700,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;
|
||||
};
|
||||
@@ -881,6 +1201,26 @@ namespace montauk::abi {
|
||||
uint32_t apIdleHint; // MWAIT hint used for AP deep idle
|
||||
};
|
||||
|
||||
// Battery status (returned by SYS_BATTERYINFO). A machine with no battery
|
||||
// device fails the call outright; `present` distinguishes a laptop with the
|
||||
// pack removed from one that is running on it.
|
||||
struct BatteryInfo {
|
||||
uint8_t present; // a battery is installed
|
||||
uint8_t charging;
|
||||
uint8_t discharging;
|
||||
uint8_t critical;
|
||||
uint8_t acOnline; // AC adapter connected
|
||||
uint8_t hasAcAdapter; // an AC adapter device exists at all
|
||||
uint8_t percent; // 0..100, 0 when capacity is unknown
|
||||
uint8_t powerUnitMilliAmps; // capacities are mAh/mA, not mWh/mW
|
||||
uint32_t remainingCapacity;
|
||||
uint32_t fullCapacity; // last full charge capacity
|
||||
uint32_t designCapacity;
|
||||
uint32_t presentRate; // mW or mA, 0 when unknown
|
||||
uint32_t voltageMv;
|
||||
int32_t minutesRemaining; // -1 when not estimable (e.g. charging)
|
||||
};
|
||||
|
||||
// Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT)
|
||||
struct CrashReportInfo {
|
||||
int pid;
|
||||
@@ -906,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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.cpuIndex = apIndex;
|
||||
ap.currentSlot = -1;
|
||||
ap.started = false;
|
||||
ap.arch.mpidr = info.hardwareId;
|
||||
|
||||
info.extraArgument = (uint64_t)≈
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
Loaded 100 of 602 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user