feat: implement kernel capability model
This commit is contained in:
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 155
|
||||
#define MONTAUK_BUILD_NUMBER 168
|
||||
|
||||
@@ -17,8 +17,14 @@
|
||||
#include <Ipc/Ipc.hpp>
|
||||
#include <Timekeeping/Time.hpp>
|
||||
#include "Path.hpp"
|
||||
#include <Fs/ProtectedPaths.hpp>
|
||||
|
||||
namespace montauk::abi {
|
||||
static bool CanModifyFilePath(const char* resolved) {
|
||||
uint64_t required = Fs::RequiredFileWriteCapability(resolved);
|
||||
return required == 0 || Sched::HasCapability(required);
|
||||
}
|
||||
|
||||
static int Sys_Open(const char* path) {
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
|
||||
@@ -108,12 +114,14 @@ namespace montauk::abi {
|
||||
static int Sys_FCreate(const char* path) {
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
|
||||
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
|
||||
return Ipc::CreateFileHandle(resolved);
|
||||
}
|
||||
|
||||
static int Sys_FDelete(const char* path) {
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
|
||||
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
|
||||
return Fs::Vfs::VfsDelete(resolved);
|
||||
}
|
||||
|
||||
@@ -138,6 +146,9 @@ namespace montauk::abi {
|
||||
bool useCurrent) {
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
|
||||
// Timestamps are file state like any other: a protected path must not
|
||||
// be mutable through a side door that skips the write check.
|
||||
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
|
||||
if (useCurrent) {
|
||||
int64_t now = Timekeeping::GetUnixTimestamp();
|
||||
atime = now;
|
||||
@@ -149,6 +160,7 @@ namespace montauk::abi {
|
||||
static int Sys_FMkdir(const char* path) {
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
|
||||
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
|
||||
return Fs::Vfs::VfsMkdir(resolved);
|
||||
}
|
||||
|
||||
@@ -157,6 +169,8 @@ namespace montauk::abi {
|
||||
char resolvedNew[256];
|
||||
if (!ResolveProcessPath(oldPath, resolvedOld, sizeof(resolvedOld))) return -1;
|
||||
if (!ResolveProcessPath(newPath, resolvedNew, sizeof(resolvedNew))) return -1;
|
||||
if (!CanModifyFilePath(resolvedOld) || !CanModifyFilePath(resolvedNew))
|
||||
return SYS_ERR_PERMISSION;
|
||||
return Fs::Vfs::VfsRename(resolvedOld, resolvedNew);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <Memory/UserRange.hpp>
|
||||
#include <cstdint>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
@@ -155,7 +156,7 @@ namespace montauk::abi {
|
||||
// user TLB entry can otherwise corrupt the frame's next owner.
|
||||
while (released != nullptr) {
|
||||
HeapAlloc* next = released->next;
|
||||
Ipc::UnmapAndFreeUserRange(proc->pml4Phys, released->va,
|
||||
Memory::UnmapAndFreeUserRange(proc->pml4Phys, released->va,
|
||||
released->numPages);
|
||||
Sched::ReleaseUserHeapRange(slot, released->va,
|
||||
released->numPages * 0x1000ULL);
|
||||
@@ -218,7 +219,7 @@ namespace montauk::abi {
|
||||
resident++;
|
||||
g_heapLocks[slot].Release();
|
||||
|
||||
Ipc::UnmapAndFreeUserRange(proc->pml4Phys, addr, pages);
|
||||
Memory::UnmapAndFreeUserRange(proc->pml4Phys, addr, pages);
|
||||
Sched::ReleaseUserHeapRange(slot, addr, size);
|
||||
g_heapLocks[slot].Acquire();
|
||||
Sched::g_allocatedPages[slot] -= resident;
|
||||
@@ -308,7 +309,7 @@ namespace montauk::abi {
|
||||
}
|
||||
}
|
||||
g_heapLocks[slot].Release();
|
||||
Ipc::ShootdownUserRange(proc->pml4Phys, addr, (uint32_t)pages);
|
||||
Memory::ShootdownUserRange(proc->pml4Phys, addr, (uint32_t)pages);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace montauk::abi {
|
||||
for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i];
|
||||
outInfo->osVersion[5] = '\0';
|
||||
|
||||
outInfo->apiVersion = 10;
|
||||
outInfo->apiVersion = 11;
|
||||
outInfo->maxProcesses = Sched::MaxProcesses;
|
||||
outInfo->buildNumber = MONTAUK_BUILD_NUMBER;
|
||||
}
|
||||
|
||||
@@ -15,12 +15,15 @@ namespace montauk::abi {
|
||||
|
||||
static constexpr uint32_t RedirOutputStreamCapacity = 64 * 1024;
|
||||
|
||||
static int Sys_SpawnRedir(const char* path, const char* args) {
|
||||
static int Sys_SpawnRedirInternal(
|
||||
const char* path, const char* args,
|
||||
const SpawnCapabilities* capabilities) {
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
|
||||
|
||||
int parentSlot = Ipc::CurrentSlot();
|
||||
int childPid = Sched::Spawn(resolved, args, false);
|
||||
int childPid = Sched::Spawn(resolved, args, false, nullptr, 0,
|
||||
capabilities);
|
||||
if (childPid < 0) return -1;
|
||||
|
||||
auto* child = Sched::GetProcessByPid(childPid);
|
||||
@@ -83,28 +86,50 @@ namespace montauk::abi {
|
||||
return childPid;
|
||||
}
|
||||
|
||||
static int Sys_SpawnRedir(const char* path, const char* args) {
|
||||
return Sys_SpawnRedirInternal(path, args, nullptr);
|
||||
}
|
||||
|
||||
static int Sys_SpawnRedirCaps(const char* path, const char* args,
|
||||
const SpawnCapabilities* requested) {
|
||||
auto* parent = Sched::GetCurrentProcessPtr();
|
||||
if (parent == nullptr || requested == nullptr) return -1;
|
||||
|
||||
SpawnCapabilities copy = *requested;
|
||||
if (!ValidCapabilityDelegation(copy, parent->delegableCaps)) {
|
||||
return SYS_ERR_PERMISSION;
|
||||
}
|
||||
return Sys_SpawnRedirInternal(path, args, ©);
|
||||
}
|
||||
|
||||
static int Sys_ChildIoRead(int childPid, char* buf, int maxLen) {
|
||||
auto* child = Sched::GetProcessByPid(childPid);
|
||||
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
||||
return SYS_ERR_PERMISSION;
|
||||
Ipc::HandleSnapshot snapshot;
|
||||
Ipc::Stream* stream = GetRedirOutStream(child, snapshot);
|
||||
if (child == nullptr || !child->redirected || stream == nullptr) return -1;
|
||||
if (!child->redirected || stream == nullptr) return -1;
|
||||
return Ipc::StreamRead(stream, (uint8_t*)buf, maxLen, true);
|
||||
}
|
||||
|
||||
static int Sys_ChildIoWrite(int childPid, const char* data, int len) {
|
||||
auto* child = Sched::GetProcessByPid(childPid);
|
||||
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
||||
return SYS_ERR_PERMISSION;
|
||||
Ipc::HandleSnapshot snapshot;
|
||||
Ipc::Stream* stream = GetRedirInStream(child, snapshot);
|
||||
if (child == nullptr || !child->redirected || stream == nullptr) return -1;
|
||||
if (!child->redirected || stream == nullptr) return -1;
|
||||
return WriteAllToStream(stream, (const uint8_t*)data, len);
|
||||
}
|
||||
|
||||
static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) {
|
||||
if (key == nullptr) return -1;
|
||||
auto* child = Sched::GetProcessByPid(childPid);
|
||||
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
||||
return SYS_ERR_PERMISSION;
|
||||
Ipc::HandleSnapshot snapshot;
|
||||
Ipc::Mailbox* mailbox = GetRedirKeyMailbox(child, snapshot);
|
||||
if (child == nullptr || !child->redirected || mailbox == nullptr) return -1;
|
||||
if (!child->redirected || mailbox == nullptr) return -1;
|
||||
|
||||
for (;;) {
|
||||
uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
|
||||
@@ -120,7 +145,9 @@ namespace montauk::abi {
|
||||
|
||||
static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) {
|
||||
auto* child = Sched::GetProcessByPid(childPid);
|
||||
if (child == nullptr || !child->redirected) return -1;
|
||||
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
|
||||
return SYS_ERR_PERMISSION;
|
||||
if (!child->redirected) return -1;
|
||||
child->termCols = cols;
|
||||
child->termRows = rows;
|
||||
return 0;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Memory/UserRange.hpp>
|
||||
#include <cstdint>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Sched/ElfLoader.hpp>
|
||||
@@ -133,7 +134,7 @@ namespace montauk::abi {
|
||||
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc != nullptr) {
|
||||
Ipc::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
|
||||
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
|
||||
(libEnd - libBase) / 0x1000ULL);
|
||||
}
|
||||
|
||||
@@ -202,7 +203,7 @@ namespace montauk::abi {
|
||||
uint64_t libBase = GetLibSlotBase(i);
|
||||
uint64_t libEnd = libBase + Sched::LIB_MAX_SIZE;
|
||||
|
||||
Ipc::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
|
||||
Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
|
||||
(libEnd - libBase) / 0x1000ULL);
|
||||
|
||||
g_libTable[slot][i].inUse = false;
|
||||
|
||||
@@ -31,6 +31,11 @@ namespace montauk::abi {
|
||||
// action; any other value records it as the pending action. Returns the
|
||||
// pending action for queries, or 0 when recording one.
|
||||
static int64_t Sys_PowerRequest(int action) {
|
||||
// Polled by the session leader every second; deliberately silent and
|
||||
// non-destructive, so it neither floods the log nor races login for
|
||||
// the request it is about to hand over by exiting.
|
||||
if (action == POWER_REQ_PEEK) return (int64_t)g_pendingPowerAction;
|
||||
|
||||
if (action == POWER_REQ_QUERY) {
|
||||
int pending = g_pendingPowerAction;
|
||||
g_pendingPowerAction = POWER_REQ_QUERY;
|
||||
|
||||
@@ -86,6 +86,39 @@ namespace montauk::abi {
|
||||
return childPid;
|
||||
}
|
||||
|
||||
static int Sys_SpawnCaps(const char* path, const char* args,
|
||||
const char* user, const SpawnCapabilities* requested) {
|
||||
auto* parent = Sched::GetCurrentProcessPtr();
|
||||
if (parent == nullptr || requested == nullptr) return -1;
|
||||
|
||||
// Snapshot all security-sensitive userspace inputs before evaluating
|
||||
// them. This prevents another thread from changing a mask or owner
|
||||
// name between validation and process creation.
|
||||
SpawnCapabilities copy = *requested;
|
||||
char childUser[32];
|
||||
const char* userOverride = nullptr;
|
||||
if (user != nullptr) {
|
||||
if (!Sched::HasCapability(CAP_USER_ADMIN))
|
||||
return SYS_ERR_PERMISSION;
|
||||
int i = 0;
|
||||
for (; i < 31 && user[i]; i++) childUser[i] = user[i];
|
||||
childUser[i] = '\0';
|
||||
userOverride = childUser;
|
||||
}
|
||||
|
||||
// Authority may only diminish down the process tree. In particular,
|
||||
// possessing a capability is insufficient to pass it: the parent must
|
||||
// also hold it in its delegable set.
|
||||
if (!ValidCapabilityDelegation(copy, parent->delegableCaps)) {
|
||||
return SYS_ERR_PERMISSION;
|
||||
}
|
||||
|
||||
char resolved[256];
|
||||
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
|
||||
return Sched::Spawn(resolved, args, true, nullptr, 0, ©,
|
||||
userOverride);
|
||||
}
|
||||
|
||||
// Copy the absolute path this process was spawned from (argv[0]).
|
||||
static int Sys_GetExecPath(char* buf, uint64_t maxLen) {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
@@ -150,12 +183,29 @@ 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++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static int Sys_Kill(int pid) {
|
||||
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) {
|
||||
int ancestor = pid;
|
||||
bool descendant = false;
|
||||
for (int depth = 0; depth < Sched::MaxProcesses; depth++) {
|
||||
auto* target = Sched::GetProcessByPid(ancestor);
|
||||
if (target == nullptr || target->parentPid < 0) break;
|
||||
if (target->parentPid == Sched::GetCurrentPid()) {
|
||||
descendant = true;
|
||||
break;
|
||||
}
|
||||
ancestor = target->parentPid;
|
||||
}
|
||||
if (!descendant) return SYS_ERR_PERMISSION;
|
||||
}
|
||||
return Sched::KillProcess(pid);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,16 @@ namespace montauk::abi {
|
||||
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
|
||||
return (int64_t)Sys_Spawn((const char*)frame->arg1,
|
||||
UserMemory::IsUserPtr(frame->arg2) ? (const char*)frame->arg2 : nullptr);
|
||||
case SYS_SPAWN_CAPS:
|
||||
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
|
||||
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
|
||||
if (frame->arg3 != 0 && !UserMemory::String(frame->arg3, 32)) return -1;
|
||||
if (!UserMemory::Readable<SpawnCapabilities>(frame->arg4)) return -1;
|
||||
return (int64_t)Sys_SpawnCaps(
|
||||
(const char*)frame->arg1,
|
||||
frame->arg2 ? (const char*)frame->arg2 : nullptr,
|
||||
frame->arg3 ? (const char*)frame->arg3 : nullptr,
|
||||
(const SpawnCapabilities*)frame->arg4);
|
||||
case SYS_SPAWN_ENV:
|
||||
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
|
||||
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
|
||||
@@ -188,8 +198,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;
|
||||
return Sys_DisplaySetMode((int)frame->arg1);
|
||||
case SYS_DISPLAYBRIGHTNESS:
|
||||
if ((int64_t)frame->arg1 >= 0 &&
|
||||
!Sched::HasCapability(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;
|
||||
@@ -210,18 +223,30 @@ namespace montauk::abi {
|
||||
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
|
||||
return (int64_t)Sys_GetArgs((char*)frame->arg1, frame->arg2);
|
||||
case SYS_RESET:
|
||||
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return SYS_ERR_PERMISSION;
|
||||
Sys_Reset();
|
||||
return 0;
|
||||
case SYS_SHUTDOWN:
|
||||
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return SYS_ERR_PERMISSION;
|
||||
Sys_Shutdown();
|
||||
return 0;
|
||||
case SYS_POWER_REQUEST:
|
||||
if (frame->arg1 != POWER_REQ_QUERY &&
|
||||
frame->arg1 != POWER_REQ_SHUTDOWN &&
|
||||
frame->arg1 != POWER_REQ_REBOOT &&
|
||||
frame->arg1 != POWER_REQ_PEEK) return -1;
|
||||
if (frame->arg1 == POWER_REQ_QUERY) {
|
||||
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return SYS_ERR_PERMISSION;
|
||||
} else if (!Sched::HasCapability(CAP_POWER_REQUEST)) {
|
||||
return SYS_ERR_PERMISSION;
|
||||
}
|
||||
return Sys_PowerRequest((int)frame->arg1);
|
||||
case SYS_GETTIME:
|
||||
if (!UserMemory::Writable<DateTime>(frame->arg1)) return -1;
|
||||
Sys_GetTime((DateTime*)frame->arg1);
|
||||
return 0;
|
||||
case SYS_SETUNIXTIME:
|
||||
if (!Sched::HasCapability(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);
|
||||
@@ -247,6 +272,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 (!UserMemory::Readable<NetCfg>(frame->arg1)) return -1;
|
||||
return (int64_t)Sys_SetNetCfg((const NetCfg*)frame->arg1);
|
||||
case SYS_NETSTATUS:
|
||||
@@ -302,6 +328,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 (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
|
||||
return Kt::ReadKernelLogBuffer((char*)frame->arg1, frame->arg2);
|
||||
case SYS_MOUSESTATE:
|
||||
@@ -316,6 +343,14 @@ namespace montauk::abi {
|
||||
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
|
||||
return (int64_t)Sys_SpawnRedir((const char*)frame->arg1,
|
||||
UserMemory::IsUserPtr(frame->arg2) ? (const char*)frame->arg2 : nullptr);
|
||||
case SYS_SPAWN_REDIR_CAPS:
|
||||
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
|
||||
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
|
||||
if (!UserMemory::Readable<SpawnCapabilities>(frame->arg3)) return -1;
|
||||
return (int64_t)Sys_SpawnRedirCaps(
|
||||
(const char*)frame->arg1,
|
||||
frame->arg2 ? (const char*)frame->arg2 : nullptr,
|
||||
(const SpawnCapabilities*)frame->arg3);
|
||||
case SYS_CHILDIO_READ:
|
||||
if ((int64_t)frame->arg3 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, true)) return -1;
|
||||
@@ -363,6 +398,7 @@ namespace montauk::abi {
|
||||
case SYS_SETSESSION:
|
||||
return (int64_t)Sys_SetSession();
|
||||
case SYS_KILLSESSION:
|
||||
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
return (int64_t)Sys_KillSession((int)frame->arg1);
|
||||
case SYS_DEVLIST:
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
@@ -388,21 +424,28 @@ namespace montauk::abi {
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(PartInfo), true)) return -1;
|
||||
return (int64_t)Sys_PartList((PartInfo*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_DISKREAD:
|
||||
if (!Sched::HasCapability(CAP_RAW_STORAGE)) return SYS_ERR_PERMISSION;
|
||||
return (int64_t)Sys_DiskRead((int)frame->arg1, frame->arg2,
|
||||
(uint32_t)frame->arg3, (void*)frame->arg4);
|
||||
case SYS_DISKWRITE:
|
||||
if (!Sched::HasCapability(CAP_RAW_STORAGE)) return SYS_ERR_PERMISSION;
|
||||
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;
|
||||
return (int64_t)Sys_GptInit((int)frame->arg1);
|
||||
case SYS_GPTADD:
|
||||
if (!Sched::HasCapability(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;
|
||||
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;
|
||||
return Sys_FsSync();
|
||||
case SYS_FSFORMAT:
|
||||
if (!Sched::HasCapability(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:
|
||||
@@ -428,6 +471,7 @@ namespace montauk::abi {
|
||||
return Sys_UsbList((UsbInterfaceInfo*)frame->arg1, (int)maxCount);
|
||||
}
|
||||
case SYS_USB_CLAIM:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (frame->arg1 == 0 || frame->arg1 > 16 || frame->arg2 > 255)
|
||||
return USB_ERR_INVALID;
|
||||
return Sys_UsbClaim((uint8_t)frame->arg1, (uint8_t)frame->arg2);
|
||||
@@ -469,16 +513,20 @@ namespace montauk::abi {
|
||||
case SYS_THREAD_SELF:
|
||||
return Sys_ThreadSelf();
|
||||
case SYS_BTSCAN:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if ((int64_t)frame->arg2 < 0) return -1;
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) return -1;
|
||||
return Sys_BtScan((BtScanResult*)frame->arg1, (int)frame->arg2, (uint32_t)frame->arg3);
|
||||
case SYS_BTCONNECT:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
||||
return Sys_BtConnect((const uint8_t*)frame->arg1);
|
||||
case SYS_BTDISCONNECT:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
||||
return Sys_BtDisconnect((const uint8_t*)frame->arg1);
|
||||
case SYS_BTSETADDR:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
||||
return Sys_BtSetAddr((const uint8_t*)frame->arg1);
|
||||
case SYS_BTBONDS:
|
||||
@@ -486,6 +534,7 @@ namespace montauk::abi {
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtBondInfo), true)) return -1;
|
||||
return Sys_BtBonds((BtBondInfo*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_BTFORGET:
|
||||
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
|
||||
return Sys_BtForget((const uint8_t*)frame->arg1);
|
||||
case SYS_BTLIST:
|
||||
@@ -496,6 +545,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 ((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);
|
||||
@@ -503,18 +553,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 (!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;
|
||||
return Sys_WifiDisconnect();
|
||||
case SYS_WIFI_SCAN_START:
|
||||
if (!Sched::HasCapability(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 (!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);
|
||||
@@ -523,12 +577,15 @@ namespace montauk::abi {
|
||||
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(NetIfInfo), true)) return -1;
|
||||
return Sys_NetIfs((NetIfInfo*)frame->arg1, (int)frame->arg2);
|
||||
case SYS_SUSPEND:
|
||||
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;
|
||||
return Sys_SetTZ((int32_t)frame->arg1);
|
||||
case SYS_GETTZ:
|
||||
return Sys_GetTZ();
|
||||
case SYS_SETUSER:
|
||||
if (!Sched::HasCapability(CAP_USER_ADMIN)) return SYS_ERR_PERMISSION;
|
||||
if (!UserMemory::String(frame->arg2, kMaxUserNameBytes)) return -1;
|
||||
return Sys_SetUser((int)frame->arg1, (const char*)frame->arg2);
|
||||
case SYS_GETUSER:
|
||||
@@ -670,7 +727,7 @@ namespace montauk::abi {
|
||||
|
||||
Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
|
||||
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", "
|
||||
<< (SYS_USB_BULK_IN_READ + 1) << " syscall slots)";
|
||||
<< (SYS_SPAWN_REDIR_CAPS + 1) << " syscall slots)";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -335,6 +335,63 @@ namespace montauk::abi {
|
||||
static constexpr uint64_t SYS_USB_BULK_IN_START = 182; // (handle, transferBytes, buffers)
|
||||
static constexpr uint64_t SYS_USB_BULK_IN_STOP = 183; // (handle)
|
||||
static constexpr uint64_t SYS_USB_BULK_IN_READ = 184; // (handle, data, len) -> bytes
|
||||
static constexpr uint64_t SYS_SPAWN_CAPS = 185;
|
||||
static constexpr uint64_t SYS_SPAWN_REDIR_CAPS = 186;
|
||||
|
||||
/* 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;
|
||||
static constexpr uint64_t CAP_POWER_REQUEST = 1ULL << 1;
|
||||
static constexpr uint64_t CAP_POWER_CONTROL = 1ULL << 2;
|
||||
static constexpr uint64_t CAP_SUSPEND = 1ULL << 3;
|
||||
static constexpr uint64_t CAP_STORAGE_ADMIN = 1ULL << 4;
|
||||
static constexpr uint64_t CAP_RAW_STORAGE = 1ULL << 5;
|
||||
static constexpr uint64_t CAP_NETWORK_ADMIN = 1ULL << 6;
|
||||
static constexpr uint64_t CAP_SET_TIME = 1ULL << 7;
|
||||
static constexpr uint64_t CAP_USER_ADMIN = 1ULL << 8;
|
||||
static constexpr uint64_t CAP_DISPLAY_ADMIN = 1ULL << 9;
|
||||
static constexpr uint64_t CAP_DEVICE_ADMIN = 1ULL << 10;
|
||||
static constexpr uint64_t CAP_LOG_READ = 1ULL << 11;
|
||||
/* Write to the program images the system boots and runs (0:/os,
|
||||
0:/apps). Deliberately separate from CAP_STORAGE_ADMIN: grants are
|
||||
keyed on binary path, so writing an image is equivalent to acquiring
|
||||
whatever that image is granted at its next launch. Formatting a data
|
||||
volume must not carry that authority with it. */
|
||||
static constexpr uint64_t CAP_SYSTEM_IMAGE = 1ULL << 12;
|
||||
static constexpr uint64_t CAP_ALL = (1ULL << 13) - 1;
|
||||
static constexpr uint64_t CAP_STANDARD_SESSION = CAP_POWER_REQUEST | CAP_SUSPEND;
|
||||
static constexpr uint64_t CAP_ADMIN_SESSION =
|
||||
CAP_STANDARD_SESSION | CAP_PROCESS_ADMIN | CAP_STORAGE_ADMIN |
|
||||
CAP_RAW_STORAGE | CAP_NETWORK_ADMIN | CAP_SET_TIME | CAP_USER_ADMIN |
|
||||
CAP_DISPLAY_ADMIN | CAP_DEVICE_ADMIN | CAP_LOG_READ;
|
||||
static_assert((CAP_STANDARD_SESSION & ~CAP_ADMIN_SESSION) == 0);
|
||||
static_assert((CAP_ADMIN_SESSION & CAP_POWER_CONTROL) == 0,
|
||||
"final power control belongs only to the session supervisor");
|
||||
static_assert((CAP_ADMIN_SESSION & CAP_SYSTEM_IMAGE) == 0,
|
||||
"an admin session must not imply authority to rewrite the "
|
||||
"programs it launches; grant CAP_SYSTEM_IMAGE per binary");
|
||||
static constexpr int SYS_ERR_PERMISSION = -13;
|
||||
|
||||
struct SpawnCapabilities {
|
||||
uint64_t permitted;
|
||||
uint64_t effective;
|
||||
uint64_t delegable;
|
||||
};
|
||||
|
||||
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 &&
|
||||
(child.delegable & ~parentDelegable) == 0;
|
||||
}
|
||||
static_assert(ValidCapabilityDelegation(
|
||||
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN, 0}, CAP_NETWORK_ADMIN));
|
||||
static_assert(!ValidCapabilityDelegation(
|
||||
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN}, 0));
|
||||
static_assert(!ValidCapabilityDelegation(
|
||||
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN | CAP_SET_TIME, 0}, CAP_ALL));
|
||||
|
||||
|
||||
// Generic USB errors. Claims are restricted to interfaces without a
|
||||
@@ -351,10 +408,17 @@ namespace montauk::abi {
|
||||
// Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
|
||||
// a pending action and exits; login.elf reads it, runs the shutdown stages,
|
||||
// then issues the matching SYS_SHUTDOWN / SYS_RESET.
|
||||
//
|
||||
// A request can also be posted from inside the session -- the shell's
|
||||
// shutdown builtin does. login only looks at it once the session leader
|
||||
// exits, so the leader has to notice and stand down: POWER_REQ_PEEK is the
|
||||
// non-destructive read it polls with. Only login consumes (QUERY), so a
|
||||
// leader that peeks cannot swallow the request it is meant to act on.
|
||||
enum PowerRequestAction : int {
|
||||
POWER_REQ_QUERY = 0, // read-and-clear the pending action
|
||||
POWER_REQ_SHUTDOWN = 1,
|
||||
POWER_REQ_REBOOT = 2,
|
||||
POWER_REQ_PEEK = 3, // read the pending action without clearing it
|
||||
};
|
||||
|
||||
static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
|
||||
@@ -631,6 +695,9 @@ 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;
|
||||
};
|
||||
|
||||
// Bluetooth scan result (returned by SYS_BTSCAN)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* Further copyright information and third party notices can be found at https://montaukos.org/license.txt.
|
||||
*/
|
||||
|
||||
#include <Fs/ProtectedPaths.hpp>
|
||||
#include <Memory/UserRange.hpp>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <Boot/Boot.hpp>
|
||||
@@ -174,6 +176,8 @@ extern "C" void kmain() {
|
||||
montauk::abi::InitializeSyscalls();
|
||||
|
||||
Sched::Initialize();
|
||||
Memory::InitUserRange();
|
||||
Fs::LogProtectedPaths();
|
||||
Ipc::Initialize();
|
||||
|
||||
#if defined (__x86_64__)
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* ProtectedPaths.cpp
|
||||
* Capability required to modify paths on the booted system volume
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*
|
||||
* Split out of Ipc.cpp: this is filesystem security policy, not IPC. It
|
||||
* lived there only because OpenFileHandleForSlot was its first caller.
|
||||
*/
|
||||
|
||||
#include "ProtectedPaths.hpp"
|
||||
|
||||
#include <Api/Syscall.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
|
||||
namespace Fs {
|
||||
|
||||
// ==== Protected system paths ====
|
||||
// The capability required to modify (create, write, delete or rename) a
|
||||
// path. The kernel only ever enumerates paths here; it never parses a
|
||||
// policy file. Userspace grant policy lives in 0:/config/capabilities.toml
|
||||
// and can only ever narrow what the kernel already delegated, so no input
|
||||
// to that file can produce authority this table does not already allow.
|
||||
//
|
||||
// Rules apply ONLY to the system volume. Drive 0 is always the boot
|
||||
// ramdisk (Fs/Boot.cpp registers it unconditionally); every other drive is
|
||||
// a partition discovered at probe time, in probe order. A user data disk
|
||||
// that happens to contain an "apps" or "config" directory must not inherit
|
||||
// system protection, and an installed system's files on another volume are
|
||||
// inert data until that disk is booted -- at which point its contents are
|
||||
// themselves the drive-0 ramdisk.
|
||||
// Anything the kernel itself reads belongs here:
|
||||
// guarding only the syscall leaves the file as an unguarded second path to
|
||||
// the same state (bluetooth.toml feeds the BD_ADDR override at bring-up).
|
||||
struct ProtectedPath {
|
||||
const char* pattern; // drive-relative, leading '/'
|
||||
bool prefix; // also match everything beneath the pattern
|
||||
uint64_t capability;
|
||||
};
|
||||
|
||||
static constexpr ProtectedPath g_protectedPaths[] = {
|
||||
// Authentication, first-boot administrator creation, trusted service
|
||||
// activation and capability grants. Readable by anyone; writable only
|
||||
// with administrative authority. The bare directory is listed so it
|
||||
// cannot be renamed or deleted out from under the files inside it.
|
||||
{"/config", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/config/users.toml", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/config/setup.toml", false, montauk::abi::CAP_USER_ADMIN},
|
||||
{"/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},
|
||||
// Read by the Bluetooth driver at controller bring-up.
|
||||
{"/config/bluetooth.toml", false, montauk::abi::CAP_DEVICE_ADMIN},
|
||||
// Program images. Capability grants are keyed on binary path, so a
|
||||
// writable image would let an unprivileged process substitute a binary
|
||||
// and inherit the grant the next time a privileged launcher runs it.
|
||||
// This is CAP_SYSTEM_IMAGE and not CAP_STORAGE_ADMIN precisely because
|
||||
// it is the trusted computing base: partitioning and formatting a data
|
||||
// volume is an ordinary administrative act, while replacing the image
|
||||
// of login.elf is a route to every capability the system can issue.
|
||||
{"/apps", true, montauk::abi::CAP_SYSTEM_IMAGE},
|
||||
{"/os", true, montauk::abi::CAP_SYSTEM_IMAGE},
|
||||
};
|
||||
|
||||
static char LowerAscii(char c) {
|
||||
return (c >= 'A' && c <= 'Z') ? (char)(c + ('a' - 'A')) : c;
|
||||
}
|
||||
|
||||
// The volume the running system was booted from.
|
||||
static constexpr uint64_t SystemDrive = 0;
|
||||
|
||||
// Strip the "<digits>:" prefix, but only for the system volume. Returns
|
||||
// nullptr for any other drive, meaning no rule applies to it.
|
||||
static const char* SystemRelativePath(const char* path) {
|
||||
if (path == nullptr) return nullptr;
|
||||
|
||||
const char* p = path;
|
||||
if (*p < '0' || *p > '9') return nullptr;
|
||||
|
||||
uint64_t drive = 0;
|
||||
while (*p >= '0' && *p <= '9') {
|
||||
drive = drive * 10 + (uint64_t)(*p - '0');
|
||||
if (drive > 0xFFFF) return nullptr; // absurd; cannot be a drive
|
||||
p++;
|
||||
}
|
||||
if (*p != ':' || drive != SystemDrive) return nullptr;
|
||||
return p + 1;
|
||||
}
|
||||
|
||||
// Case-insensitive: FAT32 resolves differing cases to the same file, so a
|
||||
// case-sensitive rule would be trivially sidestepped.
|
||||
static bool ProtectedPathMatches(const char* path, const ProtectedPath& rule) {
|
||||
const char* p = path;
|
||||
const char* q = rule.pattern;
|
||||
while (*q) {
|
||||
if (LowerAscii(*p) != LowerAscii(*q)) return false;
|
||||
p++;
|
||||
q++;
|
||||
}
|
||||
if (*p == '\0') return true; // the pattern itself
|
||||
return rule.prefix && *p == '/'; // something beneath it
|
||||
}
|
||||
|
||||
uint64_t RequiredFileWriteCapability(const char* path) {
|
||||
const char* relative = SystemRelativePath(path);
|
||||
if (relative == nullptr) return 0; // not the system volume
|
||||
|
||||
// Overlapping rules accumulate: HasCapability() requires every bit, so
|
||||
// a path covered by two rules demands both.
|
||||
uint64_t required = 0;
|
||||
for (const auto& rule : g_protectedPaths) {
|
||||
if (ProtectedPathMatches(relative, rule)) required |= rule.capability;
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
void LogProtectedPaths() {
|
||||
for (const auto& rule : g_protectedPaths) {
|
||||
Kt::KernelLogStream(Kt::INFO, "IPC") << "Protected path "
|
||||
<< rule.pattern << (rule.prefix ? "/* " : " ")
|
||||
<< "requires capability mask "
|
||||
<< kcp::hex << rule.capability << kcp::dec;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* ProtectedPaths.hpp
|
||||
* Capability required to modify paths on the booted system volume
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Fs {
|
||||
|
||||
// Capability a process must hold to create, write, delete, rename or
|
||||
// re-timestamp `path`. Returns 0 when the path is unprotected.
|
||||
uint64_t RequiredFileWriteCapability(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();
|
||||
|
||||
}
|
||||
+72
-148
@@ -8,11 +8,13 @@
|
||||
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Fs/ProtectedPaths.hpp>
|
||||
#include <Net/Tcp.hpp>
|
||||
#include <Net/Udp.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/UserRange.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Hal/Apic/Apic.hpp>
|
||||
@@ -82,6 +84,7 @@ namespace Ipc {
|
||||
|
||||
struct File : Object {
|
||||
Fs::Vfs::BackendFile backend;
|
||||
uint64_t writeCapability;
|
||||
};
|
||||
|
||||
struct UdpDgramHeader {
|
||||
@@ -178,149 +181,11 @@ namespace Ipc {
|
||||
|
||||
static void ReleaseRawObject(Object* object);
|
||||
|
||||
// MUST be a Mutex, never a Spinlock. ShootdownUserRange holds this while
|
||||
// waiting for remote CPUs to acknowledge the shootdown IPI, so a CPU that
|
||||
// is queued behind the holder has to stay interruptible long enough to
|
||||
// service that IPI itself. An interrupt-disabling Spinlock here deadlocks
|
||||
// every CPU contending for the lock, and the bounded-retry logic below
|
||||
// then reports it as a "target failed to acknowledge" Panic -- which reads
|
||||
// like a hardware fault rather than a lock-type regression.
|
||||
static kcp::Mutex g_tlbShootdownLock;
|
||||
static volatile uint64_t g_tlbShootdownSeq = 0;
|
||||
static volatile uint64_t g_tlbShootdownPml4 = 0;
|
||||
static volatile uint64_t g_tlbShootdownStartVa = 0;
|
||||
static volatile uint32_t g_tlbShootdownPages = 0;
|
||||
static volatile uint64_t g_tlbShootdownDone[Smp::MaxCPUs] = {};
|
||||
|
||||
static bool CpuCurrentlyUsesPml4(Smp::CpuData* cpu, uint64_t pml4Phys) {
|
||||
if (cpu == nullptr || pml4Phys == 0 || cpu->currentSlot < 0) return false;
|
||||
|
||||
Sched::Process* proc = Sched::GetProcessSlot(cpu->currentSlot);
|
||||
if (proc == nullptr) return false;
|
||||
if (proc->state == Sched::ProcessState::Free) return false;
|
||||
return proc->pml4Phys == pml4Phys;
|
||||
}
|
||||
|
||||
static void InvalidateLocalUserRange(uint64_t startVa, uint32_t pages) {
|
||||
if (pages == 0) return;
|
||||
|
||||
if (pages > 1024) {
|
||||
Memory::VMM::FlushTLB();
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32_t p = 0; p < pages; p++) {
|
||||
uint64_t va = startVa + (uint64_t)p * 0x1000ULL;
|
||||
asm volatile("invlpg (%0)" :: "r"(va) : "memory");
|
||||
}
|
||||
}
|
||||
|
||||
static void TlbShootdownIpiHandler(uint8_t, bool) {
|
||||
Smp::CpuData* cpu = Smp::GetCurrentCpuData();
|
||||
uint64_t seq = g_tlbShootdownSeq;
|
||||
uint64_t pml4 = g_tlbShootdownPml4;
|
||||
uint64_t startVa = g_tlbShootdownStartVa;
|
||||
uint32_t pages = g_tlbShootdownPages;
|
||||
|
||||
if (CpuCurrentlyUsesPml4(cpu, pml4)) {
|
||||
InvalidateLocalUserRange(startVa, pages);
|
||||
}
|
||||
|
||||
if (cpu != nullptr && cpu->cpuIndex >= 0 && cpu->cpuIndex < Smp::MaxCPUs) {
|
||||
asm volatile("" ::: "memory");
|
||||
g_tlbShootdownDone[cpu->cpuIndex] = seq;
|
||||
}
|
||||
}
|
||||
|
||||
void ShootdownUserRange(uint64_t pml4Phys, uint64_t startVa, uint32_t pages) {
|
||||
if (pml4Phys == 0 || pages == 0) return;
|
||||
|
||||
bool targets[Smp::MaxCPUs] = {};
|
||||
Smp::CpuData* currentCpu = Smp::GetCurrentCpuData();
|
||||
int currentCpuIndex = currentCpu ? currentCpu->cpuIndex : -1;
|
||||
|
||||
g_tlbShootdownLock.Acquire();
|
||||
uint64_t seq = g_tlbShootdownSeq + 1;
|
||||
g_tlbShootdownPml4 = pml4Phys;
|
||||
g_tlbShootdownStartVa = startVa;
|
||||
g_tlbShootdownPages = pages;
|
||||
asm volatile("" ::: "memory");
|
||||
g_tlbShootdownSeq = seq;
|
||||
|
||||
for (int i = 0; i < Smp::GetCpuCount(); i++) {
|
||||
Smp::CpuData* cpu = Smp::GetCpuData(i);
|
||||
if (cpu == nullptr || !cpu->started) continue;
|
||||
|
||||
if (i == currentCpuIndex) {
|
||||
if (CpuCurrentlyUsesPml4(cpu, pml4Phys)) {
|
||||
InvalidateLocalUserRange(startVa, pages);
|
||||
}
|
||||
g_tlbShootdownDone[i] = seq;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!CpuCurrentlyUsesPml4(cpu, pml4Phys)) {
|
||||
g_tlbShootdownDone[i] = seq;
|
||||
continue;
|
||||
}
|
||||
|
||||
targets[i] = true;
|
||||
(void)Hal::LocalApic::SendFixedIpi(cpu->lapicId,
|
||||
Hal::IRQ_VECTOR_BASE + Hal::IRQ_TLB_SHOOTDOWN);
|
||||
}
|
||||
|
||||
for (int i = 0; i < Smp::GetCpuCount(); i++) {
|
||||
if (!targets[i]) continue;
|
||||
uint32_t spins = 0;
|
||||
uint32_t retries = 0;
|
||||
while (g_tlbShootdownDone[i] != seq) {
|
||||
asm volatile("pause");
|
||||
if (++spins < 1000000) continue;
|
||||
|
||||
// Delivery normally completes in a handful of cycles. Retry a
|
||||
// bounded number of times in case the first IPI was lost while
|
||||
// the target changed interrupt state. Continuing without an
|
||||
// acknowledgement would let the caller free frames still
|
||||
// reachable through a remote stale TLB entry, so fail loudly
|
||||
// instead of either corrupting memory or spinning forever.
|
||||
spins = 0;
|
||||
if (++retries > 4) {
|
||||
Panic("TLB shootdown target failed to acknowledge", nullptr);
|
||||
}
|
||||
Smp::CpuData* cpu = Smp::GetCpuData(i);
|
||||
if (cpu != nullptr && cpu->started) {
|
||||
(void)Hal::LocalApic::SendFixedIpi(cpu->lapicId,
|
||||
Hal::IRQ_VECTOR_BASE + Hal::IRQ_TLB_SHOOTDOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_tlbShootdownLock.Release();
|
||||
}
|
||||
|
||||
void UnmapAndFreeUserRange(uint64_t pml4Phys, uint64_t startVa, uint64_t pages) {
|
||||
static constexpr uint32_t PagesPerChunk = 64;
|
||||
uint64_t physPages[PagesPerChunk];
|
||||
|
||||
for (uint64_t base = 0; base < pages; base += PagesPerChunk) {
|
||||
uint32_t count = (uint32_t)((pages - base > PagesPerChunk)
|
||||
? PagesPerChunk : pages - base);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
uint64_t pageVa = startVa + (base + i) * 0x1000ULL;
|
||||
physPages[i] = Memory::VMM::Paging::GetPhysAddr(pml4Phys, pageVa);
|
||||
Memory::VMM::Paging::UnmapUserIn(pml4Phys, pageVa);
|
||||
}
|
||||
|
||||
ShootdownUserRange(pml4Phys, startVa + base * 0x1000ULL, count);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
if (physPages[i] != 0) {
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(physPages[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ==========================================================================
|
||||
// Object lifetime
|
||||
// Pool allocation, refcounting and type-dispatched teardown.
|
||||
// Every object type routes through this layer.
|
||||
// ==========================================================================
|
||||
|
||||
static void InitObject(Object& object, HandleType type) {
|
||||
object.type = type;
|
||||
@@ -568,6 +433,11 @@ namespace Ipc {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Handle table
|
||||
// Per-process handle installation, rights, duplication and close.
|
||||
// ==========================================================================
|
||||
|
||||
int CurrentSlot() {
|
||||
auto* proc = Sched::GetCurrentProcessPtr();
|
||||
if (proc == nullptr) return -1;
|
||||
@@ -817,6 +687,11 @@ namespace Ipc {
|
||||
return InstallHandleForSlot(slot, snapshot.object, snapshot.type, snapshot.rights);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Streams
|
||||
// Byte pipes.
|
||||
// ==========================================================================
|
||||
|
||||
Stream* CreateStream(uint32_t capacity) {
|
||||
if (capacity == 0) capacity = DefaultStreamCapacity;
|
||||
|
||||
@@ -1000,6 +875,11 @@ namespace Ipc {
|
||||
return hasData;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Mailboxes
|
||||
// Discrete message queues.
|
||||
// ==========================================================================
|
||||
|
||||
Mailbox* CreateMailbox() {
|
||||
g_mailboxPoolLock.Acquire();
|
||||
for (int i = 0; i < MaxMailboxes; i++) {
|
||||
@@ -1260,9 +1140,22 @@ namespace Ipc {
|
||||
return hasMsg;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Files
|
||||
// Write authority is re-checked against the calling process on every
|
||||
// write, so passing a writable handle to a less privileged process does
|
||||
// not transfer the ability to use it. See Fs/ProtectedPaths.cpp.
|
||||
// ==========================================================================
|
||||
|
||||
int OpenFileHandleForSlot(int slot, const char* path, bool create) {
|
||||
if (slot < 0 || slot >= Sched::MaxProcesses || path == nullptr) return -1;
|
||||
|
||||
uint64_t writeCapability = Fs::RequiredFileWriteCapability(path);
|
||||
if (create && writeCapability != 0 &&
|
||||
!Sched::HasCapability(writeCapability)) {
|
||||
return montauk::abi::SYS_ERR_PERMISSION;
|
||||
}
|
||||
|
||||
Fs::Vfs::BackendFile backend = {-1, -1, 0};
|
||||
int result = create ? Fs::Vfs::CreateBackendFile(path, backend)
|
||||
: Fs::Vfs::OpenBackendFile(path, backend);
|
||||
@@ -1273,6 +1166,7 @@ namespace Ipc {
|
||||
if (g_files[i].active || g_files[i].destroying) continue;
|
||||
InitObject(g_files[i], HandleType::File);
|
||||
g_files[i].backend = backend;
|
||||
g_files[i].writeCapability = writeCapability;
|
||||
g_filePoolLock.Release();
|
||||
|
||||
uint32_t rights = RightRead | RightWait | RightDup;
|
||||
@@ -1318,7 +1212,12 @@ namespace Ipc {
|
||||
HandleSnapshot snapshot;
|
||||
if (!snapshot.Capture(CurrentSlot(), handle)) return -1;
|
||||
if (snapshot.type != HandleType::File || (snapshot.rights & RightWrite) == 0) return -1;
|
||||
return Fs::Vfs::WriteBackendFile(((File*)snapshot.object)->backend, buffer, offset, size);
|
||||
File* file = (File*)snapshot.object;
|
||||
if (file->writeCapability != 0 &&
|
||||
!Sched::HasCapability(file->writeCapability)) {
|
||||
return montauk::abi::SYS_ERR_PERMISSION;
|
||||
}
|
||||
return Fs::Vfs::WriteBackendFile(file->backend, buffer, offset, size);
|
||||
}
|
||||
|
||||
uint64_t FileGetSizeHandle(int handle) {
|
||||
@@ -1328,6 +1227,11 @@ namespace Ipc {
|
||||
return Fs::Vfs::GetBackendFileSize(((File*)snapshot.object)->backend);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Sockets
|
||||
// TCP and UDP endpoints.
|
||||
// ==========================================================================
|
||||
|
||||
static Socket* AllocateSocketObject(int type) {
|
||||
g_socketPoolLock.Acquire();
|
||||
for (int i = 0; i < MaxSockets; i++) {
|
||||
@@ -1663,6 +1567,13 @@ namespace Ipc {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Surfaces
|
||||
// Shared pixel buffers mapped into a client address space.
|
||||
// Pages MUST be unmapped from the owner before being freed, or
|
||||
// FreeUserHalf() double-frees them on process exit.
|
||||
// ==========================================================================
|
||||
|
||||
Surface* CreateSurface(uint64_t byteSize) {
|
||||
if (byteSize == 0) byteSize = 0x1000;
|
||||
uint32_t numPages = (uint32_t)((byteSize + 0xFFFu) / 0x1000u);
|
||||
@@ -1748,7 +1659,7 @@ namespace Ipc {
|
||||
for (uint32_t p = m.numPages; p < newPages; p++) {
|
||||
Memory::VMM::Paging::UnmapUserIn(pml4, m.va + (uint64_t)p * 0x1000ULL);
|
||||
}
|
||||
ShootdownUserRange(pml4, startVa, rollbackPages);
|
||||
Memory::ShootdownUserRange(pml4, startVa, rollbackPages);
|
||||
}
|
||||
g_surfaceMapLocks[s].Release();
|
||||
}
|
||||
@@ -1900,7 +1811,7 @@ namespace Ipc {
|
||||
Memory::VMM::Paging::UnmapUserIn(pml4, va);
|
||||
}
|
||||
|
||||
ShootdownUserRange(pml4, baseVa, flushPages);
|
||||
Memory::ShootdownUserRange(pml4, baseVa, flushPages);
|
||||
m.numPages = newPages;
|
||||
}
|
||||
g_surfaceMapLocks[s].Release();
|
||||
@@ -2111,7 +2022,7 @@ namespace Ipc {
|
||||
// releasing the mapping reference can then destroy the surface
|
||||
// and recycle its frames while that sibling writes through its
|
||||
// stale TLB entry. Quiesce every CPU using this PML4 first.
|
||||
ShootdownUserRange(pml4Phys, baseVa, numPages);
|
||||
Memory::ShootdownUserRange(pml4Phys, baseVa, numPages);
|
||||
|
||||
g_surfaceMaps[slot][i].used = false;
|
||||
g_surfaceMaps[slot][i].surface = nullptr;
|
||||
@@ -2127,6 +2038,11 @@ namespace Ipc {
|
||||
return unmapped > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Process handles
|
||||
// Wait-only references to a live process.
|
||||
// ==========================================================================
|
||||
|
||||
int OpenProcessHandle(int pid) {
|
||||
g_processPoolLock.Acquire();
|
||||
for (int i = 0; i < MaxProcessObjects; i++) {
|
||||
@@ -2183,6 +2099,11 @@ namespace Ipc {
|
||||
return exited;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Signals and waitsets
|
||||
// Readiness computation and multiplexed waiting.
|
||||
// ==========================================================================
|
||||
|
||||
static uint32_t CurrentSocketSignals(Socket* socket, uint32_t rights) {
|
||||
if (socket == nullptr) return SignalNone;
|
||||
|
||||
@@ -2513,6 +2434,10 @@ namespace Ipc {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Teardown and init
|
||||
// ==========================================================================
|
||||
|
||||
void CleanupProcessSlot(int slot, int /*pid*/, uint64_t pml4Phys) {
|
||||
if (slot < 0 || slot >= Sched::MaxProcesses) return;
|
||||
|
||||
@@ -2548,7 +2473,6 @@ namespace Ipc {
|
||||
for (int i = 0; i < Sched::MaxProcesses; i++) {
|
||||
g_processObjectsBySlot[i] = nullptr;
|
||||
}
|
||||
Hal::RegisterIrqHandler(Hal::IRQ_TLB_SHOOTDOWN, TlbShootdownIpiHandler);
|
||||
Kt::KernelLogStream(Kt::OK, "IPC") << "Initialized ("
|
||||
<< (uint64_t)MaxHandlesPerProcess << " handles/process, "
|
||||
<< (uint64_t)MaxStreams << " streams, "
|
||||
|
||||
@@ -171,11 +171,6 @@ namespace Ipc {
|
||||
int WaitsetWaitHandle(int waitsetHandle, WaitsetReady* outReady, uint64_t timeoutMs);
|
||||
|
||||
void NotifyObjectChanged(Object* object);
|
||||
// Invalidate a user range on every CPU currently running the address
|
||||
// space. Call this after removing PTEs and before releasing their frames.
|
||||
void ShootdownUserRange(uint64_t pml4Phys, uint64_t startVa, uint32_t pages);
|
||||
// Safely remove ordinary PFA-backed user mappings and release their frames.
|
||||
void UnmapAndFreeUserRange(uint64_t pml4Phys, uint64_t startVa, uint64_t pages);
|
||||
void CleanupProcessSlot(int slot, int pid, uint64_t pml4Phys);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* UserRange.cpp
|
||||
* Cross-CPU invalidation and teardown of user address-space mappings
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*
|
||||
* Split out of Ipc.cpp: this is paging and SMP work with no dependency on
|
||||
* any IPC object or handle pool, and it lived there only by history.
|
||||
*/
|
||||
|
||||
#include "UserRange.hpp"
|
||||
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Hal/Apic/Apic.hpp>
|
||||
#include <Hal/Apic/Interrupts.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
#include <Common/Panic.hpp>
|
||||
|
||||
namespace Memory {
|
||||
|
||||
// MUST be a Mutex, never a Spinlock. ShootdownUserRange holds this while
|
||||
// waiting for remote CPUs to acknowledge the shootdown IPI, so a CPU that
|
||||
// is queued behind the holder has to stay interruptible long enough to
|
||||
// service that IPI itself. An interrupt-disabling Spinlock here deadlocks
|
||||
// every CPU contending for the lock, and the bounded-retry logic below
|
||||
// then reports it as a "target failed to acknowledge" Panic -- which reads
|
||||
// like a hardware fault rather than a lock-type regression.
|
||||
static kcp::Mutex g_tlbShootdownLock;
|
||||
static volatile uint64_t g_tlbShootdownSeq = 0;
|
||||
static volatile uint64_t g_tlbShootdownPml4 = 0;
|
||||
static volatile uint64_t g_tlbShootdownStartVa = 0;
|
||||
static volatile uint32_t g_tlbShootdownPages = 0;
|
||||
static volatile uint64_t g_tlbShootdownDone[Smp::MaxCPUs] = {};
|
||||
|
||||
static bool CpuCurrentlyUsesPml4(Smp::CpuData* cpu, uint64_t pml4Phys) {
|
||||
if (cpu == nullptr || pml4Phys == 0 || cpu->currentSlot < 0) return false;
|
||||
|
||||
Sched::Process* proc = Sched::GetProcessSlot(cpu->currentSlot);
|
||||
if (proc == nullptr) return false;
|
||||
if (proc->state == Sched::ProcessState::Free) return false;
|
||||
return proc->pml4Phys == pml4Phys;
|
||||
}
|
||||
|
||||
static void InvalidateLocalUserRange(uint64_t startVa, uint32_t pages) {
|
||||
if (pages == 0) return;
|
||||
|
||||
if (pages > 1024) {
|
||||
Memory::VMM::FlushTLB();
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32_t p = 0; p < pages; p++) {
|
||||
uint64_t va = startVa + (uint64_t)p * 0x1000ULL;
|
||||
asm volatile("invlpg (%0)" :: "r"(va) : "memory");
|
||||
}
|
||||
}
|
||||
|
||||
static void TlbShootdownIpiHandler(uint8_t, bool) {
|
||||
Smp::CpuData* cpu = Smp::GetCurrentCpuData();
|
||||
uint64_t seq = g_tlbShootdownSeq;
|
||||
uint64_t pml4 = g_tlbShootdownPml4;
|
||||
uint64_t startVa = g_tlbShootdownStartVa;
|
||||
uint32_t pages = g_tlbShootdownPages;
|
||||
|
||||
if (CpuCurrentlyUsesPml4(cpu, pml4)) {
|
||||
InvalidateLocalUserRange(startVa, pages);
|
||||
}
|
||||
|
||||
if (cpu != nullptr && cpu->cpuIndex >= 0 && cpu->cpuIndex < Smp::MaxCPUs) {
|
||||
asm volatile("" ::: "memory");
|
||||
g_tlbShootdownDone[cpu->cpuIndex] = seq;
|
||||
}
|
||||
}
|
||||
|
||||
void ShootdownUserRange(uint64_t pml4Phys, uint64_t startVa, uint32_t pages) {
|
||||
if (pml4Phys == 0 || pages == 0) return;
|
||||
|
||||
bool targets[Smp::MaxCPUs] = {};
|
||||
Smp::CpuData* currentCpu = Smp::GetCurrentCpuData();
|
||||
int currentCpuIndex = currentCpu ? currentCpu->cpuIndex : -1;
|
||||
|
||||
g_tlbShootdownLock.Acquire();
|
||||
uint64_t seq = g_tlbShootdownSeq + 1;
|
||||
g_tlbShootdownPml4 = pml4Phys;
|
||||
g_tlbShootdownStartVa = startVa;
|
||||
g_tlbShootdownPages = pages;
|
||||
asm volatile("" ::: "memory");
|
||||
g_tlbShootdownSeq = seq;
|
||||
|
||||
for (int i = 0; i < Smp::GetCpuCount(); i++) {
|
||||
Smp::CpuData* cpu = Smp::GetCpuData(i);
|
||||
if (cpu == nullptr || !cpu->started) continue;
|
||||
|
||||
if (i == currentCpuIndex) {
|
||||
if (CpuCurrentlyUsesPml4(cpu, pml4Phys)) {
|
||||
InvalidateLocalUserRange(startVa, pages);
|
||||
}
|
||||
g_tlbShootdownDone[i] = seq;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!CpuCurrentlyUsesPml4(cpu, pml4Phys)) {
|
||||
g_tlbShootdownDone[i] = seq;
|
||||
continue;
|
||||
}
|
||||
|
||||
targets[i] = true;
|
||||
(void)Hal::LocalApic::SendFixedIpi(cpu->lapicId,
|
||||
Hal::IRQ_VECTOR_BASE + Hal::IRQ_TLB_SHOOTDOWN);
|
||||
}
|
||||
|
||||
for (int i = 0; i < Smp::GetCpuCount(); i++) {
|
||||
if (!targets[i]) continue;
|
||||
uint32_t spins = 0;
|
||||
uint32_t retries = 0;
|
||||
while (g_tlbShootdownDone[i] != seq) {
|
||||
asm volatile("pause");
|
||||
if (++spins < 1000000) continue;
|
||||
|
||||
// Delivery normally completes in a handful of cycles. Retry a
|
||||
// bounded number of times in case the first IPI was lost while
|
||||
// the target changed interrupt state. Continuing without an
|
||||
// acknowledgement would let the caller free frames still
|
||||
// reachable through a remote stale TLB entry, so fail loudly
|
||||
// instead of either corrupting memory or spinning forever.
|
||||
spins = 0;
|
||||
if (++retries > 4) {
|
||||
Panic("TLB shootdown target failed to acknowledge", nullptr);
|
||||
}
|
||||
Smp::CpuData* cpu = Smp::GetCpuData(i);
|
||||
if (cpu != nullptr && cpu->started) {
|
||||
(void)Hal::LocalApic::SendFixedIpi(cpu->lapicId,
|
||||
Hal::IRQ_VECTOR_BASE + Hal::IRQ_TLB_SHOOTDOWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_tlbShootdownLock.Release();
|
||||
}
|
||||
|
||||
void UnmapAndFreeUserRange(uint64_t pml4Phys, uint64_t startVa, uint64_t pages) {
|
||||
static constexpr uint32_t PagesPerChunk = 64;
|
||||
uint64_t physPages[PagesPerChunk];
|
||||
|
||||
for (uint64_t base = 0; base < pages; base += PagesPerChunk) {
|
||||
uint32_t count = (uint32_t)((pages - base > PagesPerChunk)
|
||||
? PagesPerChunk : pages - base);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
uint64_t pageVa = startVa + (base + i) * 0x1000ULL;
|
||||
physPages[i] = Memory::VMM::Paging::GetPhysAddr(pml4Phys, pageVa);
|
||||
Memory::VMM::Paging::UnmapUserIn(pml4Phys, pageVa);
|
||||
}
|
||||
|
||||
ShootdownUserRange(pml4Phys, startVa + base * 0x1000ULL, count);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
if (physPages[i] != 0) {
|
||||
Memory::g_pfa->Free((void*)Memory::HHDM(physPages[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InitUserRange() {
|
||||
Hal::RegisterIrqHandler(Hal::IRQ_TLB_SHOOTDOWN, TlbShootdownIpiHandler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* UserRange.hpp
|
||||
* Cross-CPU invalidation and teardown of user address-space mappings
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace Memory {
|
||||
|
||||
// Register the TLB-shootdown IPI handler. Must run before any AP is
|
||||
// booted, since a shootdown targets every CPU running the address space.
|
||||
void InitUserRange();
|
||||
|
||||
// Invalidate a user range on every CPU currently running the address
|
||||
// space. Call this after removing PTEs and before releasing their frames.
|
||||
void ShootdownUserRange(uint64_t pml4Phys, uint64_t startVa, uint32_t pages);
|
||||
|
||||
// Safely remove ordinary PFA-backed user mappings and release their frames.
|
||||
void UnmapAndFreeUserRange(uint64_t pml4Phys, uint64_t startVa, uint64_t pages);
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
* Copyright (c) 2025-2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <Memory/UserRange.hpp>
|
||||
#include "Scheduler.hpp"
|
||||
#include "ElfLoader.hpp"
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
@@ -298,6 +299,9 @@ 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].sessionId = -1;
|
||||
processTable[i].cwd[0] = '\0';
|
||||
processTable[i].runningOnCpu = -1;
|
||||
@@ -342,7 +346,9 @@ namespace Sched {
|
||||
}
|
||||
|
||||
int Spawn(const char* vfsPath, const char* args, bool startReady,
|
||||
const char* environment, uint32_t environmentLength) {
|
||||
const char* environment, uint32_t environmentLength,
|
||||
const montauk::abi::SpawnCapabilities* capabilities,
|
||||
const char* userOverride) {
|
||||
schedLock.Acquire();
|
||||
|
||||
int slot = -1;
|
||||
@@ -547,9 +553,37 @@ namespace Sched {
|
||||
proc.environmentLength = 1;
|
||||
}
|
||||
|
||||
// Inherit user string from parent, or default to "system" if no parent
|
||||
// Capabilities are kernel-owned and never inferred from the user name.
|
||||
// A normal userspace spawn receives no privileged authority; callers
|
||||
// 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;
|
||||
}
|
||||
} else {
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Inherit user string from parent, or default to "system" if no parent.
|
||||
// An explicit override is accepted only through the validated
|
||||
// SYS_SPAWN_CAPS path.
|
||||
{
|
||||
if (parentSlot >= 0) {
|
||||
if (userOverride != nullptr) {
|
||||
int i = 0;
|
||||
for (; i < 31 && userOverride[i]; i++) proc.user[i] = userOverride[i];
|
||||
proc.user[i] = '\0';
|
||||
} else if (parentSlot >= 0) {
|
||||
int i = 0;
|
||||
for (; i < 31 && processTable[parentSlot].user[i]; i++)
|
||||
proc.user[i] = processTable[parentSlot].user[i];
|
||||
@@ -684,7 +718,7 @@ namespace Sched {
|
||||
mappedPages++;
|
||||
}
|
||||
if (!ok) {
|
||||
Ipc::UnmapAndFreeUserRange(sharedPml4, base, mappedPages);
|
||||
Memory::UnmapAndFreeUserRange(sharedPml4, base, mappedPages);
|
||||
ReleaseUserHeapRange(primarySlot_, base, numPages * 0x1000ULL);
|
||||
Kt::KernelLogStream(Kt::ERROR, "Sched")
|
||||
<< "Thread TLS allocation failed";
|
||||
@@ -711,7 +745,7 @@ namespace Sched {
|
||||
void* stackMem = Memory::g_pfa->ReallocConsecutive(nullptr, StackPages);
|
||||
if (stackMem == nullptr) {
|
||||
if (threadTlsPages != 0) {
|
||||
Ipc::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages);
|
||||
Memory::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages);
|
||||
ReleaseUserHeapRange(primarySlot_, threadTlsBase,
|
||||
threadTlsPages * 0x1000ULL);
|
||||
}
|
||||
@@ -737,7 +771,7 @@ namespace Sched {
|
||||
schedLock.Release();
|
||||
Memory::g_pfa->Free(stackMem, StackPages);
|
||||
if (threadTlsPages != 0) {
|
||||
Ipc::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages);
|
||||
Memory::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages);
|
||||
ReleaseUserHeapRange(primarySlot_, threadTlsBase,
|
||||
threadTlsPages * 0x1000ULL);
|
||||
}
|
||||
@@ -885,7 +919,7 @@ namespace Sched {
|
||||
uint64_t base = thr.fsBase - blockSize;
|
||||
uint64_t pages = (blockSize + 16 + 0xFFF) / 0x1000;
|
||||
thr.fsBase = 0;
|
||||
Ipc::UnmapAndFreeUserRange(primary.pml4Phys, base, pages);
|
||||
Memory::UnmapAndFreeUserRange(primary.pml4Phys, base, pages);
|
||||
ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL);
|
||||
}
|
||||
|
||||
@@ -1418,6 +1452,11 @@ namespace Sched {
|
||||
return &processTable[primary];
|
||||
}
|
||||
|
||||
bool HasCapability(uint64_t capability) {
|
||||
Process* proc = GetCurrentProcessPtr();
|
||||
return proc != nullptr && (proc->effectiveCaps & capability) == capability;
|
||||
}
|
||||
|
||||
Process* GetCurrentThreadPtr() {
|
||||
auto* cpu = Smp::GetCurrentCpuData();
|
||||
int slot = cpu->currentSlot;
|
||||
|
||||
@@ -86,6 +86,9 @@ 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
|
||||
|
||||
@@ -144,7 +147,9 @@ namespace Sched {
|
||||
|
||||
void Initialize();
|
||||
int Spawn(const char* vfsPath, const char* args = nullptr, bool startReady = true,
|
||||
const char* environment = nullptr, uint32_t environmentLength = 0);
|
||||
const char* environment = nullptr, uint32_t environmentLength = 0,
|
||||
const montauk::abi::SpawnCapabilities* capabilities = nullptr,
|
||||
const char* userOverride = nullptr);
|
||||
int StartProcess(int pid);
|
||||
void Schedule();
|
||||
|
||||
@@ -172,6 +177,10 @@ namespace Sched {
|
||||
// Always returns the slot that owns per-process state -- never a sibling thread.
|
||||
Process* GetCurrentProcessPtr();
|
||||
|
||||
// Capability checks always consult kernel-owned process metadata. User
|
||||
// names are deliberately excluded from authorization.
|
||||
bool HasCapability(uint64_t capability);
|
||||
|
||||
// Get a pointer to the currently running thread's slot (may be a sibling).
|
||||
Process* GetCurrentThreadPtr();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user