Compare commits
36
Commits
e641500b40
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -252,6 +252,10 @@ 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
|
||||
@@ -281,6 +285,7 @@ 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
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
# CPU Power and Thermal Management
|
||||
|
||||
MontaukOS manages CPU frequency, idle states, and package temperature on
|
||||
Intel hardware. Without this, a modern many-core laptop part (the reference
|
||||
target is a TUXEDO Gemini Gen2 with an i9-13900HX, 24 cores / 32 threads)
|
||||
runs at whatever P-state the firmware left it, never drops below C1 when
|
||||
idle, and heats until the hardware throttles at TjMax - or until the
|
||||
embedded controller intervenes (on Clevo boards: forced full-volume beeping,
|
||||
then a hard power-off).
|
||||
|
||||
Everything below is feature-gated by CPUID, so virtual machines (KVM masks
|
||||
the power bits in CPUID.06H) and non-Intel CPUs skip it safely.
|
||||
|
||||
## Components
|
||||
|
||||
### HWP frequency scaling (`Hal/CpuPower.cpp`)
|
||||
|
||||
`Hal::CpuPower::InitializeBsp()` enables Hardware P-states
|
||||
(IA32_PM_ENABLE), reads the performance range from IA32_HWP_CAPABILITIES,
|
||||
and programs IA32_HWP_REQUEST with:
|
||||
|
||||
- min = lowest performance level, max = highest
|
||||
- desired = 0 (fully autonomous hardware frequency selection)
|
||||
- EPP = 0x80 (balanced; same as Linux "balance_performance")
|
||||
|
||||
This is the same mechanism intel_pstate uses on Linux and is the single
|
||||
biggest thermal win: the CPU idles near its minimum frequency and ramps
|
||||
only while there is work. APs apply the same request in `ApEntry` via
|
||||
`InitializeAp()` (HWP request MSRs are per-logical-CPU).
|
||||
|
||||
CPUs with EPB but no HWP get IA32_ENERGY_PERF_BIAS = 6 (balanced) instead.
|
||||
|
||||
### C1E promotion
|
||||
|
||||
MSR_POWER_CTL bit 1 lets HLT-idle drop core voltage/frequency to minimum
|
||||
(C1E) instead of parking at the current ratio. Set per-core on the BSP and
|
||||
every AP. Gated on the DTS CPUID bit as a real-hardware heuristic, because
|
||||
MSR_POWER_CTL is not architectural and hypervisors may not emulate it.
|
||||
|
||||
### Thermal governor (`Hal/CpuPower.cpp`)
|
||||
|
||||
A closed-loop passive-cooling controller on the BSP, stepped from
|
||||
`Sched::RunBspMaintenance()` (reached from both the idle loop and the
|
||||
timer tick, so it keeps running under full load). Every 500 ms it reads
|
||||
the package digital thermal sensor (IA32_PACKAGE_THERM_STATUS readout
|
||||
relative to TjMax from MSR_TEMPERATURE_TARGET) and adjusts the HWP
|
||||
performance ceiling:
|
||||
|
||||
- **>= 85 C (passive):** step the ceiling down 2 ratio units (~200 MHz)
|
||||
- **>= 94 C (hot):** step down 8 units per interval
|
||||
- **<= 78 C (resume):** step back up 1 unit per interval
|
||||
- between resume and passive: hold (hysteresis)
|
||||
|
||||
The ceiling propagates through a policy epoch counter: each CPU compares
|
||||
the epoch in `Sched::Tick()` (and the AP idle loop) against the last one
|
||||
it applied and rewrites its own IA32_HWP_REQUEST when it changed, since
|
||||
MSR writes only affect the executing core. APs tick every 10 ms, so a
|
||||
throttle decision reaches all cores within one tick.
|
||||
|
||||
Throttle engage/release transitions are logged (WARNING/OK, "CpuPower").
|
||||
|
||||
### Deep AP idle (`ACPI/CpuIdle.cpp`)
|
||||
|
||||
APs use deep MWAIT C-states while idle; C6 power-gates the core entirely,
|
||||
which is where most of the idle-heat win lives on many-core parts.
|
||||
|
||||
The hint comes from ACPI `_CST` FFH entries (the GAS address low byte is
|
||||
the MWAIT hint, same convention as Linux), validated against the CPUID.05H
|
||||
sub-state enumeration and capped at the C6 family (0x2F). With no usable
|
||||
`_CST`, the deepest CPUID-enumerated hint (up to C6) is used.
|
||||
|
||||
**The BSP stays in C1 (HLT).** On some laptops deep MWAIT states freeze
|
||||
the LAPIC timer even though ARAT is advertised, and the BSP's periodic
|
||||
1 ms tick is the system timekeeper (see the comment in
|
||||
`Timekeeping::IdleOnce`). APs have no timekeeping duty: an idle AP is
|
||||
woken by the reschedule IPI, and its coarse tick resumes on wake.
|
||||
|
||||
### S3 wake
|
||||
|
||||
IA32_PM_ENABLE, HWP requests, and MSR_POWER_CTL do not survive suspend.
|
||||
`Hal::CpuPower::ReapplyAfterWake()` runs in the S3 resume path, redoes the
|
||||
BSP setup, and bumps the policy epoch so every AP re-applies its HWP
|
||||
request (and re-checks C1E) on its next tick.
|
||||
|
||||
### Battery (`ACPI/Battery.cpp`, `ACPI/EmbeddedController.cpp`)
|
||||
|
||||
Battery state comes from the ACPI Control Method Battery
|
||||
(`_HID PNP0C0A`) and, for the AC adapter, `ACPI0003`:
|
||||
|
||||
- `_STA` bit 4 says whether a pack is installed. A laptop with the pack
|
||||
removed still has the device, which is why "has a battery device" and
|
||||
"has a battery" are separate answers.
|
||||
- `_BIX` (or `_BIF` on older firmware) gives the power unit (mWh vs mAh),
|
||||
the design capacity, and the last full charge capacity.
|
||||
- `_BST` gives the charge/discharge state, the present rate, the remaining
|
||||
capacity and the present voltage.
|
||||
- `_PSR` on the adapter says whether it is plugged in.
|
||||
|
||||
Percentage is remaining / last-full, clamped to 100 and falling back to the
|
||||
design capacity when firmware reports last-full as zero. Results are cached
|
||||
for two seconds so that a panel repaint does not turn into EC traffic.
|
||||
|
||||
Almost all of this reaches the CPU through the **embedded controller**.
|
||||
`Hal::EmbeddedController` finds the EC's command/data port pair from the
|
||||
ECDT table when one exists, and otherwise from `_CRS` on the `PNP0C09`
|
||||
device, then drives the polled read/write protocol (`0x80`/`0x81` with
|
||||
OBF/IBF handshaking). It is wired into the AML interpreter as the
|
||||
`EmbeddedControl` region space, so any `OperationRegion` in the DSDT that
|
||||
targets the EC now works -- battery methods being the first user.
|
||||
|
||||
Supporting `_BST` also needed real package element storage in the AML
|
||||
interpreter. Firmware overwhelmingly builds its result package with
|
||||
`Store(x, Index(BST0, n))`, which requires `Index()` to name a writable
|
||||
element rather than yield a value. `AML::PackageStore` provides that: a
|
||||
persistent region for packages declared with `Name()`, and a transient ring
|
||||
rewound at each top-level evaluation for packages built inside a method, so
|
||||
polling `_BST` cannot exhaust it.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
`SYS_POWERINFO` (149) fills a `PowerInfo` struct: package temperature,
|
||||
TjMax, HWP state and performance range, the governor's current ceiling,
|
||||
CPUID base/turbo frequency, the measured average active frequency
|
||||
(APERF/MPERF over the 500 ms governor window), and the AP idle MWAIT hint.
|
||||
Returns -1 when the CPU exposes no power features (e.g. under QEMU).
|
||||
|
||||
`SYS_BATTERYINFO` (188) fills a `BatteryInfo` struct: percentage, charge
|
||||
state, AC presence, capacities, present rate, voltage and an estimated time
|
||||
remaining. It returns -1 on a machine with no battery device at all, which
|
||||
is how the desktop panel decides not to draw an indicator. Like
|
||||
`SYS_POWERINFO` it is ungated read-only telemetry: the panel indicator has
|
||||
to work in an unprivileged session, which holds no device capability to
|
||||
spend on it.
|
||||
|
||||
The `power` program (`programs/src/power`) prints both:
|
||||
|
||||
```
|
||||
power one-shot snapshot
|
||||
power watch 1 Hz samples for 60 s (temp, avg freq, ceiling)
|
||||
power watch 300 same, for 300 s
|
||||
```
|
||||
|
||||
Boot log lines to look for (Kernel Log app):
|
||||
|
||||
```
|
||||
[CpuPower] HWP enabled: perf range 4-54, EPP=0x80
|
||||
[CpuPower] Thermal governor armed: TjMax=100C, passive=85C, hot=94C, ...
|
||||
[CpuIdle] AP deep idle: MWAIT hint 0x20
|
||||
[EC] Embedded Controller at cmd=66 data=62
|
||||
[Battery] ACPI battery at \_SB.PCI0.LPCB.EC0.BAT0 (AC adapter present)
|
||||
```
|
||||
|
||||
The desktop panel shows a battery icon left of the Wi-Fi and network icons,
|
||||
using the icon pack's `battery-level-N[-charging]-symbolic` glyphs (one per
|
||||
10% step, reloaded only when the step changes). Clicking it opens a popout
|
||||
with the percentage in numbers, the charge state, time remaining and raw
|
||||
capacity. The icon is absent entirely when `SYS_BATTERYINFO` fails, so a
|
||||
desktop machine shows nothing there.
|
||||
|
||||
## Tuning
|
||||
|
||||
The governor constants live at the top of `Hal/CpuPower.cpp`
|
||||
(`PassiveTempC`, `HotTempC`, `ResumeTempC`, `GovernorIntervalMs`, step
|
||||
sizes) and were chosen to keep the package well below both TjMax (100 C
|
||||
on the i9-13900HX) and the Clevo EC's emergency thresholds. If the fans
|
||||
still spin up aggressively under sustained load, lower `PassiveTempC`
|
||||
first; if builds feel throttled while cool, raise the recovery rate
|
||||
instead of the trip points.
|
||||
-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.
|
||||
+2584
-1004
File diff suppressed because it is too large.
Load diff
@@ -97,6 +97,35 @@ namespace Hal {
|
||||
static constexpr uint8_t ConcatResOp = 0x84;
|
||||
static constexpr uint8_t ToHexStringOp = 0x98;
|
||||
static constexpr uint8_t ToDecimalStringOp = 0x97;
|
||||
static constexpr uint8_t ExternalOp = 0x15;
|
||||
static constexpr uint8_t RefOfOp = 0x71;
|
||||
static constexpr uint8_t FindSetLeftBitOp = 0x81;
|
||||
static constexpr uint8_t FindSetRightBitOp = 0x82;
|
||||
static constexpr uint8_t NotifyOp = 0x86;
|
||||
static constexpr uint8_t MatchOp = 0x89;
|
||||
static constexpr uint8_t ObjectTypeOp = 0x8E;
|
||||
static constexpr uint8_t CreateQWordFieldOp = 0x8F;
|
||||
static constexpr uint8_t ToStringOp = 0x9C;
|
||||
static constexpr uint8_t CopyObjectOp = 0x9D;
|
||||
static constexpr uint8_t MidOp = 0x9E;
|
||||
static constexpr uint8_t ContinueOp = 0x9F;
|
||||
static constexpr uint8_t BreakPointOp = 0xCC;
|
||||
|
||||
// More extended opcodes (after ExtOpPrefix)
|
||||
static constexpr uint8_t CondRefOfOp = 0x12;
|
||||
static constexpr uint8_t CreateFieldOp = 0x13;
|
||||
static constexpr uint8_t LoadTableOp = 0x1F;
|
||||
static constexpr uint8_t LoadOp = 0x20;
|
||||
static constexpr uint8_t SignalOp = 0x24;
|
||||
static constexpr uint8_t WaitOp = 0x25;
|
||||
static constexpr uint8_t ResetOp = 0x26;
|
||||
static constexpr uint8_t FromBCDOp = 0x28;
|
||||
static constexpr uint8_t ToBCDOp = 0x29;
|
||||
static constexpr uint8_t UnloadOp = 0x2A;
|
||||
static constexpr uint8_t DebugOp = 0x31;
|
||||
static constexpr uint8_t FatalOp = 0x32;
|
||||
static constexpr uint8_t TimerOp = 0x33;
|
||||
static constexpr uint8_t DataRegionOp = 0x88;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -104,7 +133,11 @@ namespace Hal {
|
||||
|
||||
// ============================================================================
|
||||
static constexpr int MaxCallDepth = 16;
|
||||
static constexpr int MaxLoopIterations = 1024;
|
||||
|
||||
// ACPICA's ceiling. Firmware polls hardware in While loops with a
|
||||
// Sleep or Stall in the body (link training, power-good, L2/L3 ready)
|
||||
// and expects more than a token number of passes.
|
||||
static constexpr int MaxLoopIterations = 0xFFFF;
|
||||
|
||||
// Backstop cap on evaluation nesting. This counter is shared by
|
||||
// expression evaluation AND block execution, and block nesting is
|
||||
@@ -133,8 +166,10 @@ namespace Hal {
|
||||
public:
|
||||
Interpreter();
|
||||
|
||||
// Parse a DSDT or SSDT table into the namespace.
|
||||
// tableData points to the CommonSDTHeader (HHDM-mapped).
|
||||
// Parse a DSDT or SSDT table into the namespace. Tables may be
|
||||
// loaded one after another; each method remembers which table
|
||||
// its body lives in. tableData points to the CommonSDTHeader
|
||||
// (HHDM-mapped) and must stay mapped.
|
||||
bool LoadTable(void* tableData);
|
||||
|
||||
// Evaluate a named object, returning its value.
|
||||
@@ -144,6 +179,28 @@ namespace Hal {
|
||||
// Evaluate a method with arguments.
|
||||
bool EvaluateMethod(const char* path, const Object* args, int argCount, Object& result);
|
||||
|
||||
// Evaluate a node: a method is run with `args`, a field is read,
|
||||
// anything else hands back its value (non-data objects as a
|
||||
// Reference to the node).
|
||||
bool EvaluateNode(int32_t node, const Object* args, int argCount, Object& result);
|
||||
|
||||
// Find the child of `parent` named `seg` ("_PS3", "_ON"), -1 if
|
||||
// there is none. Short names are padded with '_'.
|
||||
int32_t FindChild(int32_t parent, const char* seg) const;
|
||||
|
||||
// EvaluateNode() on a named child. False when it does not exist.
|
||||
bool EvaluateChild(int32_t parent, const char* seg, const Object* args,
|
||||
int argCount, Object& result);
|
||||
|
||||
// Turn a Reference, or a NameRef/String naming an object relative
|
||||
// to `scopeNode`, into a node index. -1 when it names nothing.
|
||||
int32_t ResolveReference(const Object& obj, int32_t scopeNode) const;
|
||||
|
||||
// The PCI function a Device node describes: its _ADR on the bus
|
||||
// behind its parent (a root bridge's _BBN, or a PCI-to-PCI
|
||||
// bridge's secondary bus). False when it cannot be placed.
|
||||
bool GetPciAddress(int32_t deviceNode, uint8_t& bus, uint8_t& dev, uint8_t& fn);
|
||||
|
||||
// Read a field value. Returns integer.
|
||||
bool ReadField(int32_t nodeIndex, uint64_t& value);
|
||||
|
||||
@@ -157,6 +214,10 @@ namespace Hal {
|
||||
// Check if the interpreter has been initialized
|
||||
bool IsInitialized() const { return m_initialized; }
|
||||
|
||||
// Nodes [0, DsdtNodeCount()) came from the DSDT. Nodes past it were
|
||||
// added by SSDTs (or by methods run since).
|
||||
int32_t DsdtNodeCount() const { return m_dsdtNodeCount; }
|
||||
|
||||
// Log every opcode executed, with its offset, for the next
|
||||
// `budget` opcodes. The only way to see where a method that runs to
|
||||
// completion without returning a value actually goes.
|
||||
@@ -170,8 +231,10 @@ namespace Hal {
|
||||
// Read one element of a Package object. Works for both a package
|
||||
// whose elements were computed at run time (materialized in the
|
||||
// package store) and one that is still just the constant AML bytes
|
||||
// it was declared with. Returns false when the object is not a
|
||||
// package or the index is out of range.
|
||||
// it was declared with. A name in a constant package comes back as
|
||||
// a NameRef unless it resolves (from the root) to plain data, in
|
||||
// which case its value is returned. Returns false when the object
|
||||
// is not a package or the index is out of range.
|
||||
bool GetPackageElement(const Object& package, int index, Object& result);
|
||||
|
||||
// Number of elements a Package declares.
|
||||
@@ -183,17 +246,35 @@ namespace Hal {
|
||||
int32_t MaterializeNodePackage(int32_t nodeIndex);
|
||||
|
||||
private:
|
||||
struct ExecContext;
|
||||
|
||||
// ============================================================================
|
||||
// Parsing (table load)
|
||||
// ============================================================================
|
||||
// ctx is the method being executed when a definition appears in
|
||||
// a method body, nullptr at table load. Operands that are TermArgs
|
||||
// (region offsets, buffer sizes, bank values) are evaluated in it.
|
||||
bool ParseBlock(const uint8_t* aml, uint32_t offset, uint32_t endOffset,
|
||||
int32_t scopeNode);
|
||||
|
||||
bool ParseNamedObject(const uint8_t* aml, uint32_t* pos, uint32_t endOffset,
|
||||
int32_t scopeNode);
|
||||
int32_t scopeNode, ExecContext* ctx = nullptr);
|
||||
|
||||
bool ParseExtendedOp(const uint8_t* aml, uint32_t* pos, uint32_t endOffset,
|
||||
int32_t scopeNode);
|
||||
int32_t scopeNode, ExecContext* ctx = nullptr);
|
||||
|
||||
// The FieldList shared by Field, IndexField and BankField.
|
||||
void ParseFieldList(const uint8_t* aml, uint32_t* pos, uint32_t fieldEnd,
|
||||
int32_t scopeNode, uint8_t fieldFlags, const Object& proto);
|
||||
|
||||
// Evaluate a TermArg operand of a definition: in the running
|
||||
// method, or in the table-load context.
|
||||
bool EvalOperand(ExecContext* ctx, const uint8_t* aml, uint32_t* pos,
|
||||
uint32_t endOffset, int32_t scopeNode, Object& result);
|
||||
|
||||
// CreateBitField .. CreateQWordField and CreateField.
|
||||
bool CreateBufferField(ExecContext& ctx, uint32_t* pos, uint32_t endOffset,
|
||||
uint8_t op);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -214,6 +295,10 @@ namespace Hal {
|
||||
char* outRelative = nullptr, int relLen = 0,
|
||||
bool* outSearchable = nullptr);
|
||||
|
||||
// Read a NameString and resolve it with the search rule. -1 when
|
||||
// it names nothing.
|
||||
int32_t ReadAndResolveName(const uint8_t* aml, uint32_t* pos, int32_t scopeNode);
|
||||
|
||||
// Read a single 4-char NameSeg from AML. Advances *pos.
|
||||
void ReadNameSeg(const uint8_t* aml, uint32_t* pos, char* outSeg);
|
||||
|
||||
@@ -225,11 +310,10 @@ namespace Hal {
|
||||
uint32_t DecodePkgLength(const uint8_t* aml, uint32_t* pos);
|
||||
|
||||
// Decode one constant package element out of raw AML bytes and
|
||||
// advance *pos past it. Non-constant elements (name references,
|
||||
// locals) decode to integer 0, which is what they were worth
|
||||
// before the package store existed.
|
||||
// advance *pos past it. Names come back as NameRef; locals and
|
||||
// anything else that needs a running method decode to integer 0.
|
||||
bool DecodeConstElement(const uint8_t* data, uint32_t length,
|
||||
uint32_t* pos, Object& out);
|
||||
uint32_t* pos, Object& out) const;
|
||||
uint64_t DecodeInteger(const uint8_t* aml, uint32_t* pos);
|
||||
|
||||
// ============================================================================
|
||||
@@ -247,6 +331,8 @@ namespace Hal {
|
||||
Object ReturnValue;
|
||||
bool Returned;
|
||||
bool Broken;
|
||||
bool Continued;
|
||||
bool Aborted; // undecodable AML: unwind the method
|
||||
int Depth; // method-call nesting
|
||||
int ExprDepth; // expression/block nesting
|
||||
};
|
||||
@@ -259,36 +345,117 @@ namespace Hal {
|
||||
~DepthGuard() { Value--; }
|
||||
};
|
||||
|
||||
// Where a Store lands.
|
||||
struct Target {
|
||||
enum class Kind : uint8_t { None, Node, Local, Arg, PkgElement, BufByte };
|
||||
Kind What = Kind::None;
|
||||
int32_t Node = -1;
|
||||
int Index = 0; // local/arg number, or element index
|
||||
int32_t PkgStore = -1;
|
||||
Object* Buf = nullptr; // BufByte: the buffer being indexed
|
||||
};
|
||||
|
||||
bool ExecuteBlock(ExecContext& ctx, uint32_t offset, uint32_t endOffset);
|
||||
bool ExecuteOpcode(ExecContext& ctx, uint32_t* pos, uint32_t endOffset);
|
||||
|
||||
// Evaluate a term (expression that produces a value) within an execution context.
|
||||
bool EvalTerm(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
|
||||
// Evaluate a "SuperName" target for Store operations.
|
||||
// Returns the node index for named targets, or handles locals/args.
|
||||
// If isLocal/isArg is set, localIdx/argIdx contains the index.
|
||||
// A target may also be Index(package, n), which names a package
|
||||
// element rather than a node, local or arg. pkgStore/pkgElement
|
||||
// carry that case (pkgStore > 0 when it applies).
|
||||
bool EvalTarget(ExecContext& ctx, uint32_t* pos,
|
||||
int32_t& nodeIndex, bool& isLocal, int& localIdx,
|
||||
bool& isArg, int& argIdx,
|
||||
int32_t& pkgStore, int& pkgElement);
|
||||
// Extended (0x5B-prefixed) opcodes in expression or statement position.
|
||||
bool EvalExtOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
|
||||
// Store a value to a target (node, local, or arg).
|
||||
void StoreToTarget(ExecContext& ctx, const Object& value,
|
||||
int32_t nodeIndex, bool isLocal, int localIdx,
|
||||
bool isArg, int argIdx,
|
||||
int32_t pkgStore = -1, int pkgElement = 0);
|
||||
// EvalTerm's opcode handlers. They live out of line so the
|
||||
// recursive path only carries the locals of the one running.
|
||||
bool EvalBufferOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalPackageOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalBinary(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalUnary(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalDivide(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalStoreOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalReference(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalIndexOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalMatchOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalMidOp(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool EvalNameRef(ExecContext& ctx, uint32_t* pos, uint32_t endOffset, Object& result);
|
||||
bool CallMethod(ExecContext& ctx, int32_t node, uint32_t* pos, uint32_t endOffset,
|
||||
Object& result);
|
||||
bool IndexElement(const Object& source, uint64_t idx, Object& out);
|
||||
void ReportUnresolved(ExecContext& ctx, uint32_t namePos);
|
||||
|
||||
// Target forms that need operand evaluation of their own.
|
||||
bool EvalIndexTarget(ExecContext& ctx, uint32_t* pos, Target& target);
|
||||
bool EvalDerefTarget(ExecContext& ctx, uint32_t* pos, Target& target);
|
||||
|
||||
// A package element: like EvalTerm, except that a name is a
|
||||
// reference to the object rather than a call or a read.
|
||||
bool EvalPackageElement(ExecContext& ctx, uint32_t* pos, uint32_t endOffset,
|
||||
Object& result);
|
||||
|
||||
// Parse a SuperName/Target operand.
|
||||
bool EvalTarget(ExecContext& ctx, uint32_t* pos, Target& target);
|
||||
|
||||
// Store a value to a target, with ACPI's implicit conversion for
|
||||
// named data objects.
|
||||
void StoreToTarget(ExecContext& ctx, const Object& value, const Target& target);
|
||||
|
||||
// Parse a Target operand and store `value` there.
|
||||
void StoreResult(ExecContext& ctx, uint32_t* pos, const Object& value);
|
||||
|
||||
// The current value behind a target (for Increment and friends).
|
||||
Object ReadTarget(ExecContext& ctx, const Target& target);
|
||||
|
||||
// Run a method node. Caller holds m_evalLock.
|
||||
bool InvokeMethodNode(int32_t node, const Object* args, int argCount,
|
||||
Object& result, int depth, int exprDepth);
|
||||
|
||||
// Run a method in a context already acquired and holding its
|
||||
// arguments; releases the context.
|
||||
bool RunMethodIn(ExecContext* ctx, int32_t node, int depth, int exprDepth,
|
||||
Object& result);
|
||||
|
||||
// The value of a node as an operand: fields read, methods run
|
||||
// without arguments, non-data objects become References.
|
||||
bool NodeValue(int32_t node, Object& result, int depth, int exprDepth);
|
||||
|
||||
// Log an opcode we cannot decode and stop the method: carrying on
|
||||
// past it would execute operand bytes as code.
|
||||
bool Abort(ExecContext& ctx, uint32_t pos, const char* why);
|
||||
|
||||
// Native methods (\_OSI).
|
||||
bool RunNativeMethod(uint8_t which, const Object* args, int argCount, Object& result);
|
||||
|
||||
// Install the OS-defined objects (\_OSI, \_REV, \_OS_, \_GL_).
|
||||
void InstallPredefined();
|
||||
|
||||
// Device identification (for PCI_Config regions).
|
||||
bool IntegerChild(int32_t device, const char* seg, uint64_t fallback,
|
||||
uint64_t& value, int depth);
|
||||
bool IsPciRootBridge(int32_t device, int depth);
|
||||
bool ResolvePciAddress(int32_t node, uint8_t& bus, uint8_t& dev,
|
||||
uint8_t& fn, int depth);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Field I/O
|
||||
|
||||
// ============================================================================
|
||||
bool ReadRegion(RegionSpace space, uint64_t address, uint32_t bitWidth, uint64_t& value);
|
||||
bool WriteRegion(RegionSpace space, uint64_t address, uint32_t bitWidth, uint64_t value);
|
||||
// Access `bits` (little-endian, bitLength long) of a field unit.
|
||||
bool AccessField(int32_t fieldNode, bool write, uint8_t* bits);
|
||||
bool ReadFieldObject(int32_t fieldNode, Object& value);
|
||||
bool WriteFieldObject(int32_t fieldNode, const Object& value);
|
||||
|
||||
// One aligned access unit of a field's backing store.
|
||||
bool FieldUnitIo(const Object& field, uint64_t byteOffset, uint32_t width,
|
||||
bool write, uint64_t& value);
|
||||
|
||||
// Region access at byte `offset` into the region, `width` bytes.
|
||||
bool ReadRegion(int32_t regionNode, uint64_t offset, uint32_t width, uint64_t& value);
|
||||
bool WriteRegion(int32_t regionNode, uint64_t offset, uint32_t width, uint64_t value);
|
||||
bool RegionIo(int32_t regionNode, uint64_t offset, uint32_t width,
|
||||
bool write, uint64_t& value);
|
||||
|
||||
bool ReadBufferField(const Object& field, Object& value);
|
||||
bool WriteBufferField(const Object& field, const Object& value);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -310,16 +477,23 @@ namespace Hal {
|
||||
ExecContext* AcquireContext();
|
||||
void ReleaseContext();
|
||||
|
||||
// Run `body` as a top-level evaluation: serialized, with the stack
|
||||
// budget anchored here.
|
||||
template<typename Fn> bool TopLevel(Fn body);
|
||||
|
||||
Namespace m_ns;
|
||||
const uint8_t* m_dsdt;
|
||||
uint32_t m_dsdtLength;
|
||||
bool m_initialized;
|
||||
bool m_predefined;
|
||||
int32_t m_dsdtNodeCount;
|
||||
|
||||
ExecContext* m_ctxPool; // MaxCallDepth + 1 entries, heap
|
||||
int m_ctxInUse;
|
||||
int m_evalNesting; // top-level evaluations in progress
|
||||
uint64_t m_stackFloor; // stack address the interpreter stops at
|
||||
int m_traceBudget; // opcodes left to log, 0 = off
|
||||
ExecContext* m_loadCtx; // table-load operand context
|
||||
uint64_t m_delayedUs; // total Sleep/Stall time requested
|
||||
int m_methodScan; // >0 while scanning a method body at load
|
||||
kcp::Mutex m_evalLock;
|
||||
};
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ namespace Hal {
|
||||
s_transientNext = 0;
|
||||
}
|
||||
|
||||
bool IsPersistent(int32_t index) {
|
||||
return index > 0 && index <= MaxPersistent;
|
||||
}
|
||||
|
||||
static Entry* Lookup(int32_t index) {
|
||||
if (index <= 0) return nullptr;
|
||||
int32_t i = index - 1;
|
||||
@@ -293,6 +297,12 @@ namespace Hal {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int32_t Namespace::FindChild(int32_t parentIndex, const char* seg) const {
|
||||
char padded[MaxNameSegLen + 1];
|
||||
PadSegment(seg, padded);
|
||||
return FindChildByName(parentIndex, padded);
|
||||
}
|
||||
|
||||
int32_t Namespace::CreateNode(const char* absolutePath) {
|
||||
EnsureRoot();
|
||||
char segments[MaxPathDepth][MaxNameSegLen + 1];
|
||||
|
||||
@@ -31,6 +31,16 @@ namespace Hal {
|
||||
ThermalZone,
|
||||
PowerResource,
|
||||
BufferField,
|
||||
Event,
|
||||
// A reference to a namespace node: what RefOf() yields, and what a
|
||||
// name evaluates to when it names something that is not data (a
|
||||
// device or power resource listed in a _PRx package, say).
|
||||
Reference,
|
||||
// A name inside a constant package, kept as written ("PG00",
|
||||
// "\_SB.PCI0.PEG0") because its scope is only known to whoever
|
||||
// reads the package. Interpreter::ResolveReference() turns it
|
||||
// into a node.
|
||||
NameRef,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -63,7 +73,7 @@ namespace Hal {
|
||||
static constexpr int InitialChildren = 8;
|
||||
static constexpr int MaxStringLen = 64;
|
||||
static constexpr int MaxBufferLen = 256;
|
||||
static constexpr int MaxPackageElements = 32;
|
||||
static constexpr int MaxPackageElements = 64;
|
||||
static constexpr int MaxMethodArgs = 7;
|
||||
static constexpr int MaxMethodLocals = 8;
|
||||
|
||||
@@ -102,10 +112,23 @@ namespace Hal {
|
||||
uint32_t Length;
|
||||
} Buffer;
|
||||
|
||||
// A package's constant form: its element bytes, left in the
|
||||
// table they were declared in, and the declared element count. ACPI tables stay
|
||||
// mapped for the life of the kernel, so pointing at them is
|
||||
// both cheaper and free of the size cap a copy would carry --
|
||||
// a GPIO community table runs well past MaxBufferLen.
|
||||
struct {
|
||||
const uint8_t* Data; // first element
|
||||
uint32_t Length; // bytes of element encodings
|
||||
uint32_t Count; // NumElements
|
||||
} Package;
|
||||
|
||||
struct {
|
||||
uint8_t ArgCount; // bits 0-2 of method flags
|
||||
bool Serialized; // bit 3
|
||||
uint32_t AmlOffset; // offset into DSDT AML where the method body starts
|
||||
uint8_t Native; // NativeMethod; 0 = AML
|
||||
const uint8_t* Table; // the table the body lives in
|
||||
uint32_t AmlOffset; // offset into Table where the method body starts
|
||||
uint32_t AmlLength; // length of the method body
|
||||
} Method;
|
||||
|
||||
@@ -116,12 +139,33 @@ namespace Hal {
|
||||
} Region;
|
||||
|
||||
struct {
|
||||
uint32_t RegionNodeIndex; // index of the parent OperationRegion node
|
||||
// Field / BankField: the OperationRegion node.
|
||||
// IndexField: the index register field.
|
||||
uint32_t RegionNodeIndex;
|
||||
uint32_t BitOffset;
|
||||
uint32_t BitLength;
|
||||
uint8_t AccessType; // 0=Any, 1=Byte, 2=Word, 3=DWord, 4=QWord, 5=Buffer
|
||||
uint8_t UpdateRule; // 0=Preserve, 1=WriteAsOnes, 2=WriteAsZeros
|
||||
uint8_t Kind; // FieldKind
|
||||
int32_t DataNodeIndex; // IndexField: the data register field
|
||||
int32_t BankNodeIndex; // BankField: the bank select field
|
||||
uint64_t BankValue; // BankField: value selecting this bank
|
||||
} Field;
|
||||
|
||||
// CreateXxxField over a buffer. Source points at the object
|
||||
// holding the buffer -- a namespace node's, or a local/arg slot
|
||||
// of the method that created the field -- so reads and writes
|
||||
// go through to it.
|
||||
struct {
|
||||
Object* Source;
|
||||
uint32_t BitOffset;
|
||||
uint32_t BitLength;
|
||||
} BufferField;
|
||||
|
||||
struct {
|
||||
int32_t Node;
|
||||
} Reference;
|
||||
|
||||
struct {
|
||||
uint8_t ProcId;
|
||||
uint32_t PblkAddr;
|
||||
@@ -132,6 +176,18 @@ namespace Hal {
|
||||
Object() : Type(ObjectType::None), PkgIndex(-1), Integer(0) {}
|
||||
};
|
||||
|
||||
enum class FieldKind : uint8_t {
|
||||
Normal = 0,
|
||||
Index = 1,
|
||||
Bank = 2,
|
||||
};
|
||||
|
||||
// Methods the OS provides rather than the firmware.
|
||||
enum class NativeMethod : uint8_t {
|
||||
None = 0,
|
||||
Osi = 1, // \_OSI
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
// Package element store
|
||||
@@ -155,6 +211,9 @@ namespace Hal {
|
||||
// Rewind the transient ring. Called once per top-level evaluation.
|
||||
void ResetTransient();
|
||||
|
||||
// True for an entry that outlives the evaluation that made it.
|
||||
bool IsPersistent(int32_t index);
|
||||
|
||||
// Element access. Returns nullptr for an unknown entry or an
|
||||
// out-of-range element.
|
||||
Object* Element(int32_t index, int element);
|
||||
@@ -219,6 +278,10 @@ namespace Hal {
|
||||
// rebased onto a scope. Returns -1 if no scope contains it.
|
||||
int32_t SearchUpward(const char* relative, int32_t scopeNodeIndex) const;
|
||||
|
||||
// Find the child of `parent` named `seg`; a short name is padded
|
||||
// with '_' the way node names are stored. Returns -1 if absent.
|
||||
int32_t FindChild(int32_t parentIndex, const char* seg) const;
|
||||
|
||||
// Get a node by index.
|
||||
NamespaceNode* GetNode(int32_t index);
|
||||
const NamespaceNode* GetNode(int32_t index) const;
|
||||
|
||||
@@ -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();
|
||||
|
||||
};
|
||||
};
|
||||
@@ -453,15 +453,14 @@ namespace Hal {
|
||||
auto* nsNode = ns.GetNode(node);
|
||||
if (!nsNode) return result;
|
||||
|
||||
if (nsNode->Obj.Type == AML::ObjectType::Package) {
|
||||
// Parse the raw package buffer to extract SLP_TYP values
|
||||
const uint8_t* data = nsNode->Obj.Buffer.Data;
|
||||
uint32_t len = nsNode->Obj.Buffer.Length;
|
||||
if (len < 2) return result;
|
||||
if (nsNode->Obj.Type == AML::ObjectType::Package && nsNode->Obj.Package.Data) {
|
||||
// Parse the raw package elements to extract SLP_TYP values
|
||||
const uint8_t* data = nsNode->Obj.Package.Data;
|
||||
uint32_t len = nsNode->Obj.Package.Length;
|
||||
if (len < 1) return result;
|
||||
|
||||
// NumElements
|
||||
uint8_t numElements = data[0];
|
||||
uint32_t pos = 1;
|
||||
uint32_t numElements = nsNode->Obj.Package.Count;
|
||||
uint32_t pos = 0;
|
||||
|
||||
if (numElements >= 1 && pos < len) {
|
||||
// Decode first element
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
@@ -58,7 +58,7 @@ namespace Hal {
|
||||
// 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);
|
||||
AML::InitializeInterpreter(dsdt, xsdt);
|
||||
|
||||
// Enumerate ACPI devices now that the namespace is loaded
|
||||
if (AML::GetInterpreter().IsInitialized()) {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Hal/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>
|
||||
@@ -128,9 +129,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";
|
||||
@@ -459,7 +460,9 @@ namespace Hal {
|
||||
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
|
||||
Drivers::Graphics::IntelGPU::Reinitialize();
|
||||
}
|
||||
reinitProgress(0x0D); // sti
|
||||
reinitProgress(0x0D); // discrete GPU back off
|
||||
Drivers::Graphics::DgpuPower::Resume();
|
||||
reinitProgress(0x0E); // sti
|
||||
asm volatile("sti");
|
||||
|
||||
KernelLogStream(OK, "S3") << "Resumed from S3 suspend";
|
||||
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
};
|
||||
@@ -249,13 +249,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 +267,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 +314,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;
|
||||
@@ -522,7 +529,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
};
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
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::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
// Use a stack buffer for kernel-side results (max 16)
|
||||
Drivers::USB::Bluetooth::Hci::InquiryDevice tmpBuf[16];
|
||||
@@ -46,15 +46,15 @@ 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::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
return (int64_t)Drivers::USB::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::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
Kt::KernelLogStream(Kt::INFO, "Power") << "pid " << kcp::dec
|
||||
<< (uint64_t)Sched::GetCurrentPid()
|
||||
@@ -63,11 +63,11 @@ namespace montauk::abi {
|
||||
}
|
||||
|
||||
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::USB::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::USB::Bluetooth::SetAddress(bdAddr) ? Success : GenericFailure;
|
||||
}
|
||||
|
||||
static int64_t Sys_BtBonds(BtBondInfo* buf, int maxCount) {
|
||||
@@ -90,8 +90,8 @@ 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::USB::Bluetooth::IsInitialized()) return NoDevice;
|
||||
|
||||
return (int64_t)Drivers::USB::Bluetooth::ForgetDevice(bdAddr);
|
||||
}
|
||||
@@ -119,13 +119,13 @@ 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()) {
|
||||
buf->initialized = 0;
|
||||
return -1;
|
||||
return NoDevice;
|
||||
}
|
||||
|
||||
buf->initialized = 1;
|
||||
@@ -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 220
|
||||
#define MONTAUK_BUILD_NUMBER 4
|
||||
@@ -90,22 +90,24 @@ namespace montauk::abi {
|
||||
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;
|
||||
if (data == nullptr || len > CLIPBOARD_MAX_TEXT_BYTES)
|
||||
return InvalidArgument;
|
||||
if (Memory::g_heap == nullptr)
|
||||
return OutOfMemory;
|
||||
|
||||
for (uint32_t i = 0; i < len; i++) {
|
||||
if (data[i] == '\0')
|
||||
return -1;
|
||||
return InvalidArgument;
|
||||
}
|
||||
|
||||
const char* currentUser = ClipboardCurrentUser();
|
||||
if (currentUser == nullptr) return -1;
|
||||
if (currentUser == nullptr) return GenericFailure;
|
||||
|
||||
char user[32];
|
||||
ClipboardCopyUser(user, currentUser);
|
||||
|
||||
char* newText = (char*)Memory::g_heap->Request(len + 1);
|
||||
if (newText == nullptr) return -1;
|
||||
if (newText == nullptr) return OutOfMemory;
|
||||
for (uint32_t i = 0; i < len; i++)
|
||||
newText[i] = data[i];
|
||||
newText[len] = '\0';
|
||||
@@ -129,18 +131,18 @@ namespace montauk::abi {
|
||||
if (oldText != nullptr)
|
||||
Memory::g_heap->Free(oldText);
|
||||
|
||||
return 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -152,17 +154,17 @@ namespace montauk::abi {
|
||||
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);
|
||||
@@ -173,25 +175,25 @@ namespace montauk::abi {
|
||||
if (slot != nullptr && outSerial != nullptr)
|
||||
*outSerial = slot->serial;
|
||||
g_clipboardLock.Release();
|
||||
return 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
*outLen = slot->len;
|
||||
if (outSerial != nullptr) *outSerial = slot->serial;
|
||||
if (buffer == nullptr || bufferLen < slot->len) {
|
||||
g_clipboardLock.Release();
|
||||
return -1;
|
||||
return BufferTooSmall;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < slot->len; i++)
|
||||
buffer[i] = slot->text[i];
|
||||
g_clipboardLock.Release();
|
||||
return 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
static int Sys_ClipboardClear() {
|
||||
const char* currentUser = ClipboardCurrentUser();
|
||||
if (currentUser == nullptr) return -1;
|
||||
if (currentUser == nullptr) return GenericFailure;
|
||||
|
||||
char user[32];
|
||||
ClipboardCopyUser(user, currentUser);
|
||||
@@ -214,7 +216,7 @@ namespace montauk::abi {
|
||||
if (oldText != nullptr)
|
||||
Memory::g_heap->Free(oldText);
|
||||
|
||||
return 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -225,10 +225,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++)
|
||||
@@ -303,8 +303,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
|
||||
@@ -80,11 +99,11 @@ namespace montauk::abi {
|
||||
|
||||
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)) {
|
||||
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"
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
@@ -262,7 +262,7 @@ namespace montauk::abi {
|
||||
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;
|
||||
@@ -343,12 +343,12 @@ namespace montauk::abi {
|
||||
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;
|
||||
return Success;
|
||||
}
|
||||
|
||||
// Resolve a non-present access inside an anonymous VMA. This is used for
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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--;
|
||||
@@ -144,7 +144,7 @@ namespace montauk::abi {
|
||||
}
|
||||
|
||||
g_libLocks[slot].Release();
|
||||
return 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
// Resolve a symbol in a loaded library.
|
||||
|
||||
@@ -164,7 +164,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 +193,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 ----
|
||||
|
||||
@@ -49,7 +49,7 @@ 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() {
|
||||
@@ -88,7 +88,7 @@ namespace montauk::abi {
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -98,7 +98,7 @@ namespace montauk::abi {
|
||||
static int64_t Sys_PowerInfo(PowerInfo* out) {
|
||||
Hal::CpuPower::Snapshot s{};
|
||||
if (!Hal::CpuPower::GetSnapshot(s)) {
|
||||
return -1;
|
||||
return NoDevice;
|
||||
}
|
||||
|
||||
out->hwpActive = s.HwpActive ? 1 : 0;
|
||||
@@ -113,7 +113,7 @@ namespace montauk::abi {
|
||||
out->maxMHz = s.MaxMHz;
|
||||
out->effMHz = s.EffMHz;
|
||||
out->apIdleHint = Hal::CpuIdle::GetApIdleHint();
|
||||
return 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
// SYS_BATTERYINFO. Fills a battery snapshot. Returns -1 on a machine with
|
||||
@@ -122,7 +122,7 @@ namespace montauk::abi {
|
||||
static int64_t Sys_BatteryInfo(BatteryInfo* out) {
|
||||
Hal::Battery::Status s{};
|
||||
if (!Hal::Battery::GetStatus(s)) {
|
||||
return -1;
|
||||
return NoDevice;
|
||||
}
|
||||
|
||||
out->present = s.Present ? 1 : 0;
|
||||
@@ -139,6 +139,6 @@ namespace montauk::abi {
|
||||
out->presentRate = s.PresentRate;
|
||||
out->voltageMv = s.VoltageMv;
|
||||
out->minutesRemaining = s.MinutesRemaining;
|
||||
return 0;
|
||||
return Success;
|
||||
}
|
||||
};
|
||||
+64
-29
@@ -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;
|
||||
}
|
||||
};
|
||||
+65
-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,12 @@ namespace montauk::abi {
|
||||
return (int64_t)Fs::FsProbe::SyncAndUnmountAll();
|
||||
}
|
||||
|
||||
// Format a partition with a filesystem. Returns 0 on success, -1 on error.
|
||||
// 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 +180,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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+222
-184
@@ -11,6 +11,7 @@
|
||||
|
||||
/* 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
|
||||
@@ -29,7 +30,7 @@
|
||||
#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 "Storage.hpp" // SYS_PARTLIST, SYS_VOLINFO, 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 "Audio.hpp" // SYS_AUDIOOPEN, SYS_AUDIOCLOSE, SYS_AUDIOWRITE, SYS_AUDIOCTL
|
||||
#include "BluetoothSyscall.hpp" // SYS_BTSCAN, SYS_BTCONNECT, SYS_BTDISCONNECT, SYS_BTLIST, SYS_BTINFO
|
||||
@@ -61,54 +62,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 +121,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 +133,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 +149,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 +225,33 @@ 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_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 +264,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 +379,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 +427,54 @@ 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_FSFORMAT:
|
||||
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return -1;
|
||||
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return BadPointer;
|
||||
return (int64_t)Sys_FsFormat((const FsFormatParams*)frame->arg1);
|
||||
case SYS_AUDIOOPEN:
|
||||
return Sys_AudioOpen((uint32_t)frame->arg1, (uint8_t)frame->arg2, (uint8_t)frame->arg3);
|
||||
case SYS_AUDIOCLOSE:
|
||||
return Sys_AudioClose((int)frame->arg1);
|
||||
case SYS_AUDIOWRITE:
|
||||
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return -1;
|
||||
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
|
||||
return Sys_AudioWrite((int)frame->arg1, (const uint8_t*)frame->arg2, (uint32_t)frame->arg3);
|
||||
case SYS_AUDIOCTL:
|
||||
return Sys_AudioCtl((int)frame->arg1, (int)frame->arg2, (int)frame->arg3);
|
||||
case SYS_AUDIOLIST:
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(AudioStreamInfo), true)) return -1;
|
||||
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(AudioStreamInfo), true)) return BadPointer;
|
||||
return Sys_AudioList((AudioStreamInfo*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_AUDIOWAIT:
|
||||
return Sys_AudioWait(frame->arg1, frame->arg2);
|
||||
@@ -502,58 +515,83 @@ namespace montauk::abi {
|
||||
return Sys_UsbBulkInRead((int)frame->arg1, (uint8_t*)frame->arg2,
|
||||
(uint32_t)frame->arg3);
|
||||
case SYS_POWERINFO:
|
||||
if (!UserMemory::Writable<PowerInfo>(frame->arg1)) return -1;
|
||||
if (!UserMemory::Writable<PowerInfo>(frame->arg1)) return BadPointer;
|
||||
return Sys_PowerInfo((PowerInfo*)frame->arg1);
|
||||
case SYS_BATTERYINFO:
|
||||
// Read-only power telemetry, ungated like SYS_POWERINFO: the
|
||||
// panel indicator has to work in an unprivileged session, which
|
||||
// holds no device capability to spend on it.
|
||||
if (!UserMemory::Writable<BatteryInfo>(frame->arg1)) return -1;
|
||||
if (!UserMemory::Writable<BatteryInfo>(frame->arg1)) return BadPointer;
|
||||
return Sys_BatteryInfo((BatteryInfo*)frame->arg1);
|
||||
case SYS_GETCAPS:
|
||||
if (!UserMemory::Writable<SpawnCapabilities>(frame->arg1)) return BadPointer;
|
||||
return Sys_GetCaps((SpawnCapabilities*)frame->arg1);
|
||||
case SYS_CONSENT_REGISTER:
|
||||
return Sched::Consent::Register();
|
||||
case SYS_CONSENT_NEXT:
|
||||
if (!UserMemory::Writable<ConsentRequest>(frame->arg1)) return BadPointer;
|
||||
return Sched::Consent::Next((ConsentRequest*)frame->arg1);
|
||||
case SYS_CONSENT_REPLY:
|
||||
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
|
||||
case SYS_GETKEY_EX:
|
||||
if (frame->arg2 < sizeof(KeyEventV1) || frame->arg2 > 4096) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
|
||||
return Sys_GetKeyEx((void*)frame->arg1, frame->arg2);
|
||||
case SYS_CHILDIO_WRITEKEY_EX:
|
||||
if (frame->arg3 < sizeof(KeyEventV1) || frame->arg3 > 4096) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg2, frame->arg3, false)) return BadPointer;
|
||||
return (int64_t)Sys_ChildIoWriteKeyEx((int)frame->arg1, (const void*)frame->arg2, frame->arg3);
|
||||
case SYS_THREAD_SPAWN:
|
||||
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
|
||||
case SYS_THREAD_EXIT:
|
||||
Sys_ThreadExit((int)frame->arg1);
|
||||
return 0;
|
||||
return Success;
|
||||
case SYS_THREAD_JOIN:
|
||||
if (frame->arg2 != 0 && !UserMemory::Writable<int>(frame->arg2)) return -1;
|
||||
if (frame->arg2 != 0 && !UserMemory::Writable<int>(frame->arg2)) return BadPointer;
|
||||
return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2);
|
||||
case SYS_THREAD_SELF:
|
||||
return Sys_ThreadSelf();
|
||||
case SYS_FUTEX_WAIT:
|
||||
// Validation demand-commits the page so the scheduler can
|
||||
// read the word with its lock held without faulting.
|
||||
if (!UserMemory::Readable<uint32_t>(frame->arg1)) return BadPointer;
|
||||
return Sys_FutexWait(frame->arg1, (uint32_t)frame->arg2, frame->arg3);
|
||||
case SYS_FUTEX_WAKE:
|
||||
return Sys_FutexWake(frame->arg1, (uint32_t)frame->arg2);
|
||||
case SYS_BTSCAN:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return -1;
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
|
||||
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return BadPointer;
|
||||
return Sys_BtScan((BtScanResult*)frame->arg1, (int)frame->arg2, (uint32_t)frame->arg3);
|
||||
case SYS_BTCONNECT:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return BadPointer;
|
||||
return Sys_BtConnect((const uint8_t*)frame->arg1);
|
||||
case SYS_BTDISCONNECT:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return BadPointer;
|
||||
return Sys_BtDisconnect((const uint8_t*)frame->arg1);
|
||||
case SYS_BTSETADDR:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return BadPointer;
|
||||
return Sys_BtSetAddr((const uint8_t*)frame->arg1);
|
||||
case SYS_BTBONDS:
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtBondInfo), true)) return -1;
|
||||
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtBondInfo), true)) return BadPointer;
|
||||
return Sys_BtBonds((BtBondInfo*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_BTFORGET:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return BadPointer;
|
||||
return Sys_BtForget((const uint8_t*)frame->arg1);
|
||||
case SYS_BTLIST:
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtDevInfo), true)) return -1;
|
||||
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtDevInfo), true)) return BadPointer;
|
||||
return Sys_BtList((BtDevInfo*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_BTINFO:
|
||||
if (!UserMemory::Writable<BtAdapterInfo>(frame->arg1)) return -1;
|
||||
if (!UserMemory::Writable<BtAdapterInfo>(frame->arg1)) return BadPointer;
|
||||
return Sys_BtInfo((BtAdapterInfo*)frame->arg1);
|
||||
case SYS_WIFI_SCAN:
|
||||
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WifiNetwork), true)) return -1;
|
||||
return Sys_WifiScan((WifiNetwork*)frame->arg1, (int)frame->arg2, (uint32_t)frame->arg3);
|
||||
@@ -561,81 +599,81 @@ namespace montauk::abi {
|
||||
if (!UserMemory::Writable<WifiInfo>(frame->arg1)) return -1;
|
||||
return Sys_WifiInfo((WifiInfo*)frame->arg1);
|
||||
case SYS_WIFI_CONNECT:
|
||||
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::String(frame->arg1, 64)) return -1;
|
||||
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1;
|
||||
return Sys_WifiConnect((const char*)frame->arg1, (const char*)frame->arg2);
|
||||
case SYS_WIFI_DISCONNECT:
|
||||
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
return Sys_WifiDisconnect();
|
||||
case SYS_WIFI_SCAN_START:
|
||||
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
return Sys_WifiScanStart((uint32_t)frame->arg1);
|
||||
case SYS_WIFI_RESULTS:
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WifiNetwork), true)) return -1;
|
||||
return Sys_WifiResults((WifiNetwork*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_WIFI_CONNECT_ASYNC:
|
||||
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::String(frame->arg1, 64)) return -1;
|
||||
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1;
|
||||
return Sys_WifiConnectAsync((const char*)frame->arg1, (const char*)frame->arg2);
|
||||
case SYS_NETIFS:
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(NetIfInfo), true)) return -1;
|
||||
if ((int64_t)frame->arg2 < 0) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(NetIfInfo), true)) return BadPointer;
|
||||
return Sys_NetIfs((NetIfInfo*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_SUSPEND:
|
||||
if (!Sched::HasCapability(CAP_SUSPEND)) return SYS_ERR_PERMISSION;
|
||||
if (!Sched::HasCapability(CAP_SUSPEND)) return CapabilityDenied;
|
||||
return Sys_Suspend();
|
||||
case SYS_SETTZ:
|
||||
if (!Sched::HasCapability(CAP_SET_TIME)) return SYS_ERR_PERMISSION;
|
||||
if (!Sched::Consent::Require(CAP_SET_TIME)) return CapabilityDenied;
|
||||
return Sys_SetTZ((int32_t)frame->arg1);
|
||||
case SYS_GETTZ:
|
||||
return Sys_GetTZ();
|
||||
case SYS_SETUSER:
|
||||
if (!Sched::HasCapability(CAP_USER_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::String(frame->arg2, kMaxUserNameBytes)) return -1;
|
||||
if (!Sched::HasCapability(CAP_USER_ADMIN)) return CapabilityDenied;
|
||||
if (!UserMemory::String(frame->arg2, kMaxUserNameBytes)) return BadPointer;
|
||||
return Sys_SetUser((int)frame->arg1, (const char*)frame->arg2);
|
||||
case SYS_GETUSER:
|
||||
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
|
||||
return Sys_GetUser((char*)frame->arg1, frame->arg2);
|
||||
case SYS_GETCWD:
|
||||
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return BadPointer;
|
||||
return Sys_GetCwd((char*)frame->arg1, frame->arg2);
|
||||
case SYS_CHDIR:
|
||||
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
|
||||
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return BadPointer;
|
||||
return Sys_Chdir((const char*)frame->arg1);
|
||||
case SYS_DUPHANDLE:
|
||||
return Sys_DupHandle((int)frame->arg1);
|
||||
case SYS_WAIT_HANDLE:
|
||||
return (int64_t)Sys_WaitHandle((int)frame->arg1, (uint32_t)frame->arg2, frame->arg3);
|
||||
case SYS_STREAM_CREATE:
|
||||
if (!UserMemory::Writable<int>(frame->arg1) || !UserMemory::Writable<int>(frame->arg2)) return -1;
|
||||
if (!UserMemory::Writable<int>(frame->arg1) || !UserMemory::Writable<int>(frame->arg2)) return BadPointer;
|
||||
return Sys_StreamCreate((int*)frame->arg1, (int*)frame->arg2, (uint32_t)frame->arg3);
|
||||
case SYS_STREAM_READ:
|
||||
if ((int64_t)frame->arg3 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, true)) return -1;
|
||||
if ((int64_t)frame->arg3 < 0) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, true)) return BadPointer;
|
||||
return Sys_StreamRead((int)frame->arg1, (uint8_t*)frame->arg2, (int)frame->arg3);
|
||||
case SYS_STREAM_WRITE:
|
||||
if ((int64_t)frame->arg3 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return -1;
|
||||
if ((int64_t)frame->arg3 < 0) return InvalidArgument;
|
||||
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, false)) return BadPointer;
|
||||
return Sys_StreamWrite((int)frame->arg1, (const uint8_t*)frame->arg2, (int)frame->arg3);
|
||||
case SYS_MAILBOX_CREATE:
|
||||
if (!UserMemory::Writable<int>(frame->arg1) || !UserMemory::Writable<int>(frame->arg2)) return -1;
|
||||
if (!UserMemory::Writable<int>(frame->arg1) || !UserMemory::Writable<int>(frame->arg2)) return BadPointer;
|
||||
return Sys_MailboxCreate((int*)frame->arg1, (int*)frame->arg2);
|
||||
case SYS_MAILBOX_SEND:
|
||||
if (frame->arg4 > 0 && !UserMemory::Range(frame->arg3, frame->arg4, false)) return -1;
|
||||
if (frame->arg4 > 0 && !UserMemory::Range(frame->arg3, frame->arg4, false)) return BadPointer;
|
||||
return Sys_MailboxSend((int)frame->arg1, (uint32_t)frame->arg2,
|
||||
(const void*)frame->arg3, (uint16_t)frame->arg4,
|
||||
(int)frame->arg5);
|
||||
case SYS_MAILBOX_RECV:
|
||||
if (!UserMemory::Writable<uint16_t>(frame->arg4)) return -1;
|
||||
if (frame->arg2 != 0 && !UserMemory::Writable<uint32_t>(frame->arg2)) return -1;
|
||||
if (!UserMemory::Writable<uint16_t>(frame->arg4)) return BadPointer;
|
||||
if (frame->arg2 != 0 && !UserMemory::Writable<uint32_t>(frame->arg2)) return BadPointer;
|
||||
if (frame->arg3 != 0) {
|
||||
uint16_t maxCopy = *(uint16_t*)frame->arg4;
|
||||
if (!UserMemory::Range(frame->arg3, maxCopy, true)) return -1;
|
||||
if (!UserMemory::Range(frame->arg3, maxCopy, true)) return BadPointer;
|
||||
}
|
||||
if (frame->arg5 != 0 && !UserMemory::Writable<int>(frame->arg5)) return -1;
|
||||
if (frame->arg5 != 0 && !UserMemory::Writable<int>(frame->arg5)) return BadPointer;
|
||||
return Sys_MailboxRecv((int)frame->arg1,
|
||||
UserMemory::IsUserPtr(frame->arg2) ? (uint32_t*)frame->arg2 : nullptr,
|
||||
UserMemory::IsUserPtr(frame->arg3) ? (void*)frame->arg3 : nullptr,
|
||||
@@ -648,7 +686,7 @@ namespace montauk::abi {
|
||||
case SYS_WAITSET_REMOVE:
|
||||
return Sys_WaitsetRemove((int)frame->arg1, (int)frame->arg2);
|
||||
case SYS_WAITSET_WAIT:
|
||||
if (frame->arg2 != 0 && !UserMemory::Writable<IpcWaitResult>(frame->arg2)) return -1;
|
||||
if (frame->arg2 != 0 && !UserMemory::Writable<IpcWaitResult>(frame->arg2)) return BadPointer;
|
||||
return Sys_WaitsetWait((int)frame->arg1,
|
||||
UserMemory::IsUserPtr(frame->arg2) ? (IpcWaitResult*)frame->arg2 : nullptr,
|
||||
frame->arg3);
|
||||
@@ -670,18 +708,18 @@ 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,
|
||||
@@ -691,7 +729,7 @@ namespace montauk::abi {
|
||||
case SYS_INPUT_WAIT:
|
||||
return (int64_t)Sys_InputWait(frame->arg1, frame->arg2);
|
||||
case SYS_LOG_WRITE: {
|
||||
if (!UserMemory::String(frame->arg1, kMaxUserspaceLogEntryBytes)) return -1;
|
||||
if (!UserMemory::String(frame->arg1, kMaxUserspaceLogEntryBytes)) return BadPointer;
|
||||
|
||||
auto* process = Sched::GetCurrentProcessPtr();
|
||||
const char* username = process != nullptr && process->user[0] != '\0'
|
||||
@@ -704,12 +742,12 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+152
-13
@@ -7,6 +7,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <Common/StatusCodes.hpp>
|
||||
|
||||
namespace montauk::abi {
|
||||
|
||||
@@ -346,6 +347,45 @@ namespace montauk::abi {
|
||||
/* Battery.hpp -- ACPI battery status */
|
||||
static constexpr uint64_t SYS_BATTERYINFO = 188; // (BatteryInfo*) -> 0, -1 no battery device
|
||||
|
||||
/* Process.hpp -- the caller's own capability masks. */
|
||||
static constexpr uint64_t SYS_GETCAPS = 189; // (SpawnCapabilities*) -> 0, -1 bad pointer
|
||||
|
||||
/* Storage.hpp */
|
||||
static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer
|
||||
|
||||
/* Consent.hpp */
|
||||
static constexpr uint64_t SYS_CONSENT_REGISTER = 191; // () -> 0, -13 lacks CAP_COMPOSITOR, -1 another agent is live
|
||||
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||
|
||||
/* Unicode key events. Size-versioned: the caller passes sizeof(its
|
||||
KeyEvent); the kernel copies min(size, its own) bytes and zero-fills the
|
||||
rest, so the struct can grow without another syscall. */
|
||||
static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY
|
||||
static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status
|
||||
|
||||
/* Futexes: process-private wait queues keyed by a user address. WAIT
|
||||
blocks only if *addr still equals `expected` (checked atomically with
|
||||
enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check
|
||||
only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken,
|
||||
WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the
|
||||
number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */
|
||||
static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status
|
||||
static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status
|
||||
static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL;
|
||||
static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu;
|
||||
|
||||
/* Abnormal termination from any thread: ends the whole process (every
|
||||
thread) with exit code TerminatedAborted (256+SIGABRT). SYS_EXIT from a
|
||||
sibling thread ends only that thread, so abort() needs its own call. */
|
||||
static constexpr uint64_t SYS_ABORT = 198; // () -> does not return
|
||||
|
||||
/* Normal termination of the whole process from any thread, with an exit
|
||||
code (0..255) instead of SYS_ABORT's fixed TerminatedAborted. libc
|
||||
exit() and Rust std::process::exit() use it, so exiting from a sibling
|
||||
thread ends the process rather than only that thread. */
|
||||
static constexpr uint64_t SYS_EXIT_GROUP = 199; // (exitCode) -> does not return
|
||||
|
||||
/* Kernel-owned process capabilities. User identities may namespace
|
||||
per-user resources, but never participate in authorization decisions. */
|
||||
static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0;
|
||||
@@ -366,8 +406,21 @@ namespace montauk::abi {
|
||||
whatever that image is granted at its next launch. Formatting a data
|
||||
volume must not carry that authority with it. */
|
||||
static constexpr uint64_t CAP_SYSTEM_IMAGE = 1ULL << 12;
|
||||
static constexpr uint64_t CAP_ALL = (1ULL << 13) - 1;
|
||||
static constexpr uint64_t CAP_STANDARD_SESSION = CAP_POWER_REQUEST | CAP_SUSPEND;
|
||||
/* Map other processes' windows and deliver input events to them
|
||||
(SYS_WINMAP, SYS_WINSENDEVENT): the desktop compositor's authority.
|
||||
Anything holding it can read every window's pixels and click on any
|
||||
window, including a consent prompt it is itself waiting on, so it is
|
||||
never part of a session mask, never part of "all", and never
|
||||
delegated by the compositor to what it launches. */
|
||||
static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13;
|
||||
/* Set the backlight of the active display (SYS_DISPLAYBRIGHTNESS).
|
||||
A seat preference rather than display administration, so every
|
||||
session holds it; CAP_DISPLAY_ADMIN keeps mode setting. Reading the
|
||||
level needs nothing. */
|
||||
static constexpr uint64_t CAP_BRIGHTNESS = 1ULL << 14;
|
||||
static constexpr uint64_t CAP_ALL = (1ULL << 15) - 1;
|
||||
static constexpr uint64_t CAP_STANDARD_SESSION =
|
||||
CAP_POWER_REQUEST | CAP_SUSPEND | CAP_BRIGHTNESS;
|
||||
static constexpr uint64_t CAP_ADMIN_SESSION =
|
||||
CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN |
|
||||
CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN |
|
||||
@@ -378,28 +431,63 @@ namespace montauk::abi {
|
||||
static_assert((CAP_ADMIN_SESSION & CAP_SYSTEM_IMAGE) == 0,
|
||||
"an admin session must not imply authority to rewrite the "
|
||||
"programs it launches; grant CAP_SYSTEM_IMAGE per binary");
|
||||
static_assert((CAP_ADMIN_SESSION & CAP_COMPOSITOR) == 0,
|
||||
"a session must not delegate the compositor's authority; "
|
||||
"login grants it to the desktop directly");
|
||||
// Capability denial for the Wi-Fi and USB syscalls only, which keep their
|
||||
// own WIFI_ERR_* / USB_ERR_* spaces; everything else returns
|
||||
// CapabilityDenied from StatusCodes.
|
||||
static constexpr int SYS_ERR_PERMISSION = -13;
|
||||
|
||||
// A process holds two independent sets: what it may use (effective) and
|
||||
// what it may pass to children (delegable). A launcher can delegate what
|
||||
// it never uses itself, so neither set bounds the other.
|
||||
struct SpawnCapabilities {
|
||||
uint64_t permitted;
|
||||
uint64_t effective;
|
||||
uint64_t delegable;
|
||||
};
|
||||
|
||||
// Both of the child's sets must come out of the parent's delegable set.
|
||||
constexpr bool ValidCapabilityDelegation(const SpawnCapabilities& child,
|
||||
uint64_t parentDelegable) {
|
||||
return (child.permitted & ~CAP_ALL) == 0 &&
|
||||
(child.effective & ~child.permitted) == 0 &&
|
||||
(child.delegable & ~child.permitted) == 0 &&
|
||||
(child.permitted & ~parentDelegable) == 0 &&
|
||||
return ((child.effective | child.delegable) & ~CAP_ALL) == 0 &&
|
||||
(child.effective & ~parentDelegable) == 0 &&
|
||||
(child.delegable & ~parentDelegable) == 0;
|
||||
}
|
||||
static_assert(ValidCapabilityDelegation(
|
||||
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN, 0}, CAP_NETWORK_ADMIN));
|
||||
{CAP_NETWORK_ADMIN, 0}, CAP_NETWORK_ADMIN));
|
||||
static_assert(ValidCapabilityDelegation(
|
||||
{0, CAP_NETWORK_ADMIN}, CAP_NETWORK_ADMIN));
|
||||
static_assert(!ValidCapabilityDelegation(
|
||||
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN}, 0));
|
||||
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN}, 0));
|
||||
static_assert(!ValidCapabilityDelegation(
|
||||
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN | CAP_SET_TIME, 0}, CAP_ALL));
|
||||
{CAP_NETWORK_ADMIN | CAP_SET_TIME, 0}, CAP_NETWORK_ADMIN));
|
||||
static_assert(!ValidCapabilityDelegation(
|
||||
{0, CAP_NETWORK_ADMIN | CAP_SET_TIME}, CAP_NETWORK_ADMIN));
|
||||
|
||||
/* Capabilities a user can grant at first use through a consent prompt.
|
||||
A consent grant never exceeds what the process's parent could have
|
||||
delegated to it at spawn (its consent ceiling), is never delegable
|
||||
onwards, and is issued only by the session's consent agent -- so it is
|
||||
deferred delegation, not amplification. */
|
||||
static constexpr uint64_t CAP_CONSENT_ELIGIBLE =
|
||||
CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME |
|
||||
CAP_DISPLAY_ADMIN | CAP_LOG_READ | CAP_BRIGHTNESS;
|
||||
static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0,
|
||||
"consent can only hand out authority an admin session holds");
|
||||
|
||||
static constexpr int CONSENT_DENY = 0;
|
||||
static constexpr int CONSENT_ALLOW = 1;
|
||||
// A request left unanswered this long is denied (and not remembered).
|
||||
static constexpr uint64_t CONSENT_TIMEOUT_MS = 120000;
|
||||
|
||||
struct ConsentRequest {
|
||||
uint64_t id;
|
||||
uint64_t capability; // exactly one CAP_* bit
|
||||
int32_t pid;
|
||||
uint32_t imageProtected; // 1 when writing the image needs CAP_SYSTEM_IMAGE
|
||||
char imagePath[256];
|
||||
};
|
||||
|
||||
|
||||
// Generic USB errors. Claims are restricted to interfaces without a
|
||||
@@ -540,7 +628,10 @@ namespace montauk::abi {
|
||||
uint64_t txPackets;
|
||||
};
|
||||
|
||||
struct KeyEvent {
|
||||
// Legacy 9-byte key event, read by SYS_GETKEY and written by
|
||||
// SYS_CHILDIO_WRITEKEY. Kept so binaries built before codepoints still work.
|
||||
// `ascii` is a Windows-1252 byte.
|
||||
struct KeyEventV1 {
|
||||
uint8_t scancode;
|
||||
char ascii;
|
||||
bool pressed;
|
||||
@@ -556,6 +647,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;
|
||||
@@ -593,12 +710,13 @@ namespace montauk::abi {
|
||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
||||
uint8_t _pad[3];
|
||||
union {
|
||||
KeyEvent key;
|
||||
KeyEvent key; // 16 bytes: the union was already that size (mouse)
|
||||
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
|
||||
struct { int32_t w, h; } resize;
|
||||
struct { int32_t scale; } scale;
|
||||
};
|
||||
};
|
||||
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
|
||||
|
||||
struct WinInfo {
|
||||
int32_t id;
|
||||
@@ -676,6 +794,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;
|
||||
@@ -703,7 +843,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;
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace montauk::abi {
|
||||
|
||||
static int64_t Sys_SetTZ(int32_t offsetMinutes) {
|
||||
Timekeeping::SetTZOffset(offsetMinutes);
|
||||
return 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
static int64_t Sys_GetTZ() {
|
||||
@@ -40,6 +40,6 @@ namespace montauk::abi {
|
||||
}
|
||||
|
||||
static int64_t Sys_SetUnixTime(int64_t unixSeconds) {
|
||||
return Timekeeping::SetUnixTimestamp(unixSeconds) ? 0 : -1;
|
||||
return Timekeeping::SetUnixTimestamp(unixSeconds) ? Success : InvalidArgument;
|
||||
}
|
||||
};
|
||||
@@ -55,13 +55,13 @@ namespace WinServer {
|
||||
}
|
||||
|
||||
static int UnmapRetiredSnapshotsLocked(int windowId, int callerPid, uint64_t callerPml4) {
|
||||
int result = -1;
|
||||
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;
|
||||
result = Success;
|
||||
}
|
||||
Ipc::ReleaseSurface(g_retiredSnapshots[i].surface);
|
||||
}
|
||||
@@ -95,21 +95,22 @@ namespace WinServer {
|
||||
}
|
||||
|
||||
static int SendEventLocked(int windowId, const montauk::abi::WinEvent* event) {
|
||||
if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return -1;
|
||||
if (windowId < 0 || windowId >= MaxWindows || event == nullptr) return InvalidArgument;
|
||||
WindowSlot& slot = g_slots[windowId];
|
||||
if (!slot.used || slot.eventMailbox == nullptr) return -1;
|
||||
if (!slot.used || slot.eventMailbox == nullptr) return InvalidArgument;
|
||||
|
||||
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;
|
||||
if (rc < 0) return GenericFailure;
|
||||
return rc > 0 ? Success : Busy;
|
||||
}
|
||||
|
||||
int Create(int ownerPid, uint64_t ownerPml4, 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 +119,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 +129,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);
|
||||
@@ -146,7 +147,7 @@ namespace WinServer {
|
||||
Ipc::ReleaseMailbox(mailbox, true, true);
|
||||
Ipc::ReleaseSurface(snapshot);
|
||||
Ipc::ReleaseSurface(live);
|
||||
return -1;
|
||||
return GenericFailure;
|
||||
}
|
||||
|
||||
WindowSlot& slot = g_slots[slotIdx];
|
||||
@@ -176,40 +177,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;
|
||||
}
|
||||
|
||||
@@ -259,7 +261,7 @@ namespace WinServer {
|
||||
if (!slot.used || slot.snapshotSurface == nullptr) return result;
|
||||
|
||||
int current = Ipc::UnmapSurfaceForPid(slot.snapshotSurface, callerPid, callerPml4);
|
||||
if (current == 0) return 0;
|
||||
if (current == 0) return Success;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -271,14 +273,15 @@ namespace WinServer {
|
||||
int Resize(int windowId, int callerPid, uint64_t ownerPml4, 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,7 +290,7 @@ 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);
|
||||
@@ -297,7 +300,7 @@ namespace WinServer {
|
||||
if (Ipc::MapSurfaceForPid(newLive, callerPid, ownerPml4, heapNext, newOwnerVa, false) != 0) {
|
||||
Ipc::ReleaseSurface(newSnapshot);
|
||||
Ipc::ReleaseSurface(newLive);
|
||||
return -1;
|
||||
return GenericFailure;
|
||||
}
|
||||
|
||||
if (slot.liveSurface != nullptr) {
|
||||
@@ -327,27 +330,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,7 +369,7 @@ namespace WinServer {
|
||||
SendEventLocked(i, &ev);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
int GetScale() {
|
||||
|
||||
@@ -15,16 +15,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,
|
||||
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 +36,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,7 +45,12 @@ 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);
|
||||
@@ -53,12 +58,13 @@ namespace montauk::abi {
|
||||
|
||||
static int Sys_WinUnmap(int windowId) {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc == nullptr) return -1;
|
||||
if (proc == nullptr) return GenericFailure;
|
||||
return WinServer::Unmap(windowId, proc->pid, proc->pml4Phys);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <ACPI/AcpiEvents.hpp>
|
||||
#include <ACPI/EmbeddedController.hpp>
|
||||
#include <ACPI/Battery.hpp>
|
||||
#include <ACPI/AML/AmlParser.hpp>
|
||||
#include <Hal/Apic/ApicInit.hpp>
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
@@ -153,6 +154,12 @@ extern "C" void kmain() {
|
||||
|
||||
Pci::Initialize(g_acpi.GetXSDT());
|
||||
|
||||
// _REG and _INI, once the EC and PCI config space (ECAM, for the
|
||||
// extended registers PCI_Config regions reach) are both available:
|
||||
// firmware settles which OS it is talking to here, and gates device
|
||||
// power control on the answer.
|
||||
Hal::AML::InitializeNamespaceObjects();
|
||||
|
||||
Drivers::ProbeEarly();
|
||||
Drivers::InitializeGraphics();
|
||||
|
||||
@@ -162,6 +169,10 @@ extern "C" void kmain() {
|
||||
Drivers::PS2::Keyboard::Initialize();
|
||||
Drivers::PS2::Mouse::Initialize();
|
||||
|
||||
// Hybrid graphics: switch off a discrete GPU nothing drives before
|
||||
// the normal probe can claim its USB-C/audio functions.
|
||||
Drivers::PowerDownUnusedGpus();
|
||||
|
||||
Drivers::ProbeNormal();
|
||||
Drivers::InitializeNetwork();
|
||||
Drivers::InitializeStorage();
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* StatusCodes.hpp
|
||||
* MontaukOS system status codes.
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// Syscall return status.
|
||||
// Mirrored in userspace with positive values at programs/include/montauk/statuscodes.h.
|
||||
enum StatusCodes {
|
||||
Success = 0,
|
||||
GenericFailure = -1,
|
||||
PermissionDenied = -2,
|
||||
CapabilityDenied = -3,
|
||||
SystemIntegrityError = -4,
|
||||
BadPointer = -5,
|
||||
InvalidArgument = -6,
|
||||
UnsupportedOp = -7,
|
||||
NoDevice = -8,
|
||||
IoFailure = -9,
|
||||
BadInvocation = -10,
|
||||
NoSuchPath = -11,
|
||||
Busy = -12,
|
||||
BufferTooSmall = -13,
|
||||
OutOfMemory = -14,
|
||||
TimedOut = -15,
|
||||
WouldBlock = -16
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* TerminationCodes.hpp
|
||||
* MontaukOS process termination codes.
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Exit code recorded when the kernel ends a process, as opposed to the
|
||||
// process calling SYS_EXIT (which is masked to 0..255). Encoded as
|
||||
// 256 + POSIX signal number so libc waitpid can report WTERMSIG.
|
||||
// Mirrored in userspace at programs/include/montauk/statuscodes.h.
|
||||
enum TerminationCodes {
|
||||
TerminationBase = 256,
|
||||
TerminatedIllegalInstruction = TerminationBase + 4, // SIGILL
|
||||
TerminatedBreakpoint = TerminationBase + 5, // SIGTRAP
|
||||
TerminatedAborted = TerminationBase + 6, // SIGABRT
|
||||
TerminatedArithmeticError = TerminationBase + 8, // SIGFPE
|
||||
TerminatedKilled = TerminationBase + 9, // SIGKILL
|
||||
TerminatedAccessViolation = TerminationBase + 11 // SIGSEGV
|
||||
};
|
||||
|
||||
// Termination code for a user-mode CPU exception. The exact vector is
|
||||
// kept in the crash report; this is only the category.
|
||||
inline TerminationCodes TerminationFromVector(uint8_t vector) {
|
||||
switch (vector) {
|
||||
case 0x06: return TerminatedIllegalInstruction; // #UD
|
||||
case 0x01: // #DB
|
||||
case 0x03: return TerminatedBreakpoint; // #BP
|
||||
case 0x00: // #DE
|
||||
case 0x10: // #MF
|
||||
case 0x13: return TerminatedArithmeticError; // #XM
|
||||
default: return TerminatedAccessViolation; // #PF, #GP, #SS, #NP, #AC, #OF, #BR, #CP, ...
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
/*
|
||||
* DgpuPower.cpp
|
||||
* Powers down discrete GPUs that no MontaukOS driver runs
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "DgpuPower.hpp"
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <ACPI/AcpiPower.hpp>
|
||||
#include <ACPI/AML/AmlInterpreter.hpp>
|
||||
#include <Boot/Boot.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
namespace AcpiPower = Hal::AcpiPower;
|
||||
namespace AML = Hal::AML;
|
||||
|
||||
namespace Drivers::Graphics::DgpuPower {
|
||||
|
||||
// =========================================================================
|
||||
// Vendor interfaces
|
||||
// =========================================================================
|
||||
|
||||
static constexpr uint16_t VendorNvidia = 0x10DE;
|
||||
static constexpr uint16_t VendorAmd = 0x1002;
|
||||
|
||||
// NVIDIA Optimus _DSM (GUID a486d8f8-0bda-471b-a72b-6042a6b5bee0, rev
|
||||
// 0x100). Function 0x1A (OPTIMUS_CAPS) with "power down in PS3" set
|
||||
// arms the GPU's _PS3 to cut its power; 0x1B (OPTIMUS_FLAGS) 0x3 is
|
||||
// what the drivers send alongside it.
|
||||
static constexpr uint8_t OptimusUuid[16] = {
|
||||
0xF8, 0xD8, 0x86, 0xA4, 0xDA, 0x0B, 0x1B, 0x47,
|
||||
0xA7, 0x2B, 0x60, 0x42, 0xA6, 0xB5, 0xBE, 0xE0,
|
||||
};
|
||||
static constexpr uint64_t OptimusRevision = 0x100;
|
||||
static constexpr uint64_t OptimusCaps = 0x1A;
|
||||
static constexpr uint64_t OptimusFlags = 0x1B;
|
||||
static constexpr uint32_t OptimusPowerdown = (3u << 24) | 1u; // POWERDOWN_PS3 | FLAGS_CHANGED
|
||||
|
||||
// The older NVIDIA hybrid _DSM (GUID d4a50b75-65c7-46f7-bfb7-41514cea0244,
|
||||
// rev 0x102): function 3 (POWER) with 2 (STAMINA) switches the discrete
|
||||
// GPU off on pre-Optimus muxed laptops.
|
||||
static constexpr uint8_t NvidiaDsmUuid[16] = {
|
||||
0x75, 0x0B, 0xA5, 0xD4, 0xC7, 0x65, 0xF7, 0x46,
|
||||
0xBF, 0xB7, 0x41, 0x51, 0x4C, 0xEA, 0x02, 0x44,
|
||||
};
|
||||
static constexpr uint64_t NvidiaDsmRevision = 0x102;
|
||||
static constexpr uint64_t NvidiaDsmPower = 0x03;
|
||||
static constexpr uint64_t NvidiaDsmStamina = 0x02;
|
||||
|
||||
// AMD PowerXpress ATPX(function, buffer).
|
||||
static constexpr uint64_t AtpxVerifyInterface = 0;
|
||||
static constexpr uint64_t AtpxGetParameters = 1;
|
||||
static constexpr uint64_t AtpxPowerControl = 2;
|
||||
static constexpr uint32_t AtpxParamsSupported = 1u << 0; // function mask bits
|
||||
static constexpr uint32_t AtpxPowerSupported = 1u << 1;
|
||||
static constexpr uint32_t AtpxHybridGraphics = 1u << 14; // PX parameter flag
|
||||
|
||||
// =========================================================================
|
||||
// State
|
||||
// =========================================================================
|
||||
|
||||
static constexpr int MaxCards = 4;
|
||||
static constexpr int MaxFunctions = 8;
|
||||
static constexpr int MaxChain = 4;
|
||||
|
||||
struct PciAddr {
|
||||
uint8_t Bus, Dev, Fn;
|
||||
};
|
||||
|
||||
struct Card {
|
||||
uint8_t Bus, Dev, DisplayFn;
|
||||
uint16_t Vendor, DeviceId;
|
||||
uint8_t Functions[MaxFunctions];
|
||||
int FunctionCount;
|
||||
PciAddr Chain[MaxChain]; // bridges above it, nearest first
|
||||
int ChainCount;
|
||||
};
|
||||
|
||||
static Card g_cards[MaxCards];
|
||||
static int g_cardCount = 0;
|
||||
|
||||
int PoweredDownCount() {
|
||||
return g_cardCount;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PCI helpers
|
||||
// =========================================================================
|
||||
|
||||
static uint32_t Cfg(uint8_t bus, uint8_t dev, uint8_t fn, uint16_t off, uint8_t width) {
|
||||
return Pci::ConfigRead(bus, dev, fn, off, width);
|
||||
}
|
||||
|
||||
static bool Present(uint8_t bus, uint8_t dev, uint8_t fn) {
|
||||
return (Cfg(bus, dev, fn, 0x00, 2) & 0xFFFF) != 0xFFFF;
|
||||
}
|
||||
|
||||
// The bridge whose secondary bus is `bus`, or nullptr for a root bus.
|
||||
static const Pci::PciDevice* BridgeAbove(uint8_t bus) {
|
||||
auto& devices = Pci::GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
if (!Pci::IsBridge(d)) continue;
|
||||
if (Cfg(d.Bus, d.Device, d.Function, Pci::PCI_REG_SECONDARY_BUS, 1) == bus)
|
||||
return &d;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void BusRange(const Pci::PciDevice& b, uint8_t& secondary, uint8_t& subordinate) {
|
||||
secondary = (uint8_t)Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_SECONDARY_BUS, 1);
|
||||
subordinate = (uint8_t)Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_SUBORDINATE_BUS, 1);
|
||||
}
|
||||
|
||||
// Does anything other than the card and its own bridges live below b?
|
||||
static bool SubtreeIsCard(const Pci::PciDevice& b, const Card& card) {
|
||||
uint8_t sec, sub;
|
||||
BusRange(b, sec, sub);
|
||||
auto& devices = Pci::GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.Bus < sec || d.Bus > sub) continue;
|
||||
if (d.Bus == card.Bus && d.Device == card.Dev) continue;
|
||||
bool chain = false;
|
||||
for (int k = 0; k < card.ChainCount; k++) {
|
||||
if (card.Chain[k].Bus == d.Bus && card.Chain[k].Dev == d.Device &&
|
||||
card.Chain[k].Fn == d.Function) { chain = true; break; }
|
||||
}
|
||||
if (!chain) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Does the bridge forward this physical address downstream?
|
||||
static bool WindowContains(const Pci::PciDevice& b, uint64_t phys) {
|
||||
uint32_t mem = Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_MEM_BASE, 4);
|
||||
uint64_t base = (uint64_t)(mem & 0xFFF0) << 16;
|
||||
uint64_t limit = ((uint64_t)((mem >> 16) & 0xFFF0) << 16) | 0xFFFFF;
|
||||
if (base <= limit && phys >= base && phys <= limit) return true;
|
||||
|
||||
uint32_t pref = Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_PREF_BASE, 4);
|
||||
uint64_t pbase = (uint64_t)(pref & 0xFFF0) << 16;
|
||||
uint64_t plimit = ((uint64_t)((pref >> 16) & 0xFFF0) << 16) | 0xFFFFF;
|
||||
if ((pref & 0xF) == 0x1) {
|
||||
pbase |= (uint64_t)Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_PREF_BASE_UPPER, 4) << 32;
|
||||
plimit |= (uint64_t)Cfg(b.Bus, b.Device, b.Function, Pci::PCI_REG_PREF_LIMIT_UPPER, 4) << 32;
|
||||
}
|
||||
return pbase <= plimit && phys >= pbase && phys <= plimit;
|
||||
}
|
||||
|
||||
// Functions that ship on a graphics card: the GPU itself, its HDMI/DP
|
||||
// audio, and (NVIDIA Turing+, AMD Navi) a USB-C xHCI and UCSI controller.
|
||||
// An integrated GPU behind an internal bridge -- AMD APUs -- shares its
|
||||
// device with the platform security processor, audio DSP and the
|
||||
// machine's own USB, and must never be mistaken for a card.
|
||||
static bool IsCardFunction(const Pci::PciDevice& d) {
|
||||
if (d.ClassCode == 0x03) return true; // display
|
||||
if (d.ClassCode == 0x04 && d.SubClass == 0x03) return true; // HD audio
|
||||
if (d.ClassCode == 0x0C && d.SubClass == 0x03) return true; // USB
|
||||
if (d.ClassCode == 0x0C && d.SubClass == 0x80) return true; // UCSI
|
||||
return false;
|
||||
}
|
||||
|
||||
static int PciePortType(uint8_t bus, uint8_t dev, uint8_t fn) {
|
||||
uint8_t cap = Pci::FindCapability(bus, dev, fn, Pci::PCI_CAP_PCIE);
|
||||
if (cap == 0) return -1;
|
||||
return (int)((Cfg(bus, dev, fn, cap + 2, 2) >> 4) & 0xF);
|
||||
}
|
||||
static constexpr int PortTypeRootPort = 0x4;
|
||||
|
||||
static void LogAddr(KernelLogStream& log, uint8_t bus, uint8_t dev, uint8_t fn) {
|
||||
log << base::hex << (uint64_t)bus << ":" << (uint64_t)dev << "." << (uint64_t)fn;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Detection
|
||||
// =========================================================================
|
||||
|
||||
static bool BootFramebuffer(uint64_t& phys) {
|
||||
const auto& info = montauk::boot::Info();
|
||||
if (!info.has(montauk::boot::FeatureFramebuffer) || info.framebuffer.address == 0)
|
||||
return false;
|
||||
phys = Memory::SubHHDM(info.framebuffer.address);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decide whether `gpu` is a discrete card we may switch off, filling in
|
||||
// `card` if so. Every reason to leave it alone is logged.
|
||||
static bool Qualifies(const Pci::PciDevice& gpu, Card& card) {
|
||||
card = Card{};
|
||||
card.Bus = gpu.Bus;
|
||||
card.Dev = gpu.Device;
|
||||
card.DisplayFn = gpu.Function;
|
||||
card.Vendor = gpu.VendorId;
|
||||
card.DeviceId = gpu.DeviceId;
|
||||
|
||||
auto reject = [&](const char* why) {
|
||||
KernelLogStream log(INFO, "dGPU");
|
||||
log << "Leaving ";
|
||||
LogAddr(log, gpu.Bus, gpu.Device, gpu.Function);
|
||||
log << " (" << base::hex << (uint64_t)gpu.VendorId << ":" << (uint64_t)gpu.DeviceId
|
||||
<< ") on: " << why;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (gpu.Claimed) return reject("a driver runs it");
|
||||
|
||||
// An integrated GPU sits on the root bus. A card is behind a port.
|
||||
const Pci::PciDevice* port = BridgeAbove(gpu.Bus);
|
||||
if (!port) return reject("on the root bus (integrated)");
|
||||
|
||||
// The whole device: every function must be one a card carries, and
|
||||
// none may already belong to a driver.
|
||||
auto& devices = Pci::GetDevices();
|
||||
bool otherDisplay = false;
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.Bus == gpu.Bus && d.Device == gpu.Device) {
|
||||
if (!IsCardFunction(d)) return reject("shares its device with platform functions");
|
||||
if (d.Claimed) return reject("a driver runs one of its functions");
|
||||
if (card.FunctionCount < MaxFunctions)
|
||||
card.Functions[card.FunctionCount++] = d.Function;
|
||||
} else if (d.ClassCode == 0x03) {
|
||||
otherDisplay = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Hybrid graphics only: something else has to drive the screen.
|
||||
if (!otherDisplay) return reject("it is the only display controller");
|
||||
|
||||
// The firmware's boot display (the GOP framebuffer, or legacy VGA)
|
||||
// must not be scanned out by this card -- on a muxed laptop switched
|
||||
// to discrete mode it is, and the panel would go dark.
|
||||
uint64_t fb = 0;
|
||||
if (BootFramebuffer(fb)) {
|
||||
if (WindowContains(*port, fb)) return reject("it scans out the boot display");
|
||||
} else {
|
||||
uint16_t ctl = (uint16_t)Cfg(port->Bus, port->Device, port->Function,
|
||||
Pci::PCI_REG_BRIDGE_CONTROL, 2);
|
||||
if (ctl & Pci::PCI_BRIDGE_CTL_VGA) return reject("legacy VGA is routed to it");
|
||||
}
|
||||
|
||||
// The port, and any switch above it that serves nothing else, power
|
||||
// down with the card. The first bridge must serve only the card.
|
||||
const Pci::PciDevice* b = port;
|
||||
while (b && card.ChainCount < MaxChain) {
|
||||
card.Chain[card.ChainCount++] = { b->Bus, b->Device, b->Function };
|
||||
if (!SubtreeIsCard(*b, card)) {
|
||||
card.ChainCount--;
|
||||
break;
|
||||
}
|
||||
if (b->Bus == 0) break;
|
||||
b = BridgeAbove(b->Bus);
|
||||
}
|
||||
if (card.ChainCount == 0) return reject("its port also serves other devices");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Vendor methods
|
||||
// =========================================================================
|
||||
|
||||
// The ACPI nodes of every display controller, where the vendor _DSM or
|
||||
// ATPX may live (the integrated GPU's node, on some muxed designs).
|
||||
static int DisplayNodes(int32_t* out, int max) {
|
||||
int count = 0;
|
||||
auto& devices = Pci::GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size() && count < max; i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.ClassCode != 0x03) continue;
|
||||
int32_t node = AcpiPower::FindPciDevice(d.Bus, d.Device, d.Function);
|
||||
if (node >= 0) out[count++] = node;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static AML::Object Dword(uint32_t v) {
|
||||
uint8_t b[4] = { (uint8_t)v, (uint8_t)(v >> 8), (uint8_t)(v >> 16), (uint8_t)(v >> 24) };
|
||||
return AcpiPower::BufferArg(b, 4);
|
||||
}
|
||||
|
||||
// Arm Optimus power-down in _PS3. Returns true when firmware took it.
|
||||
static bool ArmOptimus(int32_t gpuNode) {
|
||||
if (gpuNode < 0) return false;
|
||||
uint64_t funcs = AcpiPower::DsmFunctions(gpuNode, OptimusUuid, OptimusRevision,
|
||||
AcpiPower::EmptyPackageArg());
|
||||
if (!(funcs & (1ULL << OptimusCaps))) return false;
|
||||
|
||||
AML::Object result{};
|
||||
AcpiPower::EvaluateDsm(gpuNode, OptimusUuid, OptimusRevision, OptimusCaps,
|
||||
Dword(OptimusPowerdown), result);
|
||||
if (funcs & (1ULL << OptimusFlags))
|
||||
AcpiPower::EvaluateDsm(gpuNode, OptimusUuid, OptimusRevision, OptimusFlags,
|
||||
Dword(0x3), result);
|
||||
KernelLogStream(INFO, "dGPU") << " Optimus _DSM: power-down on _PS3 armed";
|
||||
return true;
|
||||
}
|
||||
|
||||
// The node offering the legacy NVIDIA power _DSM, or -1.
|
||||
static int32_t FindNvidiaDsm(int32_t gpuNode) {
|
||||
int32_t nodes[8];
|
||||
int count = 0;
|
||||
if (gpuNode >= 0) nodes[count++] = gpuNode;
|
||||
count += DisplayNodes(nodes + count, 8 - count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
uint64_t funcs = AcpiPower::DsmFunctions(nodes[i], NvidiaDsmUuid, NvidiaDsmRevision,
|
||||
AcpiPower::EmptyPackageArg());
|
||||
if (funcs & (1ULL << NvidiaDsmPower)) return nodes[i];
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The ATPX method, if the platform offers one with power control that
|
||||
// is meant to be used (not on "hybrid" designs, which use _PR3).
|
||||
static int32_t FindAtpx() {
|
||||
auto& interp = AML::GetInterpreter();
|
||||
auto& ns = interp.GetNamespace();
|
||||
|
||||
int32_t atpx = -1;
|
||||
int32_t nodes[8];
|
||||
int count = DisplayNodes(nodes, 8);
|
||||
for (int i = 0; i < count && atpx < 0; i++)
|
||||
atpx = interp.FindChild(nodes[i], "ATPX");
|
||||
if (atpx < 0) {
|
||||
ns.WalkDescendants(ns.RootIndex(), AML::ObjectType::Method,
|
||||
[&](int32_t idx, const AML::NamespaceNode* n) {
|
||||
if (atpx < 0 && n->Name[0] == 'A' && n->Name[1] == 'T' &&
|
||||
n->Name[2] == 'P' && n->Name[3] == 'X')
|
||||
atpx = idx;
|
||||
});
|
||||
}
|
||||
if (atpx < 0) return -1;
|
||||
|
||||
AML::Object args[2] = { AcpiPower::IntegerArg(AtpxVerifyInterface),
|
||||
AcpiPower::IntegerArg(0) };
|
||||
AML::Object result{};
|
||||
if (!interp.EvaluateNode(atpx, args, 2, result) ||
|
||||
result.Type != AML::ObjectType::Buffer || result.Buffer.Length < 8)
|
||||
return -1;
|
||||
const uint8_t* r = result.Buffer.Data;
|
||||
uint32_t functions = r[4] | (r[5] << 8) | (r[6] << 16) | ((uint32_t)r[7] << 24);
|
||||
|
||||
if (functions & AtpxParamsSupported) {
|
||||
args[0] = AcpiPower::IntegerArg(AtpxGetParameters);
|
||||
if (interp.EvaluateNode(atpx, args, 2, result) &&
|
||||
result.Type == AML::ObjectType::Buffer && result.Buffer.Length >= 10) {
|
||||
const uint8_t* p = result.Buffer.Data;
|
||||
uint32_t valid = p[2] | (p[3] << 8) | (p[4] << 16) | ((uint32_t)p[5] << 24);
|
||||
uint32_t flags = p[6] | (p[7] << 8) | (p[8] << 16) | ((uint32_t)p[9] << 24);
|
||||
if (valid & flags & AtpxHybridGraphics) {
|
||||
KernelLogStream(INFO, "dGPU") << " ATPX: hybrid design, power via _PR3";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(functions & AtpxPowerSupported)) return -1;
|
||||
return atpx;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Power-down sequence
|
||||
// =========================================================================
|
||||
|
||||
static void PowerDownCard(const Card& card) {
|
||||
{
|
||||
KernelLogStream log(INFO, "dGPU");
|
||||
log << "Powering down ";
|
||||
LogAddr(log, card.Bus, card.Dev, card.DisplayFn);
|
||||
log << " (" << base::hex << (uint64_t)card.Vendor << ":"
|
||||
<< (uint64_t)card.DeviceId << ")";
|
||||
}
|
||||
|
||||
bool haveAcpi = AML::GetInterpreter().IsInitialized();
|
||||
|
||||
// 1. Quiesce every function: no decode, no DMA, no interrupts.
|
||||
for (int i = 0; i < card.FunctionCount; i++) {
|
||||
uint8_t fn = card.Functions[i];
|
||||
if (!Present(card.Bus, card.Dev, fn)) continue;
|
||||
uint16_t cmd = (uint16_t)Cfg(card.Bus, card.Dev, fn, Pci::PCI_REG_COMMAND, 2);
|
||||
cmd &= (uint16_t)~(0x7u); // I/O, memory, bus master
|
||||
Pci::ConfigWrite(card.Bus, card.Dev, fn, Pci::PCI_REG_COMMAND, 2, cmd);
|
||||
Pci::DisableInterruptDelivery(card.Bus, card.Dev, fn);
|
||||
}
|
||||
|
||||
// 2. The namespace side of the card and its ports.
|
||||
int32_t gpuNode = -1;
|
||||
int32_t owners[MaxFunctions + MaxChain];
|
||||
int ownerCount = 0;
|
||||
int32_t chainNodes[MaxChain];
|
||||
bool portHasPr3 = false;
|
||||
|
||||
if (haveAcpi) {
|
||||
AcpiPower::InvalidatePciMap();
|
||||
gpuNode = AcpiPower::FindPciDevice(card.Bus, card.Dev, card.DisplayFn);
|
||||
for (int i = 0; i < card.FunctionCount; i++) {
|
||||
int32_t n = AcpiPower::FindPciDevice(card.Bus, card.Dev, card.Functions[i]);
|
||||
if (n >= 0) owners[ownerCount++] = n;
|
||||
}
|
||||
for (int i = 0; i < card.ChainCount; i++) {
|
||||
chainNodes[i] = AcpiPower::FindPciDevice(card.Chain[i].Bus, card.Chain[i].Dev,
|
||||
card.Chain[i].Fn);
|
||||
if (chainNodes[i] >= 0) {
|
||||
owners[ownerCount++] = chainNodes[i];
|
||||
if (AcpiPower::HasObject(chainNodes[i], "_PR3")) portHasPr3 = true;
|
||||
}
|
||||
}
|
||||
|
||||
char path[128];
|
||||
if (gpuNode >= 0) {
|
||||
AML::GetInterpreter().GetNamespace().GetNodePath(gpuNode, path, sizeof(path));
|
||||
KernelLogStream(INFO, "dGPU") << " ACPI node " << path;
|
||||
} else {
|
||||
KernelLogStream(INFO, "dGPU") << " no ACPI node for the GPU";
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < card.ChainCount; i++) chainNodes[i] = -1;
|
||||
}
|
||||
|
||||
// 3. Vendor methods. With _PR3 on the port the platform does runtime
|
||||
// D3cold through power resources, and the drivers leave the vendor
|
||||
// methods alone there (they would make _PS3 cut power underneath
|
||||
// the power resources' own sequencing).
|
||||
int32_t nvidiaDsm = -1;
|
||||
int32_t atpx = -1;
|
||||
if (haveAcpi && !portHasPr3) {
|
||||
if (card.Vendor == VendorNvidia) {
|
||||
if (!ArmOptimus(gpuNode)) nvidiaDsm = FindNvidiaDsm(gpuNode);
|
||||
} else if (card.Vendor == VendorAmd) {
|
||||
atpx = FindAtpx();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. PCI D3hot, companions first, the GPU last.
|
||||
int d3hot = 0;
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
for (int i = 0; i < card.FunctionCount; i++) {
|
||||
uint8_t fn = card.Functions[i];
|
||||
if ((fn == card.DisplayFn) != (pass == 1)) continue;
|
||||
if (!Present(card.Bus, card.Dev, fn)) continue;
|
||||
if (Pci::SetPowerState(card.Bus, card.Dev, fn, 3)) d3hot++;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Vendor power switch.
|
||||
if (nvidiaDsm >= 0) {
|
||||
AML::Object result{};
|
||||
AcpiPower::EvaluateDsm(nvidiaDsm, NvidiaDsmUuid, NvidiaDsmRevision, NvidiaDsmPower,
|
||||
AcpiPower::IntegerArg(NvidiaDsmStamina), result);
|
||||
KernelLogStream(INFO, "dGPU") << " NVIDIA _DSM: discrete GPU switched off";
|
||||
}
|
||||
if (atpx >= 0) {
|
||||
uint8_t params[3] = { 3, 0, 0 }; // size = 3, dGPU state = off
|
||||
AML::Object args[2] = { AcpiPower::IntegerArg(AtpxPowerControl),
|
||||
AcpiPower::BufferArg(params, 3) };
|
||||
AML::Object result{};
|
||||
AML::GetInterpreter().EvaluateNode(atpx, args, 2, result);
|
||||
Timekeeping::BusyWaitMs(200); // the off transition needs it
|
||||
KernelLogStream(INFO, "dGPU") << " ATPX: discrete GPU switched off";
|
||||
}
|
||||
|
||||
// 6. The GPU's own ACPI D3cold: _PS3 and its power resources.
|
||||
bool d3cold = false;
|
||||
int shared = 0;
|
||||
if (gpuNode >= 0) {
|
||||
AcpiPower::D3ColdReport r{};
|
||||
AcpiPower::EnterD3Cold(gpuNode, owners, ownerCount, r);
|
||||
if (r.ResourcesOff > 0 && r.ResourcesStillOn == 0) d3cold = true;
|
||||
shared += r.ResourcesShared;
|
||||
|
||||
// Older Optimus firmware hangs the power switch on a _OFF method
|
||||
// of the GPU device itself and only calls it from _PS3 when the
|
||||
// driver's flags say so. Call it directly if the card is still
|
||||
// answering.
|
||||
if (Present(card.Bus, card.Dev, card.DisplayFn) && !portHasPr3) {
|
||||
int32_t off = AML::GetInterpreter().FindChild(gpuNode, "_OFF");
|
||||
auto* n = AML::GetInterpreter().GetNamespace().GetNode(off);
|
||||
if (n && n->Obj.Type == AML::ObjectType::Method) {
|
||||
AML::Object result{};
|
||||
AML::GetInterpreter().EvaluateNode(off, nullptr, 0, result);
|
||||
KernelLogStream(INFO, "dGPU") << " ran the GPU's _OFF";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!Present(card.Bus, card.Dev, card.DisplayFn)) d3cold = true;
|
||||
|
||||
// 7. The ports above it, nearest first. A switch port belongs to the
|
||||
// card outright; a root port only goes down when the platform says
|
||||
// it can (power resources or _PS3), as Linux requires.
|
||||
for (int i = 0; i < card.ChainCount; i++) {
|
||||
const PciAddr& p = card.Chain[i];
|
||||
int32_t node = chainNodes[i];
|
||||
bool rootPort = PciePortType(p.Bus, p.Dev, p.Fn) == PortTypeRootPort;
|
||||
bool acpiPm = node >= 0 && (AcpiPower::HasObject(node, "_PR3") ||
|
||||
AcpiPower::HasObject(node, "_PR0") ||
|
||||
AcpiPower::HasObject(node, "_PS3"));
|
||||
if (rootPort && !acpiPm) break;
|
||||
|
||||
Pci::SetPowerState(p.Bus, p.Dev, p.Fn, 3);
|
||||
if (node >= 0) {
|
||||
AcpiPower::D3ColdReport r{};
|
||||
AcpiPower::EnterD3Cold(node, owners, ownerCount, r);
|
||||
if (r.ResourcesOff > 0 && r.ResourcesStillOn == 0) d3cold = true;
|
||||
shared += r.ResourcesShared;
|
||||
}
|
||||
if (rootPort) break;
|
||||
}
|
||||
|
||||
KernelLogStream log(d3cold || d3hot ? OK : WARNING, "dGPU");
|
||||
LogAddr(log, card.Bus, card.Dev, card.DisplayFn);
|
||||
if (d3cold)
|
||||
log << " is powered off (D3cold)";
|
||||
else if (d3hot && shared)
|
||||
log << " is in D3hot; its power rail also feeds another device, so it stays on";
|
||||
else if (d3hot)
|
||||
log << " is in D3hot; the platform offered no way to cut its power";
|
||||
else
|
||||
log << " could not be powered down (no PCI or ACPI power management)";
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Public API
|
||||
// =========================================================================
|
||||
|
||||
void PowerDownUnused() {
|
||||
auto& devices = Pci::GetDevices();
|
||||
g_cardCount = 0;
|
||||
|
||||
for (uint64_t i = 0; i < devices.size() && g_cardCount < MaxCards; i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.ClassCode != 0x03 || d.Reserved) continue;
|
||||
|
||||
Card card;
|
||||
if (!Qualifies(d, card)) continue;
|
||||
|
||||
// Withhold all of its functions from the driver probe that
|
||||
// follows: the xHCI driver would otherwise claim the card's
|
||||
// USB-C controller and lose it when the card powers off.
|
||||
for (uint64_t k = 0; k < devices.size(); k++) {
|
||||
if (devices[k].Bus == card.Bus && devices[k].Device == card.Dev)
|
||||
devices[k].Reserved = true;
|
||||
}
|
||||
|
||||
g_cards[g_cardCount++] = card;
|
||||
PowerDownCard(card);
|
||||
}
|
||||
}
|
||||
|
||||
void Resume() {
|
||||
for (int i = 0; i < g_cardCount; i++) {
|
||||
KernelLogStream(INFO, "dGPU") << "Re-applying power-down after resume";
|
||||
PowerDownCard(g_cards[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* DgpuPower.hpp
|
||||
* Powers down discrete GPUs that no MontaukOS driver runs
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Drivers::Graphics::DgpuPower {
|
||||
|
||||
// A hybrid-graphics laptop boots with its discrete GPU (NVIDIA Optimus,
|
||||
// AMD PowerXpress / hybrid, Intel Arc) powered and idling at boot clocks.
|
||||
// With no driver to manage it, it stays there: several watts of heat and
|
||||
// a fan that never settles. This finds every display controller that
|
||||
// nothing claimed and that does not carry the boot display, takes its
|
||||
// whole card (GPU, HDMI audio, USB-C controller) away from the driver
|
||||
// probe, and powers it down: PCI D3hot, then the platform's D3cold path
|
||||
// through ACPI (_PR3 power resources on the port above it, _PS3, and the
|
||||
// NVIDIA Optimus / legacy and AMD ATPX vendor methods where firmware
|
||||
// relies on them).
|
||||
//
|
||||
// Call after the early (graphics) driver probe and before the normal
|
||||
// one, with the AML namespace initialized.
|
||||
void PowerDownUnused();
|
||||
|
||||
// Firmware repowers the card during S3 resume. Call once the platform is
|
||||
// back up to put every card powered down at boot back to sleep.
|
||||
void Resume();
|
||||
|
||||
// Number of cards powered down (for diagnostics).
|
||||
int PoweredDownCount();
|
||||
};
|
||||
@@ -82,8 +82,7 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
static uint8_t g_connectorType = montauk::abi::DISPLAY_CONNECTOR_UNKNOWN;
|
||||
static bool g_connectorConnected = false;
|
||||
static bool g_edidValid = false;
|
||||
static bool g_pwmSupported = false;
|
||||
static uint32_t g_pwmCtlReg = 0;
|
||||
static int g_ddiPort = -1; // DDI port driving the output, -1 unknown
|
||||
static char g_connectorName[32] = "Unknown";
|
||||
static char g_monitorName[64] = "Unknown display";
|
||||
static uint8_t g_auxEdid[256] = {};
|
||||
@@ -556,10 +555,11 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
}
|
||||
|
||||
static bool AuxI2cTransfer(uint8_t request, const uint8_t* writeData,
|
||||
int writeSize, uint8_t* readData, int readSize) {
|
||||
int writeSize, uint8_t* readData, int readSize,
|
||||
uint8_t i2cAddress = 0x50) {
|
||||
for (int retry = 0; retry < 16; retry++) {
|
||||
uint8_t reply = 0xFF;
|
||||
int result = AuxTransfer(request, 0x50, writeData, writeSize,
|
||||
int result = AuxTransfer(request, i2cAddress, writeData, writeSize,
|
||||
readData, readSize, &reply);
|
||||
uint8_t nativeReply = reply & 0x3;
|
||||
uint8_t i2cReply = reply & 0xC;
|
||||
@@ -600,7 +600,8 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
|
||||
static bool ReadDpEdid() {
|
||||
g_auxEdidSize = 0;
|
||||
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP
|
||||
if ((g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP
|
||||
&& g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP)
|
||||
|| g_auxControlReg == 0)
|
||||
return false;
|
||||
|
||||
@@ -789,9 +790,16 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
g_connectorConnected = (ReadReg(trans.conf) & PIPECONF_ENABLE) != 0;
|
||||
uint32_t ddi = ReadReg(trans.ddi);
|
||||
g_auxControlReg = 0;
|
||||
g_ddiPort = -1;
|
||||
|
||||
if (g_gpuGen <= 7) {
|
||||
if (ReadReg(LVDS) & LVDS_PORT_ENABLE) {
|
||||
// Haswell (listed as Gen 7) has the eDP transcoder; Ironlake to
|
||||
// Ivy Bridge drive eDP from the CPU's port A.
|
||||
if (trans.edp || (ReadReg(DP_A) & (1u << 31))) {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1");
|
||||
g_ddiPort = 0;
|
||||
} else if (ReadReg(LVDS) & LVDS_PORT_ENABLE) {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_LVDS;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "LVDS-1");
|
||||
} else if (ReadReg(ADPA) & ADPA_DAC_ENABLE) {
|
||||
@@ -807,14 +815,30 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
} else if (trans.edp) {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1");
|
||||
g_ddiPort = 0;
|
||||
g_auxControlReg = DP_AUX_A_CTL;
|
||||
} else {
|
||||
uint32_t mode = (ddi & TRANS_DDI_MODE_MASK) >> 24;
|
||||
uint32_t port = (ddi & TRANS_DDI_PORT_MASK) >> 27;
|
||||
const char* suffix = port == 0 ? "A" : port == 1 ? "B"
|
||||
: port == 2 ? "C" : port == 3 ? "D"
|
||||
: port == 8 ? "TC1" : port == 9 ? "TC2"
|
||||
: port == 10 ? "TC3" : port == 11 ? "TC4"
|
||||
: "unknown";
|
||||
// Gen12 widened the port select to bits 30:27 and stores port + 1
|
||||
// (0 = none); earlier DDI parts store the port in bits 30:28.
|
||||
// Gen12 numbering puts the Type-C ports from D (index 3) onward.
|
||||
int port;
|
||||
if (g_gpuGen >= 12) {
|
||||
uint32_t field = (ddi & TRANS_DDI_PORT_MASK) >> 27;
|
||||
port = field ? (int)field - 1 : -1;
|
||||
} else {
|
||||
port = (int)((ddi >> 28) & 0x7);
|
||||
}
|
||||
g_ddiPort = port;
|
||||
static const char* const legacyNames[] = {"A", "B", "C", "D", "E", "F"};
|
||||
static const char* const tcNames[] = {"TC1", "TC2", "TC3", "TC4", "TC5", "TC6"};
|
||||
const char* suffix = "unknown";
|
||||
if (port >= 0 && g_gpuGen >= 12) {
|
||||
if (port < 3) suffix = legacyNames[port];
|
||||
else if (port - 3 < 6) suffix = tcNames[port - 3];
|
||||
} else if (port >= 0 && port < 6) {
|
||||
suffix = legacyNames[port];
|
||||
}
|
||||
if (mode == 0) {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_HDMI;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "HDMI-");
|
||||
@@ -824,11 +848,9 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
} else {
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_DP;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "DP-");
|
||||
static constexpr uint32_t auxControls[] = {
|
||||
DP_AUX_A_CTL, DP_AUX_B_CTL, DP_AUX_C_CTL, DP_AUX_D_CTL,
|
||||
};
|
||||
if (port < sizeof(auxControls) / sizeof(auxControls[0]))
|
||||
g_auxControlReg = auxControls[port];
|
||||
// AUX channels are laid out per port, Type-C ones included.
|
||||
if (port >= 0 && port <= 8)
|
||||
g_auxControlReg = DP_AUX_A_CTL + (uint32_t)port * 0x100;
|
||||
}
|
||||
uint64_t len = 0;
|
||||
while (len + 1 < sizeof(g_connectorName) && g_connectorName[len]) len++;
|
||||
@@ -913,25 +935,544 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
}
|
||||
}
|
||||
|
||||
static void DiscoverPwm() {
|
||||
g_pwmSupported = false;
|
||||
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP
|
||||
&& g_connectorType != montauk::abi::DISPLAY_CONNECTOR_LVDS)
|
||||
return;
|
||||
// =========================================================================
|
||||
// Backlight
|
||||
// =========================================================================
|
||||
|
||||
uint32_t pch = ReadReg(BLC_PWM_PCH_CTL2);
|
||||
if ((pch >> 16) != 0) {
|
||||
g_pwmCtlReg = BLC_PWM_PCH_CTL2;
|
||||
g_pwmSupported = true;
|
||||
// South-display family. The PWM register layout and the GMBUS pin map
|
||||
// follow the PCH, not the GPU: a Coffee Lake on Cannon Point and a Kaby
|
||||
// Lake on Sunrise Point are both Gen 9 but have incompatible PWM blocks.
|
||||
enum class PchFamily : uint8_t { Unknown, Ibx, Lpt, Cnp, Icp };
|
||||
|
||||
enum class BacklightMethod : uint8_t { None, PwmIlk, PwmLpt, PwmCnp, DpcdAux, DdcCi };
|
||||
|
||||
static PchFamily g_pch = PchFamily::Unknown;
|
||||
static BacklightMethod g_blMethod = BacklightMethod::None;
|
||||
static uint32_t g_blFreqReg = 0; // PWM period register
|
||||
static uint32_t g_blDutyReg = 0; // PWM duty register
|
||||
static uint32_t g_blMax = 0; // DPCD / DDC full-scale value
|
||||
static int g_blPercent = -1; // last level read or written
|
||||
static bool g_dpcdModePending = false; // panel still on its PWM pin
|
||||
static bool g_ddcCandidate = false; // external monitor, DDC/CI untried
|
||||
static uint64_t g_ddcRetryMs = 0; // earliest re-probe after a failure
|
||||
static uint64_t g_ddcLastMs = 0; // MCCS needs 50ms between commands
|
||||
// Serializes backlight access: DDC/CI and DPCD changes are multi-step
|
||||
// AUX/GMBUS transactions that must not interleave.
|
||||
static kcp::Mutex g_blLock;
|
||||
|
||||
// Lowest level a panel is driven to. A zero duty cycle turns some panels
|
||||
// fully dark, which leaves nothing on screen to turn them back up with.
|
||||
static constexpr int MinPanelPercent = 1;
|
||||
|
||||
// MCCS DDC/CI (VESA Monitor Control Command Set over I2C address 0x37).
|
||||
static constexpr uint8_t DdcAddress = 0x37;
|
||||
static constexpr uint8_t DdcHostAddress = 0x51;
|
||||
static constexpr uint8_t DdcVcpBrightness = 0x10;
|
||||
static constexpr uint64_t DdcCommandGapMs = 50;
|
||||
static constexpr uint64_t DdcReplyDelayMs = 40;
|
||||
static constexpr uint64_t DdcRetryIntervalMs = 60000;
|
||||
|
||||
static constexpr uint32_t SOUTH_DSPCLK_GATE_D = 0xC2020;
|
||||
static constexpr uint32_t PCH_GMBUSUNIT_CLOCK_GATE_DISABLE = (1u << 31);
|
||||
|
||||
static PchFamily DetectPch() {
|
||||
auto& devices = Pci::GetDevices();
|
||||
for (uint64_t i = 0; i < devices.size(); i++) {
|
||||
const auto& d = devices[i];
|
||||
if (d.VendorId != VendorIntel || d.Bus != 0 || d.Device != 0x1F
|
||||
|| d.Function != 0)
|
||||
continue;
|
||||
// Device-ID families as i915 groups them (id & 0xFF80).
|
||||
switch (d.DeviceId & 0xFF80) {
|
||||
case 0x3B00: case 0x1C00: case 0x1E00:
|
||||
return PchFamily::Ibx;
|
||||
case 0x8C00: case 0x9C00: case 0x8C80: case 0x9C80: // LPT/WPT
|
||||
case 0xA100: case 0x9D00: case 0xA280: case 0xA380: // SPT/KBP/CMP-V
|
||||
return PchFamily::Lpt;
|
||||
case 0xA300: case 0x9D80: case 0x0280: case 0x0680: // CNP/CMP
|
||||
return PchFamily::Cnp;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Ice Point and everything after it share the Cannon Point PWM block
|
||||
// and the Ice Point pin map, and new PCH IDs keep appearing.
|
||||
return g_gpuGen >= 11 ? PchFamily::Icp : PchFamily::Unknown;
|
||||
}
|
||||
|
||||
static int RawToPercent(uint32_t raw, uint32_t maximum) {
|
||||
if (maximum == 0) return -1;
|
||||
if (raw > maximum) raw = maximum;
|
||||
return (int)(((uint64_t)raw * 100 + maximum / 2) / maximum);
|
||||
}
|
||||
|
||||
static uint32_t PercentToRaw(int percent, uint32_t maximum) {
|
||||
return (uint32_t)(((uint64_t)maximum * (uint32_t)percent + 50) / 100);
|
||||
}
|
||||
|
||||
static void ShortSpin() {
|
||||
for (int i = 0; i < 20000; i++) asm volatile("pause");
|
||||
}
|
||||
|
||||
// ---- Panel PWM ----------------------------------------------------------
|
||||
|
||||
static bool PwmState(uint32_t& maximum, uint32_t& duty) {
|
||||
uint32_t freq = ReadReg(g_blFreqReg);
|
||||
uint32_t value = ReadReg(g_blDutyReg);
|
||||
if (freq == 0xFFFFFFFF) return false;
|
||||
switch (g_blMethod) {
|
||||
case BacklightMethod::PwmCnp:
|
||||
maximum = freq;
|
||||
duty = value;
|
||||
break;
|
||||
case BacklightMethod::PwmLpt:
|
||||
maximum = value >> 16;
|
||||
duty = value & 0xFFFF;
|
||||
break;
|
||||
case BacklightMethod::PwmIlk:
|
||||
maximum = freq >> 16;
|
||||
duty = value & 0xFFFF;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return maximum != 0;
|
||||
}
|
||||
|
||||
static void PwmWriteDuty(uint32_t duty) {
|
||||
if (g_blMethod == BacklightMethod::PwmCnp) {
|
||||
WriteReg(g_blDutyReg, duty);
|
||||
} else {
|
||||
uint32_t cpu = ReadReg(BLC_PWM_CPU_CTL);
|
||||
if ((cpu >> 16) != 0) {
|
||||
g_pwmCtlReg = BLC_PWM_CPU_CTL;
|
||||
g_pwmSupported = true;
|
||||
uint32_t value = ReadReg(g_blDutyReg);
|
||||
WriteReg(g_blDutyReg, (value & 0xFFFF0000u) | (duty & 0xFFFF));
|
||||
}
|
||||
(void)ReadReg(g_blDutyReg);
|
||||
}
|
||||
|
||||
// Adopt the PWM controller firmware left driving the panel. A controller
|
||||
// that is not enabled is not what lights this panel, so it is skipped.
|
||||
static bool DiscoverPwmBacklight() {
|
||||
bool cnpLayout = g_pch == PchFamily::Cnp || g_pch == PchFamily::Icp;
|
||||
if (g_pch == PchFamily::Unknown && g_gpuGen >= 8) {
|
||||
// Unrecognized bridge: the LPT layout has nothing at DUTY1, so a
|
||||
// plausible duty there identifies the CNP block.
|
||||
uint32_t freq = ReadReg(BXT_BLC_PWM_FREQ1);
|
||||
uint32_t duty = ReadReg(BXT_BLC_PWM_DUTY1);
|
||||
cnpLayout = duty != 0 && duty != 0xFFFFFFFF && duty <= freq;
|
||||
}
|
||||
|
||||
if (cnpLayout) {
|
||||
// BXT/CNP firmware may route the panel through either controller.
|
||||
for (uint32_t c = 0; c < 2; c++) {
|
||||
uint32_t delta = c * BXT_BLC_PWM_CONTROLLER_DELTA;
|
||||
uint32_t ctl = ReadReg(BXT_BLC_PWM_CTL1 + delta);
|
||||
uint32_t freq = ReadReg(BXT_BLC_PWM_FREQ1 + delta);
|
||||
if (ctl == 0xFFFFFFFF || !(ctl & BLC_PWM_ENABLE)
|
||||
|| freq == 0 || freq == 0xFFFFFFFF)
|
||||
continue;
|
||||
g_blMethod = BacklightMethod::PwmCnp;
|
||||
g_blFreqReg = BXT_BLC_PWM_FREQ1 + delta;
|
||||
g_blDutyReg = BXT_BLC_PWM_DUTY1 + delta;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g_pch == PchFamily::Lpt || g_gpuGen >= 8) {
|
||||
uint32_t ctl1 = ReadReg(BLC_PWM_PCH_CTL1);
|
||||
uint32_t ctl2 = ReadReg(BLC_PWM_PCH_CTL2);
|
||||
if (ctl1 == 0xFFFFFFFF || !(ctl1 & BLC_PWM_ENABLE) || (ctl2 >> 16) == 0)
|
||||
return false;
|
||||
g_blMethod = BacklightMethod::PwmLpt;
|
||||
g_blFreqReg = BLC_PWM_PCH_CTL2;
|
||||
g_blDutyReg = BLC_PWM_PCH_CTL2;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t pch2 = ReadReg(BLC_PWM_PCH_CTL2);
|
||||
uint32_t cpu2 = ReadReg(BLC_PWM_CPU_CTL2);
|
||||
if (pch2 == 0xFFFFFFFF || (pch2 >> 16) == 0 || !(cpu2 & BLC_PWM_ENABLE))
|
||||
return false;
|
||||
g_blMethod = BacklightMethod::PwmIlk;
|
||||
g_blFreqReg = BLC_PWM_PCH_CTL2;
|
||||
g_blDutyReg = BLC_PWM_CPU_CTL;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- eDP DPCD backlight -------------------------------------------------
|
||||
|
||||
// Native AUX read or write of DPCD registers, retrying sink DEFERs.
|
||||
static bool DpcdAccess(bool write, uint32_t address, uint8_t* data, int size) {
|
||||
for (int attempt = 0; attempt < 8; attempt++) {
|
||||
uint8_t reply = 0xFF;
|
||||
int result = write
|
||||
? AuxTransfer(0x8, address, data, size, nullptr, 0, &reply)
|
||||
: AuxTransfer(0x9, address, nullptr, 0, data, size, &reply);
|
||||
if (result == size && (reply & 0x3) == 0) return true;
|
||||
if (result < 0 || (reply & 0x3) != 0x2) return false;
|
||||
ShortSpin();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Before Gen 12 the eDP output has its own transcoder; from Gen 12 it is
|
||||
// an ordinary DDI in DP mode, so ask the sink. Only eDP sinks implement
|
||||
// the 0x700 eDP register block or EDP_CONFIGURATION_CAP.
|
||||
static void DetectEmbeddedDp() {
|
||||
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_DP || g_auxControlReg == 0)
|
||||
return;
|
||||
uint8_t edpRev = 0;
|
||||
uint8_t edpConfig = 0;
|
||||
bool edp = DpcdAccess(false, DPCD_EDP_REV, &edpRev, 1) && edpRev != 0;
|
||||
if (!edp) edp = DpcdAccess(false, 0x00D, &edpConfig, 1) && edpConfig != 0;
|
||||
if (!edp) return;
|
||||
g_connectorType = montauk::abi::DISPLAY_CONNECTOR_EDP;
|
||||
CopyText(g_connectorName, sizeof(g_connectorName), "eDP-1");
|
||||
KernelLogStream(INFO, "IntelGPU") << "DDI output identified as eDP (eDP DPCD rev "
|
||||
<< base::hex << (uint64_t)edpRev << ")";
|
||||
}
|
||||
|
||||
static int DpcdBrightnessBytes() {
|
||||
return g_blMax > 0xFF ? 2 : 1;
|
||||
}
|
||||
|
||||
// Returns 0 when the panel offers no AUX brightness control, 1 when it
|
||||
// does but is still driven from its PWM pin, 2 when DPCD already owns it.
|
||||
static int ProbeDpcdBacklight() {
|
||||
if (g_connectorType != montauk::abi::DISPLAY_CONNECTOR_EDP || g_auxControlReg == 0)
|
||||
return 0;
|
||||
uint8_t caps[3] = {};
|
||||
if (!DpcdAccess(false, DPCD_EDP_REV, caps, 3)) return 0;
|
||||
if (caps[0] == 0 || !(caps[2] & EDP_BLCAP_AUX_SET)) return 0;
|
||||
uint8_t mode = 0;
|
||||
if (!DpcdAccess(false, DPCD_EDP_BACKLIGHT_MODE, &mode, 1)) return 0;
|
||||
g_blMax = (caps[2] & EDP_BLCAP_BYTE_COUNT_16) ? 0xFFFF : 0xFF;
|
||||
return (mode & EDP_BLMODE_MASK) == EDP_BLMODE_DPCD ? 2 : 1;
|
||||
}
|
||||
|
||||
static int DpcdReadPercent() {
|
||||
uint8_t bytes[2] = {};
|
||||
int count = DpcdBrightnessBytes();
|
||||
if (!DpcdAccess(false, DPCD_EDP_BRIGHTNESS_MSB, bytes, count)) return -1;
|
||||
uint32_t raw = count == 2 ? ((uint32_t)bytes[0] << 8 | bytes[1]) : bytes[0];
|
||||
return RawToPercent(raw, g_blMax);
|
||||
}
|
||||
|
||||
static bool DpcdWritePercent(int percent) {
|
||||
uint32_t raw = PercentToRaw(percent, g_blMax);
|
||||
if (raw == 0) raw = 1;
|
||||
uint8_t bytes[2];
|
||||
int count = DpcdBrightnessBytes();
|
||||
if (count == 2) {
|
||||
bytes[0] = (uint8_t)(raw >> 8);
|
||||
bytes[1] = (uint8_t)raw;
|
||||
} else {
|
||||
bytes[0] = (uint8_t)raw;
|
||||
}
|
||||
if (!DpcdAccess(true, DPCD_EDP_BRIGHTNESS_MSB, bytes, count)) return false;
|
||||
if (!g_dpcdModePending) return true;
|
||||
|
||||
// First change on a panel that was on its PWM pin: the level is
|
||||
// already programmed, so switching the source cannot flash it dark.
|
||||
uint8_t mode = 0;
|
||||
if (!DpcdAccess(false, DPCD_EDP_BACKLIGHT_MODE, &mode, 1)) return false;
|
||||
mode = (uint8_t)((mode & ~EDP_BLMODE_MASK) | EDP_BLMODE_DPCD);
|
||||
if (!DpcdAccess(true, DPCD_EDP_BACKLIGHT_MODE, &mode, 1)) return false;
|
||||
g_dpcdModePending = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- GMBUS (HDMI/DVI DDC) -----------------------------------------------
|
||||
|
||||
static int GmbusPinForPort(int port) {
|
||||
if (port < 0) return -1;
|
||||
switch (g_pch) {
|
||||
case PchFamily::Icp: {
|
||||
// Combo PHY ports use pins 1-3; Type-C ports start at pin 9.
|
||||
// Gen 11 numbers its first Type-C port C, Gen 12 numbers it D.
|
||||
int firstTc = g_gpuGen >= 12 ? 3 : 2;
|
||||
if (port < firstTc) return port + 1;
|
||||
int pin = 9 + (port - firstTc);
|
||||
return pin <= 14 ? pin : -1;
|
||||
}
|
||||
case PchFamily::Cnp:
|
||||
return port == 1 ? 1 : port == 2 ? 2 : port == 3 ? 4 : -1;
|
||||
default:
|
||||
return port == 1 ? 5 : port == 2 ? 4 : port == 3 ? 6 : -1;
|
||||
}
|
||||
}
|
||||
KernelLogStream(INFO, "IntelGPU") << "Panel PWM "
|
||||
<< (g_pwmSupported ? "available" : "not exposed");
|
||||
|
||||
static bool GmbusWait(uint32_t bit) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
for (;;) {
|
||||
uint32_t status = ReadReg(GMBUS2);
|
||||
if (status & GMBUS_SATOER) return false;
|
||||
if (status & bit) return true;
|
||||
if (Timekeeping::GetMilliseconds() - start > 50) return false;
|
||||
asm volatile("pause");
|
||||
}
|
||||
}
|
||||
|
||||
static bool GmbusWaitIdle() {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (ReadReg(GMBUS2) & GMBUS_ACTIVE) {
|
||||
if (Timekeeping::GetMilliseconds() - start > 50) return false;
|
||||
asm volatile("pause");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// One complete I2C message (START .. STOP) at 100 kHz.
|
||||
static bool GmbusTransfer(int pin, uint8_t address, bool read,
|
||||
uint8_t* data, int size) {
|
||||
if (pin < 0 || size <= 0 || size > 256) return false;
|
||||
|
||||
uint32_t gate = 0;
|
||||
if (g_pch == PchFamily::Cnp) {
|
||||
// Cannon Point drops GMBUS transfers while the unit clock gates.
|
||||
gate = ReadReg(SOUTH_DSPCLK_GATE_D);
|
||||
WriteReg(SOUTH_DSPCLK_GATE_D, gate | PCH_GMBUSUNIT_CLOCK_GATE_DISABLE);
|
||||
}
|
||||
|
||||
WriteReg(GMBUS0, (uint32_t)pin);
|
||||
uint32_t command = GMBUS_SW_RDY | GMBUS_CYCLE_WAIT
|
||||
| ((uint32_t)size << GMBUS_BYTE_COUNT_SHIFT)
|
||||
| ((uint32_t)address << 1) | (read ? GMBUS_SLAVE_READ : 0);
|
||||
bool ok = true;
|
||||
int pos = 0;
|
||||
if (read) {
|
||||
WriteReg(GMBUS1, command);
|
||||
while (ok && pos < size) {
|
||||
ok = GmbusWait(GMBUS_HW_RDY);
|
||||
if (!ok) break;
|
||||
uint32_t value = ReadReg(GMBUS3);
|
||||
for (int i = 0; i < 4 && pos < size; i++, pos++)
|
||||
data[pos] = (uint8_t)(value >> (8 * i));
|
||||
}
|
||||
} else {
|
||||
// The first dword is loaded before the command starts the cycle.
|
||||
uint32_t value = 0;
|
||||
for (int i = 0; i < 4 && pos < size; i++, pos++)
|
||||
value |= (uint32_t)data[pos] << (8 * i);
|
||||
WriteReg(GMBUS3, value);
|
||||
WriteReg(GMBUS1, command);
|
||||
while (ok && pos < size) {
|
||||
ok = GmbusWait(GMBUS_HW_RDY);
|
||||
if (!ok) break;
|
||||
value = 0;
|
||||
for (int i = 0; i < 4 && pos < size; i++, pos++)
|
||||
value |= (uint32_t)data[pos] << (8 * i);
|
||||
WriteReg(GMBUS3, value);
|
||||
}
|
||||
}
|
||||
if (ok) ok = GmbusWait(GMBUS_HW_WAIT_PHASE);
|
||||
|
||||
if (ok) {
|
||||
WriteReg(GMBUS1, GMBUS_SW_RDY | GMBUS_CYCLE_STOP);
|
||||
ok = GmbusWaitIdle();
|
||||
} else {
|
||||
// NAK or timeout: clear the error, let the bus go idle.
|
||||
WriteReg(GMBUS1, GMBUS_SW_CLR_INT);
|
||||
(void)GmbusWaitIdle();
|
||||
WriteReg(GMBUS1, 0);
|
||||
}
|
||||
WriteReg(GMBUS0, 0);
|
||||
|
||||
if (g_pch == PchFamily::Cnp) WriteReg(SOUTH_DSPCLK_GATE_D, gate);
|
||||
return ok;
|
||||
}
|
||||
|
||||
// ---- DDC/CI (external monitors) -----------------------------------------
|
||||
|
||||
static bool DdcUsesAux() {
|
||||
return g_connectorType == montauk::abi::DISPLAY_CONNECTOR_DP;
|
||||
}
|
||||
|
||||
static bool DdcWrite(uint8_t* data, int size) {
|
||||
if (DdcUsesAux()) {
|
||||
if (size > 16) return false;
|
||||
bool ok = AuxI2cTransfer(0x4, data, size, nullptr, 0, DdcAddress);
|
||||
// Address-only write without MOT ends the I2C transaction.
|
||||
bool stopped = AuxI2cTransfer(0x0, nullptr, 0, nullptr, 0, DdcAddress);
|
||||
return ok && stopped;
|
||||
}
|
||||
return GmbusTransfer(GmbusPinForPort(g_ddiPort), DdcAddress, false, data, size);
|
||||
}
|
||||
|
||||
static bool DdcRead(uint8_t* data, int size) {
|
||||
if (DdcUsesAux()) {
|
||||
if (size > 16) return false;
|
||||
if (!AuxI2cTransfer(0x5, nullptr, 0, nullptr, 0, DdcAddress)) return false;
|
||||
return AuxI2cTransfer(0x1, nullptr, 0, data, size, DdcAddress);
|
||||
}
|
||||
return GmbusTransfer(GmbusPinForPort(g_ddiPort), DdcAddress, true, data, size);
|
||||
}
|
||||
|
||||
static void DdcPace() {
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_ddcLastMs != 0 && now - g_ddcLastMs < DdcCommandGapMs)
|
||||
Timekeeping::Sleep(DdcCommandGapMs - (now - g_ddcLastMs));
|
||||
}
|
||||
|
||||
// Send an MCCS command: host source address, length, payload, checksum.
|
||||
// The checksum covers the monitor's 8-bit write address (0x6E) too.
|
||||
static bool DdcCommand(const uint8_t* payload, int length) {
|
||||
uint8_t packet[16];
|
||||
if (length + 3 > (int)sizeof(packet)) return false;
|
||||
packet[0] = DdcHostAddress;
|
||||
packet[1] = (uint8_t)(0x80 | length);
|
||||
uint8_t checksum = (uint8_t)(DdcAddress << 1) ^ packet[0] ^ packet[1];
|
||||
for (int i = 0; i < length; i++) {
|
||||
packet[2 + i] = payload[i];
|
||||
checksum ^= payload[i];
|
||||
}
|
||||
packet[2 + length] = checksum;
|
||||
DdcPace();
|
||||
bool ok = DdcWrite(packet, length + 3);
|
||||
g_ddcLastMs = Timekeeping::GetMilliseconds();
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool DdcGetBrightness(uint32_t& current, uint32_t& maximum) {
|
||||
uint8_t request[2] = {0x01, DdcVcpBrightness}; // Get VCP Feature
|
||||
if (!DdcCommand(request, 2)) return false;
|
||||
Timekeeping::Sleep(DdcReplyDelayMs);
|
||||
|
||||
// Reply: 6E 88 02 rc 10 type maxH maxL curH curL checksum. The
|
||||
// checksum covers the host's virtual address 0x50.
|
||||
uint8_t reply[11] = {};
|
||||
bool ok = DdcRead(reply, sizeof(reply));
|
||||
g_ddcLastMs = Timekeeping::GetMilliseconds();
|
||||
if (!ok) return false;
|
||||
uint8_t checksum = 0x50;
|
||||
for (int i = 0; i < 10; i++) checksum ^= reply[i];
|
||||
if (checksum != reply[10] || reply[1] != 0x88 || reply[2] != 0x02
|
||||
|| reply[3] != 0x00 || reply[4] != DdcVcpBrightness)
|
||||
return false;
|
||||
maximum = (uint32_t)reply[6] << 8 | reply[7];
|
||||
current = (uint32_t)reply[8] << 8 | reply[9];
|
||||
return maximum != 0;
|
||||
}
|
||||
|
||||
static bool DdcSetBrightness(uint32_t value) {
|
||||
uint8_t request[4] = {0x03, DdcVcpBrightness, // Set VCP Feature
|
||||
(uint8_t)(value >> 8), (uint8_t)value};
|
||||
return DdcCommand(request, 4);
|
||||
}
|
||||
|
||||
// DDC/CI takes tens of milliseconds and needs the scheduler, so it is
|
||||
// probed on first use from a syscall rather than during boot.
|
||||
static bool EnsureDdc() {
|
||||
if (g_blMethod == BacklightMethod::DdcCi) return true;
|
||||
if (!g_ddcCandidate) return false;
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_ddcRetryMs != 0 && now < g_ddcRetryMs) return false;
|
||||
|
||||
for (int attempt = 0; attempt < 3; attempt++) {
|
||||
uint32_t current = 0, maximum = 0;
|
||||
if (!DdcGetBrightness(current, maximum)) continue;
|
||||
g_blMethod = BacklightMethod::DdcCi;
|
||||
g_blMax = maximum;
|
||||
g_blPercent = RawToPercent(current, maximum);
|
||||
KernelLogStream(OK, "IntelGPU") << "Monitor brightness via DDC/CI ("
|
||||
<< base::dec << (uint64_t)current << "/" << (uint64_t)maximum << ")";
|
||||
return true;
|
||||
}
|
||||
g_ddcRetryMs = Timekeeping::GetMilliseconds() + DdcRetryIntervalMs;
|
||||
KernelLogStream(INFO, "IntelGPU") << "Monitor did not answer DDC/CI brightness query";
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Discovery and dispatch ---------------------------------------------
|
||||
|
||||
static void DiscoverBacklight() {
|
||||
g_blMethod = BacklightMethod::None;
|
||||
g_blPercent = -1;
|
||||
g_dpcdModePending = false;
|
||||
g_ddcCandidate = false;
|
||||
g_pch = DetectPch();
|
||||
DetectEmbeddedDp();
|
||||
|
||||
bool panel = g_connectorType == montauk::abi::DISPLAY_CONNECTOR_EDP
|
||||
|| g_connectorType == montauk::abi::DISPLAY_CONNECTOR_LVDS;
|
||||
if (panel) {
|
||||
// A panel firmware already put in DPCD mode ignores its PWM pin.
|
||||
// Otherwise PWM is what firmware lit it with; take DPCD over only
|
||||
// when there is no live PWM controller.
|
||||
int dpcd = ProbeDpcdBacklight();
|
||||
if (dpcd == 2) {
|
||||
g_blMethod = BacklightMethod::DpcdAux;
|
||||
} else if (!DiscoverPwmBacklight() && dpcd == 1) {
|
||||
g_blMethod = BacklightMethod::DpcdAux;
|
||||
g_dpcdModePending = true;
|
||||
g_blPercent = 100;
|
||||
}
|
||||
} else if (g_gpuGen >= 8 && g_ddiPort >= 0
|
||||
&& (g_connectorType == montauk::abi::DISPLAY_CONNECTOR_HDMI
|
||||
|| g_connectorType == montauk::abi::DISPLAY_CONNECTOR_DVI
|
||||
|| (g_connectorType == montauk::abi::DISPLAY_CONNECTOR_DP
|
||||
&& g_auxControlReg != 0))) {
|
||||
g_ddcCandidate = DdcUsesAux() || GmbusPinForPort(g_ddiPort) >= 0;
|
||||
}
|
||||
|
||||
const char* pch = g_pch == PchFamily::Ibx ? "IBX" : g_pch == PchFamily::Lpt ? "LPT"
|
||||
: g_pch == PchFamily::Cnp ? "CNP" : g_pch == PchFamily::Icp ? "ICP+"
|
||||
: "unknown";
|
||||
if (g_blMethod != BacklightMethod::None) {
|
||||
KernelLogStream(INFO, "IntelGPU") << "Backlight: " << BrightnessMethod()
|
||||
<< (g_dpcdModePending ? " (on first change)" : "")
|
||||
<< ", PCH " << pch;
|
||||
} else {
|
||||
KernelLogStream(INFO, "IntelGPU") << "Backlight: "
|
||||
<< (g_ddcCandidate ? "DDC/CI probe deferred" : "not exposed")
|
||||
<< ", PCH " << pch;
|
||||
}
|
||||
}
|
||||
|
||||
static int BrightnessLocked(int percent) {
|
||||
if (percent > 100) percent = 100;
|
||||
switch (g_blMethod) {
|
||||
case BacklightMethod::PwmIlk:
|
||||
case BacklightMethod::PwmLpt:
|
||||
case BacklightMethod::PwmCnp: {
|
||||
uint32_t maximum = 0, duty = 0;
|
||||
if (!PwmState(maximum, duty)) return -1;
|
||||
if (percent >= 0) {
|
||||
if (percent < MinPanelPercent) percent = MinPanelPercent;
|
||||
uint32_t raw = PercentToRaw(percent, maximum);
|
||||
PwmWriteDuty(raw ? raw : 1);
|
||||
if (!PwmState(maximum, duty)) return -2;
|
||||
}
|
||||
g_blPercent = RawToPercent(duty, maximum);
|
||||
return g_blPercent;
|
||||
}
|
||||
case BacklightMethod::DpcdAux: {
|
||||
if (percent < 0) {
|
||||
if (g_dpcdModePending) return g_blPercent;
|
||||
int level = DpcdReadPercent();
|
||||
if (level >= 0) g_blPercent = level;
|
||||
return g_blPercent;
|
||||
}
|
||||
if (percent < MinPanelPercent) percent = MinPanelPercent;
|
||||
if (!DpcdWritePercent(percent)) return -2;
|
||||
int level = DpcdReadPercent();
|
||||
g_blPercent = level >= 0 ? level : percent;
|
||||
return g_blPercent;
|
||||
}
|
||||
case BacklightMethod::None:
|
||||
if (!EnsureDdc()) return -1;
|
||||
[[fallthrough]];
|
||||
case BacklightMethod::DdcCi: {
|
||||
// Monitors answer queries slowly, and the OSD is the only
|
||||
// other writer, so the last known level stands in.
|
||||
if (percent < 0) return g_blPercent;
|
||||
if (!DdcSetBrightness(PercentToRaw(percent, g_blMax))) return -2;
|
||||
g_blPercent = percent;
|
||||
return g_blPercent;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void InitializeDisplayManagement() {
|
||||
@@ -942,7 +1483,7 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
DiscoverActiveTranscoder();
|
||||
ParseEdid();
|
||||
SelectCurrentMode();
|
||||
DiscoverPwm();
|
||||
DiscoverBacklight();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -1660,7 +2201,6 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
|
||||
out->capabilities = montauk::abi::DISPLAY_CAP_MODESET;
|
||||
if (g_edidValid) out->capabilities |= montauk::abi::DISPLAY_CAP_EDID;
|
||||
if (g_pwmSupported) out->capabilities |= montauk::abi::DISPLAY_CAP_BRIGHTNESS;
|
||||
if (g_flipSupported) out->capabilities |= montauk::abi::DISPLAY_CAP_PAGE_FLIP;
|
||||
for (int i = 0; i < g_modeCount; i++) {
|
||||
if (i != g_currentMode
|
||||
@@ -1672,6 +2212,7 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
out->modeCount = (uint32_t)g_modeCount;
|
||||
out->currentMode = g_currentMode;
|
||||
out->brightness = Brightness(-1);
|
||||
if (out->brightness >= 0) out->capabilities |= montauk::abi::DISPLAY_CAP_BRIGHTNESS;
|
||||
out->deviceId = g_gpuInfo.deviceId;
|
||||
out->generation = g_gpuInfo.gen;
|
||||
out->connectorType = g_connectorType;
|
||||
@@ -1737,20 +2278,28 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
}
|
||||
|
||||
int Brightness(int percent) {
|
||||
if (!g_initialized || !g_pwmSupported) return -1;
|
||||
uint32_t value = ReadReg(g_pwmCtlReg);
|
||||
uint32_t maximum = value >> 16;
|
||||
if (maximum == 0) return -1;
|
||||
if (percent < 0) {
|
||||
uint32_t duty = value & 0xFFFF;
|
||||
if (duty > maximum) duty = maximum;
|
||||
return (int)(((uint64_t)duty * 100 + maximum / 2) / maximum);
|
||||
if (!g_initialized) return -1;
|
||||
if (g_blMethod == BacklightMethod::None && !g_ddcCandidate) return -1;
|
||||
g_blLock.Acquire();
|
||||
int result = BrightnessLocked(percent);
|
||||
g_blLock.Release();
|
||||
return result;
|
||||
}
|
||||
if (percent > 100) percent = 100;
|
||||
uint32_t duty = (uint32_t)(((uint64_t)maximum * (uint32_t)percent + 50) / 100);
|
||||
WriteReg(g_pwmCtlReg, (value & 0xFFFF0000u) | duty);
|
||||
(void)ReadReg(g_pwmCtlReg);
|
||||
return Brightness(-1);
|
||||
|
||||
uint8_t ActiveConnectorType() {
|
||||
return g_connectorType;
|
||||
}
|
||||
|
||||
const char* BrightnessMethod() {
|
||||
switch (g_blMethod) {
|
||||
case BacklightMethod::PwmIlk:
|
||||
case BacklightMethod::PwmLpt:
|
||||
case BacklightMethod::PwmCnp: return "PWM";
|
||||
case BacklightMethod::DpcdAux: return "eDP AUX";
|
||||
case BacklightMethod::DdcCi: return "DDC/CI";
|
||||
case BacklightMethod::None: break;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -1993,6 +2542,12 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
<< base::dec << (uint64_t)g_frontBuffer << ")";
|
||||
}
|
||||
|
||||
// 7. Firmware POST resets the PWM duty to its own default. Put back
|
||||
// the level the user chose (register writes only; no AUX here).
|
||||
uint32_t pwmMax = 0, pwmDuty = 0;
|
||||
if (g_blPercent > 0 && PwmState(pwmMax, pwmDuty))
|
||||
PwmWriteDuty(PercentToRaw(g_blPercent, pwmMax));
|
||||
|
||||
KernelLogStream(OK, "IntelGPU") << "Display restored after S3 resume";
|
||||
}
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
static constexpr uint32_t DVOB = 0x61140; // DVO-B
|
||||
static constexpr uint32_t DVOC = 0x61160; // DVO-C
|
||||
static constexpr uint32_t LVDS = 0x61180; // LVDS panel
|
||||
static constexpr uint32_t DP_A = 0x64000; // CPU eDP (ILK-IVB)
|
||||
static constexpr uint32_t DP_B = 0x64100; // DisplayPort B
|
||||
static constexpr uint32_t DP_C = 0x64200; // DisplayPort C
|
||||
static constexpr uint32_t DP_D = 0x64300; // DisplayPort D
|
||||
@@ -277,12 +278,35 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
static constexpr uint32_t DP_AUX_MESSAGE_SHIFT = 20;
|
||||
|
||||
// PCH/CPU panel PWM. Firmware owns the frequency; the driver only changes
|
||||
// the duty field and preserves enable/polarity/reserved bits.
|
||||
// the duty field and preserves enable/polarity/reserved bits. Three
|
||||
// layouts share these addresses:
|
||||
// Ibex Peak..Panther Point (ILK-IVB): period = PCH_CTL2[31:16], duty
|
||||
// = CPU_CTL[15:0].
|
||||
// Lynx Point..Kaby Point (HSW-KBL): PCH_CTL2 = period << 16 | duty.
|
||||
// Cannon Point and later (and BXT/GLK): CTL1 enable, then full 32-bit
|
||||
// FREQ and DUTY registers, per controller at +0x100. Reading these as
|
||||
// the LPT layout corrupts the period, so the PCH must be known.
|
||||
static constexpr uint32_t BLC_PWM_CPU_CTL2 = 0x48250;
|
||||
static constexpr uint32_t BLC_PWM_CPU_CTL = 0x48254;
|
||||
static constexpr uint32_t BLC_PWM_PCH_CTL1 = 0xC8250;
|
||||
static constexpr uint32_t BLC_PWM_PCH_CTL2 = 0xC8254;
|
||||
static constexpr uint32_t BLC_PWM_ENABLE = (1u << 31);
|
||||
static constexpr uint32_t BXT_BLC_PWM_CTL1 = 0xC8250;
|
||||
static constexpr uint32_t BXT_BLC_PWM_FREQ1 = 0xC8254;
|
||||
static constexpr uint32_t BXT_BLC_PWM_DUTY1 = 0xC8258;
|
||||
static constexpr uint32_t BXT_BLC_PWM_CONTROLLER_DELTA = 0x100;
|
||||
|
||||
// eDP DPCD backlight (VESA eDP 1.2+ "AUX backlight control").
|
||||
static constexpr uint32_t DPCD_EDP_REV = 0x700;
|
||||
static constexpr uint32_t DPCD_EDP_GENERAL_CAP_1 = 0x701;
|
||||
static constexpr uint32_t DPCD_EDP_BACKLIGHT_CAP = 0x702;
|
||||
static constexpr uint32_t DPCD_EDP_BACKLIGHT_MODE = 0x721;
|
||||
static constexpr uint32_t DPCD_EDP_BRIGHTNESS_MSB = 0x722;
|
||||
static constexpr uint8_t EDP_CAP1_AUX_ENABLE = (1u << 2);
|
||||
static constexpr uint8_t EDP_BLCAP_AUX_SET = (1u << 1);
|
||||
static constexpr uint8_t EDP_BLCAP_BYTE_COUNT_16 = (1u << 2);
|
||||
static constexpr uint8_t EDP_BLMODE_MASK = 0x3;
|
||||
static constexpr uint8_t EDP_BLMODE_DPCD = 0x2;
|
||||
|
||||
// ADPA bits
|
||||
static constexpr uint32_t ADPA_DAC_ENABLE = (1u << 31);
|
||||
@@ -294,13 +318,25 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
static constexpr uint32_t LVDS_PORT_ENABLE = (1u << 31);
|
||||
static constexpr uint32_t LVDS_PIPE_B_SELECT = (1u << 30);
|
||||
|
||||
// --- GMBUS (I2C for EDID) ---
|
||||
static constexpr uint32_t GMBUS0 = 0x5100; // Clock/port select
|
||||
static constexpr uint32_t GMBUS1 = 0x5104; // Command/status
|
||||
static constexpr uint32_t GMBUS2 = 0x5108; // Status
|
||||
static constexpr uint32_t GMBUS3 = 0x510C; // Data buffer
|
||||
static constexpr uint32_t GMBUS4 = 0x5110; // Interrupt mask
|
||||
static constexpr uint32_t GMBUS5 = 0x5120; // 2-byte index
|
||||
// --- GMBUS (display I2C: DDC for HDMI/DVI) ---
|
||||
// Every supported generation is PCH-split, which moves GMBUS from the
|
||||
// legacy 0x5100 block into the PCH display range.
|
||||
static constexpr uint32_t GMBUS0 = 0xC5100; // Clock/pin select
|
||||
static constexpr uint32_t GMBUS1 = 0xC5104; // Command/status
|
||||
static constexpr uint32_t GMBUS2 = 0xC5108; // Status
|
||||
static constexpr uint32_t GMBUS3 = 0xC510C; // Data buffer
|
||||
static constexpr uint32_t GMBUS4 = 0xC5110; // Interrupt mask
|
||||
static constexpr uint32_t GMBUS5 = 0xC5120; // 2-byte index
|
||||
static constexpr uint32_t GMBUS_SW_CLR_INT = (1u << 31);
|
||||
static constexpr uint32_t GMBUS_SW_RDY = (1u << 30);
|
||||
static constexpr uint32_t GMBUS_CYCLE_WAIT = (1u << 25);
|
||||
static constexpr uint32_t GMBUS_CYCLE_STOP = (4u << 25);
|
||||
static constexpr uint32_t GMBUS_BYTE_COUNT_SHIFT = 16;
|
||||
static constexpr uint32_t GMBUS_SLAVE_READ = 1u;
|
||||
static constexpr uint32_t GMBUS_HW_WAIT_PHASE = (1u << 14);
|
||||
static constexpr uint32_t GMBUS_HW_RDY = (1u << 11);
|
||||
static constexpr uint32_t GMBUS_SATOER = (1u << 10); // NAK
|
||||
static constexpr uint32_t GMBUS_ACTIVE = (1u << 9);
|
||||
|
||||
// --- Hardware status page ---
|
||||
static constexpr uint32_t HWS_PGA = 0x02080;
|
||||
@@ -496,10 +532,19 @@ namespace Drivers::Graphics::IntelGPU {
|
||||
int SetDisplayMode(int modeIndex);
|
||||
|
||||
// percent < 0 queries the current brightness. Otherwise sets 0..100 and
|
||||
// returns the resulting percentage. Returns -1 when no PWM backlight is
|
||||
// exposed by the active output.
|
||||
// returns the resulting percentage. Returns -1 when the active output has
|
||||
// no controllable backlight, -2 when the hardware rejected the change.
|
||||
// Backends, in order: panel PWM, eDP DPCD (AUX) backlight, and DDC/CI
|
||||
// (MCCS VCP 0x10) for an external monitor on HDMI/DVI/DP. May sleep.
|
||||
int Brightness(int percent);
|
||||
|
||||
// DisplayConnectorType of the output the driver adopted from firmware.
|
||||
uint8_t ActiveConnectorType();
|
||||
|
||||
// Short name of the backlight method in use ("PWM", "eDP AUX", "DDC/CI"),
|
||||
// or nullptr when there is none.
|
||||
const char* BrightnessMethod();
|
||||
|
||||
// =========================================================================
|
||||
// Page flipping (double-buffered scanout)
|
||||
// =========================================================================
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Init.hpp"
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||
#include <Drivers/Graphics/DgpuPower.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
@@ -222,6 +223,10 @@ namespace Drivers {
|
||||
}
|
||||
}
|
||||
|
||||
void PowerDownUnusedGpus() {
|
||||
Graphics::DgpuPower::PowerDownUnused();
|
||||
}
|
||||
|
||||
void ProbeNormal() {
|
||||
Pci::ProbeAll(g_driverTable, g_driverTableCount, Pci::ProbePhase::Normal);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ namespace Drivers {
|
||||
// Post-probe: wire up GPU framebuffer to cursor subsystem.
|
||||
void InitializeGraphics();
|
||||
|
||||
// Power down discrete GPUs no driver claimed (hybrid-graphics laptops),
|
||||
// withholding their functions from the Normal-phase probe. Run between
|
||||
// ProbeEarly() and ProbeNormal(), once the timer is running.
|
||||
void PowerDownUnusedGpus();
|
||||
|
||||
// Probe PCI devices for Normal-phase drivers (xHCI, E1000, E1000E, AHCI, NVMe, IntelHDA).
|
||||
// USB class drivers (Bluetooth, etc.) initialize automatically during xHCI enumeration.
|
||||
void ProbeNormal();
|
||||
|
||||
@@ -173,6 +173,10 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
// Interrupt-to-idle-loop deferral
|
||||
volatile bool WorkPending = false;
|
||||
// When IwxProcessEvents() last ran to completion. The idle loop
|
||||
// sweeps the ring on a timer as well as on the interrupt, so a single
|
||||
// lost interrupt cannot silence the adapter for good.
|
||||
volatile uint64_t LastPumpMs = 0;
|
||||
|
||||
IwxFwInfo Fw;
|
||||
|
||||
@@ -321,6 +325,13 @@ namespace Drivers::Net::Wifi {
|
||||
// from any process/idle context; self-guarded against reentry.
|
||||
void IwxProcessEvents();
|
||||
|
||||
// A firmware error seen inside IwxProcessEvents() is only latched there:
|
||||
// reading the error table can mean a long wait for the MAC to wake, and
|
||||
// the pump runs on the BSP. The dump happens here instead, from the
|
||||
// service path, and at the latest before IwxStopDevice() wipes it.
|
||||
bool IwxFwErrorDumpPending();
|
||||
void IwxFlushFwErrorDump();
|
||||
|
||||
// Look up the firmware-advertised version of a command/notification.
|
||||
int IwxLookupCmdVer(uint8_t group, uint8_t cmd);
|
||||
int IwxLookupNotifVer(uint8_t group, uint8_t cmd);
|
||||
@@ -419,6 +430,9 @@ namespace Drivers::Net::Wifi {
|
||||
void IwxConnectRxData(const uint8_t* frame, uint32_t len);
|
||||
// Apply state changes the RX path queued (it cannot send commands itself).
|
||||
void IwxConnectService();
|
||||
// Whether IwxConnectService() has anything to do: queued RX-path work, or
|
||||
// a join in progress whose retransmission/give-up timers need watching.
|
||||
bool IwxConnectHasWork();
|
||||
int IwxConnectState();
|
||||
|
||||
// Link status for the network stack.
|
||||
|
||||
@@ -1091,6 +1091,18 @@ namespace Drivers::Net::Wifi {
|
||||
__atomic_clear(&g_inService, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
bool IwxConnectHasWork() {
|
||||
if (g_teardownPending || g_linkLostPending
|
||||
|| g_sendAssocPending || g_postAssocPending)
|
||||
return true;
|
||||
if (g_eapolTail != g_eapolHead) return true;
|
||||
// Every state between "contexts up" and "connected" runs on timers
|
||||
// (retransmission, give-up, the handshake's own retries).
|
||||
return g_state != ConnState::Idle
|
||||
&& g_state != ConnState::Connected
|
||||
&& g_state != ConnState::Failed;
|
||||
}
|
||||
|
||||
bool IwxConnectRefusedForSecurity() { return g_refusedForSecurity; }
|
||||
|
||||
int IwxConnectState() {
|
||||
|
||||
@@ -711,6 +711,9 @@ namespace Drivers::Net::Wifi {
|
||||
}
|
||||
|
||||
void IwxStopDevice() {
|
||||
// Last chance to read the firmware's error table before the reset
|
||||
// below wipes it.
|
||||
IwxFlushFwErrorDump();
|
||||
IwxDisableInterrupts();
|
||||
|
||||
if (IwxNicLock()) {
|
||||
@@ -1647,15 +1650,63 @@ namespace Drivers::Net::Wifi {
|
||||
IwxWrite32(IWX_RFH_Q0_FRBDCB_WIDX_TRG, widx & ~7u);
|
||||
}
|
||||
|
||||
// Latched by IwxProcessEvents(), performed by IwxFlushFwErrorDump().
|
||||
static volatile bool g_fwDumpPending = false;
|
||||
|
||||
bool IwxFwErrorDumpPending() { return g_fwDumpPending; }
|
||||
|
||||
void IwxFlushFwErrorDump() {
|
||||
if (!__atomic_exchange_n(&g_fwDumpPending, false, __ATOMIC_ACQ_REL)) return;
|
||||
IwxDumpFwError();
|
||||
}
|
||||
|
||||
// The first fatal error moves the adapter to Error; repeats of the same
|
||||
// cause before the reset lands are not news and must not re-log.
|
||||
static void IwxNoteFatal(const char* what, bool dump) {
|
||||
if (g_iwx.State == IwxFwState::Error) return;
|
||||
KernelLogStream(ERROR, "WiFi") << what;
|
||||
g_iwx.State = IwxFwState::Error;
|
||||
g_iwx.FwErrors++;
|
||||
if (dump) g_fwDumpPending = true;
|
||||
}
|
||||
|
||||
void IwxProcessEvents() {
|
||||
if (!g_iwx.Mmio) return;
|
||||
// Never nest. With interrupts enabled during command waits this is a
|
||||
// genuine multi-core race, so it has to be an atomic test-and-set.
|
||||
if (g_iwx.InProcessEvents.test_and_set(std::memory_order_acquire)) return;
|
||||
|
||||
// Consume the wakeup BEFORE looking at the hardware, never after.
|
||||
// This used to be cleared on the way out, after the automask had
|
||||
// already been re-armed below and after the ring sweep -- which hands
|
||||
// received frames to the network stack and so is anything but short.
|
||||
// An interrupt landing in that window set the flag only for it to be
|
||||
// wiped a moment later: the work was lost, and because MSI-X masks
|
||||
// the vector each time it fires until the driver re-arms it here, no
|
||||
// further interrupt could ever arrive to replace it. The adapter went
|
||||
// permanently silent -- no RX, no TX completions, so not even the link
|
||||
// supervision noticed -- while the link was still reported up.
|
||||
// Clearing first means an interrupt at any point after this line
|
||||
// leaves the flag set and earns another pass.
|
||||
g_iwx.WorkPending = false;
|
||||
__atomic_thread_fence(__ATOMIC_SEQ_CST);
|
||||
|
||||
if (g_msix) {
|
||||
uint32_t fh = IwxRead32(IWX_CSR_MSIX_FH_INT_CAUSES_AD);
|
||||
uint32_t hwc = IwxRead32(IWX_CSR_MSIX_HW_INT_CAUSES_AD);
|
||||
// All-ones is not a cause word, it is what a read returns when the
|
||||
// device has dropped off the bus (or its link is down). Taken at
|
||||
// face value it reads as every cause at once, firmware errors
|
||||
// included, and would be "handled" -- dump and all -- on every
|
||||
// single pass. Mid-bring-up a reset can produce the same read
|
||||
// transiently, so only a running adapter is declared gone.
|
||||
if (fh == 0xffffffff && hwc == 0xffffffff) {
|
||||
if (g_iwx.State == IwxFwState::Running)
|
||||
IwxNoteFatal("Adapter is not responding on the bus", false);
|
||||
g_iwx.LastPumpMs = Timekeeping::GetMilliseconds();
|
||||
g_iwx.InProcessEvents.clear(std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
IwxWrite32(IWX_CSR_MSIX_FH_INT_CAUSES_AD, fh);
|
||||
IwxWrite32(IWX_CSR_MSIX_HW_INT_CAUSES_AD, hwc);
|
||||
fh &= g_fhMask;
|
||||
@@ -1673,15 +1724,10 @@ namespace Drivers::Net::Wifi {
|
||||
if ((fh & IWX_MSIX_FH_INT_CAUSES_FH_ERR)
|
||||
|| (hwc & IWX_MSIX_HW_INT_CAUSES_REG_SW_ERR)
|
||||
|| (hwc & IWX_MSIX_HW_INT_CAUSES_REG_SW_ERR_V2)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Fatal firmware error";
|
||||
g_iwx.State = IwxFwState::Error;
|
||||
g_iwx.FwErrors++;
|
||||
IwxDumpFwError();
|
||||
IwxNoteFatal("Fatal firmware error", true);
|
||||
}
|
||||
if (hwc & IWX_MSIX_HW_INT_CAUSES_REG_HW_ERR) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Hardware error";
|
||||
g_iwx.State = IwxFwState::Error;
|
||||
g_iwx.FwErrors++;
|
||||
IwxNoteFatal("Hardware error", false);
|
||||
}
|
||||
if (hwc & IWX_MSIX_HW_INT_CAUSES_REG_RF_KILL) {
|
||||
if (IwxCheckRfKill()) {
|
||||
@@ -1703,10 +1749,7 @@ namespace Drivers::Net::Wifi {
|
||||
IwxWrite32(IWX_RFH_Q0_FRBDCB_WIDX_TRG, 8);
|
||||
}
|
||||
if (r1 & (1u << 25)) { // SW_ERR
|
||||
KernelLogStream(ERROR, "WiFi") << "Fatal firmware error";
|
||||
g_iwx.State = IwxFwState::Error;
|
||||
g_iwx.FwErrors++;
|
||||
IwxDumpFwError();
|
||||
IwxNoteFatal("Fatal firmware error", true);
|
||||
}
|
||||
if (r1 & ((1u << 31) | (1u << 3) | (1u << 28))) // FH_RX/SW_RX/periodic
|
||||
IwxNotifIntr();
|
||||
@@ -1717,7 +1760,7 @@ namespace Drivers::Net::Wifi {
|
||||
// been consumed by another poller, so always sweep the ring.
|
||||
IwxNotifIntr();
|
||||
|
||||
g_iwx.WorkPending = false;
|
||||
g_iwx.LastPumpMs = Timekeeping::GetMilliseconds();
|
||||
g_iwx.InProcessEvents.clear(std::memory_order_release);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
#include <Hal/Apic/Apic.hpp>
|
||||
#include <Hal/Apic/Interrupts.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <atomic>
|
||||
|
||||
@@ -417,9 +419,52 @@ namespace Drivers::Net::Wifi {
|
||||
}
|
||||
}
|
||||
|
||||
// At most one core inside the blocking half at a time.
|
||||
static volatile bool g_blockingBusy = false;
|
||||
static volatile uint64_t g_lastApKickMs = 0;
|
||||
static constexpr uint64_t AP_KICK_INTERVAL_MS = 10;
|
||||
|
||||
// The adapter is swept on a timer as well as on its interrupt whenever
|
||||
// anything is in flight. Cheap (two register reads when idle), and it
|
||||
// means no single lost interrupt can leave the link silently dead.
|
||||
static constexpr uint64_t PUMP_POLL_MS = 100;
|
||||
|
||||
// True when the caller should leave the blocking work alone because an AP
|
||||
// has it (or has just been asked to take it).
|
||||
static bool LeaveToAp() {
|
||||
auto* cpu = Smp::GetCurrentCpuData();
|
||||
if (!cpu || cpu->cpuIndex != 0) return false;
|
||||
if (g_blockingBusy) return true;
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
uint64_t last = g_lastApKickMs;
|
||||
if (last && now >= last && now - last < AP_KICK_INTERVAL_MS) return true;
|
||||
|
||||
for (int i = 1; i < Smp::GetCpuCount(); i++) {
|
||||
auto* t = Smp::GetCpuData(i);
|
||||
if (!t || !t->started) continue;
|
||||
if (t->currentSlot >= 0 || t->reservedForKernelWork) continue;
|
||||
if (Hal::LocalApic::SendFixedIpi(t->lapicId,
|
||||
Hal::IRQ_VECTOR_BASE + Hal::IRQ_RESCHEDULE)) {
|
||||
g_lastApKickMs = now;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool InitDue() {
|
||||
return g_initPending.load(std::memory_order_acquire) && !g_initialized
|
||||
&& Fs::Vfs::IsDriveRegistered(0);
|
||||
}
|
||||
|
||||
void ServiceDeferredInit() {
|
||||
if (!g_initPending.load(std::memory_order_relaxed) || g_initialized) return;
|
||||
if (!Fs::Vfs::IsDriveRegistered(0)) return; // ramdisk not mounted yet
|
||||
if (!InitDue()) return;
|
||||
if (LeaveToAp()) return;
|
||||
if (__atomic_test_and_set(&g_blockingBusy, __ATOMIC_ACQUIRE)) return;
|
||||
struct Guard {
|
||||
~Guard() { __atomic_clear(&g_blockingBusy, __ATOMIC_RELEASE); }
|
||||
} guard;
|
||||
|
||||
bool expected = true;
|
||||
if (!g_initPending.compare_exchange_strong(expected, false,
|
||||
@@ -440,20 +485,30 @@ namespace Drivers::Net::Wifi {
|
||||
static void ServiceAsync();
|
||||
static void ServiceRecovery();
|
||||
|
||||
bool HasDeferredWork() {
|
||||
if (g_initPending.load(std::memory_order_acquire) &&
|
||||
!g_initialized && Fs::Vfs::IsDriveRegistered(0)) {
|
||||
return true;
|
||||
// Whether the RX ring is due a sweep regardless of the interrupt.
|
||||
static bool PumpDue(uint64_t now) {
|
||||
if (g_iwx.State != IwxFwState::Running) return false;
|
||||
if (IwxConnectState() == (int)IwxConnStateId::Idle && !g_iwx.ScanActive)
|
||||
return false;
|
||||
uint64_t last = g_iwx.LastPumpMs;
|
||||
return now > last && now - last >= PUMP_POLL_MS;
|
||||
}
|
||||
if (!g_iwx.Mmio) return false;
|
||||
if (g_iwx.WorkPending) return true;
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_scanDeadline != 0 && now >= g_scanDeadline) return true;
|
||||
// Work that may send firmware commands, i.e. wait on the adapter.
|
||||
static bool HasBlockingWork(uint64_t now) {
|
||||
// A finished sweep only needs its generation bumped; an overrunning
|
||||
// one needs aborting. Both live in ServiceAsync().
|
||||
if (g_scanDeadline != 0 && (now >= g_scanDeadline || !g_iwx.ScanActive))
|
||||
return true;
|
||||
// The MLME/WPA state machine owns sub-second retransmission timers in
|
||||
// addition to the overall async deadline. Service it until ServiceAsync
|
||||
// observes Connected/Failed/Idle and clears this flag.
|
||||
if (g_asyncConnect) return true;
|
||||
// Work the RX path queued for IwxConnectService(). This used to ride
|
||||
// on WorkPending, but the pump that clears that flag may now run on a
|
||||
// different core from the one that services the connection.
|
||||
if (IwxConnectHasWork()) return true;
|
||||
if (IwxFwErrorDumpPending()) return true;
|
||||
if (g_iwx.State == IwxFwState::Error && g_initialized &&
|
||||
!g_recoveryGaveUp &&
|
||||
(g_lastRecoveryMs == 0 ||
|
||||
@@ -463,9 +518,30 @@ namespace Drivers::Net::Wifi {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HasDeferredWork() {
|
||||
if (InitDue()) return true;
|
||||
if (!g_iwx.Mmio) return false;
|
||||
if (g_iwx.WorkPending) return true;
|
||||
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (PumpDue(now)) return true;
|
||||
return HasBlockingWork(now);
|
||||
}
|
||||
|
||||
void ServiceEvents() {
|
||||
if (!g_iwx.Mmio) return;
|
||||
if (g_iwx.WorkPending) IwxProcessEvents();
|
||||
|
||||
// The non-blocking half: any core, the BSP included.
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_iwx.WorkPending || PumpDue(now)) IwxProcessEvents();
|
||||
|
||||
now = Timekeeping::GetMilliseconds();
|
||||
if (!HasBlockingWork(now)) return;
|
||||
if (LeaveToAp()) return;
|
||||
if (__atomic_test_and_set(&g_blockingBusy, __ATOMIC_ACQUIRE)) return;
|
||||
struct Guard {
|
||||
~Guard() { __atomic_clear(&g_blockingBusy, __ATOMIC_RELEASE); }
|
||||
} guard;
|
||||
|
||||
// Everything below can send firmware commands, and each one waits by
|
||||
// busy-spinning. This runs from ServiceDeferredWork(), which has set
|
||||
@@ -487,6 +563,11 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
if (ownsPass) IwxEndServicePass();
|
||||
|
||||
// A firmware error the pump latched. Normally the reset below dumps
|
||||
// it on its way through IwxStopDevice(); this catches the cases where
|
||||
// no reset is coming (recovery exhausted, or the adapter never came up).
|
||||
IwxFlushFwErrorDump();
|
||||
|
||||
// Deliberately outside the budget: a reset is a bring-up, not a pass of
|
||||
// routine servicing, and it has its own wall-clock handshakes to run.
|
||||
ServiceRecovery();
|
||||
|
||||
@@ -139,6 +139,7 @@ namespace Drivers::PS2::Keyboard {
|
||||
ack = Drivers::PS2::ReadData();
|
||||
|
||||
g_Modifiers = {};
|
||||
g_Modifiers.NumLock = true; // keypad types digits, as on most PCs
|
||||
g_BufferHead = 0;
|
||||
g_BufferTail = 0;
|
||||
g_ExtendedScancode = false;
|
||||
@@ -257,6 +258,9 @@ namespace Drivers::PS2::Keyboard {
|
||||
ascii = g_ScancodeToAscii[keycode];
|
||||
}
|
||||
}
|
||||
// Num Lock off: the keypad is navigation keys, which type nothing.
|
||||
if (!g_Modifiers.NumLock && IsKeypadNavScancode(keycode))
|
||||
ascii = 0;
|
||||
|
||||
KeyEvent event = {
|
||||
.Scancode = scancode,
|
||||
@@ -310,6 +314,12 @@ namespace Drivers::PS2::Keyboard {
|
||||
KeyEvent stamped = event;
|
||||
if (stamped.Scancode == ScCapsLock && stamped.Pressed)
|
||||
g_Modifiers.CapsLock = !g_Modifiers.CapsLock;
|
||||
// Num Lock likewise. Like the PS/2 path, the lock key itself is not
|
||||
// delivered to apps.
|
||||
if (stamped.Scancode == ScNumLock) {
|
||||
if (stamped.Pressed) g_Modifiers.NumLock = !g_Modifiers.NumLock;
|
||||
return;
|
||||
}
|
||||
stamped.CapsLock = g_Modifiers.CapsLock;
|
||||
PushAndNotify(stamped);
|
||||
}
|
||||
@@ -318,4 +328,8 @@ namespace Drivers::PS2::Keyboard {
|
||||
return g_Modifiers.CapsLock;
|
||||
}
|
||||
|
||||
bool GetNumLock() {
|
||||
return g_Modifiers.NumLock;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -63,4 +63,14 @@ namespace Drivers::PS2::Keyboard {
|
||||
// compute ASCII, since the lock is toggled centrally here.
|
||||
bool GetCapsLock();
|
||||
|
||||
// Current Num Lock state, owned here like Caps Lock. On (the default), the
|
||||
// keypad types digits; off, it acts as the navigation keys printed on it.
|
||||
bool GetNumLock();
|
||||
|
||||
// Set-1 scancodes of the keypad keys that are a digit/'.' with Num Lock on
|
||||
// and Home/arrows/PgUp/End/PgDn/Ins/Del with it off (not '-' and '+').
|
||||
inline bool IsKeypadNavScancode(uint8_t keycode) {
|
||||
return keycode >= 0x47 && keycode <= 0x53 && keycode != 0x4A && keycode != 0x4E;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -223,6 +223,12 @@ namespace Drivers::USB::HidKeyboard {
|
||||
if (caps && base >= 'a' && base <= 'z') upper = !shift;
|
||||
ascii = upper ? g_ScancodeToAsciiShifted[scancode] : base;
|
||||
}
|
||||
// Num Lock off: the keypad is navigation keys, which type nothing.
|
||||
// (Only keypad usages get here with these scancodes: the dedicated
|
||||
// navigation keys are nonChar already.)
|
||||
if (!Drivers::PS2::Keyboard::GetNumLock() &&
|
||||
Drivers::PS2::Keyboard::IsKeypadNavScancode(scancode))
|
||||
ascii = 0;
|
||||
|
||||
Drivers::PS2::Keyboard::KeyEvent event = {
|
||||
.Scancode = scancode,
|
||||
@@ -367,10 +373,14 @@ namespace Drivers::USB::HidKeyboard {
|
||||
uint8_t scancode = g_HidToScancode[key];
|
||||
if (scancode != 0) {
|
||||
InjectKey(scancode, true, modifiers, IsNonCharKey(key), IsExtendedKey(key));
|
||||
newKeyPressed = true;
|
||||
// Lock keys toggle once per press; repeating them while
|
||||
// held would flip Caps/Num/Scroll Lock on and off.
|
||||
if (key != 0x39 && key != 0x53 && key != 0x47) {
|
||||
g_RepeatKey = key;
|
||||
g_RepeatStartMs = now;
|
||||
g_LastRepeatMs = 0;
|
||||
newKeyPressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* BlockCache.cpp
|
||||
* Per-mount LRU cache of filesystem blocks
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "BlockCache.hpp"
|
||||
#include <Drivers/Storage/BlockDevice.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/Heap.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Fs::BlockCache {
|
||||
|
||||
// =========================================================================
|
||||
// Sizing
|
||||
// =========================================================================
|
||||
|
||||
// Capacity is free memory / FreeMemDivisor at creation, clamped to
|
||||
// [MinEntries, MaxEntries]. 32768 4 KiB blocks = 128 MiB, enough to keep
|
||||
// the whole native toolchain (cc1plus, as, ld, headers, libstdc++) hot.
|
||||
static constexpr uint32_t MinEntries = 256;
|
||||
static constexpr uint32_t MaxEntries = 32768;
|
||||
static constexpr uint64_t FreeMemDivisor = 8;
|
||||
|
||||
// Every storage driver accepts at least 128 sectors per command (AHCI and
|
||||
// the NVMe fallback both cap there), so prefetch runs stay within it.
|
||||
static constexpr uint32_t MaxRunSectors = 128;
|
||||
|
||||
static constexpr uint32_t Nil = 0xFFFFFFFF;
|
||||
|
||||
// =========================================================================
|
||||
// Types
|
||||
// =========================================================================
|
||||
|
||||
// Entries live in one array and link by index. An entry is in exactly one
|
||||
// of three states: never used (index >= nextFresh, no page), on the free
|
||||
// list (has a page, not hashed), or live (hashed and on the LRU list).
|
||||
struct Entry {
|
||||
uint32_t block;
|
||||
uint32_t hashNext; // bucket chain, or free-list link
|
||||
uint32_t lruPrev;
|
||||
uint32_t lruNext;
|
||||
uint8_t* data;
|
||||
};
|
||||
|
||||
struct Cache {
|
||||
int blockDevIndex;
|
||||
uint64_t partStartLba;
|
||||
uint32_t blockSize;
|
||||
uint32_t sectorsPerBlock;
|
||||
|
||||
Entry* entries;
|
||||
uint32_t* buckets;
|
||||
uint32_t capacity;
|
||||
uint32_t hashShift;
|
||||
int tablePages;
|
||||
|
||||
uint32_t nextFresh;
|
||||
uint32_t freeHead;
|
||||
uint32_t lruHead; // most recently used
|
||||
uint32_t lruTail; // eviction candidate
|
||||
|
||||
// Staging buffer for multi-block prefetch reads (MaxRunSectors long).
|
||||
uint8_t* staging;
|
||||
int stagingPages;
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Device I/O
|
||||
// =========================================================================
|
||||
|
||||
static bool DevRead(Cache* c, uint32_t block, uint32_t count, void* buf) {
|
||||
auto* dev = Drivers::Storage::GetBlockDevice(c->blockDevIndex);
|
||||
if (!dev) return false;
|
||||
return dev->ReadSectors(dev->Ctx,
|
||||
c->partStartLba + (uint64_t)block * c->sectorsPerBlock,
|
||||
count * c->sectorsPerBlock, buf);
|
||||
}
|
||||
|
||||
static bool DevWrite(Cache* c, uint32_t block, const void* buf) {
|
||||
auto* dev = Drivers::Storage::GetBlockDevice(c->blockDevIndex);
|
||||
if (!dev) return false;
|
||||
return dev->WriteSectors(dev->Ctx,
|
||||
c->partStartLba + (uint64_t)block * c->sectorsPerBlock,
|
||||
c->sectorsPerBlock, buf);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hash and LRU bookkeeping
|
||||
// =========================================================================
|
||||
|
||||
static uint32_t Bucket(const Cache* c, uint32_t block) {
|
||||
return (block * 0x9E3779B1u) >> c->hashShift;
|
||||
}
|
||||
|
||||
static uint32_t Lookup(Cache* c, uint32_t block) {
|
||||
uint32_t i = c->buckets[Bucket(c, block)];
|
||||
while (i != Nil) {
|
||||
if (c->entries[i].block == block) return i;
|
||||
i = c->entries[i].hashNext;
|
||||
}
|
||||
return Nil;
|
||||
}
|
||||
|
||||
static void LruUnlink(Cache* c, uint32_t i) {
|
||||
Entry& e = c->entries[i];
|
||||
if (e.lruPrev != Nil) c->entries[e.lruPrev].lruNext = e.lruNext;
|
||||
else c->lruHead = e.lruNext;
|
||||
if (e.lruNext != Nil) c->entries[e.lruNext].lruPrev = e.lruPrev;
|
||||
else c->lruTail = e.lruPrev;
|
||||
e.lruPrev = e.lruNext = Nil;
|
||||
}
|
||||
|
||||
static void LruPushFront(Cache* c, uint32_t i) {
|
||||
Entry& e = c->entries[i];
|
||||
e.lruPrev = Nil;
|
||||
e.lruNext = c->lruHead;
|
||||
if (c->lruHead != Nil) c->entries[c->lruHead].lruPrev = i;
|
||||
c->lruHead = i;
|
||||
if (c->lruTail == Nil) c->lruTail = i;
|
||||
}
|
||||
|
||||
static void Touch(Cache* c, uint32_t i) {
|
||||
if (c->lruHead == i) return;
|
||||
LruUnlink(c, i);
|
||||
LruPushFront(c, i);
|
||||
}
|
||||
|
||||
static void HashInsert(Cache* c, uint32_t i) {
|
||||
uint32_t b = Bucket(c, c->entries[i].block);
|
||||
c->entries[i].hashNext = c->buckets[b];
|
||||
c->buckets[b] = i;
|
||||
}
|
||||
|
||||
static void HashRemove(Cache* c, uint32_t i) {
|
||||
uint32_t* link = &c->buckets[Bucket(c, c->entries[i].block)];
|
||||
while (*link != Nil) {
|
||||
if (*link == i) {
|
||||
*link = c->entries[i].hashNext;
|
||||
break;
|
||||
}
|
||||
link = &c->entries[*link].hashNext;
|
||||
}
|
||||
c->entries[i].hashNext = Nil;
|
||||
}
|
||||
|
||||
// Take a live entry out of the cache and park it (and its page) on the
|
||||
// free list.
|
||||
static void Drop(Cache* c, uint32_t i) {
|
||||
HashRemove(c, i);
|
||||
LruUnlink(c, i);
|
||||
c->entries[i].block = 0;
|
||||
c->entries[i].hashNext = c->freeHead;
|
||||
c->freeHead = i;
|
||||
}
|
||||
|
||||
// Get an entry with a page, not hashed and not on the LRU. Prefers the
|
||||
// free list, then a fresh entry, then evicts the least recently used.
|
||||
// Returns Nil only if the cache is empty and no page can be allocated.
|
||||
static uint32_t Acquire(Cache* c) {
|
||||
if (c->freeHead != Nil) {
|
||||
uint32_t i = c->freeHead;
|
||||
c->freeHead = c->entries[i].hashNext;
|
||||
c->entries[i].hashNext = Nil;
|
||||
return i;
|
||||
}
|
||||
|
||||
if (c->nextFresh < c->capacity) {
|
||||
void* page = Memory::g_pfa->Allocate();
|
||||
if (page != nullptr) {
|
||||
uint32_t i = c->nextFresh++;
|
||||
Entry& e = c->entries[i];
|
||||
e.block = 0;
|
||||
e.hashNext = e.lruPrev = e.lruNext = Nil;
|
||||
e.data = (uint8_t*)page;
|
||||
return i;
|
||||
}
|
||||
// Out of memory: fall through and recycle what we already hold.
|
||||
}
|
||||
|
||||
uint32_t victim = c->lruTail;
|
||||
if (victim == Nil) return Nil;
|
||||
HashRemove(c, victim);
|
||||
LruUnlink(c, victim);
|
||||
return victim;
|
||||
}
|
||||
|
||||
// Publish a freshly filled entry under `block`.
|
||||
static void Insert(Cache* c, uint32_t i, uint32_t block) {
|
||||
c->entries[i].block = block;
|
||||
HashInsert(c, i);
|
||||
LruPushFront(c, i);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Public API
|
||||
// =========================================================================
|
||||
|
||||
Cache* Create(int blockDevIndex, uint64_t partStartLba, uint32_t blockSize) {
|
||||
if (blockSize < 512 || blockSize > 0x1000 || (blockSize % 512) != 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
montauk::abi::MemStats stats{};
|
||||
Memory::g_pfa->GetStats(&stats);
|
||||
uint64_t want = stats.freeBytes / FreeMemDivisor / blockSize;
|
||||
if (want > MaxEntries) want = MaxEntries;
|
||||
if (want < MinEntries) want = MinEntries;
|
||||
|
||||
// Round down to a power of two so the bucket count can equal capacity.
|
||||
uint32_t capacity = MinEntries;
|
||||
while ((uint64_t)capacity * 2 <= want) capacity *= 2;
|
||||
|
||||
auto* c = (Cache*)Memory::g_heap->Request(sizeof(Cache));
|
||||
if (c == nullptr) return nullptr;
|
||||
memset(c, 0, sizeof(Cache));
|
||||
|
||||
// Entries and buckets share one allocation. Physically consecutive
|
||||
// runs can be scarce on a fragmented PFA, so halve until it fits.
|
||||
while (capacity >= MinEntries) {
|
||||
uint64_t bytes = (uint64_t)capacity * (sizeof(Entry) + sizeof(uint32_t));
|
||||
int pages = (int)((bytes + 0xFFF) / 0x1000);
|
||||
void* table = Memory::g_pfa->ReallocConsecutive(nullptr, pages);
|
||||
if (table != nullptr) {
|
||||
c->entries = (Entry*)table;
|
||||
c->buckets = (uint32_t*)((uint8_t*)table + (uint64_t)capacity * sizeof(Entry));
|
||||
c->tablePages = pages;
|
||||
break;
|
||||
}
|
||||
capacity /= 2;
|
||||
}
|
||||
if (c->entries == nullptr) {
|
||||
Memory::g_heap->Free(c);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t log2 = 0;
|
||||
while ((1u << log2) < capacity) log2++;
|
||||
|
||||
c->blockDevIndex = blockDevIndex;
|
||||
c->partStartLba = partStartLba;
|
||||
c->blockSize = blockSize;
|
||||
c->sectorsPerBlock = blockSize / 512;
|
||||
c->capacity = capacity;
|
||||
c->hashShift = 32 - log2;
|
||||
c->nextFresh = 0;
|
||||
c->freeHead = Nil;
|
||||
c->lruHead = Nil;
|
||||
c->lruTail = Nil;
|
||||
for (uint32_t b = 0; b < capacity; b++) c->buckets[b] = Nil;
|
||||
|
||||
// Prefetch is an optimization; without a staging buffer it is a no-op.
|
||||
c->stagingPages = (int)((MaxRunSectors * 512 + 0xFFF) / 0x1000);
|
||||
c->staging = (uint8_t*)Memory::g_pfa->ReallocConsecutive(nullptr, c->stagingPages);
|
||||
if (c->staging == nullptr) c->stagingPages = 0;
|
||||
|
||||
KernelLogStream(OK, "BlockCache") << "Block cache: up to "
|
||||
<< base::dec << (uint64_t)capacity << " blocks ("
|
||||
<< (uint64_t)capacity * blockSize / (1024 * 1024) << " MiB)";
|
||||
return c;
|
||||
}
|
||||
|
||||
void Destroy(Cache* c) {
|
||||
if (c == nullptr) return;
|
||||
for (uint32_t i = 0; i < c->nextFresh; i++) {
|
||||
if (c->entries[i].data != nullptr) Memory::g_pfa->Free(c->entries[i].data);
|
||||
}
|
||||
if (c->staging != nullptr) Memory::g_pfa->Free(c->staging, c->stagingPages);
|
||||
Memory::g_pfa->Free(c->entries, c->tablePages);
|
||||
Memory::g_heap->Free(c);
|
||||
}
|
||||
|
||||
bool Read(Cache* c, uint32_t block, void* buf) {
|
||||
if (block == 0) return false;
|
||||
|
||||
uint32_t i = Lookup(c, block);
|
||||
if (i != Nil) {
|
||||
Touch(c, i);
|
||||
memcpy(buf, c->entries[i].data, c->blockSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
i = Acquire(c);
|
||||
if (i == Nil) return DevRead(c, block, 1, buf);
|
||||
|
||||
if (!DevRead(c, block, 1, c->entries[i].data)) {
|
||||
c->entries[i].hashNext = c->freeHead;
|
||||
c->freeHead = i;
|
||||
return false;
|
||||
}
|
||||
Insert(c, i, block);
|
||||
memcpy(buf, c->entries[i].data, c->blockSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Write(Cache* c, uint32_t block, const void* buf) {
|
||||
if (block == 0) return false;
|
||||
|
||||
uint32_t i = Lookup(c, block);
|
||||
if (!DevWrite(c, block, buf)) {
|
||||
// The on-disk contents are now unknown; don't serve a stale copy.
|
||||
if (i != Nil) Drop(c, i);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (i != Nil) {
|
||||
Touch(c, i);
|
||||
} else {
|
||||
i = Acquire(c);
|
||||
if (i == Nil) return true;
|
||||
Insert(c, i, block);
|
||||
}
|
||||
memcpy(c->entries[i].data, buf, c->blockSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Contains(Cache* c, uint32_t block) {
|
||||
return block != 0 && Lookup(c, block) != Nil;
|
||||
}
|
||||
|
||||
uint32_t MaxRunBlocks(Cache* c) {
|
||||
return MaxRunSectors / c->sectorsPerBlock;
|
||||
}
|
||||
|
||||
void Prefetch(Cache* c, uint32_t first, uint32_t count) {
|
||||
if (c->staging == nullptr || first == 0) return;
|
||||
|
||||
uint32_t maxRun = MaxRunBlocks(c);
|
||||
uint32_t end = first + count;
|
||||
if (end < first) return; // wrapped
|
||||
|
||||
uint32_t b = first;
|
||||
while (b < end) {
|
||||
if (Lookup(c, b) != Nil) { b++; continue; }
|
||||
|
||||
uint32_t run = 1;
|
||||
while (run < maxRun && b + run < end && Lookup(c, b + run) == Nil) run++;
|
||||
|
||||
if (!DevRead(c, b, run, c->staging)) return;
|
||||
|
||||
for (uint32_t k = 0; k < run; k++) {
|
||||
uint32_t i = Acquire(c);
|
||||
if (i == Nil) return;
|
||||
memcpy(c->entries[i].data, c->staging + (uint64_t)k * c->blockSize,
|
||||
c->blockSize);
|
||||
Insert(c, i, b + k);
|
||||
}
|
||||
b += run;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* BlockCache.hpp
|
||||
* Per-mount LRU cache of filesystem blocks
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Fs::BlockCache {
|
||||
|
||||
// A cache over one partition of a block device, in units of the
|
||||
// filesystem's block size (512..4096 bytes, addressed in 512-byte sectors
|
||||
// from the partition start). Write-through: Write reaches the device
|
||||
// before it returns, so the cache never holds dirty data and there is
|
||||
// nothing to flush on sync or unmount.
|
||||
//
|
||||
// Not internally locked. Callers serialize access; the VFS already holds
|
||||
// vfsLock across every driver call.
|
||||
struct Cache;
|
||||
|
||||
// Create a cache for the partition. Capacity is sized from free memory at
|
||||
// the time of the call; block pages are allocated lazily as the cache
|
||||
// fills and returned on Destroy. Returns nullptr if the bookkeeping
|
||||
// cannot be allocated (callers then do uncached I/O).
|
||||
Cache* Create(int blockDevIndex, uint64_t partStartLba, uint32_t blockSize);
|
||||
void Destroy(Cache* cache);
|
||||
|
||||
// Read one block, from the cache or the device. Block 0 is never cached.
|
||||
bool Read(Cache* cache, uint32_t block, void* buf);
|
||||
|
||||
// Write one block to the device, then update the cached copy.
|
||||
bool Write(Cache* cache, uint32_t block, const void* buf);
|
||||
|
||||
bool Contains(Cache* cache, uint32_t block);
|
||||
|
||||
// Pull `count` physically contiguous blocks starting at `first` into the
|
||||
// cache, in as few device commands as possible. Blocks already cached are
|
||||
// skipped. Best effort: a failed read just leaves those blocks uncached.
|
||||
void Prefetch(Cache* cache, uint32_t first, uint32_t count);
|
||||
|
||||
// Largest run Prefetch reads in one device command.
|
||||
uint32_t MaxRunBlocks(Cache* cache);
|
||||
|
||||
};
|
||||
@@ -67,6 +67,7 @@ namespace Fs {
|
||||
int RdMkdir(void*, const char* p) { return Ramdisk::Mkdir(p); }
|
||||
int RdRename(void*, const char* o, const char* n) { return Ramdisk::Rename(o, n); }
|
||||
const char* RdGetLabel(void*) { return Ramdisk::GetLabel(); }
|
||||
const char* RdGetFsType(void*) { return "ramdisk"; }
|
||||
int RdReadDirAt(void*, const char* p, const char** o, int m, int s) { return Ramdisk::ReadDirAt(p, o, m, s); }
|
||||
|
||||
int RdStat(void*, const char* p, Vfs::StatInfo* out) {
|
||||
@@ -99,6 +100,7 @@ namespace Fs {
|
||||
.Mkdir = RdMkdir,
|
||||
.Rename = RdRename,
|
||||
.GetLabel = RdGetLabel,
|
||||
.GetFsType = RdGetFsType,
|
||||
.ReadDirAt = RdReadDirAt,
|
||||
.Stat = RdStat,
|
||||
.SetTimes = nullptr,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "Ext2.hpp"
|
||||
#include "FsProbe.hpp"
|
||||
#include "BlockCache.hpp"
|
||||
#include <Drivers/Storage/BlockDevice.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
@@ -171,6 +172,10 @@ namespace Fs::Ext2 {
|
||||
uint8_t* blockBuf;
|
||||
int blockBufPages;
|
||||
|
||||
// Write-through cache that every ReadBlock/WriteBlock goes through.
|
||||
// Null if it could not be allocated, in which case I/O is uncached.
|
||||
BlockCache::Cache* cache;
|
||||
|
||||
// Open file handles
|
||||
Ext2File files[MaxFilesPerInstance];
|
||||
|
||||
@@ -238,12 +243,14 @@ namespace Fs::Ext2 {
|
||||
|
||||
static bool ReadBlock(const Ext2Instance& inst, uint32_t blockNum, void* buf) {
|
||||
if (blockNum == 0) return false;
|
||||
if (inst.cache) return BlockCache::Read(inst.cache, blockNum, buf);
|
||||
return ReadPartSectors(inst, BlockToPartSector(inst, blockNum),
|
||||
SectorsPerBlock(inst), buf);
|
||||
}
|
||||
|
||||
static bool WriteBlock(const Ext2Instance& inst, uint32_t blockNum, const void* buf) {
|
||||
if (blockNum == 0) return false;
|
||||
if (inst.cache) return BlockCache::Write(inst.cache, blockNum, buf);
|
||||
return WritePartSectors(inst, BlockToPartSector(inst, blockNum),
|
||||
SectorsPerBlock(inst), buf);
|
||||
}
|
||||
@@ -1294,6 +1301,20 @@ namespace Fs::Ext2 {
|
||||
uint32_t physBlock = GetPhysicalBlock(self, file.inode, logicalBlock);
|
||||
if (physBlock == 0) break;
|
||||
|
||||
// On a miss, read ahead the physically contiguous run of this
|
||||
// file's following blocks in one device command. Resolving the
|
||||
// run is cheap: the indirect blocks it walks are cached too.
|
||||
if (self.cache && !BlockCache::Contains(self.cache, physBlock)) {
|
||||
uint32_t lastLogical = (fileSize - 1) / blockSize;
|
||||
uint32_t maxRun = BlockCache::MaxRunBlocks(self.cache);
|
||||
uint32_t run = 1;
|
||||
while (run < maxRun && logicalBlock + run <= lastLogical &&
|
||||
GetPhysicalBlock(self, file.inode, logicalBlock + run) == physBlock + run) {
|
||||
run++;
|
||||
}
|
||||
BlockCache::Prefetch(self.cache, physBlock, run);
|
||||
}
|
||||
|
||||
if (!ReadBlock(self, physBlock, dataBuf)) break;
|
||||
|
||||
uint32_t available = blockSize - blockOff;
|
||||
@@ -1861,6 +1882,7 @@ namespace Fs::Ext2 {
|
||||
static int DrvMkdir(void* c, const char* p) { return MkdirImpl(CtxToInst(c), p); }
|
||||
static int DrvRename(void* c, const char* o, const char* n) { return RenameImpl(CtxToInst(c), o, n); }
|
||||
static const char* DrvGetLabel(void* c) { return GetLabelImpl(CtxToInst(c)); }
|
||||
static const char* DrvGetFsType(void*) { return "ext2"; }
|
||||
static int DrvStat(void* c, const char* p, Vfs::StatInfo* o) { return StatImpl(CtxToInst(c), p, o); }
|
||||
static int DrvSetTimes(void* c, const char* p, int64_t a, int64_t m) { return SetTimesImpl(CtxToInst(c), p, a, m); }
|
||||
|
||||
@@ -1881,6 +1903,7 @@ namespace Fs::Ext2 {
|
||||
if (self->dirNames != nullptr && self->dirNamesPages > 0) {
|
||||
Memory::g_pfa->Free(self->dirNames, self->dirNamesPages);
|
||||
}
|
||||
BlockCache::Destroy(self->cache);
|
||||
|
||||
self->active = false;
|
||||
g_instanceSlots[(std::size_t)inst] = nullptr;
|
||||
@@ -1915,6 +1938,7 @@ namespace Fs::Ext2 {
|
||||
if (self->bgdt != nullptr && self->bgdtPages > 0) {
|
||||
Memory::g_pfa->Free(self->bgdt, self->bgdtPages);
|
||||
}
|
||||
BlockCache::Destroy(self->cache);
|
||||
if (self->driver != nullptr) Memory::g_heap->Free(self->driver);
|
||||
Memory::g_heap->Free(self);
|
||||
}
|
||||
@@ -2024,6 +2048,12 @@ namespace Fs::Ext2 {
|
||||
return AbortMount(idx);
|
||||
}
|
||||
|
||||
inst.cache = BlockCache::Create(blockDevIndex, startLba, blockSize);
|
||||
if (!inst.cache) {
|
||||
KernelLogStream(WARNING, "Ext2") << "No memory for a block cache; "
|
||||
"volume I/O will be uncached";
|
||||
}
|
||||
|
||||
// Load block group descriptor table
|
||||
// BGDT starts at the block after the superblock
|
||||
uint32_t bgdtStartBlock = inst.firstDataBlock + 1;
|
||||
@@ -2071,6 +2101,7 @@ namespace Fs::Ext2 {
|
||||
.Mkdir = DrvMkdir,
|
||||
.Rename = DrvRename,
|
||||
.GetLabel = DrvGetLabel,
|
||||
.GetFsType = DrvGetFsType,
|
||||
.ReadDirAt = DrvReadDirAt,
|
||||
.Stat = DrvStat,
|
||||
.SetTimes = DrvSetTimes,
|
||||
|
||||
+98
-12
@@ -301,6 +301,53 @@ namespace Fs::Fat32 {
|
||||
// Short name generation and LFN helpers for Create
|
||||
// =========================================================================
|
||||
|
||||
// =========================================================================
|
||||
// Name encoding: VFS names are UTF-8, LFN entries are UTF-16
|
||||
// =========================================================================
|
||||
|
||||
// Decode one UTF-8 character at s (NUL-terminated); stores its length.
|
||||
// A byte that does not start a well-formed sequence is taken as one
|
||||
// Latin-1 character, so legacy single-byte names still round-trip.
|
||||
static uint32_t DecodeUtf8(const char* s, int* consumed) {
|
||||
const uint8_t* u = (const uint8_t*)s;
|
||||
uint8_t b0 = u[0];
|
||||
*consumed = 1;
|
||||
if (b0 < 0x80) return b0;
|
||||
int n;
|
||||
uint32_t cp, min;
|
||||
if (b0 >= 0xC2 && b0 <= 0xDF) { n = 2; cp = b0 & 0x1F; min = 0x80; }
|
||||
else if (b0 >= 0xE0 && b0 <= 0xEF) { n = 3; cp = b0 & 0x0F; min = 0x800; }
|
||||
else if (b0 >= 0xF0 && b0 <= 0xF4) { n = 4; cp = b0 & 0x07; min = 0x10000; }
|
||||
else return b0;
|
||||
for (int i = 1; i < n; i++) {
|
||||
if ((u[i] & 0xC0) != 0x80) return b0; // also stops at NUL
|
||||
cp = (cp << 6) | (u[i] & 0x3F);
|
||||
}
|
||||
if (cp < min || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) return b0;
|
||||
*consumed = n;
|
||||
return cp;
|
||||
}
|
||||
|
||||
static int EncodeUtf8(uint32_t cp, char* out) {
|
||||
if (cp < 0x80) { out[0] = (char)cp; return 1; }
|
||||
if (cp < 0x800) {
|
||||
out[0] = (char)(0xC0 | (cp >> 6));
|
||||
out[1] = (char)(0x80 | (cp & 0x3F));
|
||||
return 2;
|
||||
}
|
||||
if (cp < 0x10000) {
|
||||
out[0] = (char)(0xE0 | (cp >> 12));
|
||||
out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||
out[2] = (char)(0x80 | (cp & 0x3F));
|
||||
return 3;
|
||||
}
|
||||
out[0] = (char)(0xF0 | (cp >> 18));
|
||||
out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
|
||||
out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||
out[3] = (char)(0x80 | (cp & 0x3F));
|
||||
return 4;
|
||||
}
|
||||
|
||||
static void GenerateShortName(const char* longName, char* shortName) {
|
||||
memset(shortName, ' ', 11);
|
||||
|
||||
@@ -315,9 +362,15 @@ namespace Fs::Fat32 {
|
||||
// Base name (up to 8 chars, uppercase, skip invalid chars)
|
||||
int baseEnd = (lastDot >= 0) ? lastDot : nameLen;
|
||||
int j = 0;
|
||||
// A non-ASCII character becomes one '_' (8.3 names are OEM codepage;
|
||||
// the real name lives in the LFN entries).
|
||||
for (int i = 0; i < baseEnd && j < 8; i++) {
|
||||
char c = longName[i];
|
||||
if (c == ' ' || c == '.') continue;
|
||||
if ((uint8_t)c >= 0x80) {
|
||||
if (((uint8_t)c & 0xC0) == 0x80) continue;
|
||||
c = '_';
|
||||
}
|
||||
if (c >= 'a' && c <= 'z') c -= 32;
|
||||
shortName[j++] = c;
|
||||
}
|
||||
@@ -327,6 +380,10 @@ namespace Fs::Fat32 {
|
||||
j = 0;
|
||||
for (int i = lastDot + 1; longName[i] && j < 3; i++) {
|
||||
char c = longName[i];
|
||||
if ((uint8_t)c >= 0x80) {
|
||||
if (((uint8_t)c & 0xC0) == 0x80) continue;
|
||||
c = '_';
|
||||
}
|
||||
if (c >= 'a' && c <= 'z') c -= 32;
|
||||
shortName[8 + j++] = c;
|
||||
}
|
||||
@@ -403,12 +460,22 @@ namespace Fs::Fat32 {
|
||||
// Returns the number of LFN entries.
|
||||
static int BuildLfnEntries(const char* longName, uint8_t checksum,
|
||||
uint8_t* outEntries) {
|
||||
// Convert to UTF-16
|
||||
uint16_t utf16[MaxNameLen];
|
||||
// Convert UTF-8 to UTF-16 (LFN holds at most 255 code units)
|
||||
uint16_t utf16[MaxNameLen + 13];
|
||||
int len = 0;
|
||||
while (longName[len] && len < MaxNameLen - 1) {
|
||||
utf16[len] = (uint8_t)longName[len];
|
||||
len++;
|
||||
for (const char* p = longName; *p;) {
|
||||
int n;
|
||||
uint32_t cp = DecodeUtf8(p, &n);
|
||||
int units = cp >= 0x10000 ? 2 : 1;
|
||||
if (len + units > 255) break;
|
||||
if (units == 2) {
|
||||
cp -= 0x10000;
|
||||
utf16[len++] = (uint16_t)(0xD800 | (cp >> 10));
|
||||
utf16[len++] = (uint16_t)(0xDC00 | (cp & 0x3FF));
|
||||
} else {
|
||||
utf16[len++] = (uint16_t)cp;
|
||||
}
|
||||
p += n;
|
||||
}
|
||||
utf16[len] = 0;
|
||||
|
||||
@@ -464,9 +531,11 @@ namespace Fs::Fat32 {
|
||||
// Compare case-insensitively
|
||||
if (!StrEqualNoCase(longName, reconstructed)) return true;
|
||||
|
||||
// Check if long name has lowercase (short names are uppercase)
|
||||
// Check if long name has lowercase (short names are uppercase) or
|
||||
// characters the short name could not hold
|
||||
for (int i = 0; longName[i]; i++) {
|
||||
if (longName[i] >= 'a' && longName[i] <= 'z') return true;
|
||||
if ((uint8_t)longName[i] >= 0x80) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -619,10 +688,24 @@ namespace Fs::Fat32 {
|
||||
for (int i = 0; i < 2; i++) memcpy(&out[11 + i], entry + 28 + i * 2, 2);
|
||||
}
|
||||
|
||||
static void Utf16ToAscii(const uint16_t* src, int maxLen, char* dst) {
|
||||
// LFN UTF-16 -> UTF-8 name. Unpaired surrogates become U+FFFD; the name is
|
||||
// cut on a character boundary if it does not fit MaxNameLen bytes.
|
||||
static void Utf16ToUtf8(const uint16_t* src, int maxLen, char* dst,
|
||||
int dstCap = MaxNameLen) {
|
||||
int j = 0;
|
||||
for (int i = 0; i < maxLen && src[i] != 0 && j < MaxNameLen - 1; i++) {
|
||||
dst[j++] = (src[i] < 128) ? (char)src[i] : '?';
|
||||
for (int i = 0; i < maxLen && src[i] != 0 && src[i] != 0xFFFF; i++) {
|
||||
uint32_t cp = src[i];
|
||||
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < maxLen &&
|
||||
src[i + 1] >= 0xDC00 && src[i + 1] <= 0xDFFF) {
|
||||
cp = 0x10000 + ((cp - 0xD800) << 10) + (src[i + 1] - 0xDC00);
|
||||
i++;
|
||||
} else if (cp >= 0xD800 && cp <= 0xDFFF) {
|
||||
cp = 0xFFFD;
|
||||
}
|
||||
char enc[4];
|
||||
int n = EncodeUtf8(cp, enc);
|
||||
if (j + n > dstCap - 1) break;
|
||||
for (int k = 0; k < n; k++) dst[j++] = enc[k];
|
||||
}
|
||||
dst[j] = '\0';
|
||||
}
|
||||
@@ -685,7 +768,7 @@ namespace Fs::Fat32 {
|
||||
pe.attributes = attr;
|
||||
|
||||
if (hasLfn) {
|
||||
Utf16ToAscii(lfnBuf, MaxNameLen, pe.name);
|
||||
Utf16ToUtf8(lfnBuf, MaxNameLen, pe.name);
|
||||
} else {
|
||||
ParseShortName(e, pe.name);
|
||||
}
|
||||
@@ -752,7 +835,8 @@ namespace Fs::Fat32 {
|
||||
if (attr & ATTR_VOLUME_ID) { hasLfn = false; continue; }
|
||||
|
||||
if (hasLfn) {
|
||||
Utf16ToAscii(lfnBuf, MaxNameLen, outNames[count]);
|
||||
// Leave room for the '/' appended to directory names
|
||||
Utf16ToUtf8(lfnBuf, MaxNameLen, outNames[count], MaxNameLen - 1);
|
||||
} else {
|
||||
ParseShortName(e, outNames[count]);
|
||||
}
|
||||
@@ -848,7 +932,7 @@ namespace Fs::Fat32 {
|
||||
|
||||
char entryName[MaxNameLen];
|
||||
if (hasLfn) {
|
||||
Utf16ToAscii(lfnBuf, MaxNameLen, entryName);
|
||||
Utf16ToUtf8(lfnBuf, MaxNameLen, entryName);
|
||||
} else {
|
||||
ParseShortName(e, entryName);
|
||||
}
|
||||
@@ -1894,6 +1978,7 @@ namespace Fs::Fat32 {
|
||||
static int DrvMkdir(void* c, const char* p) { return MkdirImpl(CtxToInst(c), p); }
|
||||
static int DrvRename(void* c, const char* o, const char* n) { return RenameImpl(CtxToInst(c), o, n); }
|
||||
static const char* DrvGetLabel(void* c) { return GetLabelImpl(CtxToInst(c)); }
|
||||
static const char* DrvGetFsType(void*) { return "FAT32"; }
|
||||
static int DrvStat(void* c, const char* p, Vfs::StatInfo* o) { return StatImpl(CtxToInst(c), p, o); }
|
||||
|
||||
// Release everything the mount owns. The driver is either not registered
|
||||
@@ -2101,6 +2186,7 @@ namespace Fs::Fat32 {
|
||||
.Mkdir = DrvMkdir,
|
||||
.Rename = DrvRename,
|
||||
.GetLabel = DrvGetLabel,
|
||||
.GetFsType = DrvGetFsType,
|
||||
.ReadDirAt = DrvReadDirAt,
|
||||
.Stat = DrvStat,
|
||||
.SetTimes = nullptr,
|
||||
|
||||
@@ -154,18 +154,22 @@ namespace Fs::FsProbe {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int GetBlockDeviceForDrive(int driveNum) {
|
||||
int GetPartitionForDrive(int driveNum) {
|
||||
if (driveNum < 0) return -1;
|
||||
int partCount = Drivers::Storage::Gpt::GetPartitionCount();
|
||||
for (int i = 0; i < partCount && i < Drivers::Storage::Gpt::MaxPartitions; i++) {
|
||||
if (g_mounted[i] && g_driveForPart[i] == driveNum) {
|
||||
auto* part = Drivers::Storage::Gpt::GetPartition(i);
|
||||
if (part) return part->BlockDevIndex;
|
||||
}
|
||||
if (g_mounted[i] && g_driveForPart[i] == driveNum) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int GetBlockDeviceForDrive(int driveNum) {
|
||||
int partIndex = GetPartitionForDrive(driveNum);
|
||||
if (partIndex < 0) return -1;
|
||||
auto* part = Drivers::Storage::Gpt::GetPartition(partIndex);
|
||||
return part ? part->BlockDevIndex : -1;
|
||||
}
|
||||
|
||||
int UnmountPartitionsForBlockDevice(int blockDevIndex) {
|
||||
int partCount = Drivers::Storage::Gpt::GetPartitionCount();
|
||||
int unmounted = 0;
|
||||
|
||||
@@ -38,6 +38,10 @@ namespace Fs::FsProbe {
|
||||
// drive isn't backed by a partition (e.g. ramdisk).
|
||||
int GetBlockDeviceForDrive(int driveNum);
|
||||
|
||||
// Return the global GPT partition index backing a mounted VFS drive, or -1
|
||||
// if the drive isn't backed by a partition (e.g. ramdisk).
|
||||
int GetPartitionForDrive(int driveNum);
|
||||
|
||||
// Prepare persistent storage for power-off: flush every block device's
|
||||
// write cache to media, then cleanly unmount all disk-backed VFS drives.
|
||||
// The ramdisk (which has no backing block device) is left mounted. Returns
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
* ProtectedPaths.cpp
|
||||
* Capability required to modify paths on the booted system volume
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*
|
||||
* Split out of Ipc.cpp: this is filesystem security policy, not IPC. It
|
||||
* lived there only because OpenFileHandleForSlot was its first caller.
|
||||
*/
|
||||
|
||||
#include "ProtectedPaths.hpp"
|
||||
@@ -15,22 +12,9 @@
|
||||
namespace Fs {
|
||||
|
||||
// ==== Protected system paths ====
|
||||
// The capability required to modify (create, write, delete or rename) a
|
||||
// path. The kernel only ever enumerates paths here; it never parses a
|
||||
// policy file. Userspace grant policy lives in 0:/config/capabilities.toml
|
||||
// and can only ever narrow what the kernel already delegated, so no input
|
||||
// to that file can produce authority this table does not already allow.
|
||||
//
|
||||
// Rules apply ONLY to the system volume. Drive 0 is always the boot
|
||||
// ramdisk (Fs/Boot.cpp registers it unconditionally); every other drive is
|
||||
// a partition discovered at probe time, in probe order. A user data disk
|
||||
// that happens to contain an "apps" or "config" directory must not inherit
|
||||
// system protection, and an installed system's files on another volume are
|
||||
// inert data until that disk is booted -- at which point its contents are
|
||||
// themselves the drive-0 ramdisk.
|
||||
// Anything the kernel itself reads belongs here:
|
||||
// guarding only the syscall leaves the file as an unguarded second path to
|
||||
// the same state (bluetooth.toml feeds the BD_ADDR override at bring-up).
|
||||
|
||||
// Rules apply ONLY to the system volume (drive 0).
|
||||
|
||||
struct ProtectedPath {
|
||||
const char* pattern; // drive-relative, leading '/'
|
||||
bool prefix; // also match everything beneath the pattern
|
||||
@@ -38,29 +22,20 @@ namespace Fs {
|
||||
};
|
||||
|
||||
static constexpr ProtectedPath g_protectedPaths[] = {
|
||||
// Authentication, first-boot administrator creation, trusted service
|
||||
// activation and capability grants. Readable by anyone; writable only
|
||||
// with administrative authority. The bare directory is listed so it
|
||||
// cannot be renamed or deleted out from under the files inside it.
|
||||
{"/config", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/config", false, montauk::abi::CAP_USER_ADMIN}, /* listing the directory here prevents it being renamed or deleted */
|
||||
{"/config/users.toml", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/config/setup.toml", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/config/init.toml", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/config/ssh.toml", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/config/capabilities.toml",false, montauk::abi::CAP_USER_ADMIN},
|
||||
// Pre-scaled wallpaper the login screen blits before it has decoded
|
||||
// anything. It is drawn on a screen that is about to take a password,
|
||||
// so it must not be plantable by an unprivileged process.
|
||||
{"/config/wallpaper.cache", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/config/consent.toml", false, montauk::abi::CAP_USER_ADMIN},
|
||||
// Read by the Bluetooth driver at controller bring-up.
|
||||
{"/config/bluetooth.toml", false, montauk::abi::CAP_DEVICE_ADMIN},
|
||||
// Program images. Capability grants are keyed on binary path, so a
|
||||
// writable image would let an unprivileged process substitute a binary
|
||||
// and inherit the grant the next time a privileged launcher runs it.
|
||||
// This is CAP_SYSTEM_IMAGE and not CAP_STORAGE_ADMIN precisely because
|
||||
// it is the trusted computing base: partitioning and formatting a data
|
||||
// volume is an ordinary administrative act, while replacing the image
|
||||
// of login.elf is a route to every capability the system can issue.
|
||||
// Login's wallpaper cache. The directory is listed for the same reason
|
||||
// as /config: a rename only checks its own path, so an unlisted parent
|
||||
// could be swapped for one holding a forged cache.
|
||||
{"/state/cache", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/state/cache/wallpaper.cache", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/apps", true, montauk::abi::CAP_SYSTEM_IMAGE},
|
||||
{"/os", true, montauk::abi::CAP_SYSTEM_IMAGE},
|
||||
};
|
||||
@@ -70,7 +45,7 @@ namespace Fs {
|
||||
}
|
||||
|
||||
// The volume the running system was booted from.
|
||||
static constexpr uint64_t SystemDrive = 0;
|
||||
static constexpr uint64_t SystemDrive = 0; // pinned to drive 0. Must be changed in MontaukOS ever allows non-zero system drives.
|
||||
|
||||
// Strip the "<digits>:" prefix, but only for the system volume. Returns
|
||||
// nullptr for any other drive, meaning no rule applies to it.
|
||||
@@ -117,6 +92,10 @@ namespace Fs {
|
||||
return required;
|
||||
}
|
||||
|
||||
bool IsSystemImage(const char* path) {
|
||||
return (RequiredFileWriteCapability(path) & montauk::abi::CAP_SYSTEM_IMAGE) != 0;
|
||||
}
|
||||
|
||||
void LogProtectedPaths() {
|
||||
for (const auto& rule : g_protectedPaths) {
|
||||
Kt::KernelLogStream(Kt::INFO, "IPC") << "Protected path "
|
||||
|
||||
@@ -13,6 +13,11 @@ namespace Fs {
|
||||
// re-timestamp `path`. Returns 0 when the path is unprotected.
|
||||
uint64_t RequiredFileWriteCapability(const char* path);
|
||||
|
||||
// True for an installed program image: one on the system volume that
|
||||
// cannot be replaced without CAP_SYSTEM_IMAGE (0:/os, 0:/apps). Only such
|
||||
// an image is identified by its path well enough to carry a grant.
|
||||
bool IsSystemImage(const char* path);
|
||||
|
||||
// Log the rule table at boot, so a path that should be protected and is
|
||||
// not is visible rather than silently missing.
|
||||
void LogProtectedPaths();
|
||||
|
||||
@@ -89,6 +89,20 @@ namespace Fs::Ramdisk {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Mkdir/Create must not conjure entries under a missing parent: the
|
||||
// table is flat, so "a/b/" with no "a/" entry is invisible to ReadDir of
|
||||
// the parent and makes "a" fail Stat, Delete and recursive removal.
|
||||
static bool ParentExists(const char* path) {
|
||||
char normalized[MaxNameLen];
|
||||
int len = NormalizePath(path, normalized, MaxNameLen);
|
||||
if (len < 0) return false;
|
||||
while (len > 0 && normalized[len - 1] != '/') len--;
|
||||
if (len <= 1) return true; // parent is the root
|
||||
normalized[len - 1] = '\0';
|
||||
int idx = FindEntryByPath(normalized);
|
||||
return idx >= 0 && fileTable[idx].isDirectory;
|
||||
}
|
||||
|
||||
void Initialize(void* moduleData, uint64_t moduleSize) {
|
||||
Kt::KernelLogStream(Kt::OK, "Ramdisk") << "Parsing USTAR archive (" << moduleSize << " bytes)";
|
||||
|
||||
@@ -442,6 +456,8 @@ namespace Fs::Ramdisk {
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentExists(path)) return -1;
|
||||
|
||||
// Create new file entry
|
||||
FileEntry& entry = fileTable[fileCount];
|
||||
|
||||
@@ -531,6 +547,8 @@ namespace Fs::Ramdisk {
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentExists(path)) return -1;
|
||||
|
||||
// Create directory entry (stored with trailing slash for tar convention)
|
||||
FileEntry& entry = fileTable[fileCount];
|
||||
|
||||
|
||||
+18
-10
@@ -350,10 +350,10 @@ namespace Fs::Vfs {
|
||||
vfsLock.Release();
|
||||
return count;
|
||||
}
|
||||
|
||||
int VfsDriveLabel(int driveNumber, char* outLabel, int maxLen) {
|
||||
if (outLabel == nullptr || maxLen <= 0) return -1;
|
||||
outLabel[0] = '\0';
|
||||
static int CopyDriveString(int driveNumber, const char* (*FsDriver::*hook)(void*),
|
||||
char* out, int maxLen) {
|
||||
if (out == nullptr || maxLen <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
|
||||
vfsLock.Acquire();
|
||||
FsDriver* driver = DriverForLocked(driveNumber);
|
||||
@@ -362,28 +362,36 @@ namespace Fs::Vfs {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (driver->GetLabel == nullptr) {
|
||||
if (driver->*hook == nullptr) {
|
||||
vfsLock.Release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* label = driver->GetLabel(driver->ctx);
|
||||
if (label == nullptr || label[0] == '\0') {
|
||||
const char* value = (driver->*hook)(driver->ctx);
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
vfsLock.Release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int copied = 0;
|
||||
while (label[copied] != '\0' && copied < maxLen - 1) {
|
||||
outLabel[copied] = label[copied];
|
||||
while (value[copied] != '\0' && copied < maxLen - 1) {
|
||||
out[copied] = value[copied];
|
||||
copied++;
|
||||
}
|
||||
outLabel[copied] = '\0';
|
||||
out[copied] = '\0';
|
||||
|
||||
vfsLock.Release();
|
||||
return copied;
|
||||
}
|
||||
|
||||
int VfsDriveLabel(int driveNumber, char* outLabel, int maxLen) {
|
||||
return CopyDriveString(driveNumber, &FsDriver::GetLabel, outLabel, maxLen);
|
||||
}
|
||||
|
||||
int VfsDriveFsType(int driveNumber, char* outType, int maxLen) {
|
||||
return CopyDriveString(driveNumber, &FsDriver::GetFsType, outType, maxLen);
|
||||
}
|
||||
|
||||
int VfsRename(const char* oldPath, const char* newPath) {
|
||||
int oldDrive, newDrive;
|
||||
const char* oldLocal;
|
||||
|
||||
@@ -59,6 +59,9 @@ namespace Fs::Vfs {
|
||||
int (*Mkdir)(void* ctx, const char* path);
|
||||
int (*Rename)(void* ctx, const char* oldPath, const char* newPath);
|
||||
const char* (*GetLabel)(void* ctx);
|
||||
// Optional: short name of the on-disk format ("ext2", "FAT32", ...).
|
||||
// Drivers that leave this null report an empty type string.
|
||||
const char* (*GetFsType)(void* ctx);
|
||||
// Optional: paginated directory read returning entries [startIndex, startIndex+maxEntries).
|
||||
// Drivers that leave this null are read via ReadDir at startIndex 0 only.
|
||||
int (*ReadDirAt)(void* ctx, const char* path, const char** outNames, int maxEntries, int startIndex);
|
||||
@@ -103,5 +106,6 @@ namespace Fs::Vfs {
|
||||
// Returns number of registered drives, fills outDrives[] with their indices
|
||||
int VfsDriveList(int* outDrives, int maxEntries);
|
||||
int VfsDriveLabel(int driveNumber, char* outLabel, int maxLen);
|
||||
int VfsDriveFsType(int driveNumber, char* outType, int maxLen);
|
||||
|
||||
}
|
||||
+15
-7
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <Memory/Heap.hpp>
|
||||
#include <Common/Panic.hpp>
|
||||
#include <Common/TerminationCodes.hpp>
|
||||
#include <Platform/Registers.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
@@ -170,14 +171,21 @@ namespace Hal {
|
||||
CrashReport::AddReport(rep);
|
||||
Sched::SpawnCrashPad(proc->pid);
|
||||
|
||||
// Record a killed-by-signal exit code (256+sig) so a parent
|
||||
// blocked in SYS_WAITPID can tell a crash from a clean exit.
|
||||
// Record a termination code (256+sig) so a parent blocked in
|
||||
// SYS_WAITPID can tell a crash from a clean exit.
|
||||
{
|
||||
int sig = 11; /* SIGSEGV */
|
||||
if (i == 0x00) sig = 8; /* #DE -> SIGFPE */
|
||||
else if (i == 0x06) sig = 4; /* #UD -> SIGILL */
|
||||
else if (i == 0x10 || i == 0x13) sig = 8; /* x87/SIMD FP */
|
||||
Sched::SetProcessExitCode(proc->pid, 256 + sig);
|
||||
int code = TerminationFromVector(i);
|
||||
|
||||
// ExitProcess from a sibling thread ends only that thread,
|
||||
// which would leave the process running with the crash
|
||||
// swallowed. Flag the whole process (main thread and every
|
||||
// sibling) first, carrying the crash code rather than
|
||||
// KillProcess's default TerminatedKilled, so waiters see the fault.
|
||||
if (Sched::GetCurrentThreadPtr() != proc) {
|
||||
Sched::KillProcess(proc->pid, code);
|
||||
} else {
|
||||
Sched::SetProcessExitCode(proc->pid, code);
|
||||
}
|
||||
}
|
||||
|
||||
Sched::ExitProcess();
|
||||
|
||||
@@ -1153,7 +1153,7 @@ namespace Ipc {
|
||||
uint64_t writeCapability = Fs::RequiredFileWriteCapability(path);
|
||||
if (create && writeCapability != 0 &&
|
||||
!Sched::HasCapability(writeCapability)) {
|
||||
return montauk::abi::SYS_ERR_PERMISSION;
|
||||
return SystemIntegrityError;
|
||||
}
|
||||
|
||||
Fs::Vfs::BackendFile backend = {-1, -1, 0};
|
||||
@@ -1207,15 +1207,16 @@ namespace Ipc {
|
||||
|
||||
int FileWriteHandle(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) {
|
||||
if (buffer == nullptr) return -1;
|
||||
if (size > 0 && !montauk::abi::UserMemory::Range((uint64_t)buffer, size, false)) return -1;
|
||||
if (size > 0 && !montauk::abi::UserMemory::Range((uint64_t)buffer, size, false)) return BadPointer;
|
||||
|
||||
HandleSnapshot snapshot;
|
||||
if (!snapshot.Capture(CurrentSlot(), handle)) return -1;
|
||||
if (snapshot.type != HandleType::File || (snapshot.rights & RightWrite) == 0) return -1;
|
||||
if (!snapshot.Capture(CurrentSlot(), handle)) return InvalidArgument;
|
||||
if (snapshot.type != HandleType::File) return InvalidArgument;
|
||||
if ((snapshot.rights & RightWrite) == 0) return PermissionDenied;
|
||||
File* file = (File*)snapshot.object;
|
||||
if (file->writeCapability != 0 &&
|
||||
!Sched::HasCapability(file->writeCapability)) {
|
||||
return montauk::abi::SYS_ERR_PERMISSION;
|
||||
return SystemIntegrityError;
|
||||
}
|
||||
return Fs::Vfs::WriteBackendFile(file->backend, buffer, offset, size);
|
||||
}
|
||||
|
||||
@@ -213,6 +213,10 @@ namespace Memory::VMM {
|
||||
PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&PML1->entries[virtualAddressObj.GetPageIndex()]);
|
||||
|
||||
SetLeafPte(pageEntry, physicalAddress, false, true, true);
|
||||
|
||||
// The page may have been mapped before with other cache attributes
|
||||
// (an HHDM page remapped uncached): drop any stale translation.
|
||||
asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory");
|
||||
}
|
||||
|
||||
void Paging::MapUser(std::uint64_t physicalAddress, std::uint64_t virtualAddress) {
|
||||
|
||||
+93
-1
@@ -11,6 +11,7 @@
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
@@ -246,6 +247,54 @@ namespace Pci {
|
||||
return LegacyRead8(bus, device, function, (uint8_t)offset);
|
||||
}
|
||||
|
||||
static bool EcamCovers(uint8_t bus) {
|
||||
return g_useEcam && bus >= g_ecamStartBus && bus <= g_ecamEndBus;
|
||||
}
|
||||
|
||||
uint32_t ConfigRead(uint8_t bus, uint8_t device, uint8_t function, uint16_t offset, uint8_t width) {
|
||||
if (device > 31 || function > 7 || offset > 0xFFF || (offset & (width - 1)))
|
||||
return 0xFFFFFFFF;
|
||||
|
||||
if (EcamCovers(bus)) {
|
||||
switch (width) {
|
||||
case 1: return EcamRead8(g_ecamBase, bus, device, function, offset);
|
||||
case 2: return EcamRead16(g_ecamBase, bus, device, function, offset);
|
||||
case 4: return EcamRead32(g_ecamBase, bus, device, function, offset);
|
||||
default: return 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset > 0xFF) return 0xFFFFFFFF;
|
||||
switch (width) {
|
||||
case 1: return LegacyRead8(bus, device, function, (uint8_t)offset);
|
||||
case 2: return LegacyRead16(bus, device, function, (uint8_t)offset);
|
||||
case 4: return LegacyRead32(bus, device, function, (uint8_t)offset);
|
||||
default: return 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigWrite(uint8_t bus, uint8_t device, uint8_t function, uint16_t offset, uint8_t width, uint32_t value) {
|
||||
if (device > 31 || function > 7 || offset > 0xFFF || (offset & (width - 1)))
|
||||
return false;
|
||||
|
||||
if (EcamCovers(bus)) {
|
||||
switch (width) {
|
||||
case 1: EcamWrite8(g_ecamBase, bus, device, function, offset, (uint8_t)value); return true;
|
||||
case 2: EcamWrite16(g_ecamBase, bus, device, function, offset, (uint16_t)value); return true;
|
||||
case 4: EcamWrite32(g_ecamBase, bus, device, function, offset, value); return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset > 0xFF) return false;
|
||||
switch (width) {
|
||||
case 1: LegacyWrite8(bus, device, function, (uint8_t)offset, (uint8_t)value); return true;
|
||||
case 2: LegacyWrite16(bus, device, function, (uint8_t)offset, (uint16_t)value); return true;
|
||||
case 4: LegacyWrite32(bus, device, function, (uint8_t)offset, value); return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PCI capability list traversal
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -493,6 +542,46 @@ namespace Pci {
|
||||
LegacyWrite16(bus, device, function, (uint8_t)PCI_REG_COMMAND, cmd);
|
||||
}
|
||||
|
||||
bool IsBridge(const PciDevice& dev) {
|
||||
return (dev.HeaderType & 0x7F) == 0x01;
|
||||
}
|
||||
|
||||
// PMCSR lives at PM capability + 4; its low two bits are the D-state.
|
||||
static constexpr uint16_t PmcsrOffset = 4;
|
||||
|
||||
int GetPowerState(uint8_t bus, uint8_t device, uint8_t function) {
|
||||
if (ReadConfig16(bus, device, function, RegVendorId) == 0xFFFF) return -1;
|
||||
uint8_t pm = FindCapability(bus, device, function, PCI_CAP_PM);
|
||||
if (pm == 0) return -1;
|
||||
return (int)(ReadConfig16(bus, device, function, pm + PmcsrOffset) & 0x3);
|
||||
}
|
||||
|
||||
bool SetPowerState(uint8_t bus, uint8_t device, uint8_t function, int state) {
|
||||
if (state < 0 || state > 3) return false;
|
||||
uint8_t pm = FindCapability(bus, device, function, PCI_CAP_PM);
|
||||
if (pm == 0) return false;
|
||||
|
||||
// D1/D2 are optional; PMC bits 25/26 advertise them.
|
||||
uint32_t pmc = ConfigRead(bus, device, function, pm, 4);
|
||||
if (state == 1 && !(pmc & (1u << 25))) return false;
|
||||
if (state == 2 && !(pmc & (1u << 26))) return false;
|
||||
|
||||
uint16_t pmcsr = (uint16_t)ConfigRead(bus, device, function, pm + PmcsrOffset, 2);
|
||||
int current = pmcsr & 0x3;
|
||||
if (current == state) return true;
|
||||
|
||||
// Keep PME_Status (bit 15, write-1-to-clear) from being cleared by
|
||||
// accident; only the state field changes.
|
||||
pmcsr = (uint16_t)((pmcsr & ~0x8003u) | (uint16_t)state);
|
||||
ConfigWrite(bus, device, function, pm + PmcsrOffset, 2, pmcsr);
|
||||
|
||||
// PCI PM 1.2 table 5-6: 10 ms into or out of D3hot, 200 us for D2.
|
||||
if (state == 3 || current == 3) Timekeeping::BusyWaitMs(10);
|
||||
else if (state == 2 || current == 2) Timekeeping::BusyWaitUs(200);
|
||||
|
||||
return GetPowerState(bus, device, function) == state;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PCI driver matching
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -523,7 +612,9 @@ namespace Pci {
|
||||
|
||||
void ProbeAll(const PciDriverDesc* table, uint16_t count, ProbePhase phase) {
|
||||
for (uint64_t d = 0; d < g_devices.size(); d++) {
|
||||
const PciDevice& dev = g_devices[d];
|
||||
PciDevice& dev = g_devices[d];
|
||||
if (dev.Claimed || dev.Reserved)
|
||||
continue;
|
||||
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
if (table[i].Phase != phase)
|
||||
@@ -537,6 +628,7 @@ namespace Pci {
|
||||
<< "." << (uint64_t)dev.Function;
|
||||
|
||||
if (table[i].Probe(dev)) {
|
||||
dev.Claimed = true;
|
||||
KernelLogStream(OK, "PCI") << table[i].Name << " claimed device "
|
||||
<< base::hex << (uint64_t)dev.Bus << ":"
|
||||
<< (uint64_t)dev.Device << "." << (uint64_t)dev.Function;
|
||||
|
||||
@@ -40,6 +40,9 @@ namespace Pci {
|
||||
uint8_t ProgIf;
|
||||
uint8_t RevisionId;
|
||||
uint8_t HeaderType;
|
||||
|
||||
bool Claimed; // a driver's Probe() accepted it
|
||||
bool Reserved; // withheld from driver probing (e.g. powered down)
|
||||
};
|
||||
|
||||
// Configuration space access (ECAM / memory-mapped)
|
||||
@@ -60,8 +63,18 @@ namespace Pci {
|
||||
void LegacyWrite16(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint16_t value);
|
||||
void LegacyWrite32(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset, uint32_t value);
|
||||
|
||||
// Configuration access by whichever mechanism reaches the register:
|
||||
// ECAM when the bus is covered by MCFG (required for offsets >= 0x100),
|
||||
// the legacy CF8/CFC ports otherwise. width is 1, 2 or 4 and offset must
|
||||
// be aligned to it. Reads of an unreachable register return all ones;
|
||||
// writes to one return false.
|
||||
uint32_t ConfigRead(uint8_t bus, uint8_t device, uint8_t function, uint16_t offset, uint8_t width);
|
||||
bool ConfigWrite(uint8_t bus, uint8_t device, uint8_t function, uint16_t offset, uint8_t width, uint32_t value);
|
||||
|
||||
// PCI capability IDs
|
||||
constexpr uint8_t PCI_CAP_PM = 0x01;
|
||||
constexpr uint8_t PCI_CAP_MSI = 0x05;
|
||||
constexpr uint8_t PCI_CAP_PCIE = 0x10;
|
||||
constexpr uint8_t PCI_CAP_MSIX = 0x11;
|
||||
|
||||
// Walk the PCI capability linked list for a given device.
|
||||
@@ -97,6 +110,30 @@ namespace Pci {
|
||||
// Enable memory space access and bus mastering in PCI command register.
|
||||
void EnableBusMaster(uint8_t bus, uint8_t device, uint8_t function);
|
||||
|
||||
// Type 1 (PCI-to-PCI bridge) header registers
|
||||
constexpr uint16_t PCI_REG_PRIMARY_BUS = 0x18;
|
||||
constexpr uint16_t PCI_REG_SECONDARY_BUS = 0x19;
|
||||
constexpr uint16_t PCI_REG_SUBORDINATE_BUS = 0x1A;
|
||||
constexpr uint16_t PCI_REG_MEM_BASE = 0x20;
|
||||
constexpr uint16_t PCI_REG_PREF_BASE = 0x24;
|
||||
constexpr uint16_t PCI_REG_PREF_BASE_UPPER = 0x28;
|
||||
constexpr uint16_t PCI_REG_PREF_LIMIT_UPPER = 0x2C;
|
||||
constexpr uint16_t PCI_REG_BRIDGE_CONTROL = 0x3E;
|
||||
constexpr uint16_t PCI_BRIDGE_CTL_VGA = (1 << 3);
|
||||
|
||||
// True for a function with a type 1 (PCI-to-PCI bridge) header.
|
||||
bool IsBridge(const PciDevice& dev);
|
||||
|
||||
// The PCI power state of a function from its PM capability: 0..3 for
|
||||
// D0..D3hot, -1 when it has no PM capability or does not answer (which
|
||||
// is also what a function in D3cold looks like).
|
||||
int GetPowerState(uint8_t bus, uint8_t device, uint8_t function);
|
||||
|
||||
// Move a function to D0..D3hot through PMCSR and wait out the transition
|
||||
// recovery time the PCI PM spec requires. Returns false when the
|
||||
// function has no PM capability or did not take the state.
|
||||
bool SetPowerState(uint8_t bus, uint8_t device, uint8_t function, int state);
|
||||
|
||||
// Disable legacy INTx plus any enabled MSI/MSI-X capability. Returns true
|
||||
// when message-signalled delivery had been enabled and was turned off.
|
||||
bool DisableInterruptDelivery(uint8_t bus, uint8_t device, uint8_t function);
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Consent.cpp
|
||||
* First-use consent prompts for eligible capabilities
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Consent.hpp"
|
||||
#include <atomic>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
// A process that lacks an eligible capability can ask the user for it the
|
||||
// first time a syscall needs it.
|
||||
|
||||
namespace Sched::Consent {
|
||||
|
||||
using namespace montauk::abi;
|
||||
|
||||
namespace {
|
||||
|
||||
struct EligibleCap {
|
||||
const char* name;
|
||||
uint64_t bit;
|
||||
};
|
||||
|
||||
// Names for the kernel log.
|
||||
constexpr EligibleCap kEligible[] = {
|
||||
{"storage_admin", CAP_STORAGE_ADMIN},
|
||||
{"network_admin", CAP_NETWORK_ADMIN},
|
||||
{"set_time", CAP_SET_TIME},
|
||||
{"display_admin", CAP_DISPLAY_ADMIN},
|
||||
{"log_read", CAP_LOG_READ},
|
||||
{"brightness", CAP_BRIGHTNESS},
|
||||
};
|
||||
|
||||
constexpr uint64_t EligibleMask() {
|
||||
uint64_t mask = 0;
|
||||
for (const auto& cap : kEligible) mask |= cap.bit;
|
||||
return mask;
|
||||
}
|
||||
static_assert(EligibleMask() == CAP_CONSENT_ELIGIBLE);
|
||||
|
||||
constexpr int MaxRequests = 16;
|
||||
|
||||
enum : uint8_t { StateFree = 0, StateQueued = 1, StateDelivered = 2 };
|
||||
|
||||
// Waiters block on the Request object itself. Every path that retires
|
||||
// a request (reply, timeout, cancellation) wakes it, so no waiter is
|
||||
// ever left blocked on a slot that has been reused.
|
||||
struct Request {
|
||||
std::atomic<uint8_t> state{StateFree};
|
||||
std::atomic<uint64_t> id{0};
|
||||
int pid = -1;
|
||||
uint64_t capability = 0;
|
||||
};
|
||||
|
||||
Request g_requests[MaxRequests];
|
||||
kcp::Spinlock g_lock;
|
||||
int g_agentPid = -1;
|
||||
uint64_t g_nextId = 1;
|
||||
|
||||
struct WaitContext {
|
||||
Request* request;
|
||||
uint64_t id;
|
||||
};
|
||||
|
||||
bool StillPending(void* opaque) {
|
||||
auto* ctx = (WaitContext*)opaque;
|
||||
return ctx->request->state.load(std::memory_order_acquire) != StateFree &&
|
||||
ctx->request->id.load(std::memory_order_acquire) == ctx->id;
|
||||
}
|
||||
|
||||
const char* NameOf(uint64_t capability) {
|
||||
for (const auto& cap : kEligible) {
|
||||
if (cap.bit == capability) return cap.name;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
void RetireLocked(Request& request) {
|
||||
request.state.store(StateFree, std::memory_order_release);
|
||||
request.pid = -1;
|
||||
request.capability = 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Require(uint64_t capability) {
|
||||
if (HasCapability(capability)) return true;
|
||||
// Exactly one eligible bit.
|
||||
if ((capability & ~CAP_CONSENT_ELIGIBLE) != 0 ||
|
||||
capability == 0 || (capability & (capability - 1)) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Process* proc = GetCurrentProcessPtr();
|
||||
if (proc == nullptr) return false;
|
||||
int pid = proc->pid;
|
||||
|
||||
Request* request = nullptr;
|
||||
uint64_t id = 0;
|
||||
bool fresh = false;
|
||||
|
||||
g_lock.Acquire();
|
||||
if (g_agentPid >= 0 && ConsentSubjectEligible(pid, g_agentPid, capability)) {
|
||||
// A second thread asking for the same thing waits on the same prompt.
|
||||
for (auto& r : g_requests) {
|
||||
if (r.state.load(std::memory_order_relaxed) != StateFree &&
|
||||
r.pid == pid && r.capability == capability) {
|
||||
request = &r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (request == nullptr) {
|
||||
for (auto& r : g_requests) {
|
||||
if (r.state.load(std::memory_order_relaxed) == StateFree) {
|
||||
request = &r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (request != nullptr) {
|
||||
request->pid = pid;
|
||||
request->capability = capability;
|
||||
request->id.store(g_nextId++, std::memory_order_release);
|
||||
request->state.store(StateQueued, std::memory_order_release);
|
||||
fresh = true;
|
||||
}
|
||||
}
|
||||
if (request != nullptr) id = request->id.load(std::memory_order_relaxed);
|
||||
}
|
||||
g_lock.Release();
|
||||
|
||||
if (request == nullptr) return false;
|
||||
if (fresh) {
|
||||
Kt::KernelLogStream(Kt::INFO, "Consent") << "pid " << (uint64_t)pid
|
||||
<< " (" << proc->imagePath << ") asks for " << NameOf(capability);
|
||||
}
|
||||
|
||||
WaitContext ctx{request, id};
|
||||
BlockOnObjectIf(request, PromptTimeoutMs, StillPending, &ctx);
|
||||
|
||||
// Woken by a reply, a cancellation, or the timeout. Only the last
|
||||
// leaves the request live; retire it so the agent's late answer is
|
||||
// refused as stale, and release anyone else waiting on it.
|
||||
bool timedOut = false;
|
||||
g_lock.Acquire();
|
||||
if (request->state.load(std::memory_order_relaxed) != StateFree &&
|
||||
request->id.load(std::memory_order_relaxed) == id) {
|
||||
RetireLocked(*request);
|
||||
timedOut = true;
|
||||
}
|
||||
g_lock.Release();
|
||||
if (timedOut) {
|
||||
WakeObjectWaiters(request);
|
||||
Kt::KernelLogStream(Kt::INFO, "Consent") << "pid " << (uint64_t)pid
|
||||
<< ": no answer for " << NameOf(capability) << ", denied";
|
||||
}
|
||||
|
||||
return HasCapability(capability);
|
||||
}
|
||||
|
||||
int Register() {
|
||||
if (!HasCapability(CAP_COMPOSITOR)) return CapabilityDenied;
|
||||
int pid = GetCurrentPid();
|
||||
|
||||
g_lock.Acquire();
|
||||
// OnProcessExit clears a dead agent, so a live one is always current.
|
||||
if (g_agentPid >= 0 && g_agentPid != pid) {
|
||||
g_lock.Release();
|
||||
return Busy;
|
||||
}
|
||||
g_agentPid = pid;
|
||||
g_lock.Release();
|
||||
|
||||
Kt::KernelLogStream(Kt::INFO, "Consent") << "Agent registered: pid " << (uint64_t)pid;
|
||||
return Success;
|
||||
}
|
||||
|
||||
int Next(ConsentRequest* out) {
|
||||
int agent = GetCurrentPid();
|
||||
if (agent < 0 || agent != g_agentPid) return PermissionDenied;
|
||||
|
||||
ConsentRequest local;
|
||||
for (;;) {
|
||||
memset(&local, 0, sizeof(local));
|
||||
Request* picked = nullptr;
|
||||
bool found = false;
|
||||
bool subjectGone = false;
|
||||
|
||||
g_lock.Acquire();
|
||||
for (auto& r : g_requests) {
|
||||
if (r.state.load(std::memory_order_relaxed) != StateQueued) continue;
|
||||
if (picked == nullptr ||
|
||||
r.id.load(std::memory_order_relaxed) <
|
||||
picked->id.load(std::memory_order_relaxed)) {
|
||||
picked = &r;
|
||||
}
|
||||
}
|
||||
if (picked != nullptr) {
|
||||
local.id = picked->id.load(std::memory_order_relaxed);
|
||||
local.pid = picked->pid;
|
||||
local.capability = picked->capability;
|
||||
if (SnapshotConsentSubject(picked->pid, local)) {
|
||||
picked->state.store(StateDelivered, std::memory_order_release);
|
||||
found = true;
|
||||
} else {
|
||||
RetireLocked(*picked);
|
||||
subjectGone = true;
|
||||
}
|
||||
}
|
||||
g_lock.Release();
|
||||
|
||||
if (subjectGone) {
|
||||
WakeObjectWaiters(picked);
|
||||
continue;
|
||||
}
|
||||
if (!found) return 0;
|
||||
|
||||
// Copy out only after dropping the lock: the agent's buffer may
|
||||
// need a demand-paging fault.
|
||||
*out = local;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int Reply(uint64_t id, int decision) {
|
||||
int agent = GetCurrentPid();
|
||||
if (agent < 0 || agent != g_agentPid) return PermissionDenied;
|
||||
|
||||
Request* request = nullptr;
|
||||
int pid = -1;
|
||||
uint64_t capability = 0;
|
||||
bool granted = false;
|
||||
|
||||
g_lock.Acquire();
|
||||
for (auto& r : g_requests) {
|
||||
if (r.state.load(std::memory_order_relaxed) == StateDelivered &&
|
||||
r.id.load(std::memory_order_relaxed) == id) {
|
||||
request = &r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (request != nullptr) {
|
||||
pid = request->pid;
|
||||
capability = request->capability;
|
||||
// Apply before retiring, so a waiter that wakes sees the result.
|
||||
if (decision == CONSENT_ALLOW)
|
||||
granted = ApplyConsentGrant(pid, agent, capability);
|
||||
else
|
||||
ApplyConsentDenial(pid, capability);
|
||||
RetireLocked(*request);
|
||||
}
|
||||
g_lock.Release();
|
||||
|
||||
if (request == nullptr) return InvalidArgument;
|
||||
WakeObjectWaiters(request);
|
||||
|
||||
Kt::KernelLogStream(Kt::INFO, "Consent") << "pid " << (uint64_t)pid << " "
|
||||
<< NameOf(capability) << ": "
|
||||
<< (granted ? "allowed" : (decision == CONSENT_ALLOW ? "allow refused (no longer valid)" : "denied"));
|
||||
return Success;
|
||||
}
|
||||
|
||||
void OnProcessExit(int pid) {
|
||||
Request* retired[MaxRequests];
|
||||
int count = 0;
|
||||
|
||||
g_lock.Acquire();
|
||||
bool agentExited = pid == g_agentPid;
|
||||
if (agentExited) g_agentPid = -1;
|
||||
for (auto& r : g_requests) {
|
||||
if (r.state.load(std::memory_order_relaxed) == StateFree) continue;
|
||||
if (!agentExited && r.pid != pid) continue;
|
||||
RetireLocked(r);
|
||||
retired[count++] = &r;
|
||||
}
|
||||
g_lock.Release();
|
||||
|
||||
for (int i = 0; i < count; i++) WakeObjectWaiters(retired[i]);
|
||||
if (agentExited) {
|
||||
Kt::KernelLogStream(Kt::INFO, "Consent") << "Agent pid " << (uint64_t)pid
|
||||
<< " exited; " << (uint64_t)count << " pending request(s) denied";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Consent.hpp
|
||||
* First-use consent prompts for eligible capabilities
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Api/Syscall.hpp>
|
||||
#include "Scheduler.hpp"
|
||||
|
||||
namespace Sched::Consent {
|
||||
|
||||
// How long a thread waits for the user before the request is denied
|
||||
// (not remembered, so the app may ask again later).
|
||||
static constexpr uint64_t PromptTimeoutMs = montauk::abi::CONSENT_TIMEOUT_MS;
|
||||
|
||||
// Syscall-entry capability check. Returns true at once when the caller
|
||||
// holds `capability`. Otherwise, when it is eligible for consent, blocks
|
||||
// the calling thread until the session's agent answers or the prompt
|
||||
// times out, and returns whether the capability is now held. Only call
|
||||
// this where a thread may block: at the top of a syscall, with no locks.
|
||||
bool Require(uint64_t capability);
|
||||
|
||||
// Agent side (SYS_CONSENT_*).
|
||||
int Register();
|
||||
int Next(montauk::abi::ConsentRequest* out);
|
||||
int Reply(uint64_t id, int decision);
|
||||
|
||||
// Process teardown: cancel the exiting process's requests and, when it
|
||||
// is the agent, every request (their waiters see a denial).
|
||||
void OnProcessExit(int pid);
|
||||
|
||||
}
|
||||
@@ -536,16 +536,13 @@ namespace Sched {
|
||||
}
|
||||
|
||||
// Build the main thread's TLS block (x86-64 variant II): the block
|
||||
// lives below the thread pointer, TP = base + S where
|
||||
// S = align_up(memsz, align), template data at [base, base+filesz),
|
||||
// .tbss zeroed, and [TP] holds the ABI-required self-pointer.
|
||||
// lives below the thread pointer (see ComputeTlsLayout), template
|
||||
// data first, .tbss zeroed, and [TP] holds the ABI self-pointer.
|
||||
if (tlsPhdr != nullptr && outTls != nullptr) {
|
||||
uint64_t align = tlsPhdr->p_align < 16 ? 16 : tlsPhdr->p_align;
|
||||
uint64_t S = (tlsPhdr->p_memsz + align - 1) & ~(align - 1);
|
||||
TlsLayout layout = ComputeTlsLayout(tlsPhdr->p_memsz, tlsPhdr->p_align);
|
||||
uint64_t base = (maxVaddrEnd + 0x1FFFULL) & ~0xFFFULL; // page-aligned, 1 guard page gap
|
||||
uint64_t total = S + 16; // + minimal TCB
|
||||
uint64_t numPages = (total + 0xFFF) / 0x1000;
|
||||
uint64_t tp = base + S;
|
||||
uint64_t numPages = (layout.totalSize + 0xFFF) / 0x1000;
|
||||
uint64_t tp = base + layout.tpOffset;
|
||||
|
||||
bool ok = true;
|
||||
for (uint64_t p = 0; p < numPages && ok; p++) {
|
||||
@@ -562,7 +559,7 @@ namespace Sched {
|
||||
}
|
||||
|
||||
// Copy the .tdata template bytes that land in this page.
|
||||
if (!copyPageFromFile(physAddr, virtAddr, base,
|
||||
if (!copyPageFromFile(physAddr, virtAddr, base + layout.templateOffset,
|
||||
tlsPhdr->p_offset, tlsPhdr->p_filesz)) {
|
||||
ok = false;
|
||||
break;
|
||||
@@ -579,7 +576,7 @@ namespace Sched {
|
||||
outTls->templateVaddr = tlsPhdr->p_vaddr;
|
||||
outTls->fileSize = tlsPhdr->p_filesz;
|
||||
outTls->memSize = tlsPhdr->p_memsz;
|
||||
outTls->align = align;
|
||||
outTls->align = tlsPhdr->p_align;
|
||||
} else {
|
||||
Kt::KernelLogStream(Kt::ERROR, "ELF")
|
||||
<< "PT_TLS setup failed (out of pages?), "
|
||||
|
||||
@@ -60,9 +60,30 @@ namespace Sched {
|
||||
uint64_t templateVaddr; // source of .tdata in the loaded image
|
||||
uint64_t fileSize; // initialized bytes
|
||||
uint64_t memSize; // total bytes (incl. .tbss)
|
||||
uint64_t align;
|
||||
uint64_t align; // PT_TLS p_align, NOT rounded up
|
||||
};
|
||||
|
||||
// Geometry of one variant II TLS block, as offsets from its
|
||||
// page-aligned base. 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.
|
||||
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 16-byte 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};
|
||||
}
|
||||
|
||||
uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys, TlsInfo* outTls);
|
||||
|
||||
// Load a shared library into a fixed address range.
|
||||
|
||||
+252
-21
@@ -7,6 +7,8 @@
|
||||
#include <Memory/UserRange.hpp>
|
||||
#include "Scheduler.hpp"
|
||||
#include "ElfLoader.hpp"
|
||||
#include "Consent.hpp"
|
||||
#include <Fs/ProtectedPaths.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
@@ -299,9 +301,10 @@ namespace Sched {
|
||||
processTable[i].environment[0] = '\0';
|
||||
processTable[i].environmentLength = 1;
|
||||
processTable[i].user[0] = '\0';
|
||||
processTable[i].permittedCaps = 0;
|
||||
processTable[i].effectiveCaps = 0;
|
||||
processTable[i].delegableCaps = 0;
|
||||
processTable[i].consentCeiling = 0;
|
||||
processTable[i].consentDenied = 0;
|
||||
processTable[i].sessionId = -1;
|
||||
processTable[i].cwd[0] = '\0';
|
||||
processTable[i].runningOnCpu = -1;
|
||||
@@ -321,6 +324,8 @@ namespace Sched {
|
||||
processTable[i].inTail = 0;
|
||||
processTable[i].keyHead = 0;
|
||||
processTable[i].keyTail = 0;
|
||||
processTable[i].getcharUtf8Len = 0;
|
||||
processTable[i].getcharUtf8Pos = 0;
|
||||
processTable[i].termCols = 0;
|
||||
processTable[i].termRows = 0;
|
||||
processTable[i].ioOutHandle = -1;
|
||||
@@ -349,6 +354,20 @@ namespace Sched {
|
||||
const char* environment, uint32_t environmentLength,
|
||||
const montauk::abi::SpawnCapabilities* capabilities,
|
||||
const char* userOverride) {
|
||||
// Grants are keyed on the image path, so they are only as trustworthy
|
||||
// as the path: anything outside 0:/os and 0:/apps can be replaced by
|
||||
// any process, which would then run with the grant the next time a
|
||||
// privileged launcher starts it. Such a program starts with nothing
|
||||
// and asks through consent instead.
|
||||
montauk::abi::SpawnCapabilities clamped{0, 0};
|
||||
if (capabilities != nullptr &&
|
||||
(capabilities->effective | capabilities->delegable) != 0 &&
|
||||
!Fs::IsSystemImage(vfsPath)) {
|
||||
Kt::KernelLogStream(Kt::WARNING, "Sched") << "Not an installed program, "
|
||||
<< "starting without capabilities: " << vfsPath;
|
||||
capabilities = &clamped;
|
||||
}
|
||||
|
||||
schedLock.Acquire();
|
||||
|
||||
int slot = -1;
|
||||
@@ -495,6 +514,8 @@ namespace Sched {
|
||||
proc.joinerSlot = -1;
|
||||
proc.exitCode = 0;
|
||||
proc.joinable = false; // main thread is reaped by BSP, not joined
|
||||
proc.futexAddr = 0;
|
||||
proc.futexWoken = false;
|
||||
proc.savedRsp = (uint64_t)sp;
|
||||
proc.stackBase = (uint64_t)kernelStackBase;
|
||||
proc.entryPoint = entry;
|
||||
@@ -558,11 +579,9 @@ namespace Sched {
|
||||
// must use SYS_SPAWN_CAPS for an explicit, kernel-validated delegation.
|
||||
if (parentPrimarySlot >= 0) {
|
||||
if (capabilities != nullptr) {
|
||||
proc.permittedCaps = capabilities->permitted;
|
||||
proc.effectiveCaps = capabilities->effective;
|
||||
proc.delegableCaps = capabilities->delegable;
|
||||
} else {
|
||||
proc.permittedCaps = 0;
|
||||
proc.effectiveCaps = 0;
|
||||
proc.delegableCaps = 0;
|
||||
}
|
||||
@@ -570,10 +589,21 @@ namespace Sched {
|
||||
// The kernel-created init process is the root of the capability
|
||||
// delegation tree. No userspace pathname or PID receives this
|
||||
// treatment; it is reached only with no current parent process.
|
||||
proc.permittedCaps = montauk::abi::CAP_ALL;
|
||||
proc.effectiveCaps = montauk::abi::CAP_ALL;
|
||||
proc.delegableCaps = montauk::abi::CAP_ALL;
|
||||
}
|
||||
// Consent can later grant the child exactly the eligible authority
|
||||
// its parent could have delegated to it now.
|
||||
proc.consentCeiling = (parentPrimarySlot >= 0)
|
||||
? processTable[parentPrimarySlot].delegableCaps & montauk::abi::CAP_CONSENT_ELIGIBLE
|
||||
: 0;
|
||||
proc.consentDenied = 0;
|
||||
{
|
||||
int i = 0;
|
||||
for (; i < (int)sizeof(proc.imagePath) - 1 && vfsPath[i]; i++)
|
||||
proc.imagePath[i] = vfsPath[i];
|
||||
proc.imagePath[i] = '\0';
|
||||
}
|
||||
|
||||
// Inherit user string from parent, or default to "system" if no parent.
|
||||
// An explicit override is accepted only through the validated
|
||||
@@ -628,6 +658,8 @@ namespace Sched {
|
||||
proc.inTail = 0;
|
||||
proc.keyHead = 0;
|
||||
proc.keyTail = 0;
|
||||
proc.getcharUtf8Len = 0;
|
||||
proc.getcharUtf8Pos = 0;
|
||||
proc.termCols = 0;
|
||||
proc.termRows = 0;
|
||||
proc.ioOutHandle = -1;
|
||||
@@ -688,10 +720,8 @@ namespace Sched {
|
||||
uint64_t threadTlsPages = 0;
|
||||
Process& primary = processTable[primarySlot_];
|
||||
if (primary.tlsMemSize > 0) {
|
||||
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign;
|
||||
uint64_t S = (primary.tlsMemSize + align - 1) & ~(align - 1);
|
||||
uint64_t total = S + 16;
|
||||
uint64_t numPages = (total + 0xFFF) / 0x1000;
|
||||
TlsLayout layout = ComputeTlsLayout(primary.tlsMemSize, primary.tlsAlign);
|
||||
uint64_t numPages = (layout.totalSize + 0xFFF) / 0x1000;
|
||||
|
||||
uint64_t base = 0;
|
||||
if (!ReserveUserHeapRange(primarySlot_, numPages * 0x1000, base)) {
|
||||
@@ -725,9 +755,9 @@ namespace Sched {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t tp = base + S;
|
||||
memcpy((void*)base, (const void*)primary.tlsTemplateVaddr,
|
||||
primary.tlsFileSize);
|
||||
uint64_t tp = base + layout.tpOffset;
|
||||
memcpy((void*)(base + layout.templateOffset),
|
||||
(const void*)primary.tlsTemplateVaddr, primary.tlsFileSize);
|
||||
*(uint64_t*)tp = tp; /* ABI self-pointer */
|
||||
threadFsBase = tp;
|
||||
threadTlsBase = base;
|
||||
@@ -794,6 +824,8 @@ namespace Sched {
|
||||
thr.exitCode = 0;
|
||||
thr.joinable = true;
|
||||
thr.threadArg = arg;
|
||||
thr.futexAddr = 0;
|
||||
thr.futexWoken = false;
|
||||
thr.savedRsp = (uint64_t)sp;
|
||||
thr.stackBase = (uint64_t)kernelStackBase;
|
||||
thr.entryPoint = entry;
|
||||
@@ -809,6 +841,7 @@ namespace Sched {
|
||||
thr.outBuf = nullptr; thr.outHead = 0; thr.outTail = 0;
|
||||
thr.inBuf = nullptr; thr.inHead = 0; thr.inTail = 0;
|
||||
thr.keyHead = 0; thr.keyTail = 0;
|
||||
thr.getcharUtf8Len = 0; thr.getcharUtf8Pos = 0;
|
||||
thr.termCols = 0; thr.termRows = 0;
|
||||
thr.ioOutHandle = -1; thr.ioInHandle = -1;
|
||||
thr.ioKeyHandle = -1; thr.ioWaitsetHandle = -1;
|
||||
@@ -910,14 +943,13 @@ namespace Sched {
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t align = primary.tlsAlign < 16 ? 16 : primary.tlsAlign;
|
||||
uint64_t blockSize = (primary.tlsMemSize + align - 1) & ~(align - 1);
|
||||
if (blockSize > thr.fsBase || blockSize > UINT64_MAX - 16) {
|
||||
TlsLayout layout = ComputeTlsLayout(primary.tlsMemSize, primary.tlsAlign);
|
||||
if (layout.tpOffset > thr.fsBase || layout.totalSize < layout.tpOffset) {
|
||||
Panic("Invalid sibling TLS metadata during thread teardown", nullptr);
|
||||
}
|
||||
|
||||
uint64_t base = thr.fsBase - blockSize;
|
||||
uint64_t pages = (blockSize + 16 + 0xFFF) / 0x1000;
|
||||
uint64_t base = thr.fsBase - layout.tpOffset;
|
||||
uint64_t pages = (layout.totalSize + 0xFFF) / 0x1000;
|
||||
thr.fsBase = 0;
|
||||
Memory::UnmapAndFreeUserRange(primary.pml4Phys, base, pages);
|
||||
ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL);
|
||||
@@ -1638,6 +1670,10 @@ namespace Sched {
|
||||
// Clean up any windows owned by this process
|
||||
WinServer::CleanupProcess(exitingPid);
|
||||
|
||||
// Cancel this process's consent requests; if it was the consent
|
||||
// agent, deny everything still waiting on it.
|
||||
Consent::OnProcessExit(exitingPid);
|
||||
|
||||
// Release any mixer virtual streams owned by this process so the slot
|
||||
// and its ring buffer don't leak when an app forgets to audio_close.
|
||||
Drivers::Audio::Mixer::CleanupProcess(exitingPid);
|
||||
@@ -1670,6 +1706,8 @@ namespace Sched {
|
||||
proc.inTail = 0;
|
||||
proc.keyHead = 0;
|
||||
proc.keyTail = 0;
|
||||
proc.getcharUtf8Len = 0;
|
||||
proc.getcharUtf8Pos = 0;
|
||||
proc.termCols = 0;
|
||||
proc.termRows = 0;
|
||||
proc.ioOutHandle = -1;
|
||||
@@ -1750,10 +1788,13 @@ namespace Sched {
|
||||
}
|
||||
}
|
||||
|
||||
int KillProcess(int pid) {
|
||||
// Refuse to kill PID 0 (init) or caller's own process
|
||||
int KillProcess(int pid, int exitCode) {
|
||||
// Refuse to kill PID 0 (init). Killing the caller's own process is
|
||||
// allowed: a faulting/aborting sibling thread shares the pid and has
|
||||
// no other way to take the process down. The caller's own slot gets
|
||||
// killPending below and exits at syscall return (ExitIfKilled) or
|
||||
// via the ExitProcess the fault handler calls next.
|
||||
if (pid == 0) return -1;
|
||||
if (pid == GetCurrentPid()) return -1;
|
||||
|
||||
schedLock.Acquire();
|
||||
|
||||
@@ -1776,7 +1817,7 @@ namespace Sched {
|
||||
return -1;
|
||||
}
|
||||
|
||||
processTable[primarySlot_].exitCode = 256 + 9; /* killed (SIGKILL) */
|
||||
processTable[primarySlot_].exitCode = exitCode;
|
||||
|
||||
// Flag the main thread so its next tick (or scheduler dispatch)
|
||||
// routes through ExitProcess, which sweeps every sibling thread
|
||||
@@ -1844,7 +1885,7 @@ namespace Sched {
|
||||
continue;
|
||||
}
|
||||
|
||||
primary.exitCode = 256 + 9; /* killed (SIGKILL) */
|
||||
primary.exitCode = TerminatedKilled;
|
||||
primary.killPending = true;
|
||||
if (primary.state == ProcessState::Blocked) {
|
||||
primary.state = ProcessState::Ready;
|
||||
@@ -2079,6 +2120,129 @@ namespace Sched {
|
||||
}
|
||||
}
|
||||
|
||||
// Monotonic enrollment counter for FIFO futex wakeups. Only touched
|
||||
// under schedLock.
|
||||
static uint64_t futexNextSeq = 1;
|
||||
|
||||
int FutexWait(uint64_t uaddr, uint32_t expected, uint64_t timeoutNs) {
|
||||
if ((uaddr & 3) != 0) return InvalidArgument;
|
||||
|
||||
ExitIfKilled();
|
||||
|
||||
auto* cpu = Smp::GetCurrentCpuData();
|
||||
int slot = cpu ? cpu->currentSlot : -1;
|
||||
if (slot < 0) return GenericFailure;
|
||||
Process& self = processTable[slot];
|
||||
|
||||
// Round up so a short timeout never turns into "check only".
|
||||
uint64_t timeoutMs = 0;
|
||||
if (timeoutNs != FutexWaitForever) {
|
||||
if (timeoutNs == 0) {
|
||||
timeoutMs = 0;
|
||||
} else {
|
||||
timeoutMs = timeoutNs / 1000000ULL + ((timeoutNs % 1000000ULL) ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
schedLock.Acquire();
|
||||
|
||||
// The word is read here, with schedLock held and interrupts off, so
|
||||
// it must not fault: the caller validated (and demand-committed) the
|
||||
// page, and the page walk below re-confirms it is still mapped. An
|
||||
// aligned 32-bit word never straddles a page.
|
||||
if (!Memory::VMM::Paging::IsUserRangeAccessible(self.pml4Phys, uaddr, 4, false)) {
|
||||
schedLock.Release();
|
||||
return BadPointer;
|
||||
}
|
||||
uint32_t current = __atomic_load_n((volatile uint32_t*)uaddr, __ATOMIC_SEQ_CST);
|
||||
if (current != expected) {
|
||||
schedLock.Release();
|
||||
return WouldBlock;
|
||||
}
|
||||
if (timeoutNs == 0) {
|
||||
schedLock.Release();
|
||||
return TimedOut;
|
||||
}
|
||||
|
||||
self.state = ProcessState::Blocked;
|
||||
self.waitingForPid = -1;
|
||||
self.waitingOnObject = nullptr;
|
||||
self.futexAddr = uaddr;
|
||||
self.futexSeq = futexNextSeq++;
|
||||
self.futexWoken = false;
|
||||
self.sleepUntilTick = (timeoutNs == FutexWaitForever)
|
||||
? 0
|
||||
: Timekeeping::GetTicks() + timeoutMs;
|
||||
self.runningOnCpu = -1;
|
||||
SwitchAwayFromBlockedCurrentLocked();
|
||||
|
||||
// Whatever made this thread runnable (FutexWake, the deadline, or a
|
||||
// kill) it is no longer parked; clear the key before this thread can
|
||||
// block for some other reason and be mistaken for a futex waiter.
|
||||
schedLock.Acquire();
|
||||
bool woken = self.futexWoken;
|
||||
self.futexAddr = 0;
|
||||
self.futexWoken = false;
|
||||
schedLock.Release();
|
||||
|
||||
ExitIfKilled();
|
||||
return woken ? Success : TimedOut;
|
||||
}
|
||||
|
||||
int FutexWake(uint64_t uaddr, uint32_t count) {
|
||||
if ((uaddr & 3) != 0 || uaddr == 0) return InvalidArgument;
|
||||
if (count == 0) return 0;
|
||||
|
||||
auto* cpu = Smp::GetCurrentCpuData();
|
||||
int slot = cpu ? cpu->currentSlot : -1;
|
||||
if (slot < 0) return GenericFailure;
|
||||
|
||||
schedLock.Acquire();
|
||||
int primary = processTable[slot].primarySlot;
|
||||
if (primary < 0) primary = slot;
|
||||
|
||||
// Gather this process's waiters on uaddr. At most MaxProcesses.
|
||||
int waiters[MaxProcesses];
|
||||
int n = 0;
|
||||
for (int i = 0; i < MaxProcesses; i++) {
|
||||
Process& p = processTable[i];
|
||||
if (p.state != ProcessState::Blocked) continue;
|
||||
if (p.futexAddr != uaddr) continue;
|
||||
if (p.primarySlot != primary) continue;
|
||||
waiters[n++] = i;
|
||||
}
|
||||
|
||||
// Waking everyone needs no ordering; otherwise pick oldest-first.
|
||||
bool ordered = (uint32_t)n > count;
|
||||
int woken = 0;
|
||||
while (woken < n && (uint32_t)woken < count) {
|
||||
if (ordered) {
|
||||
int best = woken;
|
||||
for (int j = woken + 1; j < n; j++) {
|
||||
if (processTable[waiters[j]].futexSeq < processTable[waiters[best]].futexSeq)
|
||||
best = j;
|
||||
}
|
||||
int tmp = waiters[woken]; waiters[woken] = waiters[best]; waiters[best] = tmp;
|
||||
}
|
||||
|
||||
Process& p = processTable[waiters[woken]];
|
||||
p.futexAddr = 0;
|
||||
p.futexWoken = true;
|
||||
p.sleepUntilTick = 0;
|
||||
p.state = ProcessState::Ready;
|
||||
readyCount++;
|
||||
woken++;
|
||||
}
|
||||
schedLock.Release();
|
||||
|
||||
if (woken > 0) {
|
||||
// Re-read: this thread may have migrated since `cpu` was sampled.
|
||||
auto* here = Smp::GetCurrentCpuData();
|
||||
KickOneIdleCpu(here ? here->cpuIndex : -1);
|
||||
}
|
||||
return woken;
|
||||
}
|
||||
|
||||
bool IsAlive(int pid) {
|
||||
for (int i = 0; i < MaxProcesses; i++) {
|
||||
if (processTable[i].pid == pid) {
|
||||
@@ -2104,6 +2268,73 @@ namespace Sched {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ==== Consent support ====
|
||||
|
||||
static Process* FindLiveProcessLocked(int pid) {
|
||||
if (pid < 0) return nullptr;
|
||||
for (int i = 0; i < MaxProcesses; i++) {
|
||||
Process& p = processTable[i];
|
||||
if (p.primarySlot != i || p.pid != pid) continue;
|
||||
if (p.state == ProcessState::Ready || p.state == ProcessState::Running ||
|
||||
p.state == ProcessState::Blocked) {
|
||||
return &p;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool ConsentPairValidLocked(const Process* subject, const Process* agent,
|
||||
uint64_t capability) {
|
||||
return subject != nullptr && agent != nullptr && subject != agent &&
|
||||
agent->sessionId >= 0 && subject->sessionId == agent->sessionId &&
|
||||
(agent->effectiveCaps & montauk::abi::CAP_COMPOSITOR) != 0 &&
|
||||
(agent->delegableCaps & capability) == capability &&
|
||||
(subject->consentCeiling & capability) == capability;
|
||||
}
|
||||
|
||||
bool ConsentSubjectEligible(int pid, int agentPid, uint64_t capability) {
|
||||
schedLock.Acquire();
|
||||
Process* subject = FindLiveProcessLocked(pid);
|
||||
bool ok = ConsentPairValidLocked(subject, FindLiveProcessLocked(agentPid), capability) &&
|
||||
(subject->consentDenied & capability) == 0;
|
||||
schedLock.Release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ApplyConsentGrant(int pid, int agentPid, uint64_t capability) {
|
||||
schedLock.Acquire();
|
||||
Process* subject = FindLiveProcessLocked(pid);
|
||||
bool ok = ConsentPairValidLocked(subject, FindLiveProcessLocked(agentPid), capability);
|
||||
if (ok) {
|
||||
// Usable, never delegable: a consented app cannot hand the
|
||||
// authority on to anything it launches.
|
||||
subject->effectiveCaps |= capability;
|
||||
}
|
||||
schedLock.Release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
void ApplyConsentDenial(int pid, uint64_t capability) {
|
||||
schedLock.Acquire();
|
||||
if (Process* subject = FindLiveProcessLocked(pid))
|
||||
subject->consentDenied |= capability;
|
||||
schedLock.Release();
|
||||
}
|
||||
|
||||
bool SnapshotConsentSubject(int pid, montauk::abi::ConsentRequest& out) {
|
||||
schedLock.Acquire();
|
||||
Process* subject = FindLiveProcessLocked(pid);
|
||||
if (subject != nullptr) {
|
||||
memcpy(out.imagePath, subject->imagePath, sizeof(out.imagePath));
|
||||
out.imagePath[sizeof(out.imagePath) - 1] = '\0';
|
||||
// Only an image that cannot be rewritten without CAP_SYSTEM_IMAGE
|
||||
// is identified by its path well enough to remember an answer.
|
||||
out.imageProtected = Fs::IsSystemImage(out.imagePath);
|
||||
}
|
||||
schedLock.Release();
|
||||
return subject != nullptr;
|
||||
}
|
||||
|
||||
Process* GetProcessSlot(int slot) {
|
||||
if (slot < 0 || slot >= MaxProcesses) return nullptr;
|
||||
return &processTable[slot];
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Common/TerminationCodes.hpp>
|
||||
#include <atomic>
|
||||
#include <Api/Syscall.hpp>
|
||||
|
||||
@@ -91,12 +92,16 @@ namespace Sched {
|
||||
char environment[EnvironmentBytes]; // NUL-separated NAME=VALUE entries
|
||||
uint32_t environmentLength;
|
||||
char user[32]; // Owner user name (inherited from parent on spawn)
|
||||
uint64_t permittedCaps; // Authority owned by this process
|
||||
uint64_t effectiveCaps; // Authority currently usable by syscalls
|
||||
uint64_t delegableCaps; // Authority this process may pass to children
|
||||
int sessionId; // Process-session leader PID (inherited on spawn)
|
||||
char cwd[256]; // Absolute current working directory
|
||||
|
||||
// First-use consent (Sched/Consent.cpp). Primary slot only.
|
||||
uint64_t consentCeiling; // eligible caps the parent could have delegated at spawn
|
||||
uint64_t consentDenied; // caps refused for the rest of this process's life
|
||||
char imagePath[256]; // full resolved image path; name[] truncates
|
||||
|
||||
// Thread-local storage. fsBase is loaded into IA32_FS_BASE when
|
||||
// this thread is dispatched (0 = no TLS). The tls* template fields
|
||||
// live on the primary slot and describe PT_TLS so sibling threads
|
||||
@@ -122,6 +127,13 @@ namespace Sched {
|
||||
bool joinable = true; // Sibling thread keeps Terminated state for the joiner
|
||||
uint64_t threadArg = 0; // Argument passed into entryPoint on first dispatch
|
||||
|
||||
// Futex wait state (see FutexWait). futexAddr is the user VA this
|
||||
// thread is parked on, 0 when not in a futex wait. futexSeq orders
|
||||
// waiters so FutexWake releases the longest-waiting first.
|
||||
uint64_t futexAddr = 0;
|
||||
uint64_t futexSeq = 0;
|
||||
bool futexWoken = false;
|
||||
|
||||
// I/O redirection for GUI terminal
|
||||
bool redirected = false;
|
||||
int parentPid = -1;
|
||||
@@ -131,9 +143,13 @@ namespace Sched {
|
||||
uint8_t* inBuf = nullptr; // 4KB ring: parent writes, child reads (getchar)
|
||||
volatile uint32_t inHead = 0;
|
||||
volatile uint32_t inTail = 0;
|
||||
montauk::abi::KeyEvent keyBuf[64]; // parent injects, child reads (getkey/iskeyavailable)
|
||||
volatile uint32_t keyHead = 0;
|
||||
volatile uint32_t keyTail = 0;
|
||||
// SYS_GETCHAR returns a key's UTF-8 encoding one byte per call; the
|
||||
// bytes not handed out yet wait here.
|
||||
uint8_t getcharUtf8[4] = {};
|
||||
uint8_t getcharUtf8Len = 0;
|
||||
uint8_t getcharUtf8Pos = 0;
|
||||
static constexpr uint32_t IoBufSize = 4096;
|
||||
|
||||
// GUI terminal dimensions (set by desktop, read by SYS_TERMSIZE)
|
||||
@@ -262,10 +278,35 @@ namespace Sched {
|
||||
// Wake any processes blocked on the given object.
|
||||
void WakeObjectWaiters(void* object);
|
||||
|
||||
// ====================================================================
|
||||
// Futexes
|
||||
// ====================================================================
|
||||
|
||||
// Futexes are private to a process: the key is (primary slot, user VA),
|
||||
// so two processes waiting on the same address never see each other.
|
||||
static constexpr uint64_t FutexWaitForever = ~0ULL;
|
||||
|
||||
// Block the current thread on the 32-bit word at `uaddr` if it still
|
||||
// holds `expected`. The comparison and the enrollment happen atomically
|
||||
// under the scheduler lock, so a FutexWake issued after the waker changed
|
||||
// the word can never be lost. `timeoutNs` is rounded up to the 1 ms tick;
|
||||
// FutexWaitForever waits without a deadline, 0 only performs the check.
|
||||
// Returns Success when woken by FutexWake, WouldBlock when the word did
|
||||
// not hold `expected`, TimedOut when the deadline passed, InvalidArgument
|
||||
// for a misaligned address, BadPointer when the word is not mapped.
|
||||
// Spurious Success returns are not possible, but callers must still
|
||||
// re-check their condition: another thread may have run in between.
|
||||
int FutexWait(uint64_t uaddr, uint32_t expected, uint64_t timeoutNs);
|
||||
|
||||
// Wake up to `count` threads of the current process blocked on `uaddr`,
|
||||
// longest-waiting first. Returns the number woken.
|
||||
int FutexWake(uint64_t uaddr, uint32_t count);
|
||||
|
||||
// Kill a process by PID. If the process is running on another CPU,
|
||||
// sets a kill-pending flag checked on the next timer tick.
|
||||
// sets a kill-pending flag checked on the next timer tick. exitCode is
|
||||
// what a waiter sees (a TerminationCodes value).
|
||||
// Returns 0 on success, -1 on failure.
|
||||
int KillProcess(int pid);
|
||||
int KillProcess(int pid, int exitCode = TerminatedKilled);
|
||||
|
||||
// Start a new process session for the caller, or terminate every process
|
||||
// belonging to a session. KillSession returns the number of live members
|
||||
@@ -276,6 +317,19 @@ namespace Sched {
|
||||
// Find a process by PID (returns nullptr if not found or not alive)
|
||||
Process* GetProcessByPid(int pid);
|
||||
|
||||
// Consent support for Sched/Consent.cpp. Each validates the (subject,
|
||||
// agent) pair and reads or mutates both under the scheduler lock, so a
|
||||
// grant can never land on a slot that exited and was reused.
|
||||
//
|
||||
// The pair is valid when both are live, distinct, in the same session,
|
||||
// the agent still holds CAP_COMPOSITOR and has `capability` delegable,
|
||||
// and `capability` lies within the subject's consent ceiling.
|
||||
bool ConsentSubjectEligible(int pid, int agentPid, uint64_t capability);
|
||||
bool ApplyConsentGrant(int pid, int agentPid, uint64_t capability);
|
||||
void ApplyConsentDenial(int pid, uint64_t capability);
|
||||
// Fill imagePath/imageProtected for the request; false if pid is gone.
|
||||
bool SnapshotConsentSubject(int pid, montauk::abi::ConsentRequest& out);
|
||||
|
||||
// Spawn the crashpad to report on a faulted process (pid `crasherPid`).
|
||||
// Called by the exception handler when a process faults. If the crasher had
|
||||
// a redirected console, the crashpad inherits its out-stream so it can print
|
||||
|
||||
@@ -365,6 +365,35 @@ namespace Timekeeping {
|
||||
Hal::HaltWithInterruptsDisabled();
|
||||
}
|
||||
|
||||
void BusyWaitUs(uint64_t us) {
|
||||
// An I/O write to port 0x80 is an ISA-bus cycle of roughly a
|
||||
// microsecond on every chipset since the 486, and never shorter.
|
||||
for (uint64_t i = 0; i < us; i++)
|
||||
Io::IoPortWait();
|
||||
}
|
||||
|
||||
void BusyWaitMs(uint64_t ms) {
|
||||
if (ms == 0) return;
|
||||
|
||||
uint64_t flags;
|
||||
asm volatile("pushfq; pop %0" : "=r"(flags));
|
||||
bool ticking = g_ticksPerMs != 0 && (flags & (1 << 9));
|
||||
|
||||
// Either clock alone is long enough, so stop at whichever says so
|
||||
// first. The tick is the accurate one; the port writes bound the wait
|
||||
// when the tick is not running yet (or is masked), so this can never
|
||||
// hang on a timer that does not fire. One extra tick of slack because
|
||||
// the first may already be nearly over.
|
||||
uint64_t start = g_tickCount.load(std::memory_order_relaxed);
|
||||
uint64_t portBudget = (ms + 1) * 1000;
|
||||
for (uint64_t spins = 0; spins < portBudget; spins++) {
|
||||
if (ticking &&
|
||||
g_tickCount.load(std::memory_order_relaxed) - start >= ms + 1)
|
||||
return;
|
||||
Io::IoPortWait();
|
||||
}
|
||||
}
|
||||
|
||||
void Sleep(uint64_t ms) {
|
||||
if (ms == 0) return;
|
||||
// Use BlockForSleep to properly deschedule the process instead
|
||||
|
||||
@@ -43,4 +43,12 @@ namespace Timekeeping {
|
||||
|
||||
// Busy-wait sleep for the given number of milliseconds
|
||||
void Sleep(uint64_t ms);
|
||||
|
||||
// Spin for at least `ms` milliseconds without descheduling. Usable from
|
||||
// boot code, before the scheduler runs, and with interrupts disabled
|
||||
// (falls back to timed I/O port writes when the tick is not advancing).
|
||||
void BusyWaitMs(uint64_t ms);
|
||||
|
||||
// Spin for at least `us` microseconds. Port-0x80 timed, so it errs long.
|
||||
void BusyWaitUs(uint64_t us);
|
||||
};
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
# Timeout in seconds that Limine will use before automatically booting.
|
||||
timeout: 0
|
||||
|
||||
timeout: 3
|
||||
interface_branding: MontaukOS
|
||||
# The entry name that will be displayed in the boot menu.
|
||||
/montaukos
|
||||
/MontaukOS
|
||||
# We use the Limine boot protocol.
|
||||
protocol: limine
|
||||
|
||||
|
||||
+55
-14
@@ -49,7 +49,7 @@ BINDIR := bin
|
||||
PROGRAMS := $(notdir $(wildcard src/*))
|
||||
|
||||
# Programs with custom Makefiles (built separately).
|
||||
CUSTOM_BUILDS := 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display keyboard sshserver terminal syslog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad sshd rtlsdr sdr
|
||||
CUSTOM_BUILDS := 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver terminal syslog procmgr powermgr calculator charmap desktop login shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs libloader crashpad sshd rtlsdr sdr
|
||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||
|
||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||
@@ -58,7 +58,7 @@ TARGETS := $(addprefix $(BINDIR)/os/,$(addsuffix .elf,$(SYSTEM_PROGRAMS)))
|
||||
# Man pages source directory.
|
||||
MANDIR := man
|
||||
MANSRC := $(wildcard $(MANDIR)/*.*)
|
||||
MANDST := $(patsubst $(MANDIR)/%,$(BINDIR)/man/%,$(MANSRC))
|
||||
MANDST := $(patsubst $(MANDIR)/%,$(BINDIR)/os/man/%,$(MANSRC))
|
||||
|
||||
# Web content source directory.
|
||||
WWWDIR := www
|
||||
@@ -96,9 +96,9 @@ WPDIR := data/wallpapers
|
||||
WPSRC := $(wildcard $(WPDIR)/*.jpg)
|
||||
WPDST := $(patsubst $(WPDIR)/%,$(BINDIR)/os/wallpapers/%,$(WPSRC))
|
||||
|
||||
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls
|
||||
.PHONY: all clean 2048 fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver sshd terminal syslog procmgr powermgr calculator charmap login desktop shell paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs osdata bearssl libc tls libjpeg libjpegwrite install-apps libloader crashpad rtlsdr sdr check-syscalls gen-syscalls rust
|
||||
|
||||
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
|
||||
all: bearssl libc libjpeg libjpegwrite tls libloader rtlsdr sdr devkit $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities sshserver terminal syslog procmgr powermgr calculator charmap 2048 paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell sshd icons fonts install-apps crashpad rust $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(OSDATADST) $(FWDST) $(LICDST) $(WPDST)
|
||||
|
||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||
@@ -225,6 +225,10 @@ network: libc
|
||||
display: libc
|
||||
$(MAKE) -C src/display
|
||||
|
||||
# Build capability grant viewer (depends on libc).
|
||||
capabilities: libc
|
||||
$(MAKE) -C src/capabilities
|
||||
|
||||
# Build keyboard layout settings applet (depends on libc).
|
||||
keyboard: libc
|
||||
$(MAKE) -C src/keyboard
|
||||
@@ -281,11 +285,11 @@ shell:
|
||||
desktop: libc libjpeg bearssl libloader dialogs configs
|
||||
$(MAKE) -C src/desktop
|
||||
|
||||
# Copy SVG icons for the desktop into bin/icons/.
|
||||
# Copy SVG icons for the desktop into bin/os/icons/.
|
||||
icons:
|
||||
../scripts/copy_icons.sh
|
||||
|
||||
# Copy TTF fonts for the desktop into bin/fonts/.
|
||||
# Copy TTF fonts for the desktop into bin/os/fonts/.
|
||||
fonts:
|
||||
../scripts/copy_fonts.sh
|
||||
|
||||
@@ -316,6 +320,36 @@ mandelbrot: libc
|
||||
screenshot: libc libjpegwrite libloader dialogs
|
||||
$(MAKE) -C src/screenshot
|
||||
|
||||
# Rust programs (programs/rust): std and no_std programs for the built-in
|
||||
# x86_64-unknown-montauk target of the forked rustc (Phase 2), registered with
|
||||
# rustup as the 'montauk' toolchain (see programs/rust/README.md). Optional:
|
||||
# skipped with a notice when that toolchain is missing, so the default build
|
||||
# never requires Rust. RUSTUP_AUTO_INSTALL=0 keeps 'make' from downloading one.
|
||||
# Rust links the toolchain sysroot libc (like ports), not lib/libc directly:
|
||||
# re-run build-montauk-toolchain.sh after libc changes. Cargo tracks neither
|
||||
# that archive nor a rebuilt std in the linked toolchain, so the programs are
|
||||
# cleaned (and rebuilt) when either is newer than the last build.
|
||||
CARGO ?= $(firstword $(wildcard $(HOME)/.cargo/bin/cargo) $(shell command -v cargo 2>/dev/null))
|
||||
RUST_DIR := rust
|
||||
RUST_OUT := $(RUST_DIR)/target/x86_64-unknown-montauk/release
|
||||
RUST_PROGRAMS := rusthello ruststd
|
||||
RUST_LIBC := ../toolchain/sysroot/usr/lib/libc.a
|
||||
|
||||
rust:
|
||||
@if [ -z "$(CARGO)" ] || ! (cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 rustc --print target-list 2>/dev/null | grep -qx x86_64-unknown-montauk); then \
|
||||
echo "rust: no 'montauk' rustup toolchain - skipping Rust programs (see programs/rust/README.md)"; \
|
||||
else \
|
||||
stdlib=$$(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 rustc --print sysroot)/lib/rustlib/x86_64-unknown-montauk/lib; \
|
||||
if [ ! -f $(RUST_OUT)/.link-stamp ] || [ $(RUST_LIBC) -nt $(RUST_OUT)/.link-stamp ] || \
|
||||
[ -n "$$(find $$stdlib -name '*.rlib' -newer $(RUST_OUT)/.link-stamp 2>/dev/null | head -1)" ]; then \
|
||||
(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) clean --release --quiet $(addprefix -p ,$(RUST_PROGRAMS))) || exit 1; \
|
||||
fi; \
|
||||
(cd $(RUST_DIR) && RUSTUP_AUTO_INSTALL=0 $(CARGO) build --release) || exit 1; \
|
||||
touch $(RUST_OUT)/.link-stamp; \
|
||||
mkdir -p $(BINDIR)/os; \
|
||||
for p in $(RUST_PROGRAMS); do cp $(RUST_OUT)/$$p $(BINDIR)/os/$$p.elf || exit 1; done; \
|
||||
fi
|
||||
|
||||
# Verify the TCC C API header (libc/montauk.h) exposes every kernel syscall with
|
||||
# matching numbers. Fails the build on drift; 'make gen-syscalls' refreshes them.
|
||||
check-syscalls:
|
||||
@@ -357,12 +391,12 @@ crashpad: libc
|
||||
$(MAKE) -C src/crashpad
|
||||
|
||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||
install-apps: 2048 paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network display keyboard sshserver terminal syslog procmgr powermgr calculator charmap screenshot texteditor mandelbrot printers timezone crashpad
|
||||
install-apps: 2048 paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer audio music video bluetooth network display capabilities keyboard sshserver terminal syslog procmgr powermgr calculator charmap screenshot texteditor mandelbrot printers timezone crashpad
|
||||
../scripts/install_apps.sh
|
||||
|
||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
||||
$(BINDIR)/man/%: $(MANDIR)/%
|
||||
mkdir -p $(BINDIR)/man
|
||||
# Copy man pages into bin/os/man/ so mkramdisk.sh picks them up.
|
||||
$(BINDIR)/os/man/%: $(MANDIR)/%
|
||||
mkdir -p $(BINDIR)/os/man
|
||||
cp $< $@
|
||||
|
||||
# Copy web content into bin/www/ so mkramdisk.sh picks them up.
|
||||
@@ -400,11 +434,19 @@ $(BINDIR)/os/wallpapers/%: $(WPDIR)/%
|
||||
cp $< $@
|
||||
|
||||
# Build each system program from its source files.
|
||||
$(BINDIR)/os/%.elf: src/%/main.cpp link.ld GNUmakefile $(LIBC_LIB)
|
||||
# -MMD records the headers main.cpp pulled in, so editing a shared header
|
||||
# (montauk/capabilities.h, Api/Syscall.hpp, ...) rebuilds every program that
|
||||
# uses it. A missing depfile matches the empty rule below, which forces a
|
||||
# rebuild; that is how programs built before this tracking existed catch up.
|
||||
$(BINDIR)/os/%.elf: src/%/main.cpp link.ld GNUmakefile $(LIBC_LIB) obj/%/main.elf.d
|
||||
mkdir -p $(BINDIR)/os obj/$*
|
||||
$(CXX) $(CXXFLAGS) -c src/$*/main.cpp -o obj/$*/main.o
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -MT $@ -MF obj/$*/main.elf.d -c src/$*/main.cpp -o obj/$*/main.o
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) obj/$*/main.o $(LIBC_LIB) -o $@
|
||||
|
||||
obj/%/main.elf.d: ;
|
||||
.PRECIOUS: obj/%/main.elf.d
|
||||
-include $(wildcard obj/*/main.elf.d)
|
||||
|
||||
# ============================================================================
|
||||
# Native development kit (Montauk SDK): binutils built for MontaukOS
|
||||
# plus a target sysroot (headers, libc.a, crt objects) under 0:/sdk.
|
||||
@@ -458,7 +500,6 @@ ifneq ($(wildcard ../toolchain/native-gcc/sdk/bin/gcc),)
|
||||
$(BINDIR)/sdk/lib/gcc/x86_64-montauk/14.2.0/
|
||||
cp ../toolchain/native/sdk/bin/ld $(BINDIR)/sdk/libexec/gcc/x86_64-montauk/14.2.0/ld
|
||||
mkdir -p $(BINDIR)/tmp
|
||||
printf 'scratch space for compiler intermediates\n' > $(BINDIR)/tmp/readme.txt
|
||||
else
|
||||
@echo "======================================================================"
|
||||
@echo "devkit: WARNING: toolchain/native-gcc/ not built."
|
||||
@@ -475,7 +516,7 @@ else
|
||||
endif
|
||||
|
||||
clean:
|
||||
rm -rf $(BINDIR) obj
|
||||
rm -rf $(BINDIR) obj $(RUST_DIR)/target
|
||||
$(MAKE) -C lib/bearssl clean
|
||||
$(MAKE) -C lib/libc clean
|
||||
$(MAKE) -C lib/libjpeg clean
|
||||
|
||||
@@ -3,17 +3,8 @@
|
||||
#
|
||||
# Capability names: process_admin, power_request, power_control, suspend,
|
||||
# storage_admin, raw_storage, network_admin, set_time, user_admin,
|
||||
# display_admin, device_admin, log_read, system_image, and "all".
|
||||
#
|
||||
# "all" is every capability except system_image, which must always be named
|
||||
# explicitly: it is write access to 0:/os and 0:/apps, and since grants are
|
||||
# keyed on binary path, holding it is equivalent to holding every capability
|
||||
# the system can issue from the next launch onwards. Nothing is granted it.
|
||||
#
|
||||
# A grant is always clamped by the kernel to what the launching process may
|
||||
# actually delegate, so "all" in a launcher entry means "whatever this session
|
||||
# was given", not "root".
|
||||
#
|
||||
# display_admin, device_admin, log_read, system_image, compositor, brightness
|
||||
# and "all".
|
||||
#
|
||||
|
||||
# ==== System services (started by init) ====
|
||||
@@ -28,7 +19,6 @@ effective = ["set_time"]
|
||||
|
||||
[grant.login]
|
||||
path = "0:/os/login.elf"
|
||||
permitted = ["all"]
|
||||
effective = [
|
||||
"power_control", "storage_admin", "device_admin",
|
||||
"user_admin", "process_admin", "log_read",
|
||||
@@ -36,7 +26,8 @@ effective = [
|
||||
delegable = [
|
||||
"power_request", "suspend", "process_admin", "storage_admin",
|
||||
"raw_storage", "network_admin", "set_time", "user_admin",
|
||||
"display_admin", "device_admin", "log_read",
|
||||
"display_admin", "device_admin", "log_read", "compositor",
|
||||
"brightness",
|
||||
]
|
||||
|
||||
[grant.sshd]
|
||||
@@ -48,20 +39,15 @@ path = "0:/os/desktop.elf"
|
||||
effective = [
|
||||
"power_request", "suspend", "process_admin", "storage_admin",
|
||||
"raw_storage", "network_admin", "set_time", "user_admin",
|
||||
"display_admin", "device_admin", "log_read",
|
||||
"display_admin", "device_admin", "log_read", "compositor",
|
||||
"brightness",
|
||||
]
|
||||
delegable = [
|
||||
"power_request", "suspend", "process_admin", "storage_admin",
|
||||
"raw_storage", "network_admin", "set_time", "user_admin",
|
||||
"display_admin", "device_admin", "log_read",
|
||||
"display_admin", "device_admin", "log_read", "brightness",
|
||||
]
|
||||
|
||||
# The console session launchers. terminal.elf needs no authority of its own;
|
||||
# it exists to pass the session's authority to the shell it hosts. The shell
|
||||
# in turn exercises only power_request and suspend, but delegates on the same
|
||||
# terms as the desktop so that the tools below work from a console. Both are
|
||||
# clamped to the launching session: a standard session narrows this to
|
||||
# power_request and suspend, and an unprivileged one to nothing.
|
||||
[grant.terminal]
|
||||
path = "0:/apps/terminal/terminal.elf"
|
||||
delegable = ["all"]
|
||||
@@ -85,7 +71,7 @@ delegable = ["network_admin"]
|
||||
|
||||
[grant.display]
|
||||
path = "0:/apps/display/display.elf"
|
||||
effective = ["display_admin"]
|
||||
effective = ["display_admin", "brightness"]
|
||||
|
||||
[grant.disks]
|
||||
path = "0:/apps/disks/disks.elf"
|
||||
@@ -103,6 +89,11 @@ effective = ["set_time"]
|
||||
path = "0:/apps/bluetooth/bluetooth.elf"
|
||||
effective = ["device_admin"]
|
||||
|
||||
# Writes 0:/config/consent.toml when a stored consent decision is forgotten.
|
||||
[grant.capabilities]
|
||||
path = "0:/apps/capabilities/capabilities.elf"
|
||||
effective = ["user_admin"]
|
||||
|
||||
[grant.syslog]
|
||||
path = "0:/apps/syslog/syslog.elf"
|
||||
effective = ["log_read"]
|
||||
|
||||
@@ -15,15 +15,10 @@
|
||||
# base: character produced unshifted, one per scancode
|
||||
# shift: character produced with Shift held, one per scancode
|
||||
# altgr: character produced with AltGr held, one per scancode
|
||||
#
|
||||
# Characters are Windows-1252 byte values.
|
||||
#
|
||||
# A value of 0x00 means "no override" - the key keeps whatever the kernel's
|
||||
# US scancode table produced. This is how a layout declares only the keys it
|
||||
# actually changes.
|
||||
# dead_keys: (optional) characters that are dead keys in this layout
|
||||
|
||||
[registry]
|
||||
layouts = ["en", "no"]
|
||||
layouts = ["en", "no", "sv", "da", "cs"]
|
||||
|
||||
# =============================================================================
|
||||
# LAYOUTS
|
||||
@@ -54,6 +49,92 @@ shift = [
|
||||
]
|
||||
altgr = [
|
||||
0x40, 0xA3, 0x24, 0x00, 0x7B, 0x5B, 0x5D, 0x7D, # @ GBP $ . { [ ] }
|
||||
0x00, 0xB4, 0x80, 0x00, 0x7E, 0x00, 0x00, 0x00, # . ' EUR . ~ . . .
|
||||
0x00, 0xB4, 0x20AC, 0x00, 0x7E, 0x00, 0x00, 0x00, # . ' EUR . ~ . . .
|
||||
0x00, 0x00, 0x00, 0x00, 0x5C, # . . . . backslash
|
||||
]
|
||||
|
||||
[layouts.sv]
|
||||
name = "Swedish"
|
||||
short_name = "sv"
|
||||
scancodes = [
|
||||
0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
|
||||
0x0b, 0x0c, 0x0d, 0x12, 0x1a, 0x1b, 0x27, 0x28,
|
||||
0x29, 0x2b, 0x33, 0x34, 0x35, 0x56,
|
||||
]
|
||||
base = [
|
||||
0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, # 2 3 4 5 6 7 8 9
|
||||
0x30, 0x2B, 0xB4, 0x00, 0xE5, 0xA8, 0xF6, 0xE4, # 0 + ' . aa ".." oe ae
|
||||
0xA7, 0x27, 0x2C, 0x2E, 0x2D, 0x3C, # sec ' , . - <
|
||||
]
|
||||
shift = [
|
||||
0x22, 0x23, 0xA4, 0x25, 0x26, 0x2F, 0x28, 0x29, # " # cur % & / ( )
|
||||
0x3D, 0x3F, 0x60, 0x00, 0xC5, 0x5E, 0xD6, 0xC4, # = ? ` . AA ^ OE AE
|
||||
0xBD, 0x2A, 0x3B, 0x3A, 0x5F, 0x3E, # 1/2 * ; : _ >
|
||||
]
|
||||
altgr = [
|
||||
0x40, 0xA3, 0x24, 0x20AC, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ EUR YEN { [ ]
|
||||
0x7D, 0x5C, 0xB1, 0x20AC, 0x00, 0x7E, 0xF8, 0xE6, # } bslash +- EUR . ~ o/ ae
|
||||
0xB6, 0xB4, 0x00, 0xB7, 0x00, 0x7C, # par ' . mid . |
|
||||
]
|
||||
|
||||
[layouts.da]
|
||||
name = "Danish"
|
||||
short_name = "da"
|
||||
scancodes = [
|
||||
0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
|
||||
0x0b, 0x0c, 0x0d, 0x12, 0x1a, 0x1b, 0x27, 0x28,
|
||||
0x29, 0x2b, 0x33, 0x34, 0x35, 0x56,
|
||||
]
|
||||
base = [
|
||||
0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, # 2 3 4 5 6 7 8 9
|
||||
0x30, 0x2B, 0xB4, 0x00, 0xE5, 0xA8, 0xE6, 0xF8, # 0 + ' . aa ".." ae oe
|
||||
0xBD, 0x27, 0x2C, 0x2E, 0x2D, 0x3C, # 1/2 ' , . - <
|
||||
]
|
||||
shift = [
|
||||
0x22, 0x23, 0xA4, 0x25, 0x26, 0x2F, 0x28, 0x29, # " # cur % & / ( )
|
||||
0x3D, 0x3F, 0x60, 0x00, 0xC5, 0x5E, 0xC6, 0xD8, # = ? ` . AA ^ AE OE
|
||||
0xA7, 0x2A, 0x3B, 0x3A, 0x5F, 0x3E, # sec * ; : _ >
|
||||
]
|
||||
altgr = [
|
||||
0x40, 0xA3, 0x24, 0xBD, 0xA5, 0x7B, 0x5B, 0x5D, # @ GBP $ 1/2 YEN { [ ]
|
||||
0x7D, 0xB1, 0x7C, 0x20AC, 0x00, 0x7E, 0xB4, 0x5E, # } +- | EUR . ~ ' ^
|
||||
0xBE, 0x00, 0x00, 0xB7, 0x00, 0x5C, # 3/4 . . mid . backslash
|
||||
]
|
||||
|
||||
[layouts.cs]
|
||||
name = "Czech"
|
||||
short_name = "cs"
|
||||
# QWERTZ. The number row types accented letters (digits with Shift), and
|
||||
# AltGr on the number row gives the dead accents.
|
||||
scancodes = [
|
||||
0x29, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x10, 0x11, 0x12,
|
||||
0x15, 0x1a, 0x1b, 0x21, 0x22, 0x25, 0x26, 0x27,
|
||||
0x28, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31,
|
||||
0x33, 0x34, 0x35, 0x56,
|
||||
]
|
||||
base = [
|
||||
0x3B, 0x2B, 0x11B, 0x161, 0x10D, 0x159, 0x17E, 0xFD, # ; + e-caron s-caron c-caron r-caron z-caron y-acute
|
||||
0xE1, 0xED, 0xE9, 0x3D, 0xB4, 0x00, 0x00, 0x00, # a-acute i-acute e-acute = (dead acute) . . .
|
||||
0x7A, 0xFA, 0x29, 0x00, 0x00, 0x00, 0x00, 0x16F, # z u-acute ) . . . . u-ring
|
||||
0xA7, 0xA8, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, # section (dead diaeresis) y . . . . .
|
||||
0x2C, 0x2E, 0x2D, 0x5C, # , . - backslash
|
||||
]
|
||||
shift = [
|
||||
0xB0, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, # (dead ring) 1 2 3 4 5 6 7
|
||||
0x38, 0x39, 0x30, 0x25, 0x2C7, 0x00, 0x00, 0x00, # 8 9 0 % (dead caron) . . .
|
||||
0x5A, 0x2F, 0x28, 0x00, 0x00, 0x00, 0x00, 0x22, # Z / ( . . . . "
|
||||
0x21, 0x27, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, # ! ' Y . . . . .
|
||||
0x3F, 0x3A, 0x5F, 0x7C, # ? : _ |
|
||||
]
|
||||
altgr = [
|
||||
0x00, 0x7E, 0x2C7, 0x5E, 0x2D8, 0xB0, 0x2DB, 0x60, # . dead: tilde caron circumflex breve ring ogonek grave
|
||||
0x2D9, 0xB4, 0x2DD, 0xA8, 0xB8, 0x5C, 0x7C, 0x20AC, # dead: dot acute double-acute diaeresis cedilla; backslash | EUR
|
||||
0x00, 0xF7, 0xD7, 0x5B, 0x5D, 0x142, 0x141, 0x24, # . divide multiply [ ] l-stroke L-stroke $
|
||||
0xDF, 0xA4, 0x00, 0x23, 0x26, 0x40, 0x7B, 0x7D, # sharp-s currency . # & @ { }
|
||||
0x3C, 0x3E, 0x2A, 0x00, # < > * .
|
||||
]
|
||||
dead_keys = [
|
||||
0xB4, 0x2C7, 0xA8, 0xB0, 0x7E, 0x5E,
|
||||
0x2D8, 0x2DB, 0x60, 0x2D9, 0x2DD, 0xB8,
|
||||
]
|
||||
@@ -7,6 +7,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/statuscodes.h>
|
||||
|
||||
namespace montauk::abi {
|
||||
|
||||
@@ -259,6 +260,45 @@ namespace montauk::abi {
|
||||
/* Battery.hpp -- ACPI battery status */
|
||||
static constexpr uint64_t SYS_BATTERYINFO = 188; // (BatteryInfo*) -> 0, -1 no battery device
|
||||
|
||||
// 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
|
||||
|
||||
/* 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;
|
||||
@@ -279,8 +319,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 |
|
||||
@@ -291,28 +344,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];
|
||||
};
|
||||
|
||||
|
||||
static constexpr int USB_ERR_INVALID = -1;
|
||||
@@ -471,7 +559,10 @@ namespace montauk::abi {
|
||||
uint32_t buildNumber; // monotonic kernel build number
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -487,6 +578,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;
|
||||
@@ -524,12 +641,13 @@ namespace montauk::abi {
|
||||
uint8_t type; // 0=key, 1=mouse, 2=resize, 3=close, 4=scale
|
||||
uint8_t _pad[3];
|
||||
union {
|
||||
KeyEvent key;
|
||||
KeyEvent key; // 16 bytes: the union was already that size (mouse)
|
||||
struct { int32_t x, y, scroll; uint8_t buttons, prev_buttons; } mouse;
|
||||
struct { int32_t w, h; } resize;
|
||||
struct { int32_t scale; } scale;
|
||||
};
|
||||
};
|
||||
static_assert(sizeof(WinEvent) == 20, "WinEvent layout is shared with existing binaries");
|
||||
|
||||
struct WinInfo {
|
||||
int32_t id;
|
||||
@@ -600,6 +718,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;
|
||||
@@ -799,7 +939,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;
|
||||
};
|
||||
|
||||
@@ -165,9 +165,9 @@ struct Canvas {
|
||||
if (!str || !font_data || scale <= 0) return;
|
||||
uint32_t pixel = c.to_pixel();
|
||||
int cx = x;
|
||||
for (int i = 0; str[i]; i++, cx += FONT_WIDTH * scale) {
|
||||
for (const char* p = str; *p; cx += FONT_WIDTH * scale) {
|
||||
const uint8_t* glyph =
|
||||
&font_data[(unsigned char)str[i] * FONT_HEIGHT];
|
||||
&font_data[cp437_index(montauk::utf8::next(&p)) * FONT_HEIGHT];
|
||||
for (int row = 0; row < FONT_HEIGHT; row++) {
|
||||
uint8_t bits = glyph[row];
|
||||
if (!bits) continue;
|
||||
|
||||
@@ -9,12 +9,32 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/utf8.h>
|
||||
|
||||
namespace gui {
|
||||
|
||||
// The clipboard carries UTF-8. Text that is not well-formed (legacy
|
||||
// Windows-1252 bytes) is converted on the way in and on the way out, so
|
||||
// every reader can assume valid UTF-8.
|
||||
inline char* clipboard_utf8_copy(const char* text, int len, int* out_len) {
|
||||
int cap = len * 3 + 1; // each stray byte grows to at most 3
|
||||
char* out = (char*)montauk::malloc(cap);
|
||||
if (!out) return nullptr;
|
||||
*out_len = montauk::utf8::normalize(text, len, out, cap);
|
||||
return out;
|
||||
}
|
||||
|
||||
inline bool clipboard_set_text(const char* text, int len) {
|
||||
if (len < 0) return false;
|
||||
if (len > 0 && text == nullptr) return false;
|
||||
if (len > 0 && !montauk::utf8::valid(text, len)) {
|
||||
int fixed_len = 0;
|
||||
char* fixed = clipboard_utf8_copy(text, len, &fixed_len);
|
||||
if (!fixed) return false;
|
||||
bool ok = montauk::clipboard_set_text(fixed, (uint32_t)fixed_len) == 0;
|
||||
montauk::mfree(fixed);
|
||||
return ok;
|
||||
}
|
||||
return montauk::clipboard_set_text(text, (uint32_t)len) == 0;
|
||||
}
|
||||
|
||||
@@ -53,6 +73,14 @@ namespace gui {
|
||||
}
|
||||
|
||||
text[actual_len] = '\0';
|
||||
if (!montauk::utf8::valid(text, (int)actual_len)) {
|
||||
int fixed_len = 0;
|
||||
char* fixed = clipboard_utf8_copy(text, (int)actual_len, &fixed_len);
|
||||
montauk::mfree(text);
|
||||
if (!fixed) return nullptr;
|
||||
text = fixed;
|
||||
actual_len = (uint32_t)fixed_len;
|
||||
}
|
||||
if (out_len) *out_len = (int)actual_len;
|
||||
if (out_serial) *out_serial = serial;
|
||||
return text;
|
||||
|
||||
@@ -227,6 +227,18 @@ struct DesktopState {
|
||||
montauk::abi::BatteryInfo battery_info;
|
||||
uint64_t battery_last_poll;
|
||||
|
||||
// Display brightness. Present only while the active output has a
|
||||
// controllable backlight (laptop panel, or a monitor answering DDC/CI).
|
||||
SvgIcon icon_brightness;
|
||||
bool brightness_present;
|
||||
bool brightness_popup_open;
|
||||
Rect brightness_icon_rect;
|
||||
int brightness_level; // 0-100, as shown on the slider
|
||||
bool brightness_dragging;
|
||||
int brightness_pending; // drag sample not yet sent, -1 none
|
||||
uint64_t brightness_last_send;
|
||||
uint64_t brightness_last_poll;
|
||||
|
||||
bool vol_popup_open;
|
||||
Rect vol_icon_rect;
|
||||
int vol_level; // 0-100
|
||||
|
||||
@@ -19,6 +19,34 @@ static constexpr int FONT_HEIGHT = 16;
|
||||
// TrueType fonts, while login/desktop can recover when TrueType setup fails.
|
||||
extern const uint8_t font_data[256 * 16] __attribute__((weak));
|
||||
|
||||
// The bitmap fallback font is Code Page 437. Map a Unicode codepoint to its
|
||||
// CP437 cell ('?' when it has none) so UTF-8 text still draws sensibly,
|
||||
// including the box-drawing characters terminal programs use.
|
||||
inline uint8_t cp437_index(uint32_t cp) {
|
||||
if (cp < 0x80) return (uint8_t)cp;
|
||||
static constexpr uint16_t high[128] = {
|
||||
0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7,
|
||||
0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5,
|
||||
0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9,
|
||||
0x00FF,0x00D6,0x00DC,0x00A2,0x00A3,0x00A5,0x20A7,0x0192,
|
||||
0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA,
|
||||
0x00BF,0x2310,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB,
|
||||
0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,
|
||||
0x2555,0x2563,0x2551,0x2557,0x255D,0x255C,0x255B,0x2510,
|
||||
0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F,
|
||||
0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567,
|
||||
0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256B,
|
||||
0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580,
|
||||
0x03B1,0x00DF,0x0393,0x03C0,0x03A3,0x03C3,0x00B5,0x03C4,
|
||||
0x03A6,0x0398,0x03A9,0x03B4,0x221E,0x03C6,0x03B5,0x2229,
|
||||
0x2261,0x00B1,0x2265,0x2264,0x2320,0x2321,0x00F7,0x2248,
|
||||
0x00B0,0x2219,0x00B7,0x221A,0x207F,0x00B2,0x25A0,0x00A0,
|
||||
};
|
||||
for (int i = 0; i < 128; i++)
|
||||
if (high[i] == cp) return (uint8_t)(0x80 + i);
|
||||
return '?';
|
||||
}
|
||||
|
||||
// Dynamic font height: TTF line height or 16 (bitmap fallback)
|
||||
inline int system_font_height() {
|
||||
if (fonts::system_font && fonts::system_font->valid)
|
||||
@@ -74,8 +102,8 @@ inline void draw_text(Framebuffer& fb, int x, int y, const char* text, Color fg)
|
||||
fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; text[i]; i++) {
|
||||
draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg);
|
||||
for (const char* p = text; *p; x += FONT_WIDTH) {
|
||||
draw_char(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,18 +112,23 @@ inline void draw_text_bg(Framebuffer& fb, int x, int y, const char* text, Color
|
||||
fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; text[i]; i++) {
|
||||
draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg);
|
||||
for (const char* p = text; *p; x += FONT_WIDTH) {
|
||||
draw_char_bg(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg, bg);
|
||||
}
|
||||
}
|
||||
|
||||
// Width of the first `len` bytes of a UTF-8 string in the UI font.
|
||||
inline int text_width_n(const char* text, int len) {
|
||||
if (fonts::system_font && fonts::system_font->valid)
|
||||
return fonts::system_font->measure_text_n(text, len, fonts::UI_SIZE);
|
||||
return montauk::utf8::count(text, len) * FONT_WIDTH;
|
||||
}
|
||||
|
||||
inline int text_width(const char* text) {
|
||||
if (fonts::system_font && fonts::system_font->valid) {
|
||||
return fonts::system_font->measure_text(text, fonts::UI_SIZE);
|
||||
}
|
||||
int len = 0;
|
||||
while (text[len]) len++;
|
||||
return len * FONT_WIDTH;
|
||||
return montauk::utf8::count(text) * FONT_WIDTH;
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
@@ -296,15 +296,13 @@ inline bool text_input_has_selection(const TextInputState& state, const char* te
|
||||
return text_input_selection_start(state, len) != text_input_selection_end(state, len);
|
||||
}
|
||||
|
||||
// Width of the first `count` bytes (a character boundary). Masked fields
|
||||
// draw one '*' per character, however many bytes it takes.
|
||||
inline int text_input_prefix_width(const char* text, int count, bool masked) {
|
||||
char prefix[512];
|
||||
int len = 0;
|
||||
while (text && text[len] && len < count && len < (int)sizeof(prefix) - 1) {
|
||||
prefix[len] = masked ? '*' : text[len];
|
||||
len++;
|
||||
}
|
||||
prefix[len] = '\0';
|
||||
return text_width(prefix);
|
||||
while (text && text[len] && len < count) len++;
|
||||
if (!masked) return text_width_n(text, len);
|
||||
return montauk::utf8::count(text, len) * text_width("*");
|
||||
}
|
||||
|
||||
inline int text_input_index_from_x(const Rect& bounds, const char* value,
|
||||
@@ -314,11 +312,13 @@ inline int text_input_index_from_x(const Rect& bounds, const char* value,
|
||||
|
||||
int len = text_input_len(value);
|
||||
int prev_w = 0;
|
||||
for (int i = 0; i < len; i++) {
|
||||
int cur_w = text_input_prefix_width(value, i + 1, masked);
|
||||
for (int i = 0; i < len;) {
|
||||
int next = montauk::utf8::next_boundary(value, len, i);
|
||||
int cur_w = text_input_prefix_width(value, next, masked);
|
||||
int midpoint = prev_w + (cur_w - prev_w) / 2;
|
||||
if (rel_x < midpoint) return i;
|
||||
prev_w = cur_w;
|
||||
i = next;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
@@ -360,8 +360,11 @@ inline int text_input_mouse(TextInputState& state,
|
||||
return TEXT_INPUT_NONE;
|
||||
}
|
||||
|
||||
// Bytes >= 0x80 (any non-ASCII character) count as word characters, so word
|
||||
// jumps never stop inside a multi-byte sequence.
|
||||
inline bool text_input_word_char(char ch) {
|
||||
return (ch >= 'a' && ch <= 'z') ||
|
||||
return (uint8_t)ch >= 0x80 ||
|
||||
(ch >= 'a' && ch <= 'z') ||
|
||||
(ch >= 'A' && ch <= 'Z') ||
|
||||
(ch >= '0' && ch <= '9') ||
|
||||
ch == '_';
|
||||
@@ -574,8 +577,10 @@ inline int text_input_key(TextInputState& state,
|
||||
if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE;
|
||||
|
||||
int len = text_input_len(text, cap);
|
||||
state.cursor = text_input_clamp_cursor(state.cursor, len);
|
||||
state.selection_anchor = text_input_clamp_cursor(state.selection_anchor, len);
|
||||
state.cursor = montauk::utf8::floor_boundary(
|
||||
text, len, text_input_clamp_cursor(state.cursor, len));
|
||||
state.selection_anchor = montauk::utf8::floor_boundary(
|
||||
text, len, text_input_clamp_cursor(state.selection_anchor, len));
|
||||
|
||||
if (state.context.open) {
|
||||
context_menu_close(state.context);
|
||||
@@ -606,20 +611,22 @@ inline int text_input_key(TextInputState& state,
|
||||
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
||||
}
|
||||
|
||||
if (key.scancode == 0x4B || key.scancode == 0x4D ||
|
||||
key.scancode == 0x47 || key.scancode == 0x4F) {
|
||||
if (montauk::nav_scancode(key) == 0x4B || montauk::nav_scancode(key) == 0x4D ||
|
||||
montauk::nav_scancode(key) == 0x47 || montauk::nav_scancode(key) == 0x4F) {
|
||||
int next = state.cursor;
|
||||
if (!key.shift && sel_start != sel_end && key.scancode == 0x4B)
|
||||
if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4B)
|
||||
next = sel_start;
|
||||
else if (!key.shift && sel_start != sel_end && key.scancode == 0x4D)
|
||||
else if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4D)
|
||||
next = sel_end;
|
||||
else if (key.scancode == 0x4B)
|
||||
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) : state.cursor - 1;
|
||||
else if (key.scancode == 0x4D)
|
||||
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor) : state.cursor + 1;
|
||||
else if (key.scancode == 0x47)
|
||||
else if (montauk::nav_scancode(key) == 0x4B)
|
||||
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor)
|
||||
: montauk::utf8::prev_boundary(text, state.cursor);
|
||||
else if (montauk::nav_scancode(key) == 0x4D)
|
||||
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor)
|
||||
: montauk::utf8::next_boundary(text, len, state.cursor);
|
||||
else if (montauk::nav_scancode(key) == 0x47)
|
||||
next = 0;
|
||||
else if (key.scancode == 0x4F)
|
||||
else if (montauk::nav_scancode(key) == 0x4F)
|
||||
next = len;
|
||||
|
||||
text_input_set_cursor(state, len, next, key.shift);
|
||||
@@ -641,15 +648,16 @@ inline int text_input_key(TextInputState& state,
|
||||
changed = true;
|
||||
}
|
||||
} else if (state.cursor > 0) {
|
||||
text_input_delete_range(text, &len, state.cursor - 1, state.cursor);
|
||||
state.cursor--;
|
||||
int start = montauk::utf8::prev_boundary(text, state.cursor);
|
||||
text_input_delete_range(text, &len, start, state.cursor);
|
||||
state.cursor = start;
|
||||
changed = true;
|
||||
}
|
||||
state.selection_anchor = state.cursor;
|
||||
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
||||
}
|
||||
|
||||
if (key.scancode == 0x53) {
|
||||
if (montauk::nav_scancode(key) == 0x53) {
|
||||
if (sel_start != sel_end) {
|
||||
text_input_delete_range(text, &len, sel_start, sel_end);
|
||||
state.cursor = sel_start;
|
||||
@@ -661,15 +669,22 @@ inline int text_input_key(TextInputState& state,
|
||||
changed = true;
|
||||
}
|
||||
} else if (state.cursor < len) {
|
||||
text_input_delete_range(text, &len, state.cursor, state.cursor + 1);
|
||||
int end = montauk::utf8::next_boundary(text, len, state.cursor);
|
||||
text_input_delete_range(text, &len, state.cursor, end);
|
||||
changed = true;
|
||||
}
|
||||
state.selection_anchor = state.cursor;
|
||||
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
|
||||
}
|
||||
|
||||
if (key.ctrl || key.alt || !text_input_char_allowed(key.ascii, filter, userdata))
|
||||
// Typed text is inserted as UTF-8; the filter sees each encoded byte.
|
||||
char typed[montauk::utf8::MAX_BYTES];
|
||||
int typed_len = key.ctrl || key.alt ? 0 : montauk::utf8::key_utf8(key, typed);
|
||||
if (typed_len == 0) return TEXT_INPUT_NONE;
|
||||
for (int i = 0; i < typed_len; i++) {
|
||||
if (!text_input_char_allowed(typed[i], filter, userdata))
|
||||
return TEXT_INPUT_NONE;
|
||||
}
|
||||
|
||||
if (sel_start != sel_end) {
|
||||
text_input_delete_range(text, &len, sel_start, sel_end);
|
||||
@@ -677,11 +692,12 @@ inline int text_input_key(TextInputState& state,
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (len < cap - 1) {
|
||||
if (len + typed_len < cap) {
|
||||
for (int i = len; i >= state.cursor; i--)
|
||||
text[i + 1] = text[i];
|
||||
text[state.cursor++] = key.ascii;
|
||||
len++;
|
||||
text[i + typed_len] = text[i];
|
||||
for (int i = 0; i < typed_len; i++)
|
||||
text[state.cursor++] = typed[i];
|
||||
len += typed_len;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -1251,23 +1267,35 @@ inline void draw_badge(Canvas& c,
|
||||
c.text(x + theme.gap_sm, y + 2, label, fg);
|
||||
}
|
||||
|
||||
// One '*' per UTF-8 character.
|
||||
inline const char* mask_text(const char* value, char* buffer, int cap) {
|
||||
int len = 0;
|
||||
while (value && value[len] && len < cap - 1) {
|
||||
buffer[len] = '*';
|
||||
len++;
|
||||
const char* p = value ? value : "";
|
||||
while (*p && len < cap - 1) {
|
||||
montauk::utf8::next(&p);
|
||||
buffer[len++] = '*';
|
||||
}
|
||||
buffer[len] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// First `count` bytes of value (a character boundary), or one '*' per
|
||||
// character in them when masked.
|
||||
inline void copy_prefix(char* buffer, int cap, const char* value, int count, bool masked) {
|
||||
if (cap <= 0) return;
|
||||
int len = 0;
|
||||
if (masked) {
|
||||
const char* p = value ? value : "";
|
||||
while (*p && p - value < count && len < cap - 1) {
|
||||
montauk::utf8::next(&p);
|
||||
buffer[len++] = '*';
|
||||
}
|
||||
} else {
|
||||
while (value && value[len] && len < count && len < cap - 1) {
|
||||
buffer[len] = masked ? '*' : value[len];
|
||||
buffer[len] = value[len];
|
||||
len++;
|
||||
}
|
||||
}
|
||||
buffer[len] = '\0';
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
#include "gui/font.hpp"
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/utf8.h>
|
||||
#include <Api/Syscall.hpp>
|
||||
#include <montauk/capabilities.h>
|
||||
|
||||
namespace gui {
|
||||
|
||||
struct TermCell {
|
||||
char ch;
|
||||
uint32_t ch; // Unicode codepoint (every cell is one column wide)
|
||||
Color fg;
|
||||
Color bg;
|
||||
};
|
||||
@@ -58,6 +59,11 @@ struct TerminalState {
|
||||
int csi_params[8];
|
||||
int csi_param_count;
|
||||
int csi_current_param;
|
||||
|
||||
// Partial UTF-8 sequence carried across terminal_feed calls (child output
|
||||
// arrives in arbitrary chunks, so a character can straddle two reads).
|
||||
char utf8_pending[4];
|
||||
int utf8_pending_len;
|
||||
};
|
||||
|
||||
// ==== Palette ====
|
||||
@@ -244,6 +250,7 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
|
||||
t->csi_private = false;
|
||||
t->csi_param_count = 0;
|
||||
t->csi_current_param = 0;
|
||||
t->utf8_pending_len = 0;
|
||||
t->child_pid = 0;
|
||||
|
||||
int total_cells = (rows + max_sb) * cols;
|
||||
@@ -275,7 +282,7 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) {
|
||||
montauk::childio_settermsz(t->child_pid, cols, rows);
|
||||
}
|
||||
|
||||
static inline void terminal_put_char(TerminalState* t, char ch) {
|
||||
static inline void terminal_put_char(TerminalState* t, uint32_t ch) {
|
||||
if (t->cursor_x >= t->cols) {
|
||||
t->cursor_x = 0;
|
||||
t->cursor_y++;
|
||||
@@ -549,11 +556,61 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
// Emit pending bytes that did not form a valid UTF-8 sequence: each one is a
|
||||
// Windows-1252 character, matching how the GUI renders legacy text.
|
||||
static inline void terminal_flush_utf8(TerminalState* t) {
|
||||
for (int i = 0; i < t->utf8_pending_len; i++)
|
||||
terminal_put_char(t, montauk::utf8::from_cp1252((uint8_t)t->utf8_pending[i]));
|
||||
t->utf8_pending_len = 0;
|
||||
}
|
||||
|
||||
static inline int terminal_utf8_expected(uint8_t lead) {
|
||||
if (lead >= 0xC2 && lead <= 0xDF) return 2;
|
||||
if (lead >= 0xE0 && lead <= 0xEF) return 3;
|
||||
if (lead >= 0xF0 && lead <= 0xF4) return 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Feed one byte >= 0x80 of printable text through the UTF-8 assembler.
|
||||
static inline void terminal_feed_high(TerminalState* t, uint8_t b) {
|
||||
if (t->utf8_pending_len > 0) {
|
||||
if ((b & 0xC0) == 0x80) {
|
||||
t->utf8_pending[t->utf8_pending_len++] = (char)b;
|
||||
int want = terminal_utf8_expected((uint8_t)t->utf8_pending[0]);
|
||||
uint32_t cp = 0;
|
||||
int n = montauk::utf8::sequence_length(t->utf8_pending,
|
||||
t->utf8_pending_len, &cp);
|
||||
if (n == want) {
|
||||
terminal_put_char(t, cp);
|
||||
t->utf8_pending_len = 0;
|
||||
} else if (t->utf8_pending_len >= want) {
|
||||
terminal_flush_utf8(t); // overlong / surrogate / out of range
|
||||
}
|
||||
return;
|
||||
}
|
||||
terminal_flush_utf8(t);
|
||||
}
|
||||
if (terminal_utf8_expected(b)) {
|
||||
t->utf8_pending[0] = (char)b;
|
||||
t->utf8_pending_len = 1;
|
||||
} else {
|
||||
terminal_put_char(t, montauk::utf8::from_cp1252(b));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
||||
if (len > 0) t->dirty = true;
|
||||
for (int i = 0; i < len; i++) {
|
||||
char ch = data[i];
|
||||
|
||||
if (t->parse_state == TerminalState::STATE_NORMAL) {
|
||||
if ((uint8_t)ch >= 0x80) {
|
||||
terminal_feed_high(t, (uint8_t)ch);
|
||||
continue;
|
||||
}
|
||||
if (t->utf8_pending_len > 0) terminal_flush_utf8(t);
|
||||
}
|
||||
|
||||
switch (t->parse_state) {
|
||||
case TerminalState::STATE_NORMAL:
|
||||
if (ch == '\033') {
|
||||
@@ -575,9 +632,8 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
||||
while (t->cursor_x < next) {
|
||||
terminal_put_char(t, ' ');
|
||||
}
|
||||
} else if (ch >= 32 || ch < 0) {
|
||||
// Printable character (also treat high-bit chars as printable)
|
||||
terminal_put_char(t, ch);
|
||||
} else if (ch >= 32) {
|
||||
terminal_put_char(t, (uint8_t)ch);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -641,9 +697,9 @@ static inline void terminal_fill_pixel_rect(uint32_t* pixels, int pw, int ph,
|
||||
}
|
||||
|
||||
static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph,
|
||||
int x, int y, unsigned char ch,
|
||||
int x, int y, uint32_t ch,
|
||||
uint32_t color) {
|
||||
const uint8_t* glyph = &font_data[ch * FONT_HEIGHT];
|
||||
const uint8_t* glyph = &font_data[cp437_index(ch) * FONT_HEIGHT];
|
||||
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
|
||||
int dy = y + fy;
|
||||
if (dy >= ph) break;
|
||||
@@ -669,14 +725,14 @@ static inline void terminal_draw_cell_pixels(const TermCell& cell,
|
||||
cell.bg.to_pixel());
|
||||
}
|
||||
|
||||
if (cell.ch <= 32 && cell.ch >= 0) return;
|
||||
if (cell.ch <= 32) return;
|
||||
|
||||
if (use_ttf) {
|
||||
int baseline = py + gc->ascent;
|
||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
||||
px, baseline, (unsigned char)cell.ch, cell.fg, gc);
|
||||
px, baseline, (int)cell.ch, cell.fg, gc);
|
||||
} else {
|
||||
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
|
||||
terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
|
||||
cell.fg.to_pixel());
|
||||
}
|
||||
}
|
||||
@@ -691,14 +747,14 @@ static inline void terminal_draw_cursor_pixels(const TermCell& cell,
|
||||
terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h,
|
||||
g_term_palette.cursor.to_pixel());
|
||||
|
||||
if (cell.ch <= 32 && cell.ch >= 0) return;
|
||||
if (cell.ch <= 32) return;
|
||||
|
||||
if (use_ttf) {
|
||||
int baseline = py + gc->ascent;
|
||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
||||
px, baseline, (unsigned char)cell.ch, g_term_palette.bg, gc);
|
||||
px, baseline, (int)cell.ch, g_term_palette.bg, gc);
|
||||
} else {
|
||||
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
|
||||
terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
|
||||
g_term_palette.bg.to_pixel());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/utf8.h>
|
||||
#include "gui/gui.hpp"
|
||||
#include "gui/framebuffer.hpp"
|
||||
|
||||
@@ -41,39 +42,12 @@
|
||||
|
||||
namespace gui {
|
||||
|
||||
// Windows-1252 byte -> Unicode codepoint, for the remaining code that still
|
||||
// handles single-byte text (PDF simple-font encodings). UTF-8 strings go
|
||||
// through measure_text/draw, which decode with montauk::utf8.
|
||||
inline int decode_single_byte_codepoint(int codepoint) {
|
||||
if (codepoint < 0 || codepoint > 255) return codepoint;
|
||||
|
||||
switch (codepoint) {
|
||||
case 0x80: return 0x20AC; // Euro Sign
|
||||
case 0x82: return 0x201A; // Single Low-9 Quotation Mark
|
||||
case 0x83: return 0x0192; // Latin Small Letter F With Hook
|
||||
case 0x84: return 0x201E; // Double Low-9 Quotation Mark
|
||||
case 0x85: return 0x2026; // Horizontal Ellipsis
|
||||
case 0x86: return 0x2020; // Dagger
|
||||
case 0x87: return 0x2021; // Double Dagger
|
||||
case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent
|
||||
case 0x89: return 0x2030; // Per Mille Sign
|
||||
case 0x8A: return 0x0160; // Latin Capital Letter S With Caron
|
||||
case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark
|
||||
case 0x8C: return 0x0152; // Latin Capital Ligature OE
|
||||
case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron
|
||||
case 0x91: return 0x2018; // Left Single Quotation Mark
|
||||
case 0x92: return 0x2019; // Right Single Quotation Mark
|
||||
case 0x93: return 0x201C; // Left Double Quotation Mark
|
||||
case 0x94: return 0x201D; // Right Double Quotation Mark
|
||||
case 0x95: return 0x2022; // Bullet
|
||||
case 0x96: return 0x2013; // En Dash
|
||||
case 0x97: return 0x2014; // Em Dash
|
||||
case 0x98: return 0x02DC; // Small Tilde
|
||||
case 0x99: return 0x2122; // Trade Mark Sign
|
||||
case 0x9A: return 0x0161; // Latin Small Letter S With Caron
|
||||
case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark
|
||||
case 0x9C: return 0x0153; // Latin Small Ligature OE
|
||||
case 0x9E: return 0x017E; // Latin Small Letter Z With Caron
|
||||
case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis
|
||||
default: return codepoint;
|
||||
}
|
||||
return (int)montauk::utf8::from_cp1252((uint8_t)codepoint);
|
||||
}
|
||||
|
||||
struct CachedGlyph {
|
||||
@@ -84,8 +58,18 @@ struct CachedGlyph {
|
||||
bool loaded;
|
||||
};
|
||||
|
||||
// Glyphs above U+00FF live in a small open-addressed table so CJK, symbols
|
||||
// and the like cost nothing until they are drawn.
|
||||
struct ExtGlyph {
|
||||
uint32_t codepoint; // 0 = empty slot
|
||||
CachedGlyph glyph;
|
||||
};
|
||||
|
||||
struct GlyphCache {
|
||||
CachedGlyph glyphs[256]; // Single-byte text with Windows-1252 punctuation mapping
|
||||
CachedGlyph glyphs[256]; // Unicode U+0000-U+00FF, indexed directly
|
||||
ExtGlyph* ext; // everything above, hashed by codepoint
|
||||
int ext_cap; // power of two, 0 until first use
|
||||
int ext_count;
|
||||
int pixel_size;
|
||||
float scale;
|
||||
int ascent, descent, line_gap;
|
||||
@@ -142,8 +126,21 @@ struct TrueTypeFont {
|
||||
return true;
|
||||
}
|
||||
|
||||
void init_cache(GlyphCache* gc, int pixel_size) {
|
||||
// Free any existing glyph bitmaps
|
||||
static void free_ext(GlyphCache* gc) {
|
||||
if (gc->ext) {
|
||||
for (int i = 0; i < gc->ext_cap; i++) {
|
||||
if (gc->ext[i].codepoint && gc->ext[i].glyph.bitmap)
|
||||
montauk::mfree(gc->ext[i].glyph.bitmap);
|
||||
}
|
||||
montauk::mfree(gc->ext);
|
||||
}
|
||||
gc->ext = nullptr;
|
||||
gc->ext_cap = 0;
|
||||
gc->ext_count = 0;
|
||||
}
|
||||
|
||||
// Release every glyph bitmap in a cache (the cache stays usable).
|
||||
static void free_cache(GlyphCache* gc) {
|
||||
for (int i = 0; i < 256; i++) {
|
||||
if (gc->glyphs[i].bitmap) {
|
||||
montauk::mfree(gc->glyphs[i].bitmap);
|
||||
@@ -151,6 +148,11 @@ struct TrueTypeFont {
|
||||
gc->glyphs[i].bitmap = nullptr;
|
||||
gc->glyphs[i].loaded = false;
|
||||
}
|
||||
free_ext(gc);
|
||||
}
|
||||
|
||||
void init_cache(GlyphCache* gc, int pixel_size) {
|
||||
free_cache(gc);
|
||||
|
||||
gc->pixel_size = pixel_size;
|
||||
gc->scale = em_scaling
|
||||
@@ -179,11 +181,8 @@ struct TrueTypeFont {
|
||||
return gc;
|
||||
}
|
||||
|
||||
// Evict oldest cache (slot 0) — free its glyph bitmaps
|
||||
for (int g = 0; g < 256; g++) {
|
||||
if (caches[0].glyphs[g].bitmap)
|
||||
montauk::mfree(caches[0].glyphs[g].bitmap);
|
||||
}
|
||||
// Evict oldest cache (slot 0) - free its glyph bitmaps
|
||||
free_cache(&caches[0]);
|
||||
// Shift remaining caches down
|
||||
for (int i = 0; i < 3; i++)
|
||||
montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache));
|
||||
@@ -192,13 +191,54 @@ struct TrueTypeFont {
|
||||
return &caches[3];
|
||||
}
|
||||
|
||||
static constexpr int EXT_MAX = 2048; // flush the table beyond this
|
||||
|
||||
CachedGlyph* ext_slot(GlyphCache* gc, uint32_t codepoint) {
|
||||
if (gc->ext_cap == 0 || gc->ext_count * 4 >= gc->ext_cap * 3) {
|
||||
int new_cap = gc->ext_cap ? gc->ext_cap * 2 : 64;
|
||||
if (gc->ext_count >= EXT_MAX) {
|
||||
// A document full of distinct glyphs: start over rather than
|
||||
// grow without bound. Bitmaps are rebuilt on demand.
|
||||
free_ext(gc);
|
||||
new_cap = 64;
|
||||
}
|
||||
ExtGlyph* table = (ExtGlyph*)montauk::malloc(sizeof(ExtGlyph) * new_cap);
|
||||
if (!table) return nullptr;
|
||||
montauk::memset(table, 0, sizeof(ExtGlyph) * new_cap);
|
||||
for (int i = 0; i < gc->ext_cap; i++) {
|
||||
ExtGlyph& e = gc->ext[i];
|
||||
if (!e.codepoint) continue;
|
||||
uint32_t h = (e.codepoint * 2654435761u) & (new_cap - 1);
|
||||
while (table[h].codepoint) h = (h + 1) & (new_cap - 1);
|
||||
table[h] = e;
|
||||
}
|
||||
if (gc->ext) montauk::mfree(gc->ext);
|
||||
gc->ext = table;
|
||||
gc->ext_cap = new_cap;
|
||||
}
|
||||
uint32_t mask = gc->ext_cap - 1;
|
||||
uint32_t h = (codepoint * 2654435761u) & mask;
|
||||
while (gc->ext[h].codepoint && gc->ext[h].codepoint != codepoint)
|
||||
h = (h + 1) & mask;
|
||||
if (!gc->ext[h].codepoint) {
|
||||
gc->ext[h].codepoint = codepoint;
|
||||
montauk::memset(&gc->ext[h].glyph, 0, sizeof(CachedGlyph));
|
||||
gc->ext_count++;
|
||||
}
|
||||
return &gc->ext[h].glyph;
|
||||
}
|
||||
|
||||
// Glyph for a Unicode codepoint. Codepoints the font lacks render as its
|
||||
// .notdef box, so missing characters stay visible instead of vanishing.
|
||||
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
|
||||
if (codepoint < 0 || codepoint >= 256) return nullptr;
|
||||
CachedGlyph* g = &gc->glyphs[codepoint];
|
||||
if (codepoint < 0 || codepoint > 0x10FFFF) return nullptr;
|
||||
CachedGlyph* g = codepoint < 256 ? &gc->glyphs[codepoint]
|
||||
: ext_slot(gc, (uint32_t)codepoint);
|
||||
if (!g) return nullptr;
|
||||
if (g->loaded) return g;
|
||||
|
||||
g->loaded = true;
|
||||
int font_codepoint = decode_single_byte_codepoint(codepoint);
|
||||
int font_codepoint = codepoint;
|
||||
int advance, lsb;
|
||||
stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb);
|
||||
g->advance = (int)(advance * gc->scale);
|
||||
@@ -222,12 +262,34 @@ struct TrueTypeFont {
|
||||
return g;
|
||||
}
|
||||
|
||||
// Advance of one codepoint at a size (0 if the font is unusable).
|
||||
int char_advance(int codepoint, int pixel_size) {
|
||||
if (!valid) return 0;
|
||||
CachedGlyph* g = get_glyph(get_cache(pixel_size), codepoint);
|
||||
return g ? g->advance : 0;
|
||||
}
|
||||
|
||||
// Width of the first `len` bytes of a UTF-8 string.
|
||||
int measure_text_n(const char* text, int len, int pixel_size) {
|
||||
if (!valid) return 0;
|
||||
GlyphCache* gc = get_cache(pixel_size);
|
||||
int w = 0;
|
||||
for (int pos = 0; pos < len && text[pos];) {
|
||||
int n;
|
||||
uint32_t cp = montauk::utf8::decode(text + pos, len - pos, &n);
|
||||
CachedGlyph* g = get_glyph(gc, (int)cp);
|
||||
if (g) w += g->advance;
|
||||
pos += n;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
int measure_text(const char* text, int pixel_size) {
|
||||
if (!valid) return 0;
|
||||
GlyphCache* gc = get_cache(pixel_size);
|
||||
int w = 0;
|
||||
for (int i = 0; text[i]; i++) {
|
||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
||||
for (const char* p = text; *p;) {
|
||||
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||
if (g) w += g->advance;
|
||||
}
|
||||
return w;
|
||||
@@ -253,8 +315,8 @@ struct TrueTypeFont {
|
||||
uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) |
|
||||
((uint32_t)color.g << 8) | color.b;
|
||||
|
||||
for (int i = 0; text[i]; i++) {
|
||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
||||
for (const char* p = text; *p;) {
|
||||
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||
if (!g) continue;
|
||||
|
||||
if (g->bitmap) {
|
||||
@@ -325,8 +387,8 @@ struct TrueTypeFont {
|
||||
int baseline = y + gc->ascent;
|
||||
bool drew_pixel = false;
|
||||
|
||||
for (int i = 0; text[i]; i++) {
|
||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
||||
for (const char* p = text; *p;) {
|
||||
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||
if (!g) continue;
|
||||
|
||||
if (g->bitmap) {
|
||||
@@ -378,8 +440,8 @@ struct TrueTypeFont {
|
||||
int clip_x2 = clip_x + clip_w;
|
||||
int clip_y2 = clip_y + clip_h;
|
||||
|
||||
for (int i = 0; text[i]; i++) {
|
||||
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
|
||||
for (const char* p = text; *p;) {
|
||||
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
|
||||
if (!g) continue;
|
||||
|
||||
if (g->bitmap) {
|
||||
@@ -417,7 +479,7 @@ struct TrueTypeFont {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw single character to buffer, returning advance width
|
||||
// Draw one Unicode codepoint to buffer, returning advance width
|
||||
int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
|
||||
int x, int baseline, int codepoint,
|
||||
Color color, GlyphCache* gc) {
|
||||
@@ -483,10 +545,10 @@ namespace fonts {
|
||||
return f;
|
||||
};
|
||||
|
||||
system_font = load("0:/fonts/Roboto-Medium.ttf");
|
||||
system_bold = load("0:/fonts/Roboto-Bold.ttf");
|
||||
mono = load("0:/fonts/JetBrainsMono-Regular.ttf");
|
||||
mono_bold = load("0:/fonts/JetBrainsMono-Bold.ttf");
|
||||
system_font = load("0:/os/fonts/Roboto-Medium.ttf");
|
||||
system_bold = load("0:/os/fonts/Roboto-Bold.ttf");
|
||||
mono = load("0:/os/fonts/JetBrainsMono-Regular.ttf");
|
||||
mono_bold = load("0:/os/fonts/JetBrainsMono-Bold.ttf");
|
||||
|
||||
return system_font != nullptr;
|
||||
}
|
||||
|
||||
@@ -212,11 +212,7 @@ struct TextBox {
|
||||
|
||||
// Draw cursor if focused
|
||||
if (focused) {
|
||||
// Measure text up to cursor position for proportional fonts
|
||||
char prefix[256];
|
||||
for (int i = 0; i < cursor && i < 255; i++) prefix[i] = text[i];
|
||||
prefix[cursor < 255 ? cursor : 255] = '\0';
|
||||
int cx = tx + text_width(prefix);
|
||||
int cx = tx + text_width_n(text, cursor);
|
||||
draw_vline(fb, cx, ty, fh, cursor_color);
|
||||
}
|
||||
}
|
||||
@@ -230,34 +226,35 @@ struct TextBox {
|
||||
void handle_key(const montauk::abi::KeyEvent& key) {
|
||||
if (!focused || !key.pressed) return;
|
||||
|
||||
char typed[montauk::utf8::MAX_BYTES];
|
||||
int typed_len = montauk::utf8::key_utf8(key, typed);
|
||||
|
||||
if (key.ascii == '\b' || key.scancode == 0x0E) {
|
||||
// Backspace
|
||||
// Backspace: remove the whole character before the cursor
|
||||
if (cursor > 0) {
|
||||
for (int i = cursor - 1; i < text_len - 1; i++) {
|
||||
text[i] = text[i + 1];
|
||||
int start = montauk::utf8::prev_boundary(text, cursor);
|
||||
int n = cursor - start;
|
||||
for (int i = start; i <= text_len - n; i++)
|
||||
text[i] = text[i + n];
|
||||
text_len -= n;
|
||||
cursor = start;
|
||||
}
|
||||
text_len--;
|
||||
cursor--;
|
||||
text[text_len] = '\0';
|
||||
} else if (typed_len > 0) {
|
||||
// Printable character, inserted as UTF-8
|
||||
if (text_len + typed_len <= 254) {
|
||||
for (int i = text_len; i >= cursor; i--)
|
||||
text[i + typed_len] = text[i];
|
||||
for (int i = 0; i < typed_len; i++)
|
||||
text[cursor + i] = typed[i];
|
||||
cursor += typed_len;
|
||||
text_len += typed_len;
|
||||
}
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127) {
|
||||
// Printable character
|
||||
if (text_len < 254) {
|
||||
for (int i = text_len; i > cursor; i--) {
|
||||
text[i] = text[i - 1];
|
||||
}
|
||||
text[cursor] = key.ascii;
|
||||
cursor++;
|
||||
text_len++;
|
||||
text[text_len] = '\0';
|
||||
}
|
||||
} else if (key.scancode == 0x4B) {
|
||||
} else if (montauk::nav_scancode(key) == 0x4B) {
|
||||
// Left arrow
|
||||
if (cursor > 0) cursor--;
|
||||
} else if (key.scancode == 0x4D) {
|
||||
cursor = montauk::utf8::prev_boundary(text, cursor);
|
||||
} else if (montauk::nav_scancode(key) == 0x4D) {
|
||||
// Right arrow
|
||||
if (cursor < text_len) cursor++;
|
||||
cursor = montauk::utf8::next_boundary(text, text_len, cursor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -212,8 +212,42 @@ extern "C" {
|
||||
#define MTK_SYS_SPAWN_REDIR_CAPS 186
|
||||
#define MTK_SYS_ALLOC_EAGER 187
|
||||
#define MTK_SYS_BATTERYINFO 188
|
||||
#define MTK_SYS_GETCAPS 189
|
||||
#define MTK_SYS_VOLINFO 190
|
||||
#define MTK_SYS_CONSENT_REGISTER 191
|
||||
#define MTK_SYS_CONSENT_NEXT 192
|
||||
#define MTK_SYS_CONSENT_REPLY 193
|
||||
#define MTK_SYS_GETKEY_EX 194
|
||||
#define MTK_SYS_CHILDIO_WRITEKEY_EX 195
|
||||
#define MTK_SYS_FUTEX_WAIT 196
|
||||
#define MTK_SYS_FUTEX_WAKE 197
|
||||
#define MTK_SYS_ABORT 198
|
||||
#define MTK_SYS_EXIT_GROUP 199
|
||||
/* @SYSCALLS-END */
|
||||
|
||||
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
||||
/* @STATUS-BEGIN
|
||||
Generated from Common/StatusCodes.hpp by scripts/montauk-syscalls.py.
|
||||
Do not edit by hand; run 'make gen-syscalls' to refresh. */
|
||||
#define MTK_STATUS_SUCCESS (0)
|
||||
#define MTK_STATUS_GENERIC_FAILURE (-1)
|
||||
#define MTK_STATUS_PERMISSION_DENIED (-2)
|
||||
#define MTK_STATUS_CAPABILITY_DENIED (-3)
|
||||
#define MTK_STATUS_SYSTEM_INTEGRITY_ERROR (-4)
|
||||
#define MTK_STATUS_BAD_POINTER (-5)
|
||||
#define MTK_STATUS_INVALID_ARGUMENT (-6)
|
||||
#define MTK_STATUS_UNSUPPORTED_OP (-7)
|
||||
#define MTK_STATUS_NO_DEVICE (-8)
|
||||
#define MTK_STATUS_IO_FAILURE (-9)
|
||||
#define MTK_STATUS_BAD_INVOCATION (-10)
|
||||
#define MTK_STATUS_NO_SUCH_PATH (-11)
|
||||
#define MTK_STATUS_BUSY (-12)
|
||||
#define MTK_STATUS_BUFFER_TOO_SMALL (-13)
|
||||
#define MTK_STATUS_OUT_OF_MEMORY (-14)
|
||||
#define MTK_STATUS_TIMED_OUT (-15)
|
||||
#define MTK_STATUS_WOULD_BLOCK (-16)
|
||||
/* @STATUS-END */
|
||||
|
||||
#define MTK_SOCK_TCP 1
|
||||
#define MTK_SOCK_UDP 2
|
||||
#define MTK_IPC_SIGNAL_READABLE (1u << 0)
|
||||
@@ -463,6 +497,21 @@ static inline void mtk_sleep_ms(unsigned long ms) {
|
||||
_mtk_syscall1(MTK_SYS_SLEEP_MS, (long)ms);
|
||||
}
|
||||
|
||||
/* Futexes (process-private). mtk_futex_wait blocks while *addr == expected;
|
||||
returns 0 when woken, MTK_STATUS_TIMED_OUT, or MTK_STATUS_WOULD_BLOCK when
|
||||
the word had already changed. timeout_ns: MTK_FUTEX_WAIT_FOREVER for no
|
||||
deadline, 0 to only check. mtk_futex_wake returns the number woken. */
|
||||
#define MTK_FUTEX_WAIT_FOREVER (~0UL)
|
||||
#define MTK_FUTEX_WAKE_ALL 0xFFFFFFFFu
|
||||
|
||||
static inline int mtk_futex_wait(uint32_t *addr, uint32_t expected, unsigned long timeout_ns) {
|
||||
return (int)_mtk_syscall3(MTK_SYS_FUTEX_WAIT, (long)addr, (long)expected, (long)timeout_ns);
|
||||
}
|
||||
|
||||
static inline int mtk_futex_wake(uint32_t *addr, uint32_t count) {
|
||||
return (int)_mtk_syscall2(MTK_SYS_FUTEX_WAKE, (long)addr, (long)count);
|
||||
}
|
||||
|
||||
static inline int mtk_getpid(void) {
|
||||
return (int)_mtk_syscall0(MTK_SYS_GETPID);
|
||||
}
|
||||
|
||||
Loaded 100 of 274 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user