diff --git a/docs/consent.md b/docs/consent.md deleted file mode 100644 index dcd17ca..0000000 --- a/docs/consent.md +++ /dev/null @@ -1,178 +0,0 @@ -# Consent Prompts - -A program that has no entry in `0:/config/capabilities.toml` starts with no -capabilities. For five of them, it can ask the user the first time it needs -one, instead of failing. This works much like TCC on macOS: the desktop shows -a prompt. For installed programs the answer is remembered; for any other -binary it lasts until the process exits. - -| Capability | The prompt says the app wants to... | -|-----------------|-----------------------------------------------------------------------| -| `storage_admin` | format, partition and mount disks | -| `network_admin` | change network settings and join Wi-Fi networks | -| `set_time` | change the system clock and time zone | -| `display_admin` | change the screen resolution and brightness | -| `log_read` | read the system log, which can reveal what other apps and devices are doing | - -No other capability can be asked for. The eligible set is -`CAP_CONSENT_ELIGIBLE` in `Api/Syscall.hpp`. - -## How a request flows - -1. A syscall gated on an eligible capability calls - `Sched::Consent::Require()` (`kernel/src/Sched/Consent.cpp`) instead of - `HasCapability()`. All of these checks sit at the top of their syscall in - `Api/Syscall.cpp`, where the thread holds no locks and may block. -2. If the caller lacks the capability and is allowed to ask (see below), the - kernel queues a request and blocks the calling thread. A second thread of - the same process asking for the same capability waits on the same request. -3. The session's consent agent, the desktop, picks the request up with - `SYS_CONSENT_NEXT` from its main loop. If `0:/config/consent.toml` already - holds an answer for this program and capability, the desktop replies - without showing anything. Otherwise it opens the prompt. -4. `SYS_CONSENT_REPLY` either grants the capability or records a denial. The - waiting thread wakes, and its syscall proceeds or returns - `SYS_ERR_PERMISSION`. - -If nobody answers within `CONSENT_TIMEOUT_MS` (120 s), the request is denied. -If the agent exits, every pending request is denied. If the app exits, its -requests are cancelled. - -## Who may ask - -A process may ask for a capability only when all of these hold: - -- The capability is within its **consent ceiling**: the part of its parent's - delegable set, recorded at spawn, that is eligible for consent. -- The session has a registered agent. Registering (`SYS_CONSENT_REGISTER`) - requires `CAP_COMPOSITOR`, so only the desktop can be the agent. -- The process is in the agent's session and is not the agent itself. -- The agent still holds the capability as delegable. -- It has not already been refused that capability during its lifetime. - -These rules have some consequences: - -- **Standard sessions never prompt.** A standard session holds none of the - eligible capabilities, so every ceiling under it is empty. -- **Console sessions and SSH sessions never prompt.** They have no agent - (sshd is not in the desktop's session), so a remote user cannot put a - prompt on the local screen. -- **A sandboxing launcher still works.** If a parent spawns a child with - nothing delegable, the child's ceiling is empty and it cannot ask. -- After a refusal the process gets `SYS_ERR_PERMISSION` immediately every - time, so it cannot flood the user with prompts. - -## Why this keeps the capability model's invariants - -1. **Authority only narrows down the process tree.** A grant never exceeds - the process's consent ceiling, which is exactly what its parent could have - given it at spawn. The grant must also be within the agent's own delegable - set. Consent-granted capabilities are added only to the effective set, - never to the delegable set, so a consented app cannot pass them on to - anything it launches. Consent delays a delegation the parent could already - have made; it never exceeds one. The kernel re-checks all of this under - the scheduler lock when it applies the grant, so a grant can never land on - a process slot that has exited and been reused. -2. **Capabilities are kernel-owned.** The kernel identifies the program by - the path it loaded, never by a user name. - -## Program identity - -A stored answer is keyed on the program's path, and a path identifies a -program only if nobody else can replace the file. On the system volume, -writing anything under `0:/os` or `0:/apps` requires `CAP_SYSTEM_IMAGE` -(`Fs/ProtectedPaths.cpp`), which no session holds. So a program there is -**installed**, and its path is a trustworthy identity. - -The kernel decides this, not the desktop: each `ConsentRequest` carries -`imageProtected`, computed from the resolved image path with the same rule -that guards writes. Paths are normalized by the same resolver the write check -uses, so `0:/os/../users/x.elf` is not mistaken for an installed program. - -Any other binary, such as one in a user's home directory or on a data volume, -could be swapped for another at the same path. Its answers are never stored: -it is asked each time it runs, and the answer lasts until that process exits. - -The same rule applies to static grants. `Sched::Spawn` starts any image -outside `0:/os` and `0:/apps` with no capabilities, whatever its launcher -asked for, and logs a warning. Otherwise a program with no authority could -overwrite a granted binary in, say, a home directory, and then run with the -grant the next time the shell or desktop launched it. A program installed -elsewhere gets its authority through consent instead. The Capabilities app -flags such grants. - -## The prompt - -The prompt is a normal window inside the desktop, not a full-screen overlay. -It is placed over the requesting app's window when that app has one, and at -the centre of the screen otherwise. It shows: - -- the app's name: its menu label when the path belongs to an installed app, - otherwise the file name -- what it wants to do -- the full path - -The prompt window belongs to the desktop itself (`programs/src/desktop/consent.cpp`), -not to the window server, so no process can map its pixels or send it input. -`SYS_WINMAP` and `SYS_WINSENDEVENT` require `CAP_COMPOSITOR` anyway. This -leaves clickjacking, where another window is raised over the prompt or steals -focus just as the user clicks. To prevent that, **Allow** stays disabled -until the prompt has been focused and on top for 700 ms without interruption. -Losing focus or being covered restarts the delay. - -| Action | Result | Saved? | -|--------------------------------|---------------------------------|--------| -| Allow (click, once armed) | Granted | Installed programs only | -| Don't Allow (click, or Return once armed) | Denied | Installed programs only | -| Escape, or the title-bar close | Denied for this process only | No | -| Timeout, logout | Denied for this process only | No | - -For a program that is not installed, the prompt also says that the answer -applies only until the program exits. - -Only one prompt is shown at a time. Further requests wait in the kernel's -queue, which holds up to 16 requests. - -## Stored decisions - -Decisions are stored in `0:/config/consent.toml`: - -```toml -[decision.1] -path = "0:/apps/wifitool/wifitool.elf" -capability = "network_admin" -decision = "allow" -``` - -A decision applies when the path and the capability match. Only installed -programs are ever stored, and the desktop looks up only requests the kernel -marked `imageProtected`. A new answer for the same path and capability -replaces the old one. Updating an installed program keeps its decisions, -because installing it already required `CAP_SYSTEM_IMAGE`. The file is a -protected path that requires `CAP_USER_ADMIN` to write, the same as -`capabilities.toml`: anything that can edit it could grant itself access. - -The **Consent** tab of the Capabilities app lists the stored decisions and can -**Forget** one, so the program is asked again next time. A copy that is -already running keeps a granted capability until it exits. - -## Syscalls - -| Number | Name | Caller | Purpose | -|--------|------------------------|--------|---------------------------------------------| -| 191 | `SYS_CONSENT_REGISTER` | agent | Become the session's consent agent (needs `CAP_COMPOSITOR`) | -| 192 | `SYS_CONSENT_NEXT` | agent | Take the oldest queued request (`ConsentRequest`) | -| 193 | `SYS_CONSENT_REPLY` | agent | `CONSENT_ALLOW` / `CONSENT_DENY` a delivered request | - -## Not covered yet - -- **Elevation for standard users**, for example entering an administrator's - password in the prompt. This needs a different source of authority, because - a standard session has nothing to delegate. -- **Console sessions.** A text prompt in the terminal could be faked by - anything that prints to it. -- **Per-user decisions.** The table is system-wide, like the grants. -- **Revoking from a running process.** Forgetting a decision only affects - the next launch. -- **App-supplied reasons.** The prompt describes the capability in the - system's own words. A program cannot add text of its own. diff --git a/docs/power.md b/docs/power.md deleted file mode 100644 index 9a296f3..0000000 --- a/docs/power.md +++ /dev/null @@ -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. diff --git a/docs/ssh.md b/docs/ssh.md deleted file mode 100644 index e55cb7c..0000000 --- a/docs/ssh.md +++ /dev/null @@ -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. diff --git a/docs/wifi.md b/docs/wifi.md deleted file mode 100644 index f897e33..0000000 --- a/docs/wifi.md +++ /dev/null @@ -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 join one -dhcp pick up an address -wifi status what you are connected to -wifi saved / wifi forget 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. diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index fbbcbbe..0bc423d 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -352,9 +352,7 @@ namespace montauk::abi { /* Storage.hpp */ static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer - /* Consent.hpp -- first-use consent prompts (docs/consent.md). Only the - session's consent agent (the desktop) may register, drain requests - and answer them. */ + /* 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 diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index d07253c..1790f04 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -265,9 +265,7 @@ namespace montauk::abi { /* Storage.hpp */ static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer - /* Consent.hpp -- first-use consent prompts (docs/consent.md). Only the - session's consent agent (the desktop) may register, drain requests - and answer them. */ + /* 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 diff --git a/programs/include/montauk/consent.h b/programs/include/montauk/consent.h index b8b82d3..93abc45 100644 --- a/programs/include/montauk/consent.h +++ b/programs/include/montauk/consent.h @@ -1,7 +1,7 @@ /* * consent.h * First-use consent: prompt wording and the stored-decision table - * (0:/config/consent.toml). See docs/consent.md. + * (0:/config/consent.toml). * Copyright (c) 2026 Daniel Hammer */ diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index eee411f..468e0bd 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -132,7 +132,7 @@ namespace montauk { return (int)syscall1(montauk::abi::SYS_GETCAPS, (uint64_t)out); } - // Consent agent (the desktop only; see docs/consent.md). + // Consent agent. inline int consent_register() { return (int)syscall0(montauk::abi::SYS_CONSENT_REGISTER); } diff --git a/programs/src/desktop/consent.cpp b/programs/src/desktop/consent.cpp index 0ba3b46..854a788 100644 --- a/programs/src/desktop/consent.cpp +++ b/programs/src/desktop/consent.cpp @@ -1,7 +1,6 @@ /* * consent.cpp - * Consent agent: answers the kernel's first-use capability requests from - * stored decisions, or asks the user in a prompt window (docs/consent.md) + * Desktpp consent agent * Copyright (c) 2026 Daniel Hammer */ @@ -9,15 +8,6 @@ #include #include -// The prompt is an ordinary window inside the desktop, but an INTERNAL one: -// its pixels live in the desktop's heap, not in a window-server surface, so -// no app can map it (SYS_WINMAP) and only real input routed by the desktop -// reaches it. What remains is clickjacking -- an app raising a window over -// the prompt, or stealing focus, just as the user clicks. So Allow only -// arms after the prompt has been focused and on top for ARM_DELAY_MS without -// interruption; losing either restarts the delay. Don't Allow and closing -// are always safe and never wait. - namespace { constexpr int PROMPT_W = 460;