feat: add permission consent prompts, drop permitted capability mask
This commit is contained in:
1 parent
c54181103f
commit
ae5e84592d
25 files changed
+1632
-94
No files matched your search
+178
@@ -0,0 +1,178 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user