diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 5378e53..9a9ec1c 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 155 +#define MONTAUK_BUILD_NUMBER 168 diff --git a/kernel/src/Api/Filesystem.hpp b/kernel/src/Api/Filesystem.hpp index 4589c8d..3311a08 100644 --- a/kernel/src/Api/Filesystem.hpp +++ b/kernel/src/Api/Filesystem.hpp @@ -17,8 +17,14 @@ #include #include #include "Path.hpp" +#include 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); } diff --git a/kernel/src/Api/Heap.hpp b/kernel/src/Api/Heap.hpp index cfec302..7b61e25 100644 --- a/kernel/src/Api/Heap.hpp +++ b/kernel/src/Api/Heap.hpp @@ -5,6 +5,7 @@ */ #pragma once +#include #include #include #include @@ -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; } diff --git a/kernel/src/Api/Info.hpp b/kernel/src/Api/Info.hpp index 30c8302..24c02d0 100644 --- a/kernel/src/Api/Info.hpp +++ b/kernel/src/Api/Info.hpp @@ -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; } diff --git a/kernel/src/Api/IoRedir.hpp b/kernel/src/Api/IoRedir.hpp index f07fa09..b8d1726 100644 --- a/kernel/src/Api/IoRedir.hpp +++ b/kernel/src/Api/IoRedir.hpp @@ -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; diff --git a/kernel/src/Api/LibSyscall.hpp b/kernel/src/Api/LibSyscall.hpp index c779926..5341235 100644 --- a/kernel/src/Api/LibSyscall.hpp +++ b/kernel/src/Api/LibSyscall.hpp @@ -4,6 +4,7 @@ * Copyright (c) 2026 Daniel Hammer */ +#include #include #include #include @@ -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; diff --git a/kernel/src/Api/Power.hpp b/kernel/src/Api/Power.hpp index 39b63d6..1b070d5 100644 --- a/kernel/src/Api/Power.hpp +++ b/kernel/src/Api/Power.hpp @@ -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; diff --git a/kernel/src/Api/Process.hpp b/kernel/src/Api/Process.hpp index b3b31f4..215a0de 100644 --- a/kernel/src/Api/Process.hpp +++ b/kernel/src/Api/Process.hpp @@ -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); } diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 918ce3d..0d881ed 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -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(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(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(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(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(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(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(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(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)"; } } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index e63d78c..71c7a9c 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -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) diff --git a/kernel/src/Boot/Main.cpp b/kernel/src/Boot/Main.cpp index 5d14a82..f83e438 100644 --- a/kernel/src/Boot/Main.cpp +++ b/kernel/src/Boot/Main.cpp @@ -5,6 +5,8 @@ * Further copyright information and third party notices can be found at https://montaukos.org/license.txt. */ +#include +#include #include #include #include @@ -174,6 +176,8 @@ extern "C" void kmain() { montauk::abi::InitializeSyscalls(); Sched::Initialize(); + Memory::InitUserRange(); + Fs::LogProtectedPaths(); Ipc::Initialize(); #if defined (__x86_64__) diff --git a/kernel/src/Fs/ProtectedPaths.cpp b/kernel/src/Fs/ProtectedPaths.cpp new file mode 100644 index 0000000..6d8963e --- /dev/null +++ b/kernel/src/Fs/ProtectedPaths.cpp @@ -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 +#include + +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 ":" 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; + } + } +} diff --git a/kernel/src/Fs/ProtectedPaths.hpp b/kernel/src/Fs/ProtectedPaths.hpp new file mode 100644 index 0000000..fc5281c --- /dev/null +++ b/kernel/src/Fs/ProtectedPaths.hpp @@ -0,0 +1,20 @@ +/* + * ProtectedPaths.hpp + * Capability required to modify paths on the booted system volume + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +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(); + +} diff --git a/kernel/src/Ipc/Ipc.cpp b/kernel/src/Ipc/Ipc.cpp index 63dd51d..7112fca 100644 --- a/kernel/src/Ipc/Ipc.cpp +++ b/kernel/src/Ipc/Ipc.cpp @@ -8,11 +8,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -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, " diff --git a/kernel/src/Ipc/Ipc.hpp b/kernel/src/Ipc/Ipc.hpp index 3316fe1..365c1c5 100644 --- a/kernel/src/Ipc/Ipc.hpp +++ b/kernel/src/Ipc/Ipc.hpp @@ -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); } diff --git a/kernel/src/Memory/UserRange.cpp b/kernel/src/Memory/UserRange.cpp new file mode 100644 index 0000000..7aaa8b9 --- /dev/null +++ b/kernel/src/Memory/UserRange.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +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); + } + +} diff --git a/kernel/src/Memory/UserRange.hpp b/kernel/src/Memory/UserRange.hpp new file mode 100644 index 0000000..e03216f --- /dev/null +++ b/kernel/src/Memory/UserRange.hpp @@ -0,0 +1,23 @@ +/* + * UserRange.hpp + * Cross-CPU invalidation and teardown of user address-space mappings + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +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); + +} diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 9ed462e..1395a80 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -4,6 +4,7 @@ * Copyright (c) 2025-2026 Daniel Hammer */ +#include #include "Scheduler.hpp" #include "ElfLoader.hpp" #include @@ -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; diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index 0965ad6..7baffaf 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -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(); diff --git a/programs/data/config/capabilities.toml b/programs/data/config/capabilities.toml new file mode 100644 index 0000000..9c529db --- /dev/null +++ b/programs/data/config/capabilities.toml @@ -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"] diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 0814b5c..81fd7b0 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -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 { diff --git a/programs/include/gui/terminal.hpp b/programs/include/gui/terminal.hpp index 70f41aa..ac8a4b4 100644 --- a/programs/include/gui/terminal.hpp +++ b/programs/include/gui/terminal.hpp @@ -10,6 +10,7 @@ #include #include #include +#include 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); } diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index dd49f4c..f813811 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -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); } /* ==================================================================== diff --git a/programs/include/montauk/capabilities.h b/programs/include/montauk/capabilities.h new file mode 100644 index 0000000..3dc080a --- /dev/null +++ b/programs/include/montauk/capabilities.h @@ -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 +#include +#include +#include + +/* + * 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.." 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 diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index e963fa3..326670c 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -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); } diff --git a/programs/include/montauk/user.h b/programs/include/montauk/user.h index 85dbae5..efff1e0 100644 --- a/programs/include/montauk/user.h +++ b/programs/include/montauk/user.h @@ -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, diff --git a/programs/man/shell.1 b/programs/man/shell.1 index 05ff4f9..076c626 100644 --- a/programs/man/shell.1 +++ b/programs/man/shell.1 @@ -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 Send ICMP echo requests -- see ping(1) diff --git a/programs/man/syscalls.2 b/programs/man/syscalls.2 index 95474b8..f685e19 100644 --- a/programs/man/syscalls.2 +++ b/programs/man/syscalls.2 @@ -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) diff --git a/programs/src/desktop/apps/app_external_launchers.cpp b/programs/src/desktop/apps/app_external_launchers.cpp index a1a376c..1834c87 100644 --- a/programs/src/desktop/apps/app_external_launchers.cpp +++ b/programs/src/desktop/apps/app_external_launchers.cpp @@ -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"); } diff --git a/programs/src/desktop/apps/apps_common.hpp b/programs/src/desktop/apps/apps_common.hpp index 3cf929a..666ef60 100644 --- a/programs/src/desktop/apps/apps_common.hpp +++ b/programs/src/desktop/apps/apps_common.hpp @@ -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); diff --git a/programs/src/desktop/desktop_catalog.cpp b/programs/src/desktop/desktop_catalog.cpp index 855f366..d08d284 100644 --- a/programs/src/desktop/desktop_catalog.cpp +++ b/programs/src/desktop/desktop_catalog.cpp @@ -5,6 +5,7 @@ */ #include "desktop_internal.hpp" +#include 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; } diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index 0dde205..17dfe47 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -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); diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index d27053b..0afd20f 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -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; diff --git a/programs/src/desktop/wifi.cpp b/programs/src/desktop/wifi.cpp index 43fa35f..2792caf 100644 --- a/programs/src/desktop/wifi.cpp +++ b/programs/src/desktop/wifi.cpp @@ -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) { diff --git a/programs/src/init/main.cpp b/programs/src/init/main.cpp index 356aaae..2cdb03a 100644 --- a/programs/src/init/main.cpp +++ b/programs/src/init/main.cpp @@ -7,6 +7,7 @@ #include #include +#include #include // ---- 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); diff --git a/programs/src/login/login_input.cpp b/programs/src/login/login_input.cpp index fed0def..fbcdd01 100644 --- a/programs/src/login/login_input.cpp +++ b/programs/src/login/login_input.cpp @@ -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); diff --git a/programs/src/login/login_shutdown.cpp b/programs/src/login/login_shutdown.cpp index d00fe77..5aa4dae 100644 --- a/programs/src/login/login_shutdown.cpp +++ b/programs/src/login/login_shutdown.cpp @@ -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); } diff --git a/programs/src/login/main.cpp b/programs/src/login/main.cpp index 3ffcd3a..bb9ed9f 100644 --- a/programs/src/login/main.cpp +++ b/programs/src/login/main.cpp @@ -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); } diff --git a/programs/src/network/main.cpp b/programs/src/network/main.cpp index 868ab27..600ec7e 100644 --- a/programs/src/network/main.cpp +++ b/programs/src/network/main.cpp @@ -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 { diff --git a/programs/src/procmgr/main.cpp b/programs/src/procmgr/main.cpp index 16d9a99..d88ffcd 100644 --- a/programs/src/procmgr/main.cpp +++ b/programs/src/procmgr/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include 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 diff --git a/programs/src/reset/main.cpp b/programs/src/reset/main.cpp index 9d23762..0601529 100644 --- a/programs/src/reset/main.cpp +++ b/programs/src/reset/main.cpp @@ -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); } diff --git a/programs/src/shell/Makefile b/programs/src/shell/Makefile index 755098f..f44fc74 100644 --- a/programs/src/shell/Makefile +++ b/programs/src/shell/Makefile @@ -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) diff --git a/programs/src/shell/builtins.cpp b/programs/src/shell/builtins.cpp index 8c1bb5c..522b423 100644 --- a/programs/src/shell/builtins.cpp +++ b/programs/src/shell/builtins.cpp @@ -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 Send ICMP echo requests\n"); diff --git a/programs/src/shell/exec.cpp b/programs/src/shell/exec.cpp index 9747d7f..5c8c647 100644 --- a/programs/src/shell/exec.cpp +++ b/programs/src/shell/exec.cpp @@ -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; diff --git a/programs/src/shell/main.cpp b/programs/src/shell/main.cpp index b1ab645..27ff89f 100644 --- a/programs/src/shell/main.cpp +++ b/programs/src/shell/main.cpp @@ -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]; diff --git a/programs/src/shell/shell.h b/programs/src/shell/shell.h index 10f82bd..1b3c4d1 100644 --- a/programs/src/shell/shell.h +++ b/programs/src/shell/shell.h @@ -10,6 +10,7 @@ #include #include #include +#include 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 ---- @@ -144,4 +146,4 @@ constexpr const char* shell_builtins[] = { "true", "false", "exit" -}; \ No newline at end of file +}; diff --git a/programs/src/shutdown/main.cpp b/programs/src/shutdown/main.cpp index cbcdce3..5835477 100644 --- a/programs/src/shutdown/main.cpp +++ b/programs/src/shutdown/main.cpp @@ -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); }