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
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 239
|
||||
#define MONTAUK_BUILD_NUMBER 251
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user