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();
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# MontaukOS capability grants
|
||||
#
|
||||
#
|
||||
# Capability names: process_admin, power_request, power_control, suspend,
|
||||
# storage_admin, raw_storage, network_admin, set_time, user_admin,
|
||||
# display_admin, device_admin, log_read, system_image, and "all".
|
||||
#
|
||||
# "all" is every capability except system_image, which must always be named
|
||||
# explicitly: it is write access to 0:/os and 0:/apps, and since grants are
|
||||
# keyed on binary path, holding it is equivalent to holding every capability
|
||||
# the system can issue from the next launch onwards. Nothing is granted it.
|
||||
#
|
||||
# A grant is always clamped by the kernel to what the launching process may
|
||||
# actually delegate, so "all" in a launcher entry means "whatever this session
|
||||
# was given", not "root".
|
||||
#
|
||||
#
|
||||
|
||||
# ==== System services (started by init) ====
|
||||
|
||||
[grant.dhcp]
|
||||
path = "0:/os/dhcp.elf"
|
||||
effective = ["network_admin"]
|
||||
|
||||
[grant.ntp]
|
||||
path = "0:/os/ntp.elf"
|
||||
effective = ["set_time"]
|
||||
|
||||
[grant.login]
|
||||
path = "0:/os/login.elf"
|
||||
permitted = ["all"]
|
||||
effective = [
|
||||
"power_control", "storage_admin", "device_admin",
|
||||
"user_admin", "process_admin", "log_read",
|
||||
]
|
||||
delegable = [
|
||||
"power_request", "suspend", "process_admin", "storage_admin",
|
||||
"raw_storage", "network_admin", "set_time", "user_admin",
|
||||
"display_admin", "device_admin", "log_read",
|
||||
]
|
||||
|
||||
[grant.sshd]
|
||||
path = "0:/os/sshd.elf"
|
||||
effective = ["user_admin"]
|
||||
|
||||
[grant.desktop]
|
||||
path = "0:/os/desktop.elf"
|
||||
effective = [
|
||||
"power_request", "suspend", "process_admin", "storage_admin",
|
||||
"raw_storage", "network_admin", "set_time", "user_admin",
|
||||
"display_admin", "device_admin", "log_read",
|
||||
]
|
||||
delegable = [
|
||||
"power_request", "suspend", "process_admin", "storage_admin",
|
||||
"raw_storage", "network_admin", "set_time", "user_admin",
|
||||
"display_admin", "device_admin", "log_read",
|
||||
]
|
||||
|
||||
# The console session launchers. terminal.elf needs no authority of its own;
|
||||
# it exists to pass the session's authority to the shell it hosts. The shell
|
||||
# in turn exercises only power_request and suspend, but delegates on the same
|
||||
# terms as the desktop so that the tools below work from a console. Both are
|
||||
# clamped to the launching session: a standard session narrows this to
|
||||
# power_request and suspend, and an unprivileged one to nothing.
|
||||
[grant.terminal]
|
||||
path = "0:/apps/terminal/terminal.elf"
|
||||
delegable = ["all"]
|
||||
|
||||
[grant.shell]
|
||||
path = "0:/os/shell.elf"
|
||||
effective = ["power_request", "suspend"]
|
||||
delegable = ["all"]
|
||||
|
||||
# ==== Settings and administrative applications ====
|
||||
|
||||
[grant.procmgr]
|
||||
path = "0:/apps/procmgr/procmgr.elf"
|
||||
effective = ["process_admin"]
|
||||
|
||||
# May restart the DHCP client, so it needs to pass network_admin on.
|
||||
[grant.network]
|
||||
path = "0:/apps/network/network.elf"
|
||||
effective = ["network_admin"]
|
||||
delegable = ["network_admin"]
|
||||
|
||||
[grant.display]
|
||||
path = "0:/apps/display/display.elf"
|
||||
effective = ["display_admin"]
|
||||
|
||||
[grant.disks]
|
||||
path = "0:/apps/disks/disks.elf"
|
||||
effective = ["storage_admin", "raw_storage"]
|
||||
|
||||
[grant.installer]
|
||||
path = "0:/apps/installer/installer.elf"
|
||||
effective = ["storage_admin", "raw_storage"]
|
||||
|
||||
[grant.timezone]
|
||||
path = "0:/apps/timezone/timezone.elf"
|
||||
effective = ["set_time"]
|
||||
|
||||
[grant.bluetooth]
|
||||
path = "0:/apps/bluetooth/bluetooth.elf"
|
||||
effective = ["device_admin"]
|
||||
|
||||
[grant.syslog]
|
||||
path = "0:/apps/syslog/syslog.elf"
|
||||
effective = ["log_read"]
|
||||
|
||||
[grant.sshserver]
|
||||
path = "0:/apps/sshserver/sshserver.elf"
|
||||
effective = ["user_admin"]
|
||||
|
||||
# ==== Console tools ====
|
||||
|
||||
[grant.ifconfig]
|
||||
path = "0:/os/ifconfig.elf"
|
||||
effective = ["network_admin"]
|
||||
|
||||
[grant.wifi]
|
||||
path = "0:/os/wifi.elf"
|
||||
effective = ["network_admin"]
|
||||
|
||||
[grant.sdr]
|
||||
path = "0:/os/sdr.elf"
|
||||
effective = ["device_admin"]
|
||||
@@ -250,6 +250,63 @@ namespace montauk::abi {
|
||||
|
||||
static constexpr uint64_t SYS_LOG_WRITE = 176; // (logMessage) -> 0
|
||||
static constexpr uint64_t SYS_TERMINAL_ATTACHED = 177; // () -> 1 when connected to a userspace terminal
|
||||
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));
|
||||
|
||||
|
||||
static constexpr int USB_ERR_INVALID = -1;
|
||||
@@ -264,10 +321,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;
|
||||
@@ -729,6 +793,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;
|
||||
};
|
||||
|
||||
struct MemStats {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <Api/Syscall.hpp>
|
||||
#include <montauk/capabilities.h>
|
||||
|
||||
namespace gui {
|
||||
|
||||
@@ -212,7 +213,19 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) {
|
||||
terminal_init_cells(t, cols, rows, TERM_MAX_SCROLLBACK);
|
||||
t->cursor_visible = true;
|
||||
|
||||
t->child_pid = montauk::spawn_redir("0:/os/shell.elf");
|
||||
// A console shell is a session launcher, exactly like the desktop, and is
|
||||
// granted on exactly the same terms: 0:/config/capabilities.toml decides
|
||||
// what shell.elf receives, and the kernel clamps that to what this session
|
||||
// was actually delegated. Deriving the grant here instead would be a
|
||||
// second, hardcoded list of "capabilities a console may confer" -- one the
|
||||
// table cannot see and cannot keep in step with.
|
||||
static constexpr const char* kShellPath = "0:/os/shell.elf";
|
||||
montauk::abi::SpawnCapabilities caps =
|
||||
montauk::caps::for_binary(kShellPath, montauk::caps::self_delegable());
|
||||
|
||||
t->child_pid = (caps.permitted != 0)
|
||||
? montauk::spawn_redir_with_caps(kShellPath, nullptr, caps)
|
||||
: montauk::spawn_redir(kShellPath);
|
||||
if (t->child_pid > 0)
|
||||
montauk::childio_settermsz(t->child_pid, cols, rows);
|
||||
}
|
||||
|
||||
@@ -208,6 +208,8 @@ extern "C" {
|
||||
#define MTK_SYS_USB_BULK_IN_START 182
|
||||
#define MTK_SYS_USB_BULK_IN_STOP 183
|
||||
#define MTK_SYS_USB_BULK_IN_READ 184
|
||||
#define MTK_SYS_SPAWN_CAPS 185
|
||||
#define MTK_SYS_SPAWN_REDIR_CAPS 186
|
||||
/* @SYSCALLS-END */
|
||||
|
||||
#define MTK_SOCK_TCP 1
|
||||
@@ -683,8 +685,8 @@ static inline int mtk_set_unix_time(int64_t unix_seconds) {
|
||||
return (int)_mtk_syscall1(MTK_SYS_SETUNIXTIME, (long)unix_seconds);
|
||||
}
|
||||
|
||||
static inline void mtk_settz(int offset_minutes) {
|
||||
_mtk_syscall1(MTK_SYS_SETTZ, (long)offset_minutes);
|
||||
static inline int mtk_settz(int offset_minutes) {
|
||||
return (int)_mtk_syscall1(MTK_SYS_SETTZ, (long)offset_minutes);
|
||||
}
|
||||
|
||||
static inline int mtk_gettz(void) {
|
||||
@@ -883,12 +885,12 @@ static inline int mtk_audio_ctl(int handle, int cmd, int value) {
|
||||
Power management
|
||||
==================================================================== */
|
||||
|
||||
static inline void mtk_reset(void) {
|
||||
_mtk_syscall0(MTK_SYS_RESET);
|
||||
static inline int mtk_reset(void) {
|
||||
return (int)_mtk_syscall0(MTK_SYS_RESET);
|
||||
}
|
||||
|
||||
static inline void mtk_shutdown(void) {
|
||||
_mtk_syscall0(MTK_SYS_SHUTDOWN);
|
||||
static inline int mtk_shutdown(void) {
|
||||
return (int)_mtk_syscall0(MTK_SYS_SHUTDOWN);
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* capabilities.h
|
||||
* Shared reader for the capability grant table (0:/config/capabilities.toml)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/toml.h>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/heap.h>
|
||||
|
||||
/*
|
||||
* Launchers (init, the desktop, the shell) look up the authority a program
|
||||
* should receive here instead of each carrying its own compiled-in table.
|
||||
*
|
||||
* This file is advisory, never authoritative. Every grant still goes
|
||||
* through SYS_SPAWN_CAPS and is validated in the kernel against the
|
||||
* caller's own delegable set, so nothing written here can produce authority
|
||||
* the kernel has not already delegated to the launcher. A missing,
|
||||
* truncated or hostile file can only ever result in a program receiving
|
||||
* less authority than intended. That is why the table can live in
|
||||
* userspace TOML: the kernel enumerates protected paths, userspace
|
||||
* interprets policy.
|
||||
*
|
||||
* Grants are keyed on the resolved binary path, which is what makes the
|
||||
* table safe to hand to init: pointing a privileged service entry at a
|
||||
* different executable looks up the new path, finds no entry, and grants
|
||||
* nothing. The kernel write-protects 0:/apps and 0:/os so the path cannot
|
||||
* be made to refer to a substituted image.
|
||||
*/
|
||||
|
||||
namespace montauk {
|
||||
namespace caps {
|
||||
|
||||
inline constexpr const char* GRANT_CONFIG = "capabilities";
|
||||
inline constexpr const char* GRANT_PREFIX = "grant.";
|
||||
inline constexpr int MAX_SCAN_PROCS = 256;
|
||||
|
||||
struct CapName {
|
||||
const char* name;
|
||||
uint64_t bit;
|
||||
};
|
||||
|
||||
// Names as they appear in the config file. Kept in the same order as the
|
||||
// CAP_* bit definitions in Api/Syscall.hpp.
|
||||
inline constexpr CapName NAMES[] = {
|
||||
{"process_admin", montauk::abi::CAP_PROCESS_ADMIN},
|
||||
{"power_request", montauk::abi::CAP_POWER_REQUEST},
|
||||
{"power_control", montauk::abi::CAP_POWER_CONTROL},
|
||||
{"suspend", montauk::abi::CAP_SUSPEND},
|
||||
{"storage_admin", montauk::abi::CAP_STORAGE_ADMIN},
|
||||
{"raw_storage", montauk::abi::CAP_RAW_STORAGE},
|
||||
{"network_admin", montauk::abi::CAP_NETWORK_ADMIN},
|
||||
{"set_time", montauk::abi::CAP_SET_TIME},
|
||||
{"user_admin", montauk::abi::CAP_USER_ADMIN},
|
||||
{"display_admin", montauk::abi::CAP_DISPLAY_ADMIN},
|
||||
{"device_admin", montauk::abi::CAP_DEVICE_ADMIN},
|
||||
{"log_read", montauk::abi::CAP_LOG_READ},
|
||||
{"system_image", montauk::abi::CAP_SYSTEM_IMAGE},
|
||||
};
|
||||
|
||||
inline uint64_t bit_for_name(const char* name) {
|
||||
if (name == nullptr || name[0] == '\0') return 0;
|
||||
// "all" means "everything this launcher may pass on", which the
|
||||
// caller-delegable clamp in for_binary() then narrows. It excludes
|
||||
// CAP_SYSTEM_IMAGE: authority to rewrite a program image is never
|
||||
// something a wildcard should hand out, only an explicit name.
|
||||
if (montauk::streq(name, "all"))
|
||||
return montauk::abi::CAP_ALL & ~montauk::abi::CAP_SYSTEM_IMAGE;
|
||||
for (const auto& entry : NAMES) {
|
||||
if (montauk::streq(entry.name, name)) return entry.bit;
|
||||
}
|
||||
// Unknown names are ignored rather than rejected. Failing closed
|
||||
// costs a program some authority; failing open would hand out
|
||||
// authority nobody asked for.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Read an array-of-strings key into a capability mask. A missing key is
|
||||
// an empty mask, which is the correct default for an absent grant.
|
||||
inline uint64_t mask_from_key(const montauk::toml::Doc& doc, const char* key) {
|
||||
montauk::toml::Value* arr = doc.get_array(key);
|
||||
if (arr == nullptr) return 0;
|
||||
|
||||
uint64_t mask = 0;
|
||||
for (int i = 0; i < arr->array.count; i++) {
|
||||
montauk::toml::Value* item = arr->array.items[i];
|
||||
if (item == nullptr || item->type != montauk::toml::Type::String) continue;
|
||||
mask |= bit_for_name(item->str);
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
// Append `suffix` to the "grant.<id>." stem of `path_key`.
|
||||
// Returns false if the key is not of that shape or does not fit.
|
||||
inline bool build_sibling_key(const char* path_key, const char* suffix,
|
||||
char* out, int outSz) {
|
||||
int prefixLen = 0;
|
||||
for (; GRANT_PREFIX[prefixLen]; prefixLen++) {
|
||||
if (path_key[prefixLen] != GRANT_PREFIX[prefixLen]) return false;
|
||||
}
|
||||
|
||||
// Copy through the final '.' so "grant.foo.path" yields "grant.foo.".
|
||||
int lastDot = -1;
|
||||
for (int i = 0; path_key[i]; i++) {
|
||||
if (path_key[i] == '.') lastDot = i;
|
||||
}
|
||||
if (lastDot < prefixLen) return false;
|
||||
|
||||
int n = 0;
|
||||
for (; n <= lastDot && n < outSz - 1; n++) out[n] = path_key[n];
|
||||
for (int i = 0; suffix[i] && n < outSz - 1; i++) out[n++] = suffix[i];
|
||||
out[n] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
// Look up the grant declared for `binary_path`. Returns false when the
|
||||
// path has no entry, which is the common case and means "no authority".
|
||||
inline bool lookup(const char* binary_path,
|
||||
montauk::abi::SpawnCapabilities& out) {
|
||||
out = {0, 0, 0};
|
||||
if (binary_path == nullptr || binary_path[0] == '\0') return false;
|
||||
|
||||
montauk::toml::Doc doc = montauk::config::load(GRANT_CONFIG);
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < doc.entries.count && !found; i++) {
|
||||
montauk::toml::Value* entry = doc.entries.items[i];
|
||||
if (entry == nullptr || entry->key == nullptr) continue;
|
||||
if (entry->type != montauk::toml::Type::String) continue;
|
||||
|
||||
char sibling[128];
|
||||
if (!build_sibling_key(entry->key, "path", sibling, sizeof(sibling))) continue;
|
||||
if (!montauk::streq(sibling, entry->key)) continue;
|
||||
if (!montauk::streq(entry->str, binary_path)) continue;
|
||||
|
||||
build_sibling_key(entry->key, "effective", sibling, sizeof(sibling));
|
||||
uint64_t effective = mask_from_key(doc, sibling);
|
||||
build_sibling_key(entry->key, "delegable", sibling, sizeof(sibling));
|
||||
uint64_t delegable = mask_from_key(doc, sibling);
|
||||
build_sibling_key(entry->key, "permitted", sibling, sizeof(sibling));
|
||||
uint64_t permitted = mask_from_key(doc, sibling);
|
||||
|
||||
// A grant that does not name `permitted` owns exactly what it can
|
||||
// use or pass on. Declaring it separately is only needed by a
|
||||
// supervisor that holds authority in reserve (login).
|
||||
if (permitted == 0) permitted = effective | delegable;
|
||||
|
||||
out.permitted = permitted;
|
||||
out.effective = effective;
|
||||
out.delegable = delegable;
|
||||
found = true;
|
||||
}
|
||||
|
||||
doc.destroy();
|
||||
return found;
|
||||
}
|
||||
|
||||
// The calling process's own capability masks.
|
||||
//
|
||||
// There is no syscall to ask "what am I?", so this scans the process table
|
||||
// for our own PID. The buffer is heap-allocated because ProcInfo is large
|
||||
// enough that MAX_SCAN_PROCS of them would be a ~29 KB stack frame.
|
||||
inline bool self(montauk::abi::SpawnCapabilities& out) {
|
||||
out = {0, 0, 0};
|
||||
|
||||
auto* table = (montauk::abi::ProcInfo*)montauk::malloc(
|
||||
sizeof(montauk::abi::ProcInfo) * MAX_SCAN_PROCS);
|
||||
if (table == nullptr) return false;
|
||||
|
||||
int count = montauk::proclist(table, MAX_SCAN_PROCS);
|
||||
int self_pid = montauk::getpid();
|
||||
|
||||
bool found = false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (table[i].pid != self_pid) continue;
|
||||
out.permitted = table[i].permittedCaps;
|
||||
out.effective = table[i].effectiveCaps;
|
||||
out.delegable = table[i].delegableCaps;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
montauk::mfree(table);
|
||||
return found;
|
||||
}
|
||||
|
||||
inline uint64_t self_delegable() {
|
||||
montauk::abi::SpawnCapabilities mine;
|
||||
return self(mine) ? mine.delegable : 0;
|
||||
}
|
||||
|
||||
// Build a spawn request for `binary_path`, clamped to what the caller may
|
||||
// actually delegate. The kernel enforces the same bound; clamping here
|
||||
// means a launcher that holds less authority than the table declares
|
||||
// degrades to a reduced grant instead of failing the spawn outright.
|
||||
inline montauk::abi::SpawnCapabilities for_binary(const char* binary_path,
|
||||
uint64_t caller_delegable) {
|
||||
montauk::abi::SpawnCapabilities caps{0, 0, 0};
|
||||
|
||||
// A caller with nothing to delegate cannot produce a non-empty grant,
|
||||
// so skip the file read entirely. This is the common case: every
|
||||
// unprivileged session, on every launch.
|
||||
if (caller_delegable == 0) return caps;
|
||||
|
||||
if (!lookup(binary_path, caps)) return caps;
|
||||
|
||||
caps.permitted &= caller_delegable;
|
||||
caps.effective &= caps.permitted;
|
||||
caps.delegable &= caps.permitted;
|
||||
return caps;
|
||||
}
|
||||
|
||||
} // namespace caps
|
||||
} // namespace montauk
|
||||
@@ -120,6 +120,13 @@ namespace montauk {
|
||||
inline int spawn(const char* path, const char* args = nullptr) {
|
||||
return (int)syscall2(montauk::abi::SYS_SPAWN, (uint64_t)path, (uint64_t)args);
|
||||
}
|
||||
inline int spawn_with_caps(const char* path, const char* args,
|
||||
const char* user,
|
||||
const montauk::abi::SpawnCapabilities& capabilities) {
|
||||
return (int)syscall4(montauk::abi::SYS_SPAWN_CAPS, (uint64_t)path,
|
||||
(uint64_t)args, (uint64_t)user,
|
||||
(uint64_t)&capabilities);
|
||||
}
|
||||
inline int chdir(const char* path) {
|
||||
return (int)syscall1(montauk::abi::SYS_CHDIR, (uint64_t)path);
|
||||
}
|
||||
@@ -391,7 +398,10 @@ namespace montauk {
|
||||
}
|
||||
|
||||
// Timezone offset (total minutes from UTC)
|
||||
inline void settz(int offset_minutes) { syscall1(montauk::abi::SYS_SETTZ, (uint64_t)(int64_t)offset_minutes); }
|
||||
inline int settz(int offset_minutes) {
|
||||
return (int)syscall1(montauk::abi::SYS_SETTZ,
|
||||
(uint64_t)(int64_t)offset_minutes);
|
||||
}
|
||||
inline int gettz() { return (int)syscall0(montauk::abi::SYS_GETTZ); }
|
||||
|
||||
// Random number generation
|
||||
@@ -400,14 +410,12 @@ namespace montauk {
|
||||
}
|
||||
|
||||
// Power management
|
||||
[[noreturn]] inline void reset() {
|
||||
syscall0(montauk::abi::SYS_RESET);
|
||||
__builtin_unreachable();
|
||||
inline int reset() {
|
||||
return (int)syscall0(montauk::abi::SYS_RESET);
|
||||
}
|
||||
|
||||
[[noreturn]] inline void shutdown() {
|
||||
syscall0(montauk::abi::SYS_SHUTDOWN);
|
||||
__builtin_unreachable();
|
||||
inline int shutdown() {
|
||||
return (int)syscall0(montauk::abi::SYS_SHUTDOWN);
|
||||
}
|
||||
|
||||
inline int suspend() {
|
||||
@@ -423,6 +431,13 @@ namespace montauk {
|
||||
return (int)syscall1(montauk::abi::SYS_POWER_REQUEST, (uint64_t)(int64_t)action);
|
||||
}
|
||||
|
||||
// Non-destructive read of the pending request, for a session leader that
|
||||
// must stand down when something inside its session (the shell's shutdown
|
||||
// builtin, say) asked for power-off. Returns POWER_REQ_QUERY when idle.
|
||||
inline int power_request_pending() {
|
||||
return power_request(montauk::abi::POWER_REQ_PEEK);
|
||||
}
|
||||
|
||||
// Mouse
|
||||
inline void mouse_state(montauk::abi::MouseState* out) { syscall1(montauk::abi::SYS_MOUSESTATE, (uint64_t)out); }
|
||||
inline void set_mouse_bounds(int32_t maxX, int32_t maxY) {
|
||||
@@ -447,6 +462,13 @@ namespace montauk {
|
||||
inline int spawn_redir(const char* path, const char* args = nullptr) {
|
||||
return (int)syscall2(montauk::abi::SYS_SPAWN_REDIR, (uint64_t)path, (uint64_t)args);
|
||||
}
|
||||
inline int spawn_redir_with_caps(
|
||||
const char* path, const char* args,
|
||||
const montauk::abi::SpawnCapabilities& capabilities) {
|
||||
return (int)syscall3(montauk::abi::SYS_SPAWN_REDIR_CAPS,
|
||||
(uint64_t)path, (uint64_t)args,
|
||||
(uint64_t)&capabilities);
|
||||
}
|
||||
inline int childio_read(int childPid, char* buf, int maxLen) {
|
||||
return (int)syscall3(montauk::abi::SYS_CHILDIO_READ, (uint64_t)childPid, (uint64_t)buf, (uint64_t)maxLen);
|
||||
}
|
||||
|
||||
@@ -219,6 +219,16 @@ namespace user {
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool is_admin(const char* username) {
|
||||
UserInfo users[MAX_USERS];
|
||||
int count = load_users(users, MAX_USERS);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (montauk::streq(users[i].username, username))
|
||||
return montauk::streq(users[i].role, "admin");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- User management ----
|
||||
|
||||
inline bool create_user(const char* username, const char* display_name,
|
||||
|
||||
@@ -129,8 +129,14 @@
|
||||
fontscale [n] Get or set terminal font scale -- see fontscale(1)
|
||||
lua Lua interpreter
|
||||
tcc TinyCC (in-system C compiler)
|
||||
reset Reboot the system
|
||||
shutdown Shut down the system
|
||||
reset / reboot Request a supervised system reboot
|
||||
shutdown / poweroff Request a supervised system shutdown
|
||||
suspend Enter ACPI sleep
|
||||
|
||||
Interactive shutdown and reboot are shell builtins: they post a
|
||||
capability-checked request and exit the console, allowing login.elf to
|
||||
flush filesystems and perform the final power operation. They do not grant
|
||||
CAP_POWER_CONTROL to the shell.
|
||||
|
||||
.SS Network commands
|
||||
ping <host> Send ICMP echo requests -- see ping(1)
|
||||
|
||||
+147
-35
@@ -3,7 +3,7 @@
|
||||
syscalls - overview of MontaukOS system calls
|
||||
|
||||
.SH DESCRIPTION
|
||||
MontaukOS provides 176 system calls (numbers 0-184, with numbers
|
||||
MontaukOS provides 178 system calls (numbers 0-186, with numbers
|
||||
140-148 reserved) for userspace programs. Syscalls use the x86-64
|
||||
SYSCALL instruction with the following register convention:
|
||||
|
||||
@@ -20,6 +20,66 @@
|
||||
montauk:: namespace. This page groups syscalls the same way the
|
||||
kernel source does (one subsystem header per group).
|
||||
|
||||
.SH CAPABILITY SECURITY
|
||||
Privileged authority is stored in kernel-owned process credentials, not
|
||||
inferred from a process name, PID, executable path, or merely from the
|
||||
owner name returned by SYS_GETUSER. The owner identity may namespace
|
||||
per-user resources such as the clipboard, but no owner name implies
|
||||
administrative authority. The kernel-created init process is
|
||||
the root of the delegation tree. Login authenticates a user and delegates
|
||||
the appropriate session capabilities; there is no special "system" user
|
||||
shortcut in the kernel.
|
||||
|
||||
Each process has three uint64_t masks, exposed in ProcInfo:
|
||||
|
||||
permitted capabilities owned by the process
|
||||
effective permitted capabilities accepted by syscall checks
|
||||
delegable permitted capabilities that may be given to children
|
||||
|
||||
Effective and delegable must be subsets of permitted. SYS_SPAWN and the
|
||||
other ordinary spawn variants give the child no capabilities. A parent
|
||||
uses SYS_SPAWN_CAPS to make an explicit delegation. Every requested
|
||||
permitted or delegable bit must be present in the parent's delegable mask,
|
||||
so a non-delegable grant cannot be propagated through another generation.
|
||||
Invalid or unauthorized requests return SYS_ERR_PERMISSION (-13).
|
||||
|
||||
Capability bits and protected operations are:
|
||||
|
||||
CAP_PROCESS_ADMIN kill unrelated processes or whole sessions
|
||||
CAP_POWER_REQUEST post a graceful shutdown/reboot request
|
||||
CAP_POWER_CONTROL consume power requests; reset or power off
|
||||
CAP_SUSPEND enter ACPI sleep
|
||||
CAP_STORAGE_ADMIN change partitions, mounts, or filesystems
|
||||
CAP_RAW_STORAGE raw disk reads and writes
|
||||
CAP_NETWORK_ADMIN change network/Wi-Fi configuration
|
||||
CAP_SET_TIME set wall-clock time or timezone
|
||||
CAP_USER_ADMIN manage users/trusted config; override owner at spawn
|
||||
CAP_DISPLAY_ADMIN set display mode or brightness
|
||||
CAP_DEVICE_ADMIN claim USB interfaces or change Bluetooth state
|
||||
CAP_LOG_READ read the kernel log
|
||||
CAP_SYSTEM_IMAGE write the program images in 0:/os and 0:/apps
|
||||
|
||||
CAP_STANDARD_SESSION contains CAP_POWER_REQUEST and CAP_SUSPEND.
|
||||
CAP_ADMIN_SESSION adds the administrative capabilities above except
|
||||
CAP_POWER_CONTROL and CAP_SYSTEM_IMAGE. Final shutdown/reset authority is
|
||||
deliberately retained by login, the trusted session supervisor.
|
||||
SYS_POWERINFO remains readable without a capability, so the powermgr GUI
|
||||
and power command are monitors, not privileged power daemons.
|
||||
|
||||
CAP_SYSTEM_IMAGE is likewise excluded from every session and from the
|
||||
"all" wildcard in 0:/config/capabilities.toml, and must be named
|
||||
explicitly to be granted. Capability grants are keyed on binary path, so
|
||||
write access to 0:/os or 0:/apps is equivalent to holding whatever those
|
||||
images are granted the next time a launcher runs them. Partitioning or
|
||||
formatting a volume is CAP_STORAGE_ADMIN and does not carry it.
|
||||
|
||||
The authentication and trusted-service configuration files users.toml,
|
||||
setup.toml, init.toml, and ssh.toml are readable by ordinary processes but
|
||||
require CAP_USER_ADMIN to create, replace, delete, or write. The kernel
|
||||
also protects the 0:/config directory entry against replacement. This
|
||||
prevents changing a userspace role string from becoming a route to new
|
||||
kernel authority at the next login or boot.
|
||||
|
||||
.SH PROCESS MANAGEMENT
|
||||
.B SYS_EXIT (0)
|
||||
Terminate the calling process.
|
||||
@@ -39,9 +99,18 @@
|
||||
|
||||
.B SYS_SPAWN (20)
|
||||
Spawn a new process from an ELF binary on the VFS. The child inherits
|
||||
a snapshot of the caller's environment.
|
||||
a snapshot of the caller's environment but no capabilities.
|
||||
int montauk::spawn(const char* path, const char* args = nullptr);
|
||||
|
||||
.B SYS_SPAWN_CAPS (185)
|
||||
Spawn a child with explicit permitted, effective, and delegable masks.
|
||||
The child masks must satisfy the subset rules described under CAPABILITY
|
||||
SECURITY. Passing a non-null user override additionally requires the
|
||||
caller to have effective CAP_USER_ADMIN; null inherits the parent owner.
|
||||
int montauk::spawn_with_caps(
|
||||
const char* path, const char* args, const char* user,
|
||||
const montauk::abi::SpawnCapabilities& capabilities);
|
||||
|
||||
.B SYS_WAITPID (23)
|
||||
Block until the given process has exited. Returns 0-255 for a normal
|
||||
exit, 256 plus the signal number if it was killed or crashed, or 0 if
|
||||
@@ -64,11 +133,13 @@
|
||||
|
||||
.B SYS_PROCLIST (61)
|
||||
List running processes (pid, parent, state, name, heap usage,
|
||||
accumulated CPU time).
|
||||
accumulated CPU time, and permitted/effective/delegable capability masks).
|
||||
int montauk::proclist(montauk::abi::ProcInfo* buf, int max);
|
||||
|
||||
.B SYS_KILL (62)
|
||||
Terminate another process by PID.
|
||||
Terminate a process by PID. Any process may terminate one of its own
|
||||
descendants. Terminating an unrelated process requires
|
||||
CAP_PROCESS_ADMIN.
|
||||
int montauk::kill(int pid);
|
||||
|
||||
.B SYS_SETSESSION (174)
|
||||
@@ -79,6 +150,7 @@
|
||||
.B SYS_KILLSESSION (175)
|
||||
Terminate all live processes in a process session. Returns the number of
|
||||
members signalled; repeat until zero to wait for complete teardown.
|
||||
Requires CAP_PROCESS_ADMIN.
|
||||
int montauk::killsession(int sessionId);
|
||||
|
||||
.B SYS_CHDIR (96)
|
||||
@@ -93,7 +165,8 @@
|
||||
|
||||
.B SYS_SETUSER (92)
|
||||
Associate a process with a logged-in user name (used by login/session
|
||||
management).
|
||||
management and per-user resource isolation). Requires CAP_USER_ADMIN.
|
||||
The name never grants capabilities or implies administrator status.
|
||||
int montauk::setuser(int pid, const char* name);
|
||||
|
||||
.B SYS_GETUSER (93)
|
||||
@@ -256,12 +329,14 @@
|
||||
|
||||
.B SYS_SETUNIXTIME (153)
|
||||
Set the system wall clock from a UTC Unix timestamp. Returns 0 on
|
||||
success or -1 if the timestamp is outside the supported range.
|
||||
success or -1 if the timestamp is outside the supported range. Requires
|
||||
CAP_SET_TIME.
|
||||
int montauk::set_unix_time(int64_t unixSeconds);
|
||||
|
||||
.B SYS_SETTZ (90)
|
||||
Set the system-wide timezone offset, in minutes from UTC.
|
||||
void montauk::settz(int offset_minutes);
|
||||
Set the system-wide timezone offset, in minutes from UTC. Requires
|
||||
CAP_SET_TIME.
|
||||
int montauk::settz(int offset_minutes);
|
||||
|
||||
.B SYS_GETTZ (91)
|
||||
Get the current timezone offset, in minutes from UTC.
|
||||
@@ -328,7 +403,8 @@
|
||||
void montauk::get_netcfg(montauk::abi::NetCfg* out);
|
||||
|
||||
.B SYS_SETNETCFG (38)
|
||||
Set the network configuration (IP, mask, gateway, DNS server).
|
||||
Set the network configuration (IP, mask, gateway, DNS server). Requires
|
||||
CAP_NETWORK_ADMIN.
|
||||
int montauk::set_netcfg(const montauk::abi::NetCfg* cfg);
|
||||
|
||||
.B SYS_NETSTATUS (125)
|
||||
@@ -346,6 +422,7 @@
|
||||
.B SYS_WIFI_SCAN (158)
|
||||
Perform a channel scan and block until it finishes or timeoutMs
|
||||
elapses. Returns the number of results, or -1 if no adapter is ready.
|
||||
Requires CAP_NETWORK_ADMIN because it changes radio state.
|
||||
int montauk::wifi_scan(montauk::abi::WifiNetwork* buf,
|
||||
int maxCount, uint32_t timeoutMs);
|
||||
|
||||
@@ -355,16 +432,18 @@
|
||||
|
||||
.B SYS_WIFI_CONNECT (160)
|
||||
Join a network and block until the link is up or the attempt fails.
|
||||
Requires CAP_NETWORK_ADMIN.
|
||||
int montauk::wifi_connect(const char* ssid,
|
||||
const char* password);
|
||||
|
||||
.B SYS_WIFI_DISCONNECT (161)
|
||||
Disconnect from the current Wi-Fi network.
|
||||
Disconnect from the current Wi-Fi network. Requires CAP_NETWORK_ADMIN.
|
||||
int montauk::wifi_disconnect();
|
||||
|
||||
.B SYS_WIFI_SCAN_START (162)
|
||||
Start a non-blocking channel scan. Returns 0 if started, 1 if a scan
|
||||
is already running, or -1 if no adapter is ready.
|
||||
is already running, or -1 if no adapter is ready. Requires
|
||||
CAP_NETWORK_ADMIN.
|
||||
int montauk::wifi_scan_start(uint32_t timeoutMs);
|
||||
|
||||
.B SYS_WIFI_RESULTS (163)
|
||||
@@ -374,7 +453,7 @@
|
||||
|
||||
.B SYS_WIFI_CONNECT_ASYNC (164)
|
||||
Start a non-blocking network join. Observe SYS_WIFI_INFO for progress
|
||||
and the final result.
|
||||
and the final result. Requires CAP_NETWORK_ADMIN.
|
||||
int montauk::wifi_connect_async(const char* ssid,
|
||||
const char* password);
|
||||
|
||||
@@ -450,11 +529,13 @@
|
||||
int maxCount);
|
||||
|
||||
.B SYS_DISPLAYSETMODE (156)
|
||||
Switch to a mode returned by SYS_DISPLAYMODES.
|
||||
Switch to a mode returned by SYS_DISPLAYMODES. Requires
|
||||
CAP_DISPLAY_ADMIN.
|
||||
int montauk::display_set_mode(int modeIndex);
|
||||
|
||||
.B SYS_DISPLAYBRIGHTNESS (157)
|
||||
Set brightness to 0-100 percent, or pass -1 to query it.
|
||||
Set brightness to 0-100 percent, or pass -1 to query it. Setting requires
|
||||
CAP_DISPLAY_ADMIN; querying does not.
|
||||
int montauk::display_brightness(int percent = -1);
|
||||
|
||||
.SH TERMINAL
|
||||
@@ -478,15 +559,18 @@
|
||||
|
||||
.SH POWER MANAGEMENT
|
||||
.B SYS_RESET (26)
|
||||
Reboot the system.
|
||||
[[noreturn]] void montauk::reset();
|
||||
Reboot the system. Requires CAP_POWER_CONTROL. A successful call does not
|
||||
return; an unauthorized call returns SYS_ERR_PERMISSION.
|
||||
int montauk::reset();
|
||||
|
||||
.B SYS_SHUTDOWN (27)
|
||||
Shut down the system.
|
||||
[[noreturn]] void montauk::shutdown();
|
||||
Shut down the system. Requires CAP_POWER_CONTROL. A successful call does
|
||||
not return; an unauthorized call returns SYS_ERR_PERMISSION.
|
||||
int montauk::shutdown();
|
||||
|
||||
.B SYS_SUSPEND (89)
|
||||
Enter ACPI S3 sleep. Returns after wake, 0 on success.
|
||||
Enter ACPI S3 sleep. Returns after wake, 0 on success. Requires
|
||||
CAP_SUSPEND.
|
||||
int montauk::suspend();
|
||||
|
||||
.B SYS_POWER_REQUEST (135)
|
||||
@@ -494,8 +578,16 @@
|
||||
posts a pending action (POWER_REQ_SHUTDOWN / POWER_REQ_REBOOT)
|
||||
then exits; login.elf reads it with POWER_REQ_QUERY
|
||||
(read-and-clear), runs the shutdown stages, and finally calls
|
||||
shutdown()/reset(). See montauk::abi::PowerRequestAction.
|
||||
shutdown()/reset(). Posting requires CAP_POWER_REQUEST; querying and
|
||||
consuming the request requires CAP_POWER_CONTROL. See
|
||||
montauk::abi::PowerRequestAction.
|
||||
|
||||
A request may also come from inside a session -- the shell's shutdown
|
||||
builtin posts one. Since login only reads the request after the session
|
||||
leader exits, the leader polls POWER_REQ_PEEK, a non-destructive read
|
||||
requiring only CAP_POWER_REQUEST, and exits when one is pending.
|
||||
int montauk::power_request(int action);
|
||||
int montauk::power_request_pending();
|
||||
|
||||
.B SYS_POWERINFO (149)
|
||||
Get the CPU power/thermal snapshot (HWP state, throttling,
|
||||
@@ -507,7 +599,7 @@
|
||||
|
||||
.SH KERNEL LOG
|
||||
.B SYS_LOG (46)
|
||||
Read from the kernel ring log buffer.
|
||||
Read from the kernel ring log buffer. Requires CAP_LOG_READ.
|
||||
int64_t montauk::read_log(char* buf, uint64_t size);
|
||||
|
||||
.B SYS_LOG_WRITE (176)
|
||||
@@ -520,9 +612,20 @@
|
||||
to the framebuffer console.
|
||||
|
||||
.B SYS_SPAWN_REDIR (49)
|
||||
Spawn a process with its console I/O redirected to the caller.
|
||||
Spawn a process with its console I/O redirected to the caller. The child
|
||||
receives no capabilities.
|
||||
int montauk::spawn_redir(const char* path, const char* args = nullptr);
|
||||
|
||||
.B SYS_SPAWN_REDIR_CAPS (186)
|
||||
Spawn a redirected child with explicit capability masks. It applies the
|
||||
same subset and parent-delegable checks as SYS_SPAWN_CAPS. An
|
||||
administrative console explicitly lets its shell delegate selected
|
||||
capabilities; the shell's executable policy gives each trusted tool a
|
||||
delegable mask of zero, preventing further propagation.
|
||||
int montauk::spawn_redir_with_caps(
|
||||
const char* path, const char* args,
|
||||
const montauk::abi::SpawnCapabilities& capabilities);
|
||||
|
||||
.B SYS_CHILDIO_READ (50)
|
||||
Read buffered output produced by a redirected child.
|
||||
int montauk::childio_read(int childPid, char* buf, int maxLen);
|
||||
@@ -625,37 +728,41 @@
|
||||
int montauk::partlist(montauk::abi::PartInfo* buf, int max);
|
||||
|
||||
.B SYS_DISKREAD (71)
|
||||
Raw, driver-agnostic sector read from a block device.
|
||||
Raw, driver-agnostic sector read from a block device. Requires
|
||||
CAP_RAW_STORAGE.
|
||||
int64_t montauk::disk_read(int blockDev, uint64_t lba,
|
||||
uint32_t sectorCount, void* buf);
|
||||
|
||||
.B SYS_DISKWRITE (72)
|
||||
Raw, driver-agnostic sector write to a block device.
|
||||
Raw, driver-agnostic sector write to a block device. Requires
|
||||
CAP_RAW_STORAGE.
|
||||
int64_t montauk::disk_write(int blockDev, uint64_t lba,
|
||||
uint32_t sectorCount, const void* buf);
|
||||
|
||||
.B SYS_GPTINIT (73)
|
||||
Initialize a fresh GPT partition table on a block device.
|
||||
Initialize a fresh GPT partition table on a block device. Requires
|
||||
CAP_STORAGE_ADMIN.
|
||||
int montauk::gpt_init(int blockDev);
|
||||
|
||||
.B SYS_GPTADD (74)
|
||||
Add a partition to an existing GPT table.
|
||||
Add a partition to an existing GPT table. Requires CAP_STORAGE_ADMIN.
|
||||
int montauk::gpt_add(const montauk::abi::GptAddParams* params);
|
||||
|
||||
.B SYS_FSMOUNT (75)
|
||||
Mount a partition's filesystem onto a drive number.
|
||||
Mount a partition's filesystem onto a drive number. Requires
|
||||
CAP_STORAGE_ADMIN.
|
||||
int montauk::fs_mount(int partIndex, int driveNum);
|
||||
|
||||
.B SYS_FSFORMAT (76)
|
||||
Format a partition with a filesystem (FS_TYPE_FAT32 or
|
||||
FS_TYPE_EXT2).
|
||||
FS_TYPE_EXT2). Requires CAP_STORAGE_ADMIN.
|
||||
int montauk::fs_format(const montauk::abi::FsFormatParams* params);
|
||||
|
||||
.B SYS_FS_SYNC (134)
|
||||
Flush all block-device write caches and cleanly unmount
|
||||
disk-backed volumes ahead of power-off. Returns the number of
|
||||
volumes unmounted. Part of the graceful shutdown sequence
|
||||
(see SYS_POWER_REQUEST).
|
||||
(see SYS_POWER_REQUEST). Requires CAP_STORAGE_ADMIN.
|
||||
int montauk::fs_sync();
|
||||
|
||||
.SH AUDIO
|
||||
@@ -708,16 +815,18 @@
|
||||
|
||||
.SH BLUETOOTH
|
||||
.B SYS_BTSCAN (84)
|
||||
Scan for discoverable Bluetooth devices for up to timeoutMs.
|
||||
Scan for discoverable Bluetooth devices for up to timeoutMs. Requires
|
||||
CAP_DEVICE_ADMIN because it changes radio state.
|
||||
int montauk::bt_scan(montauk::abi::BtScanResult* buf, int maxCount,
|
||||
uint32_t timeoutMs);
|
||||
|
||||
.B SYS_BTCONNECT (85)
|
||||
Connect (and pair/bond if needed) to a device by BD_ADDR.
|
||||
Connect (and pair/bond if needed) to a device by BD_ADDR. Requires
|
||||
CAP_DEVICE_ADMIN.
|
||||
int montauk::bt_connect(const uint8_t* bdAddr);
|
||||
|
||||
.B SYS_BTDISCONNECT (86)
|
||||
Disconnect from a device by BD_ADDR.
|
||||
Disconnect from a device by BD_ADDR. Requires CAP_DEVICE_ADMIN.
|
||||
int montauk::bt_disconnect(const uint8_t* bdAddr);
|
||||
|
||||
.B SYS_BTLIST (87)
|
||||
@@ -731,7 +840,8 @@
|
||||
.B SYS_BTSETADDR (137)
|
||||
Change the adapter's BD_ADDR (6-byte buffer, byte 0 is the
|
||||
least-significant octet). Volatile -- apply after the last
|
||||
controller reset and persist separately to bluetooth.toml.
|
||||
controller reset and persist separately to bluetooth.toml. Requires
|
||||
CAP_DEVICE_ADMIN.
|
||||
int montauk::bt_set_addr(const uint8_t* bdAddr);
|
||||
|
||||
.B SYS_BTBONDS (138)
|
||||
@@ -739,7 +849,8 @@
|
||||
int montauk::bt_bonds(montauk::abi::BtBondInfo* buf, int maxCount);
|
||||
|
||||
.B SYS_BTFORGET (139)
|
||||
Forget a paired device; it must re-pair next time.
|
||||
Forget a paired device; it must re-pair next time. Requires
|
||||
CAP_DEVICE_ADMIN.
|
||||
int montauk::bt_forget(const uint8_t* bdAddr);
|
||||
|
||||
.SH GENERIC USB INTERFACES
|
||||
@@ -759,7 +870,8 @@
|
||||
|
||||
.B SYS_USB_CLAIM (179)
|
||||
Exclusively claim an unbound interface. Returns a generation-checked handle
|
||||
owned by the calling process.
|
||||
owned by the calling process. Requires CAP_DEVICE_ADMIN; subsequent
|
||||
operations are authorized by ownership of that handle.
|
||||
int montauk::usb_claim(uint8_t slotId, uint8_t interfaceNumber);
|
||||
|
||||
.B SYS_USB_CLOSE (180)
|
||||
|
||||
@@ -6,33 +6,31 @@
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
static void spawn_app(const char* path, const char* args = nullptr) {
|
||||
if (path && path[0]) {
|
||||
montauk::spawn(path, args);
|
||||
}
|
||||
static void spawn_app(DesktopState* ds, const char* path, const char* args = nullptr) {
|
||||
if (path && path[0]) desktop_spawn_app(ds, path, args);
|
||||
}
|
||||
|
||||
void open_terminal(DesktopState* ds) {
|
||||
const char* home = (ds && ds->home_dir[0]) ? ds->home_dir : nullptr;
|
||||
spawn_app("0:/apps/terminal/terminal.elf", home);
|
||||
spawn_app(ds, "0:/apps/terminal/terminal.elf", home);
|
||||
}
|
||||
|
||||
void open_calculator(DesktopState* ds) {
|
||||
(void)ds;
|
||||
spawn_app("0:/apps/calculator/calculator.elf");
|
||||
spawn_app(ds, "0:/apps/calculator/calculator.elf");
|
||||
}
|
||||
|
||||
void open_texteditor(DesktopState* ds) {
|
||||
(void)ds;
|
||||
spawn_app("0:/apps/texteditor/texteditor.elf");
|
||||
spawn_app(ds, "0:/apps/texteditor/texteditor.elf");
|
||||
}
|
||||
|
||||
void open_syslog(DesktopState* ds) {
|
||||
(void)ds;
|
||||
spawn_app("0:/apps/klog/syslog.elf");
|
||||
spawn_app(ds, "0:/apps/syslog/syslog.elf");
|
||||
}
|
||||
|
||||
void open_wordprocessor(DesktopState* ds) {
|
||||
(void)ds;
|
||||
spawn_app("0:/apps/wordprocessor/wordprocessor.elf");
|
||||
spawn_app(ds, "0:/apps/wordprocessor/wordprocessor.elf");
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ inline void* operator new(unsigned long, void* p) { return p; }
|
||||
|
||||
using namespace gui;
|
||||
|
||||
// Central launch policy implemented by desktop_catalog.cpp. Standalone app
|
||||
// wrappers include this header directly, so keep the declaration here rather
|
||||
// than only in desktop_internal.hpp.
|
||||
int desktop_spawn_app(DesktopState* ds, const char* path,
|
||||
const char* args = nullptr);
|
||||
|
||||
// ============================================================================
|
||||
// Minimal snprintf
|
||||
// ============================================================================
|
||||
@@ -261,8 +267,8 @@ void open_sleep_dialog(DesktopState* ds);
|
||||
// user returns to the login screen, where the shutdown stages run (Bluetooth
|
||||
// teardown, filesystem flush) before the final ACPI power-off / reset. Pass
|
||||
// montauk::abi::POWER_REQ_SHUTDOWN or montauk::abi::POWER_REQ_REBOOT.
|
||||
[[noreturn]] inline void desktop_request_power(int action) {
|
||||
montauk::power_request(action);
|
||||
montauk::exit(0);
|
||||
inline void desktop_request_power(int action) {
|
||||
if (montauk::power_request(action) == 0)
|
||||
montauk::exit(0);
|
||||
}
|
||||
bool desktop_poll_external_windows(DesktopState* ds);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
#include "desktop_internal.hpp"
|
||||
#include <montauk/capabilities.h>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -24,6 +25,18 @@ static void sort_item_indices(DesktopState* ds, int* indices, int count) {
|
||||
|
||||
} // namespace
|
||||
|
||||
int desktop_spawn_app(DesktopState* ds, const char* path, const char* args) {
|
||||
if (!ds || !path || !path[0]) return -1;
|
||||
// The grant table decides what each program may receive; the kernel bounds
|
||||
// it by what this session was actually delegated, so a non-admin session
|
||||
// clamps to nothing without the desktop having to decide that itself.
|
||||
montauk::abi::SpawnCapabilities caps =
|
||||
montauk::caps::for_binary(path, montauk::caps::self_delegable());
|
||||
// Applications inherit the desktop's kernel-owned owner identity. An
|
||||
// explicit user override is reserved for trusted session creators.
|
||||
return montauk::spawn_with_caps(path, args, nullptr, caps);
|
||||
}
|
||||
|
||||
int desktop_list_item_indices(DesktopState* ds,
|
||||
DesktopItemSection section,
|
||||
int* out,
|
||||
@@ -54,9 +67,9 @@ bool desktop_launch_item(DesktopState* ds, const DesktopItem* item) {
|
||||
case DESKTOP_ITEM_LAUNCH_EXECUTABLE:
|
||||
if (!item->binary_path[0]) return false;
|
||||
if (item->launch_with_home) {
|
||||
return montauk::spawn(item->binary_path, ds->home_dir) >= 0;
|
||||
return desktop_spawn_app(ds, item->binary_path, ds->home_dir) >= 0;
|
||||
}
|
||||
return montauk::spawn(item->binary_path) >= 0;
|
||||
return desktop_spawn_app(ds, item->binary_path) >= 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -381,9 +381,9 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
} else if (!row.is_category) {
|
||||
if (row.external) {
|
||||
if (row.launch_with_home) {
|
||||
montauk::spawn(row.binary_path, ds->home_dir);
|
||||
desktop_spawn_app(ds, row.binary_path, ds->home_dir);
|
||||
} else {
|
||||
montauk::spawn(row.binary_path);
|
||||
desktop_spawn_app(ds, row.binary_path);
|
||||
}
|
||||
} else {
|
||||
desktop_launch_builtin(ds, row.app_id);
|
||||
|
||||
@@ -834,6 +834,19 @@ void gui::desktop_run(DesktopState* ds) {
|
||||
|
||||
if (now >= nextClockPollMs) {
|
||||
nextClockPollMs = now + 1000;
|
||||
|
||||
// A power request can be posted by anything in this session that
|
||||
// holds CAP_POWER_REQUEST -- the shell's shutdown builtin, run in
|
||||
// a terminal window, is the common case. login only reads it once
|
||||
// the session leader exits, so ending the session is our job; it
|
||||
// is the same handoff desktop_request_power() performs, just
|
||||
// reached from a request we did not originate.
|
||||
int pendingPower = montauk::power_request_pending();
|
||||
if (pendingPower == montauk::abi::POWER_REQ_SHUTDOWN ||
|
||||
pendingPower == montauk::abi::POWER_REQ_REBOOT) {
|
||||
montauk::exit(0);
|
||||
}
|
||||
|
||||
uint64_t clockToken = desktop_clock_token();
|
||||
if (clockToken != lastClockToken) {
|
||||
lastClockToken = clockToken;
|
||||
|
||||
@@ -328,7 +328,7 @@ bool desktop_wifi_poll(DesktopState* ds, uint64_t now) {
|
||||
if (ds->wifi_dhcp_pending && info.connected && ds->cached_net_cfg.ipAddress == 0) {
|
||||
ds->wifi_dhcp_pending = false;
|
||||
ds->wifi_dhcp_waiting = true;
|
||||
montauk::spawn("0:/os/dhcp.elf");
|
||||
desktop_spawn_app(ds, "0:/os/dhcp.elf");
|
||||
wifi_set_status(ds, "Requesting an address...");
|
||||
changed = true;
|
||||
} else if (ds->wifi_dhcp_pending && info.connected) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/capabilities.h>
|
||||
#include <libc/stdio.h>
|
||||
|
||||
// ---- ANSI color codes ----
|
||||
@@ -80,7 +81,11 @@ static bool run_service(const char* path, const char* name, bool wait = true) {
|
||||
snprintf(msg, sizeof(msg), "Starting %s", name);
|
||||
log_info(msg);
|
||||
|
||||
int pid = montauk::spawn(path);
|
||||
// Grants are keyed on the executable, not the service id, so repointing a
|
||||
// privileged entry in init.toml at another program transfers no authority.
|
||||
montauk::abi::SpawnCapabilities caps =
|
||||
montauk::caps::for_binary(path, montauk::caps::self_delegable());
|
||||
int pid = montauk::spawn_with_caps(path, nullptr, "system", caps);
|
||||
if (pid < 0) {
|
||||
snprintf(msg, sizeof(msg), "Failed to start %s", name);
|
||||
log_err(msg);
|
||||
|
||||
@@ -52,16 +52,30 @@ void launch_session(LoginState* ls) {
|
||||
montauk::memset(ls->password, 0, sizeof(ls->password));
|
||||
ls->password_len = 0;
|
||||
|
||||
// A session may pass on what it holds and no more. Authority is never
|
||||
// propagated implicitly by spawn(): the desktop and the console both
|
||||
// consult 0:/config/capabilities.toml to decide which program receives
|
||||
// which subset, and the kernel clamps that to this set. A standard
|
||||
// session is delegable for the same reason an admin one is -- otherwise
|
||||
// its own terminal could not hand the shell the suspend it already has.
|
||||
montauk::abi::SpawnCapabilities caps{};
|
||||
caps.permitted = montauk::user::is_admin(ls->username)
|
||||
? montauk::abi::CAP_ADMIN_SESSION
|
||||
: montauk::abi::CAP_STANDARD_SESSION;
|
||||
caps.effective = caps.permitted;
|
||||
caps.delegable = caps.permitted;
|
||||
|
||||
int pid;
|
||||
if (ls->session_mode == SESSION_CONSOLE) {
|
||||
char console_args[96];
|
||||
build_console_args(ls, console_args, (int)sizeof(console_args));
|
||||
pid = montauk::spawn("0:/apps/terminal/terminal.elf", console_args);
|
||||
pid = montauk::spawn_with_caps("0:/apps/terminal/terminal.elf",
|
||||
console_args, ls->username, caps);
|
||||
} else {
|
||||
pid = montauk::spawn("0:/os/desktop.elf", ls->username);
|
||||
pid = montauk::spawn_with_caps("0:/os/desktop.elf", ls->username,
|
||||
ls->username, caps);
|
||||
}
|
||||
if (pid >= 0) {
|
||||
montauk::setuser(pid, ls->username);
|
||||
montauk::waitpid(pid);
|
||||
if (ls->session_mode == SESSION_DESKTOP) {
|
||||
terminate_desktop_session(pid);
|
||||
|
||||
@@ -250,10 +250,12 @@ void perform_graceful_shutdown(LoginState* ls, int action) {
|
||||
|
||||
// ==== Stage 4: dispatch the ACPI power-off / reset ====
|
||||
show_stage(ls, heading, rebooting ? "Restarting now..." : "Powering off...");
|
||||
if (rebooting) {
|
||||
montauk::reset();
|
||||
} else {
|
||||
montauk::shutdown();
|
||||
}
|
||||
__builtin_unreachable();
|
||||
int rc = rebooting ? montauk::reset() : montauk::shutdown();
|
||||
// A successful power-control syscall never returns. If it does return,
|
||||
// keep the trusted supervisor alive and make the authorization failure
|
||||
// visible instead of falling through an unreachable-code assumption.
|
||||
show_stage(ls, heading, rc == montauk::abi::SYS_ERR_PERMISSION
|
||||
? "Power control permission denied."
|
||||
: "Power control failed.");
|
||||
for (;;) montauk::sleep_ms(1000);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,16 @@ static void maybe_run_setup_session() {
|
||||
montauk::user::set_session(user);
|
||||
doc.destroy();
|
||||
|
||||
int pid = montauk::spawn("0:/os/desktop.elf", user);
|
||||
montauk::abi::SpawnCapabilities caps{};
|
||||
bool admin = montauk::streq(role, "admin");
|
||||
caps.permitted = caps.effective = admin
|
||||
? montauk::abi::CAP_ADMIN_SESSION
|
||||
: montauk::abi::CAP_STANDARD_SESSION;
|
||||
// Matches launch_session(): a session delegates from what it holds, and
|
||||
// the grant table decides which program receives which part of it.
|
||||
caps.delegable = caps.permitted;
|
||||
int pid = montauk::spawn_with_caps("0:/os/desktop.elf", user, user, caps);
|
||||
if (pid >= 0) {
|
||||
montauk::setuser(pid, user);
|
||||
montauk::waitpid(pid);
|
||||
terminate_desktop_session(pid);
|
||||
}
|
||||
|
||||
@@ -951,7 +951,13 @@ static void render() {
|
||||
}
|
||||
|
||||
static void launch_dhcp() {
|
||||
int pid = montauk::spawn("0:/os/dhcp.elf");
|
||||
montauk::abi::SpawnCapabilities caps{
|
||||
montauk::abi::CAP_NETWORK_ADMIN,
|
||||
montauk::abi::CAP_NETWORK_ADMIN,
|
||||
0
|
||||
};
|
||||
int pid = montauk::spawn_with_caps("0:/os/dhcp.elf", nullptr,
|
||||
nullptr, caps);
|
||||
if (pid >= 0) {
|
||||
set_status("DHCP client started");
|
||||
} else {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/capabilities.h>
|
||||
#include <gui/mtk.hpp>
|
||||
|
||||
extern "C" {
|
||||
@@ -392,8 +393,26 @@ static bool refresh_state(bool force) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ending another process needs CAP_PROCESS_ADMIN; without it the kernel only
|
||||
// permits killing our own descendants. Capabilities cannot change over a
|
||||
// process lifetime and this is consulted from the render path, so resolve it
|
||||
// once rather than per frame.
|
||||
static bool g_process_admin = false;
|
||||
static bool g_process_admin_known = false;
|
||||
|
||||
static bool process_admin_granted() {
|
||||
if (!g_process_admin_known) {
|
||||
montauk::abi::SpawnCapabilities mine;
|
||||
montauk::caps::self(mine);
|
||||
g_process_admin = (mine.effective & montauk::abi::CAP_PROCESS_ADMIN) != 0;
|
||||
g_process_admin_known = true;
|
||||
}
|
||||
return g_process_admin;
|
||||
}
|
||||
|
||||
static bool can_kill_selected() {
|
||||
return g_pm.active_tab == PM_TAB_PROCESSES
|
||||
return process_admin_granted()
|
||||
&& g_pm.active_tab == PM_TAB_PROCESSES
|
||||
&& g_pm.selected >= 0
|
||||
&& g_pm.selected < g_pm.proc_count
|
||||
&& g_pm.procs[g_pm.selected].pid != 0
|
||||
|
||||
@@ -8,5 +8,10 @@
|
||||
|
||||
extern "C" void _start() {
|
||||
montauk::print("Rebooting...\n");
|
||||
montauk::reset();
|
||||
int rc = montauk::reset();
|
||||
if (rc == montauk::abi::SYS_ERR_PERMISSION)
|
||||
montauk::print("reset: permission denied\n");
|
||||
else
|
||||
montauk::print("reset: reboot failed\n");
|
||||
montauk::exit(1);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ endif
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
LIBDIR := ../../lib
|
||||
OBJDIR := obj
|
||||
|
||||
# ---- C++ compiler flags ----
|
||||
@@ -63,9 +64,9 @@ TARGET := $(BINDIR)/os/shell.elf
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/liblibc.a
|
||||
mkdir -p $(BINDIR)/os
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) -o $@
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp shell.h Makefile
|
||||
mkdir -p $(OBJDIR)
|
||||
|
||||
@@ -68,6 +68,7 @@ void cmd_help() {
|
||||
montauk::print(" fontscale [n] Set terminal font scale (1-8)\n");
|
||||
montauk::print(" reset Reboot the system\n");
|
||||
montauk::print(" shutdown Shut down the system\n");
|
||||
montauk::print(" suspend Suspend the system\n");
|
||||
montauk::print("\n");
|
||||
montauk::print("Network commands:\n");
|
||||
montauk::print(" ping <ip> Send ICMP echo requests\n");
|
||||
|
||||
@@ -41,7 +41,17 @@ static void print_exit_code(int code) {
|
||||
|
||||
static bool try_exec(const char* path, const char* args) {
|
||||
if (!file_exists(path)) return false;
|
||||
int pid = montauk::spawn(path, args);
|
||||
// Least-privilege hygiene, NOT a security boundary: this never decides
|
||||
// what may run, only which capabilities a tool receives. The user in an
|
||||
// admin console can already do everything these tools do, so the point is
|
||||
// to keep a bug in one tool from reaching authority it has no use for.
|
||||
// The shell delegates on the same terms as the desktop; the table decides
|
||||
// per binary, and the kernel re-checks the bound on every spawn.
|
||||
montauk::abi::SpawnCapabilities caps =
|
||||
montauk::caps::for_binary(path, shell_delegable_caps);
|
||||
int pid = (caps.permitted != 0)
|
||||
? montauk::spawn_with_caps(path, args, nullptr, caps)
|
||||
: montauk::spawn(path, args);
|
||||
if (pid < 0) return false;
|
||||
print_exit_code(montauk::waitpid(pid));
|
||||
return true;
|
||||
|
||||
@@ -13,6 +13,7 @@ int current_drive = 0;
|
||||
int last_exit = 0;
|
||||
char session_user[32] = "";
|
||||
char session_home[64] = "";
|
||||
uint64_t shell_delegable_caps = 0;
|
||||
|
||||
void sync_cwd() {
|
||||
char abs[128];
|
||||
@@ -46,6 +47,8 @@ void read_session() {
|
||||
|
||||
scopy(session_home, "0:/users/", sizeof(session_home));
|
||||
scat(session_home, session_user, sizeof(session_home));
|
||||
|
||||
shell_delegable_caps = montauk::caps::self_delegable();
|
||||
}
|
||||
|
||||
// ---- Command history ----
|
||||
@@ -184,6 +187,29 @@ static int process_command(const char* line) {
|
||||
if (streq(cmd, "true")) { return 0; }
|
||||
if (streq(cmd, "false")) { return 1; }
|
||||
|
||||
// Route interactive power actions through the trusted login supervisor.
|
||||
// Posting the request and exiting lets terminal.elf close, after which
|
||||
// login performs filesystem/Bluetooth cleanup and the final power syscall.
|
||||
if (streq(cmd, "shutdown") || streq(cmd, "poweroff") || streq(cmd, "halt") ||
|
||||
streq(cmd, "reboot") || streq(cmd, "reset")) {
|
||||
int action = (streq(cmd, "reboot") || streq(cmd, "reset"))
|
||||
? montauk::abi::POWER_REQ_REBOOT
|
||||
: montauk::abi::POWER_REQ_SHUTDOWN;
|
||||
int rc = montauk::power_request(action);
|
||||
if (rc == 0) montauk::exit(0);
|
||||
montauk::print(rc == montauk::abi::SYS_ERR_PERMISSION
|
||||
? "power: permission denied\n"
|
||||
: "power: request failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (streq(cmd, "suspend")) {
|
||||
int rc = montauk::suspend();
|
||||
if (rc == montauk::abi::SYS_ERR_PERMISSION)
|
||||
montauk::print("suspend: permission denied\n");
|
||||
return rc == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
if (streq(cmd, "pwd")) {
|
||||
sync_cwd();
|
||||
char path[128];
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/capabilities.h>
|
||||
|
||||
using montauk::slen;
|
||||
using montauk::streq;
|
||||
@@ -55,6 +56,7 @@ extern int current_drive;
|
||||
extern int last_exit;
|
||||
extern char session_user[32];
|
||||
extern char session_home[64];
|
||||
extern uint64_t shell_delegable_caps;
|
||||
|
||||
// ---- Inline path helpers ----
|
||||
|
||||
|
||||
@@ -10,6 +10,15 @@ extern "C" void _start() {
|
||||
montauk::print("Shutting down...\n");
|
||||
// This low-level utility bypasses the login graceful-shutdown view, so flush
|
||||
// pending writes and unmount disk-backed volumes here before powering off.
|
||||
montauk::fs_sync();
|
||||
montauk::shutdown();
|
||||
int sync_rc = montauk::fs_sync();
|
||||
if (sync_rc == montauk::abi::SYS_ERR_PERMISSION) {
|
||||
montauk::print("shutdown: permission denied\n");
|
||||
montauk::exit(1);
|
||||
}
|
||||
int rc = montauk::shutdown();
|
||||
if (rc == montauk::abi::SYS_ERR_PERMISSION)
|
||||
montauk::print("shutdown: permission denied\n");
|
||||
else
|
||||
montauk::print("shutdown: power-off failed\n");
|
||||
montauk::exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user