feat: add permission consent prompts, drop permitted capability mask

This commit is contained in:
daniel committed 2026-09-30 08:32:48 +02:00
1 parent c54181103f
commit ae5e84592d
25 files changed
+1632 -94

No files matched your search

+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once
#define MONTAUK_BUILD_NUMBER 239
#define MONTAUK_BUILD_NUMBER 251
-2
View File
@@ -144,7 +144,6 @@ namespace montauk::abi {
static int Sys_GetCaps(SpawnCapabilities* out) {
auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr || out == nullptr) return -1;
out->permitted = proc->permittedCaps;
out->effective = proc->effectiveCaps;
out->delegable = proc->delegableCaps;
return 0;
@@ -214,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++;
+24 -16
View File
@@ -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
@@ -200,11 +201,11 @@ namespace montauk::abi {
(uint64_t)frame->arg2 * sizeof(DisplayModeInfo), true)) return -1;
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 SYS_ERR_PERMISSION;
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_DISPLAY_ADMIN)) return SYS_ERR_PERMISSION;
return Sys_DisplayBrightness((int)frame->arg1);
case SYS_GETEXECPATH:
if (!UserMemory::Range(frame->arg2 ? frame->arg1 : frame->arg1, frame->arg2, true)) return -1;
@@ -248,7 +249,7 @@ namespace montauk::abi {
Sys_GetTime((DateTime*)frame->arg1);
return 0;
case SYS_SETUNIXTIME:
if (!Sched::HasCapability(CAP_SET_TIME)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_SET_TIME)) return SYS_ERR_PERMISSION;
return Sys_SetUnixTime((int64_t)frame->arg1);
case SYS_SOCKET:
return (int64_t)Sys_Socket((int)frame->arg1);
@@ -274,7 +275,7 @@ namespace montauk::abi {
Sys_GetNetCfg((NetCfg*)frame->arg1);
return 0;
case SYS_SETNETCFG:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Readable<NetCfg>(frame->arg1)) return -1;
return (int64_t)Sys_SetNetCfg((const NetCfg*)frame->arg1);
case SYS_NETSTATUS:
@@ -330,7 +331,7 @@ namespace montauk::abi {
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
return Sys_GetRandom((uint8_t*)frame->arg1, frame->arg2);
case SYS_LOG:
if (!Sched::HasCapability(CAP_LOG_READ)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_LOG_READ)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
return Kt::ReadKernelLogBuffer((char*)frame->arg1, frame->arg2);
case SYS_MOUSESTATE:
@@ -438,20 +439,20 @@ namespace montauk::abi {
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 SYS_ERR_PERMISSION;
return (int64_t)Sys_GptInit((int)frame->arg1);
case SYS_GPTADD:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Readable<GptAddParams>(frame->arg1)) return -1;
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 SYS_ERR_PERMISSION;
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 SYS_ERR_PERMISSION;
return Sys_FsSync();
case SYS_FSFORMAT:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return -1;
return (int64_t)Sys_FsFormat((const FsFormatParams*)frame->arg1);
case SYS_AUDIOOPEN:
@@ -517,6 +518,13 @@ namespace montauk::abi {
case SYS_GETCAPS:
if (!UserMemory::Writable<SpawnCapabilities>(frame->arg1)) return -1;
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 -1;
return Sched::Consent::Next((ConsentRequest*)frame->arg1);
case SYS_CONSENT_REPLY:
return Sched::Consent::Reply(frame->arg1, (int)frame->arg2);
case SYS_THREAD_SPAWN:
return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3);
case SYS_THREAD_EXIT:
@@ -560,7 +568,7 @@ namespace montauk::abi {
if (!UserMemory::Writable<BtAdapterInfo>(frame->arg1)) return -1;
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);
@@ -568,22 +576,22 @@ 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);
@@ -595,7 +603,7 @@ namespace montauk::abi {
if (!Sched::HasCapability(CAP_SUSPEND)) return SYS_ERR_PERMISSION;
return Sys_Suspend();
case SYS_SETTZ:
if (!Sched::HasCapability(CAP_SET_TIME)) return SYS_ERR_PERMISSION;
if (!Sched::Consent::Require(CAP_SET_TIME)) return SYS_ERR_PERMISSION;
return Sys_SetTZ((int32_t)frame->arg1);
case SYS_GETTZ:
return Sys_GetTZ();
+44 -9
View File
@@ -352,6 +352,13 @@ 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. */
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
/* 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;
@@ -396,26 +403,55 @@ namespace montauk::abi {
"login grants it to the desktop directly");
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;
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
@@ -741,7 +777,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;
};
+5
View File
@@ -28,6 +28,7 @@ namespace Fs {
{"/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},
{"/config/consent.toml", false, montauk::abi::CAP_USER_ADMIN},
{"/config/wallpaper.cache", false, montauk::abi::CAP_USER_ADMIN},
// Read by the Bluetooth driver at controller bring-up.
{"/config/bluetooth.toml", false, montauk::abi::CAP_DEVICE_ADMIN},
@@ -87,6 +88,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 "
+5
View File
@@ -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();
+302
View File
@@ -0,0 +1,302 @@
/*
* 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. The request goes to the session's consent
// agent (the desktop, which holds CAP_COMPOSITOR), which answers from its
// stored decisions or by showing a prompt.
//
// This keeps both capability invariants:
// 1. Authority only narrows down the process tree. A grant is bounded by the
// process's consent ceiling -- what its parent could have delegated to it
// at spawn -- and by the agent's own delegable set, and is only ever added
// to the effective set. Consent defers a delegation; it never exceeds one.
// 2. Capabilities are kernel-owned. The kernel identifies the program by the
// path it loaded, never by a user name, and tells the agent whether that
// path is protected by CAP_SYSTEM_IMAGE (only then may it be remembered).
//
// Lock order: g_lock, then the scheduler lock (inside the Sched helpers).
// Nothing takes g_lock while holding the scheduler lock; the wait predicate
// that runs under the scheduler lock only reads atomics.
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},
};
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 SYS_ERR_PERMISSION;
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 -1;
}
g_agentPid = pid;
g_lock.Release();
Kt::KernelLogStream(Kt::INFO, "Consent") << "Agent registered: pid " << (uint64_t)pid;
return 0;
}
int Next(ConsentRequest* out) {
int agent = GetCurrentPid();
if (agent < 0 || agent != g_agentPid) return SYS_ERR_PERMISSION;
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 SYS_ERR_PERMISSION;
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 -1;
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 0;
}
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";
}
}
}
+34
View File
@@ -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);
}
+101 -4
View File
@@ -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;
@@ -349,6 +352,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;
@@ -558,11 +575,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 +585,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
@@ -1638,6 +1664,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);
@@ -2104,6 +2134,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];
+18 -1
View File
@@ -91,12 +91,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
@@ -276,6 +280,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