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
@@ -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