179 lines
9.1 KiB
Markdown
179 lines
9.1 KiB
Markdown
# 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.
|