feat: implement kernel capability model

This commit is contained in:
2026-08-29 16:53:04 +02:00
parent 9051b8a16e
commit e7646bbbdb
47 changed files with 1507 additions and 264 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 155 #define MONTAUK_BUILD_NUMBER 168
+14
View File
@@ -17,8 +17,14 @@
#include <Ipc/Ipc.hpp> #include <Ipc/Ipc.hpp>
#include <Timekeeping/Time.hpp> #include <Timekeeping/Time.hpp>
#include "Path.hpp" #include "Path.hpp"
#include <Fs/ProtectedPaths.hpp>
namespace montauk::abi { 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) { static int Sys_Open(const char* path) {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
@@ -108,12 +114,14 @@ namespace montauk::abi {
static int Sys_FCreate(const char* path) { static int Sys_FCreate(const char* path) {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
return Ipc::CreateFileHandle(resolved); return Ipc::CreateFileHandle(resolved);
} }
static int Sys_FDelete(const char* path) { static int Sys_FDelete(const char* path) {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
return Fs::Vfs::VfsDelete(resolved); return Fs::Vfs::VfsDelete(resolved);
} }
@@ -138,6 +146,9 @@ namespace montauk::abi {
bool useCurrent) { bool useCurrent) {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; 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) { if (useCurrent) {
int64_t now = Timekeeping::GetUnixTimestamp(); int64_t now = Timekeeping::GetUnixTimestamp();
atime = now; atime = now;
@@ -149,6 +160,7 @@ namespace montauk::abi {
static int Sys_FMkdir(const char* path) { static int Sys_FMkdir(const char* path) {
char resolved[256]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
if (!CanModifyFilePath(resolved)) return SYS_ERR_PERMISSION;
return Fs::Vfs::VfsMkdir(resolved); return Fs::Vfs::VfsMkdir(resolved);
} }
@@ -157,6 +169,8 @@ namespace montauk::abi {
char resolvedNew[256]; char resolvedNew[256];
if (!ResolveProcessPath(oldPath, resolvedOld, sizeof(resolvedOld))) return -1; if (!ResolveProcessPath(oldPath, resolvedOld, sizeof(resolvedOld))) return -1;
if (!ResolveProcessPath(newPath, resolvedNew, sizeof(resolvedNew))) 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); return Fs::Vfs::VfsRename(resolvedOld, resolvedNew);
} }
+4 -3
View File
@@ -5,6 +5,7 @@
*/ */
#pragma once #pragma once
#include <Memory/UserRange.hpp>
#include <cstdint> #include <cstdint>
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
#include <Memory/Paging.hpp> #include <Memory/Paging.hpp>
@@ -155,7 +156,7 @@ namespace montauk::abi {
// user TLB entry can otherwise corrupt the frame's next owner. // user TLB entry can otherwise corrupt the frame's next owner.
while (released != nullptr) { while (released != nullptr) {
HeapAlloc* next = released->next; HeapAlloc* next = released->next;
Ipc::UnmapAndFreeUserRange(proc->pml4Phys, released->va, Memory::UnmapAndFreeUserRange(proc->pml4Phys, released->va,
released->numPages); released->numPages);
Sched::ReleaseUserHeapRange(slot, released->va, Sched::ReleaseUserHeapRange(slot, released->va,
released->numPages * 0x1000ULL); released->numPages * 0x1000ULL);
@@ -218,7 +219,7 @@ namespace montauk::abi {
resident++; resident++;
g_heapLocks[slot].Release(); g_heapLocks[slot].Release();
Ipc::UnmapAndFreeUserRange(proc->pml4Phys, addr, pages); Memory::UnmapAndFreeUserRange(proc->pml4Phys, addr, pages);
Sched::ReleaseUserHeapRange(slot, addr, size); Sched::ReleaseUserHeapRange(slot, addr, size);
g_heapLocks[slot].Acquire(); g_heapLocks[slot].Acquire();
Sched::g_allocatedPages[slot] -= resident; Sched::g_allocatedPages[slot] -= resident;
@@ -308,7 +309,7 @@ namespace montauk::abi {
} }
} }
g_heapLocks[slot].Release(); g_heapLocks[slot].Release();
Ipc::ShootdownUserRange(proc->pml4Phys, addr, (uint32_t)pages); Memory::ShootdownUserRange(proc->pml4Phys, addr, (uint32_t)pages);
return 0; return 0;
} }
+1 -1
View File
@@ -23,7 +23,7 @@ namespace montauk::abi {
for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i]; for (int i = 0; ver[i]; i++) outInfo->osVersion[i] = ver[i];
outInfo->osVersion[5] = '\0'; outInfo->osVersion[5] = '\0';
outInfo->apiVersion = 10; outInfo->apiVersion = 11;
outInfo->maxProcesses = Sched::MaxProcesses; outInfo->maxProcesses = Sched::MaxProcesses;
outInfo->buildNumber = MONTAUK_BUILD_NUMBER; outInfo->buildNumber = MONTAUK_BUILD_NUMBER;
} }
+33 -6
View File
@@ -15,12 +15,15 @@ namespace montauk::abi {
static constexpr uint32_t RedirOutputStreamCapacity = 64 * 1024; 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]; char resolved[256];
if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1; if (!ResolveProcessPath(path, resolved, sizeof(resolved))) return -1;
int parentSlot = Ipc::CurrentSlot(); 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; if (childPid < 0) return -1;
auto* child = Sched::GetProcessByPid(childPid); auto* child = Sched::GetProcessByPid(childPid);
@@ -83,28 +86,50 @@ namespace montauk::abi {
return childPid; 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, &copy);
}
static int Sys_ChildIoRead(int childPid, char* buf, int maxLen) { static int Sys_ChildIoRead(int childPid, char* buf, int maxLen) {
auto* child = Sched::GetProcessByPid(childPid); auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
return SYS_ERR_PERMISSION;
Ipc::HandleSnapshot snapshot; Ipc::HandleSnapshot snapshot;
Ipc::Stream* stream = GetRedirOutStream(child, 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); return Ipc::StreamRead(stream, (uint8_t*)buf, maxLen, true);
} }
static int Sys_ChildIoWrite(int childPid, const char* data, int len) { static int Sys_ChildIoWrite(int childPid, const char* data, int len) {
auto* child = Sched::GetProcessByPid(childPid); auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
return SYS_ERR_PERMISSION;
Ipc::HandleSnapshot snapshot; Ipc::HandleSnapshot snapshot;
Ipc::Stream* stream = GetRedirInStream(child, 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); return WriteAllToStream(stream, (const uint8_t*)data, len);
} }
static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) { static int Sys_ChildIoWriteKey(int childPid, const KeyEvent* key) {
if (key == nullptr) return -1; if (key == nullptr) return -1;
auto* child = Sched::GetProcessByPid(childPid); auto* child = Sched::GetProcessByPid(childPid);
if (child == nullptr || child->parentPid != Sched::GetCurrentPid())
return SYS_ERR_PERMISSION;
Ipc::HandleSnapshot snapshot; Ipc::HandleSnapshot snapshot;
Ipc::Mailbox* mailbox = GetRedirKeyMailbox(child, snapshot); Ipc::Mailbox* mailbox = GetRedirKeyMailbox(child, snapshot);
if (child == nullptr || !child->redirected || mailbox == nullptr) return -1; if (!child->redirected || mailbox == nullptr) return -1;
for (;;) { for (;;) {
uint64_t observedWake = Sched::ObserveObjectWake(mailbox); uint64_t observedWake = Sched::ObserveObjectWake(mailbox);
@@ -120,7 +145,9 @@ namespace montauk::abi {
static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) { static int Sys_ChildIoSetTermsz(int childPid, int cols, int rows) {
auto* child = Sched::GetProcessByPid(childPid); 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->termCols = cols;
child->termRows = rows; child->termRows = rows;
return 0; return 0;
+3 -2
View File
@@ -4,6 +4,7 @@
* Copyright (c) 2026 Daniel Hammer * Copyright (c) 2026 Daniel Hammer
*/ */
#include <Memory/UserRange.hpp>
#include <cstdint> #include <cstdint>
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
#include <Sched/ElfLoader.hpp> #include <Sched/ElfLoader.hpp>
@@ -133,7 +134,7 @@ namespace montauk::abi {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc != nullptr) { if (proc != nullptr) {
Ipc::UnmapAndFreeUserRange(proc->pml4Phys, libBase, Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
(libEnd - libBase) / 0x1000ULL); (libEnd - libBase) / 0x1000ULL);
} }
@@ -202,7 +203,7 @@ namespace montauk::abi {
uint64_t libBase = GetLibSlotBase(i); uint64_t libBase = GetLibSlotBase(i);
uint64_t libEnd = libBase + Sched::LIB_MAX_SIZE; uint64_t libEnd = libBase + Sched::LIB_MAX_SIZE;
Ipc::UnmapAndFreeUserRange(proc->pml4Phys, libBase, Memory::UnmapAndFreeUserRange(proc->pml4Phys, libBase,
(libEnd - libBase) / 0x1000ULL); (libEnd - libBase) / 0x1000ULL);
g_libTable[slot][i].inUse = false; g_libTable[slot][i].inUse = false;
+5
View File
@@ -31,6 +31,11 @@ namespace montauk::abi {
// action; any other value records it as the pending action. Returns the // action; any other value records it as the pending action. Returns the
// pending action for queries, or 0 when recording one. // pending action for queries, or 0 when recording one.
static int64_t Sys_PowerRequest(int action) { 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) { if (action == POWER_REQ_QUERY) {
int pending = g_pendingPowerAction; int pending = g_pendingPowerAction;
g_pendingPowerAction = POWER_REQ_QUERY; g_pendingPowerAction = POWER_REQ_QUERY;
+50
View File
@@ -86,6 +86,39 @@ namespace montauk::abi {
return childPid; 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, &copy,
userOverride);
}
// Copy the absolute path this process was spawned from (argv[0]). // Copy the absolute path this process was spawned from (argv[0]).
static int Sys_GetExecPath(char* buf, uint64_t maxLen) { static int Sys_GetExecPath(char* buf, uint64_t maxLen) {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
@@ -150,12 +183,29 @@ namespace montauk::abi {
} }
buf[count].heapUsed = Sched::g_allocatedPages[i] * 0x1000; buf[count].heapUsed = Sched::g_allocatedPages[i] * 0x1000;
buf[count].cpuTimeMs = proc->cpuTimeMs; buf[count].cpuTimeMs = proc->cpuTimeMs;
buf[count].permittedCaps = proc->permittedCaps;
buf[count].effectiveCaps = proc->effectiveCaps;
buf[count].delegableCaps = proc->delegableCaps;
count++; count++;
} }
return count; return count;
} }
static int Sys_Kill(int pid) { 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); return Sched::KillProcess(pid);
} }
+58 -1
View File
@@ -150,6 +150,16 @@ namespace montauk::abi {
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1; if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
return (int64_t)Sys_Spawn((const char*)frame->arg1, return (int64_t)Sys_Spawn((const char*)frame->arg1,
UserMemory::IsUserPtr(frame->arg2) ? (const char*)frame->arg2 : nullptr); UserMemory::IsUserPtr(frame->arg2) ? (const char*)frame->arg2 : nullptr);
case SYS_SPAWN_CAPS:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
if (frame->arg3 != 0 && !UserMemory::String(frame->arg3, 32)) return -1;
if (!UserMemory::Readable<SpawnCapabilities>(frame->arg4)) return -1;
return (int64_t)Sys_SpawnCaps(
(const char*)frame->arg1,
frame->arg2 ? (const char*)frame->arg2 : nullptr,
frame->arg3 ? (const char*)frame->arg3 : nullptr,
(const SpawnCapabilities*)frame->arg4);
case SYS_SPAWN_ENV: case SYS_SPAWN_ENV:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1; if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) 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; (uint64_t)frame->arg2 * sizeof(DisplayModeInfo), true)) return -1;
return Sys_DisplayModes((DisplayModeInfo*)frame->arg1, (int)frame->arg2); return Sys_DisplayModes((DisplayModeInfo*)frame->arg1, (int)frame->arg2);
case SYS_DISPLAYSETMODE: case SYS_DISPLAYSETMODE:
if (!Sched::HasCapability(CAP_DISPLAY_ADMIN)) return SYS_ERR_PERMISSION;
return Sys_DisplaySetMode((int)frame->arg1); return Sys_DisplaySetMode((int)frame->arg1);
case SYS_DISPLAYBRIGHTNESS: case SYS_DISPLAYBRIGHTNESS:
if ((int64_t)frame->arg1 >= 0 &&
!Sched::HasCapability(CAP_DISPLAY_ADMIN)) return SYS_ERR_PERMISSION;
return Sys_DisplayBrightness((int)frame->arg1); return Sys_DisplayBrightness((int)frame->arg1);
case SYS_GETEXECPATH: case SYS_GETEXECPATH:
if (!UserMemory::Range(frame->arg2 ? frame->arg1 : frame->arg1, frame->arg2, true)) return -1; 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; if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
return (int64_t)Sys_GetArgs((char*)frame->arg1, frame->arg2); return (int64_t)Sys_GetArgs((char*)frame->arg1, frame->arg2);
case SYS_RESET: case SYS_RESET:
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return SYS_ERR_PERMISSION;
Sys_Reset(); Sys_Reset();
return 0; return 0;
case SYS_SHUTDOWN: case SYS_SHUTDOWN:
if (!Sched::HasCapability(CAP_POWER_CONTROL)) return SYS_ERR_PERMISSION;
Sys_Shutdown(); Sys_Shutdown();
return 0; return 0;
case SYS_POWER_REQUEST: 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); return Sys_PowerRequest((int)frame->arg1);
case SYS_GETTIME: case SYS_GETTIME:
if (!UserMemory::Writable<DateTime>(frame->arg1)) return -1; if (!UserMemory::Writable<DateTime>(frame->arg1)) return -1;
Sys_GetTime((DateTime*)frame->arg1); Sys_GetTime((DateTime*)frame->arg1);
return 0; return 0;
case SYS_SETUNIXTIME: case SYS_SETUNIXTIME:
if (!Sched::HasCapability(CAP_SET_TIME)) return SYS_ERR_PERMISSION;
return Sys_SetUnixTime((int64_t)frame->arg1); return Sys_SetUnixTime((int64_t)frame->arg1);
case SYS_SOCKET: case SYS_SOCKET:
return (int64_t)Sys_Socket((int)frame->arg1); return (int64_t)Sys_Socket((int)frame->arg1);
@@ -247,6 +272,7 @@ namespace montauk::abi {
Sys_GetNetCfg((NetCfg*)frame->arg1); Sys_GetNetCfg((NetCfg*)frame->arg1);
return 0; return 0;
case SYS_SETNETCFG: case SYS_SETNETCFG:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Readable<NetCfg>(frame->arg1)) return -1; if (!UserMemory::Readable<NetCfg>(frame->arg1)) return -1;
return (int64_t)Sys_SetNetCfg((const NetCfg*)frame->arg1); return (int64_t)Sys_SetNetCfg((const NetCfg*)frame->arg1);
case SYS_NETSTATUS: case SYS_NETSTATUS:
@@ -302,6 +328,7 @@ namespace montauk::abi {
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1; if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
return Sys_GetRandom((uint8_t*)frame->arg1, frame->arg2); return Sys_GetRandom((uint8_t*)frame->arg1, frame->arg2);
case SYS_LOG: case SYS_LOG:
if (!Sched::HasCapability(CAP_LOG_READ)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1; if (!UserMemory::Range(frame->arg1, frame->arg2, true)) return -1;
return Kt::ReadKernelLogBuffer((char*)frame->arg1, frame->arg2); return Kt::ReadKernelLogBuffer((char*)frame->arg1, frame->arg2);
case SYS_MOUSESTATE: case SYS_MOUSESTATE:
@@ -316,6 +343,14 @@ namespace montauk::abi {
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1; if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
return (int64_t)Sys_SpawnRedir((const char*)frame->arg1, return (int64_t)Sys_SpawnRedir((const char*)frame->arg1,
UserMemory::IsUserPtr(frame->arg2) ? (const char*)frame->arg2 : nullptr); UserMemory::IsUserPtr(frame->arg2) ? (const char*)frame->arg2 : nullptr);
case SYS_SPAWN_REDIR_CAPS:
if (!UserMemory::String(frame->arg1, kMaxPathBytes)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, kMaxArgsBytes)) return -1;
if (!UserMemory::Readable<SpawnCapabilities>(frame->arg3)) return -1;
return (int64_t)Sys_SpawnRedirCaps(
(const char*)frame->arg1,
frame->arg2 ? (const char*)frame->arg2 : nullptr,
(const SpawnCapabilities*)frame->arg3);
case SYS_CHILDIO_READ: case SYS_CHILDIO_READ:
if ((int64_t)frame->arg3 < 0) return -1; if ((int64_t)frame->arg3 < 0) return -1;
if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, true)) return -1; if (!UserMemory::Range(frame->arg2, (uint64_t)frame->arg3, true)) return -1;
@@ -363,6 +398,7 @@ namespace montauk::abi {
case SYS_SETSESSION: case SYS_SETSESSION:
return (int64_t)Sys_SetSession(); return (int64_t)Sys_SetSession();
case SYS_KILLSESSION: case SYS_KILLSESSION:
if (!Sched::HasCapability(CAP_PROCESS_ADMIN)) return SYS_ERR_PERMISSION;
return (int64_t)Sys_KillSession((int)frame->arg1); return (int64_t)Sys_KillSession((int)frame->arg1);
case SYS_DEVLIST: case SYS_DEVLIST:
if ((int64_t)frame->arg2 < 0) return -1; 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; 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); return (int64_t)Sys_PartList((PartInfo*)frame->arg1, (int)frame->arg2);
case SYS_DISKREAD: case SYS_DISKREAD:
if (!Sched::HasCapability(CAP_RAW_STORAGE)) return SYS_ERR_PERMISSION;
return (int64_t)Sys_DiskRead((int)frame->arg1, frame->arg2, return (int64_t)Sys_DiskRead((int)frame->arg1, frame->arg2,
(uint32_t)frame->arg3, (void*)frame->arg4); (uint32_t)frame->arg3, (void*)frame->arg4);
case SYS_DISKWRITE: case SYS_DISKWRITE:
if (!Sched::HasCapability(CAP_RAW_STORAGE)) return SYS_ERR_PERMISSION;
return (int64_t)Sys_DiskWrite((int)frame->arg1, frame->arg2, return (int64_t)Sys_DiskWrite((int)frame->arg1, frame->arg2,
(uint32_t)frame->arg3, (const void*)frame->arg4); (uint32_t)frame->arg3, (const void*)frame->arg4);
case SYS_GPTINIT: case SYS_GPTINIT:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
return (int64_t)Sys_GptInit((int)frame->arg1); return (int64_t)Sys_GptInit((int)frame->arg1);
case SYS_GPTADD: case SYS_GPTADD:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Readable<GptAddParams>(frame->arg1)) return -1; if (!UserMemory::Readable<GptAddParams>(frame->arg1)) return -1;
return (int64_t)Sys_GptAdd((const GptAddParams*)frame->arg1); return (int64_t)Sys_GptAdd((const GptAddParams*)frame->arg1);
case SYS_FSMOUNT: case SYS_FSMOUNT:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
return (int64_t)Sys_FsMount((int)frame->arg1, (int)frame->arg2); return (int64_t)Sys_FsMount((int)frame->arg1, (int)frame->arg2);
case SYS_FS_SYNC: case SYS_FS_SYNC:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
return Sys_FsSync(); return Sys_FsSync();
case SYS_FSFORMAT: case SYS_FSFORMAT:
if (!Sched::HasCapability(CAP_STORAGE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return -1; if (!UserMemory::Readable<FsFormatParams>(frame->arg1)) return -1;
return (int64_t)Sys_FsFormat((const FsFormatParams*)frame->arg1); return (int64_t)Sys_FsFormat((const FsFormatParams*)frame->arg1);
case SYS_AUDIOOPEN: case SYS_AUDIOOPEN:
@@ -428,6 +471,7 @@ namespace montauk::abi {
return Sys_UsbList((UsbInterfaceInfo*)frame->arg1, (int)maxCount); return Sys_UsbList((UsbInterfaceInfo*)frame->arg1, (int)maxCount);
} }
case SYS_USB_CLAIM: case SYS_USB_CLAIM:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if (frame->arg1 == 0 || frame->arg1 > 16 || frame->arg2 > 255) if (frame->arg1 == 0 || frame->arg1 > 16 || frame->arg2 > 255)
return USB_ERR_INVALID; return USB_ERR_INVALID;
return Sys_UsbClaim((uint8_t)frame->arg1, (uint8_t)frame->arg2); return Sys_UsbClaim((uint8_t)frame->arg1, (uint8_t)frame->arg2);
@@ -469,16 +513,20 @@ namespace montauk::abi {
case SYS_THREAD_SELF: case SYS_THREAD_SELF:
return Sys_ThreadSelf(); return Sys_ThreadSelf();
case SYS_BTSCAN: case SYS_BTSCAN:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if ((int64_t)frame->arg2 < 0) return -1; if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtScanResult), true)) 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); return Sys_BtScan((BtScanResult*)frame->arg1, (int)frame->arg2, (uint32_t)frame->arg3);
case SYS_BTCONNECT: case SYS_BTCONNECT:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, 6, false)) return -1; if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
return Sys_BtConnect((const uint8_t*)frame->arg1); return Sys_BtConnect((const uint8_t*)frame->arg1);
case SYS_BTDISCONNECT: case SYS_BTDISCONNECT:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, 6, false)) return -1; if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
return Sys_BtDisconnect((const uint8_t*)frame->arg1); return Sys_BtDisconnect((const uint8_t*)frame->arg1);
case SYS_BTSETADDR: case SYS_BTSETADDR:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, 6, false)) return -1; if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
return Sys_BtSetAddr((const uint8_t*)frame->arg1); return Sys_BtSetAddr((const uint8_t*)frame->arg1);
case SYS_BTBONDS: case SYS_BTBONDS:
@@ -486,6 +534,7 @@ namespace montauk::abi {
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtBondInfo), true)) return -1; if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(BtBondInfo), true)) return -1;
return Sys_BtBonds((BtBondInfo*)frame->arg1, (int)frame->arg2); return Sys_BtBonds((BtBondInfo*)frame->arg1, (int)frame->arg2);
case SYS_BTFORGET: case SYS_BTFORGET:
if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::Range(frame->arg1, 6, false)) return -1; if (!UserMemory::Range(frame->arg1, 6, false)) return -1;
return Sys_BtForget((const uint8_t*)frame->arg1); return Sys_BtForget((const uint8_t*)frame->arg1);
case SYS_BTLIST: case SYS_BTLIST:
@@ -496,6 +545,7 @@ namespace montauk::abi {
if (!UserMemory::Writable<BtAdapterInfo>(frame->arg1)) return -1; if (!UserMemory::Writable<BtAdapterInfo>(frame->arg1)) return -1;
return Sys_BtInfo((BtAdapterInfo*)frame->arg1); return Sys_BtInfo((BtAdapterInfo*)frame->arg1);
case SYS_WIFI_SCAN: case SYS_WIFI_SCAN:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if ((int64_t)frame->arg2 < 0) return -1; if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WifiNetwork), true)) 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); return Sys_WifiScan((WifiNetwork*)frame->arg1, (int)frame->arg2, (uint32_t)frame->arg3);
@@ -503,18 +553,22 @@ namespace montauk::abi {
if (!UserMemory::Writable<WifiInfo>(frame->arg1)) return -1; if (!UserMemory::Writable<WifiInfo>(frame->arg1)) return -1;
return Sys_WifiInfo((WifiInfo*)frame->arg1); return Sys_WifiInfo((WifiInfo*)frame->arg1);
case SYS_WIFI_CONNECT: case SYS_WIFI_CONNECT:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::String(frame->arg1, 64)) return -1; if (!UserMemory::String(frame->arg1, 64)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1; if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1;
return Sys_WifiConnect((const char*)frame->arg1, (const char*)frame->arg2); return Sys_WifiConnect((const char*)frame->arg1, (const char*)frame->arg2);
case SYS_WIFI_DISCONNECT: case SYS_WIFI_DISCONNECT:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
return Sys_WifiDisconnect(); return Sys_WifiDisconnect();
case SYS_WIFI_SCAN_START: case SYS_WIFI_SCAN_START:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
return Sys_WifiScanStart((uint32_t)frame->arg1); return Sys_WifiScanStart((uint32_t)frame->arg1);
case SYS_WIFI_RESULTS: case SYS_WIFI_RESULTS:
if ((int64_t)frame->arg2 < 0) return -1; if ((int64_t)frame->arg2 < 0) return -1;
if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(WifiNetwork), true)) 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); return Sys_WifiResults((WifiNetwork*)frame->arg1, (int)frame->arg2);
case SYS_WIFI_CONNECT_ASYNC: case SYS_WIFI_CONNECT_ASYNC:
if (!Sched::HasCapability(CAP_NETWORK_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::String(frame->arg1, 64)) return -1; if (!UserMemory::String(frame->arg1, 64)) return -1;
if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1; if (frame->arg2 != 0 && !UserMemory::String(frame->arg2, 128)) return -1;
return Sys_WifiConnectAsync((const char*)frame->arg1, (const char*)frame->arg2); 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; if (!UserMemory::Range(frame->arg1, (uint64_t)frame->arg2 * sizeof(NetIfInfo), true)) return -1;
return Sys_NetIfs((NetIfInfo*)frame->arg1, (int)frame->arg2); return Sys_NetIfs((NetIfInfo*)frame->arg1, (int)frame->arg2);
case SYS_SUSPEND: case SYS_SUSPEND:
if (!Sched::HasCapability(CAP_SUSPEND)) return SYS_ERR_PERMISSION;
return Sys_Suspend(); return Sys_Suspend();
case SYS_SETTZ: case SYS_SETTZ:
if (!Sched::HasCapability(CAP_SET_TIME)) return SYS_ERR_PERMISSION;
return Sys_SetTZ((int32_t)frame->arg1); return Sys_SetTZ((int32_t)frame->arg1);
case SYS_GETTZ: case SYS_GETTZ:
return Sys_GetTZ(); return Sys_GetTZ();
case SYS_SETUSER: case SYS_SETUSER:
if (!Sched::HasCapability(CAP_USER_ADMIN)) return SYS_ERR_PERMISSION;
if (!UserMemory::String(frame->arg2, kMaxUserNameBytes)) return -1; if (!UserMemory::String(frame->arg2, kMaxUserNameBytes)) return -1;
return Sys_SetUser((int)frame->arg1, (const char*)frame->arg2); return Sys_SetUser((int)frame->arg1, (const char*)frame->arg2);
case SYS_GETUSER: case SYS_GETUSER:
@@ -670,7 +727,7 @@ namespace montauk::abi {
Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR=" Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
<< kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", " << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", "
<< (SYS_USB_BULK_IN_READ + 1) << " syscall slots)"; << (SYS_SPAWN_REDIR_CAPS + 1) << " syscall slots)";
} }
} }
+67
View File
@@ -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_START = 182; // (handle, transferBytes, buffers)
static constexpr uint64_t SYS_USB_BULK_IN_STOP = 183; // (handle) 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_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 // 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 // Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
// a pending action and exits; login.elf reads it, runs the shutdown stages, // a pending action and exits; login.elf reads it, runs the shutdown stages,
// then issues the matching SYS_SHUTDOWN / SYS_RESET. // 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 { enum PowerRequestAction : int {
POWER_REQ_QUERY = 0, // read-and-clear the pending action POWER_REQ_QUERY = 0, // read-and-clear the pending action
POWER_REQ_SHUTDOWN = 1, POWER_REQ_SHUTDOWN = 1,
POWER_REQ_REBOOT = 2, 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; static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
@@ -631,6 +695,9 @@ namespace montauk::abi {
char name[64]; char name[64];
uint64_t heapUsed; // Distance from UserHeapBase to high-water mark uint64_t heapUsed; // Distance from UserHeapBase to high-water mark
uint64_t cpuTimeMs; // accumulated scheduler runtime uint64_t cpuTimeMs; // accumulated scheduler runtime
uint64_t permittedCaps;
uint64_t effectiveCaps;
uint64_t delegableCaps;
}; };
// Bluetooth scan result (returned by SYS_BTSCAN) // Bluetooth scan result (returned by SYS_BTSCAN)
+4
View File
@@ -5,6 +5,8 @@
* Further copyright information and third party notices can be found at https://montaukos.org/license.txt. * Further copyright information and third party notices can be found at https://montaukos.org/license.txt.
*/ */
#include <Fs/ProtectedPaths.hpp>
#include <Memory/UserRange.hpp>
#include <cstdint> #include <cstdint>
#include <cstddef> #include <cstddef>
#include <Boot/Boot.hpp> #include <Boot/Boot.hpp>
@@ -174,6 +176,8 @@ extern "C" void kmain() {
montauk::abi::InitializeSyscalls(); montauk::abi::InitializeSyscalls();
Sched::Initialize(); Sched::Initialize();
Memory::InitUserRange();
Fs::LogProtectedPaths();
Ipc::Initialize(); Ipc::Initialize();
#if defined (__x86_64__) #if defined (__x86_64__)
+124
View File
@@ -0,0 +1,124 @@
/*
* ProtectedPaths.cpp
* Capability required to modify paths on the booted system volume
* Copyright (c) 2026 Daniel Hammer
*
* Split out of Ipc.cpp: this is filesystem security policy, not IPC. It
* lived there only because OpenFileHandleForSlot was its first caller.
*/
#include "ProtectedPaths.hpp"
#include <Api/Syscall.hpp>
#include <Terminal/Terminal.hpp>
namespace Fs {
// ==== Protected system paths ====
// The capability required to modify (create, write, delete or rename) a
// path. The kernel only ever enumerates paths here; it never parses a
// policy file. Userspace grant policy lives in 0:/config/capabilities.toml
// and can only ever narrow what the kernel already delegated, so no input
// to that file can produce authority this table does not already allow.
//
// Rules apply ONLY to the system volume. Drive 0 is always the boot
// ramdisk (Fs/Boot.cpp registers it unconditionally); every other drive is
// a partition discovered at probe time, in probe order. A user data disk
// that happens to contain an "apps" or "config" directory must not inherit
// system protection, and an installed system's files on another volume are
// inert data until that disk is booted -- at which point its contents are
// themselves the drive-0 ramdisk.
// Anything the kernel itself reads belongs here:
// guarding only the syscall leaves the file as an unguarded second path to
// the same state (bluetooth.toml feeds the BD_ADDR override at bring-up).
struct ProtectedPath {
const char* pattern; // drive-relative, leading '/'
bool prefix; // also match everything beneath the pattern
uint64_t capability;
};
static constexpr ProtectedPath g_protectedPaths[] = {
// Authentication, first-boot administrator creation, trusted service
// activation and capability grants. Readable by anyone; writable only
// with administrative authority. The bare directory is listed so it
// cannot be renamed or deleted out from under the files inside it.
{"/config", false, montauk::abi::CAP_USER_ADMIN},
{"/config/users.toml", false, montauk::abi::CAP_USER_ADMIN},
{"/config/setup.toml", false, montauk::abi::CAP_USER_ADMIN},
{"/config/init.toml", false, montauk::abi::CAP_USER_ADMIN},
{"/config/ssh.toml", false, montauk::abi::CAP_USER_ADMIN},
{"/config/capabilities.toml",false, montauk::abi::CAP_USER_ADMIN},
// Read by the Bluetooth driver at controller bring-up.
{"/config/bluetooth.toml", false, montauk::abi::CAP_DEVICE_ADMIN},
// Program images. Capability grants are keyed on binary path, so a
// writable image would let an unprivileged process substitute a binary
// and inherit the grant the next time a privileged launcher runs it.
// This is CAP_SYSTEM_IMAGE and not CAP_STORAGE_ADMIN precisely because
// it is the trusted computing base: partitioning and formatting a data
// volume is an ordinary administrative act, while replacing the image
// of login.elf is a route to every capability the system can issue.
{"/apps", true, montauk::abi::CAP_SYSTEM_IMAGE},
{"/os", true, montauk::abi::CAP_SYSTEM_IMAGE},
};
static char LowerAscii(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + ('a' - 'A')) : c;
}
// The volume the running system was booted from.
static constexpr uint64_t SystemDrive = 0;
// Strip the "<digits>:" prefix, but only for the system volume. Returns
// nullptr for any other drive, meaning no rule applies to it.
static const char* SystemRelativePath(const char* path) {
if (path == nullptr) return nullptr;
const char* p = path;
if (*p < '0' || *p > '9') return nullptr;
uint64_t drive = 0;
while (*p >= '0' && *p <= '9') {
drive = drive * 10 + (uint64_t)(*p - '0');
if (drive > 0xFFFF) return nullptr; // absurd; cannot be a drive
p++;
}
if (*p != ':' || drive != SystemDrive) return nullptr;
return p + 1;
}
// Case-insensitive: FAT32 resolves differing cases to the same file, so a
// case-sensitive rule would be trivially sidestepped.
static bool ProtectedPathMatches(const char* path, const ProtectedPath& rule) {
const char* p = path;
const char* q = rule.pattern;
while (*q) {
if (LowerAscii(*p) != LowerAscii(*q)) return false;
p++;
q++;
}
if (*p == '\0') return true; // the pattern itself
return rule.prefix && *p == '/'; // something beneath it
}
uint64_t RequiredFileWriteCapability(const char* path) {
const char* relative = SystemRelativePath(path);
if (relative == nullptr) return 0; // not the system volume
// Overlapping rules accumulate: HasCapability() requires every bit, so
// a path covered by two rules demands both.
uint64_t required = 0;
for (const auto& rule : g_protectedPaths) {
if (ProtectedPathMatches(relative, rule)) required |= rule.capability;
}
return required;
}
void LogProtectedPaths() {
for (const auto& rule : g_protectedPaths) {
Kt::KernelLogStream(Kt::INFO, "IPC") << "Protected path "
<< rule.pattern << (rule.prefix ? "/* " : " ")
<< "requires capability mask "
<< kcp::hex << rule.capability << kcp::dec;
}
}
}
+20
View File
@@ -0,0 +1,20 @@
/*
* ProtectedPaths.hpp
* Capability required to modify paths on the booted system volume
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Fs {
// Capability a process must hold to create, write, delete, rename or
// re-timestamp `path`. Returns 0 when the path is unprotected.
uint64_t RequiredFileWriteCapability(const char* path);
// Log the rule table at boot, so a path that should be protected and is
// not is visible rather than silently missing.
void LogProtectedPaths();
}
+72 -148
View File
@@ -8,11 +8,13 @@
#include <Sched/Scheduler.hpp> #include <Sched/Scheduler.hpp>
#include <Fs/Vfs.hpp> #include <Fs/Vfs.hpp>
#include <Fs/ProtectedPaths.hpp>
#include <Net/Tcp.hpp> #include <Net/Tcp.hpp>
#include <Net/Udp.hpp> #include <Net/Udp.hpp>
#include <Memory/PageFrameAllocator.hpp> #include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp> #include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp> #include <Memory/Paging.hpp>
#include <Memory/UserRange.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <CppLib/Spinlock.hpp> #include <CppLib/Spinlock.hpp>
#include <Hal/Apic/Apic.hpp> #include <Hal/Apic/Apic.hpp>
@@ -82,6 +84,7 @@ namespace Ipc {
struct File : Object { struct File : Object {
Fs::Vfs::BackendFile backend; Fs::Vfs::BackendFile backend;
uint64_t writeCapability;
}; };
struct UdpDgramHeader { struct UdpDgramHeader {
@@ -178,149 +181,11 @@ namespace Ipc {
static void ReleaseRawObject(Object* object); 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 // Object lifetime
// is queued behind the holder has to stay interruptible long enough to // Pool allocation, refcounting and type-dispatched teardown.
// service that IPI itself. An interrupt-disabling Spinlock here deadlocks // Every object type routes through this layer.
// 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]));
}
}
}
}
static void InitObject(Object& object, HandleType type) { static void InitObject(Object& object, HandleType type) {
object.type = type; object.type = type;
@@ -568,6 +433,11 @@ namespace Ipc {
} }
} }
// ==========================================================================
// Handle table
// Per-process handle installation, rights, duplication and close.
// ==========================================================================
int CurrentSlot() { int CurrentSlot() {
auto* proc = Sched::GetCurrentProcessPtr(); auto* proc = Sched::GetCurrentProcessPtr();
if (proc == nullptr) return -1; if (proc == nullptr) return -1;
@@ -817,6 +687,11 @@ namespace Ipc {
return InstallHandleForSlot(slot, snapshot.object, snapshot.type, snapshot.rights); return InstallHandleForSlot(slot, snapshot.object, snapshot.type, snapshot.rights);
} }
// ==========================================================================
// Streams
// Byte pipes.
// ==========================================================================
Stream* CreateStream(uint32_t capacity) { Stream* CreateStream(uint32_t capacity) {
if (capacity == 0) capacity = DefaultStreamCapacity; if (capacity == 0) capacity = DefaultStreamCapacity;
@@ -1000,6 +875,11 @@ namespace Ipc {
return hasData; return hasData;
} }
// ==========================================================================
// Mailboxes
// Discrete message queues.
// ==========================================================================
Mailbox* CreateMailbox() { Mailbox* CreateMailbox() {
g_mailboxPoolLock.Acquire(); g_mailboxPoolLock.Acquire();
for (int i = 0; i < MaxMailboxes; i++) { for (int i = 0; i < MaxMailboxes; i++) {
@@ -1260,9 +1140,22 @@ namespace Ipc {
return hasMsg; 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) { int OpenFileHandleForSlot(int slot, const char* path, bool create) {
if (slot < 0 || slot >= Sched::MaxProcesses || path == nullptr) return -1; 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}; Fs::Vfs::BackendFile backend = {-1, -1, 0};
int result = create ? Fs::Vfs::CreateBackendFile(path, backend) int result = create ? Fs::Vfs::CreateBackendFile(path, backend)
: Fs::Vfs::OpenBackendFile(path, backend); : Fs::Vfs::OpenBackendFile(path, backend);
@@ -1273,6 +1166,7 @@ namespace Ipc {
if (g_files[i].active || g_files[i].destroying) continue; if (g_files[i].active || g_files[i].destroying) continue;
InitObject(g_files[i], HandleType::File); InitObject(g_files[i], HandleType::File);
g_files[i].backend = backend; g_files[i].backend = backend;
g_files[i].writeCapability = writeCapability;
g_filePoolLock.Release(); g_filePoolLock.Release();
uint32_t rights = RightRead | RightWait | RightDup; uint32_t rights = RightRead | RightWait | RightDup;
@@ -1318,7 +1212,12 @@ namespace Ipc {
HandleSnapshot snapshot; HandleSnapshot snapshot;
if (!snapshot.Capture(CurrentSlot(), handle)) return -1; if (!snapshot.Capture(CurrentSlot(), handle)) return -1;
if (snapshot.type != HandleType::File || (snapshot.rights & RightWrite) == 0) 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) { uint64_t FileGetSizeHandle(int handle) {
@@ -1328,6 +1227,11 @@ namespace Ipc {
return Fs::Vfs::GetBackendFileSize(((File*)snapshot.object)->backend); return Fs::Vfs::GetBackendFileSize(((File*)snapshot.object)->backend);
} }
// ==========================================================================
// Sockets
// TCP and UDP endpoints.
// ==========================================================================
static Socket* AllocateSocketObject(int type) { static Socket* AllocateSocketObject(int type) {
g_socketPoolLock.Acquire(); g_socketPoolLock.Acquire();
for (int i = 0; i < MaxSockets; i++) { 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) { Surface* CreateSurface(uint64_t byteSize) {
if (byteSize == 0) byteSize = 0x1000; if (byteSize == 0) byteSize = 0x1000;
uint32_t numPages = (uint32_t)((byteSize + 0xFFFu) / 0x1000u); uint32_t numPages = (uint32_t)((byteSize + 0xFFFu) / 0x1000u);
@@ -1748,7 +1659,7 @@ namespace Ipc {
for (uint32_t p = m.numPages; p < newPages; p++) { for (uint32_t p = m.numPages; p < newPages; p++) {
Memory::VMM::Paging::UnmapUserIn(pml4, m.va + (uint64_t)p * 0x1000ULL); Memory::VMM::Paging::UnmapUserIn(pml4, m.va + (uint64_t)p * 0x1000ULL);
} }
ShootdownUserRange(pml4, startVa, rollbackPages); Memory::ShootdownUserRange(pml4, startVa, rollbackPages);
} }
g_surfaceMapLocks[s].Release(); g_surfaceMapLocks[s].Release();
} }
@@ -1900,7 +1811,7 @@ namespace Ipc {
Memory::VMM::Paging::UnmapUserIn(pml4, va); Memory::VMM::Paging::UnmapUserIn(pml4, va);
} }
ShootdownUserRange(pml4, baseVa, flushPages); Memory::ShootdownUserRange(pml4, baseVa, flushPages);
m.numPages = newPages; m.numPages = newPages;
} }
g_surfaceMapLocks[s].Release(); g_surfaceMapLocks[s].Release();
@@ -2111,7 +2022,7 @@ namespace Ipc {
// releasing the mapping reference can then destroy the surface // releasing the mapping reference can then destroy the surface
// and recycle its frames while that sibling writes through its // and recycle its frames while that sibling writes through its
// stale TLB entry. Quiesce every CPU using this PML4 first. // 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].used = false;
g_surfaceMaps[slot][i].surface = nullptr; g_surfaceMaps[slot][i].surface = nullptr;
@@ -2127,6 +2038,11 @@ namespace Ipc {
return unmapped > 0 ? 0 : -1; return unmapped > 0 ? 0 : -1;
} }
// ==========================================================================
// Process handles
// Wait-only references to a live process.
// ==========================================================================
int OpenProcessHandle(int pid) { int OpenProcessHandle(int pid) {
g_processPoolLock.Acquire(); g_processPoolLock.Acquire();
for (int i = 0; i < MaxProcessObjects; i++) { for (int i = 0; i < MaxProcessObjects; i++) {
@@ -2183,6 +2099,11 @@ namespace Ipc {
return exited; return exited;
} }
// ==========================================================================
// Signals and waitsets
// Readiness computation and multiplexed waiting.
// ==========================================================================
static uint32_t CurrentSocketSignals(Socket* socket, uint32_t rights) { static uint32_t CurrentSocketSignals(Socket* socket, uint32_t rights) {
if (socket == nullptr) return SignalNone; if (socket == nullptr) return SignalNone;
@@ -2513,6 +2434,10 @@ namespace Ipc {
} }
} }
// ==========================================================================
// Teardown and init
// ==========================================================================
void CleanupProcessSlot(int slot, int /*pid*/, uint64_t pml4Phys) { void CleanupProcessSlot(int slot, int /*pid*/, uint64_t pml4Phys) {
if (slot < 0 || slot >= Sched::MaxProcesses) return; if (slot < 0 || slot >= Sched::MaxProcesses) return;
@@ -2548,7 +2473,6 @@ namespace Ipc {
for (int i = 0; i < Sched::MaxProcesses; i++) { for (int i = 0; i < Sched::MaxProcesses; i++) {
g_processObjectsBySlot[i] = nullptr; g_processObjectsBySlot[i] = nullptr;
} }
Hal::RegisterIrqHandler(Hal::IRQ_TLB_SHOOTDOWN, TlbShootdownIpiHandler);
Kt::KernelLogStream(Kt::OK, "IPC") << "Initialized (" Kt::KernelLogStream(Kt::OK, "IPC") << "Initialized ("
<< (uint64_t)MaxHandlesPerProcess << " handles/process, " << (uint64_t)MaxHandlesPerProcess << " handles/process, "
<< (uint64_t)MaxStreams << " streams, " << (uint64_t)MaxStreams << " streams, "
-5
View File
@@ -171,11 +171,6 @@ namespace Ipc {
int WaitsetWaitHandle(int waitsetHandle, WaitsetReady* outReady, uint64_t timeoutMs); int WaitsetWaitHandle(int waitsetHandle, WaitsetReady* outReady, uint64_t timeoutMs);
void NotifyObjectChanged(Object* object); 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); void CleanupProcessSlot(int slot, int pid, uint64_t pml4Phys);
} }
+172
View File
@@ -0,0 +1,172 @@
/*
* UserRange.cpp
* Cross-CPU invalidation and teardown of user address-space mappings
* Copyright (c) 2026 Daniel Hammer
*
* Split out of Ipc.cpp: this is paging and SMP work with no dependency on
* any IPC object or handle pool, and it lived there only by history.
*/
#include "UserRange.hpp"
#include <Sched/Scheduler.hpp>
#include <Memory/PageFrameAllocator.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
#include <CppLib/Spinlock.hpp>
#include <Hal/Apic/Apic.hpp>
#include <Hal/Apic/Interrupts.hpp>
#include <Hal/SmpBoot.hpp>
#include <Common/Panic.hpp>
namespace Memory {
// MUST be a Mutex, never a Spinlock. ShootdownUserRange holds this while
// waiting for remote CPUs to acknowledge the shootdown IPI, so a CPU that
// is queued behind the holder has to stay interruptible long enough to
// service that IPI itself. An interrupt-disabling Spinlock here deadlocks
// every CPU contending for the lock, and the bounded-retry logic below
// then reports it as a "target failed to acknowledge" Panic -- which reads
// like a hardware fault rather than a lock-type regression.
static kcp::Mutex g_tlbShootdownLock;
static volatile uint64_t g_tlbShootdownSeq = 0;
static volatile uint64_t g_tlbShootdownPml4 = 0;
static volatile uint64_t g_tlbShootdownStartVa = 0;
static volatile uint32_t g_tlbShootdownPages = 0;
static volatile uint64_t g_tlbShootdownDone[Smp::MaxCPUs] = {};
static bool CpuCurrentlyUsesPml4(Smp::CpuData* cpu, uint64_t pml4Phys) {
if (cpu == nullptr || pml4Phys == 0 || cpu->currentSlot < 0) return false;
Sched::Process* proc = Sched::GetProcessSlot(cpu->currentSlot);
if (proc == nullptr) return false;
if (proc->state == Sched::ProcessState::Free) return false;
return proc->pml4Phys == pml4Phys;
}
static void InvalidateLocalUserRange(uint64_t startVa, uint32_t pages) {
if (pages == 0) return;
if (pages > 1024) {
Memory::VMM::FlushTLB();
return;
}
for (uint32_t p = 0; p < pages; p++) {
uint64_t va = startVa + (uint64_t)p * 0x1000ULL;
asm volatile("invlpg (%0)" :: "r"(va) : "memory");
}
}
static void TlbShootdownIpiHandler(uint8_t, bool) {
Smp::CpuData* cpu = Smp::GetCurrentCpuData();
uint64_t seq = g_tlbShootdownSeq;
uint64_t pml4 = g_tlbShootdownPml4;
uint64_t startVa = g_tlbShootdownStartVa;
uint32_t pages = g_tlbShootdownPages;
if (CpuCurrentlyUsesPml4(cpu, pml4)) {
InvalidateLocalUserRange(startVa, pages);
}
if (cpu != nullptr && cpu->cpuIndex >= 0 && cpu->cpuIndex < Smp::MaxCPUs) {
asm volatile("" ::: "memory");
g_tlbShootdownDone[cpu->cpuIndex] = seq;
}
}
void ShootdownUserRange(uint64_t pml4Phys, uint64_t startVa, uint32_t pages) {
if (pml4Phys == 0 || pages == 0) return;
bool targets[Smp::MaxCPUs] = {};
Smp::CpuData* currentCpu = Smp::GetCurrentCpuData();
int currentCpuIndex = currentCpu ? currentCpu->cpuIndex : -1;
g_tlbShootdownLock.Acquire();
uint64_t seq = g_tlbShootdownSeq + 1;
g_tlbShootdownPml4 = pml4Phys;
g_tlbShootdownStartVa = startVa;
g_tlbShootdownPages = pages;
asm volatile("" ::: "memory");
g_tlbShootdownSeq = seq;
for (int i = 0; i < Smp::GetCpuCount(); i++) {
Smp::CpuData* cpu = Smp::GetCpuData(i);
if (cpu == nullptr || !cpu->started) continue;
if (i == currentCpuIndex) {
if (CpuCurrentlyUsesPml4(cpu, pml4Phys)) {
InvalidateLocalUserRange(startVa, pages);
}
g_tlbShootdownDone[i] = seq;
continue;
}
if (!CpuCurrentlyUsesPml4(cpu, pml4Phys)) {
g_tlbShootdownDone[i] = seq;
continue;
}
targets[i] = true;
(void)Hal::LocalApic::SendFixedIpi(cpu->lapicId,
Hal::IRQ_VECTOR_BASE + Hal::IRQ_TLB_SHOOTDOWN);
}
for (int i = 0; i < Smp::GetCpuCount(); i++) {
if (!targets[i]) continue;
uint32_t spins = 0;
uint32_t retries = 0;
while (g_tlbShootdownDone[i] != seq) {
asm volatile("pause");
if (++spins < 1000000) continue;
// Delivery normally completes in a handful of cycles. Retry a
// bounded number of times in case the first IPI was lost while
// the target changed interrupt state. Continuing without an
// acknowledgement would let the caller free frames still
// reachable through a remote stale TLB entry, so fail loudly
// instead of either corrupting memory or spinning forever.
spins = 0;
if (++retries > 4) {
Panic("TLB shootdown target failed to acknowledge", nullptr);
}
Smp::CpuData* cpu = Smp::GetCpuData(i);
if (cpu != nullptr && cpu->started) {
(void)Hal::LocalApic::SendFixedIpi(cpu->lapicId,
Hal::IRQ_VECTOR_BASE + Hal::IRQ_TLB_SHOOTDOWN);
}
}
}
g_tlbShootdownLock.Release();
}
void UnmapAndFreeUserRange(uint64_t pml4Phys, uint64_t startVa, uint64_t pages) {
static constexpr uint32_t PagesPerChunk = 64;
uint64_t physPages[PagesPerChunk];
for (uint64_t base = 0; base < pages; base += PagesPerChunk) {
uint32_t count = (uint32_t)((pages - base > PagesPerChunk)
? PagesPerChunk : pages - base);
for (uint32_t i = 0; i < count; i++) {
uint64_t pageVa = startVa + (base + i) * 0x1000ULL;
physPages[i] = Memory::VMM::Paging::GetPhysAddr(pml4Phys, pageVa);
Memory::VMM::Paging::UnmapUserIn(pml4Phys, pageVa);
}
ShootdownUserRange(pml4Phys, startVa + base * 0x1000ULL, count);
for (uint32_t i = 0; i < count; i++) {
if (physPages[i] != 0) {
Memory::g_pfa->Free((void*)Memory::HHDM(physPages[i]));
}
}
}
}
void InitUserRange() {
Hal::RegisterIrqHandler(Hal::IRQ_TLB_SHOOTDOWN, TlbShootdownIpiHandler);
}
}
+23
View File
@@ -0,0 +1,23 @@
/*
* UserRange.hpp
* Cross-CPU invalidation and teardown of user address-space mappings
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
namespace Memory {
// Register the TLB-shootdown IPI handler. Must run before any AP is
// booted, since a shootdown targets every CPU running the address space.
void InitUserRange();
// Invalidate a user range on every CPU currently running the address
// space. Call this after removing PTEs and before releasing their frames.
void ShootdownUserRange(uint64_t pml4Phys, uint64_t startVa, uint32_t pages);
// Safely remove ordinary PFA-backed user mappings and release their frames.
void UnmapAndFreeUserRange(uint64_t pml4Phys, uint64_t startVa, uint64_t pages);
}
+46 -7
View File
@@ -4,6 +4,7 @@
* Copyright (c) 2025-2026 Daniel Hammer * Copyright (c) 2025-2026 Daniel Hammer
*/ */
#include <Memory/UserRange.hpp>
#include "Scheduler.hpp" #include "Scheduler.hpp"
#include "ElfLoader.hpp" #include "ElfLoader.hpp"
#include <Memory/PageFrameAllocator.hpp> #include <Memory/PageFrameAllocator.hpp>
@@ -298,6 +299,9 @@ namespace Sched {
processTable[i].environment[0] = '\0'; processTable[i].environment[0] = '\0';
processTable[i].environmentLength = 1; processTable[i].environmentLength = 1;
processTable[i].user[0] = '\0'; processTable[i].user[0] = '\0';
processTable[i].permittedCaps = 0;
processTable[i].effectiveCaps = 0;
processTable[i].delegableCaps = 0;
processTable[i].sessionId = -1; processTable[i].sessionId = -1;
processTable[i].cwd[0] = '\0'; processTable[i].cwd[0] = '\0';
processTable[i].runningOnCpu = -1; processTable[i].runningOnCpu = -1;
@@ -342,7 +346,9 @@ namespace Sched {
} }
int Spawn(const char* vfsPath, const char* args, bool startReady, 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(); schedLock.Acquire();
int slot = -1; int slot = -1;
@@ -547,9 +553,37 @@ namespace Sched {
proc.environmentLength = 1; 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; int i = 0;
for (; i < 31 && processTable[parentSlot].user[i]; i++) for (; i < 31 && processTable[parentSlot].user[i]; i++)
proc.user[i] = processTable[parentSlot].user[i]; proc.user[i] = processTable[parentSlot].user[i];
@@ -684,7 +718,7 @@ namespace Sched {
mappedPages++; mappedPages++;
} }
if (!ok) { if (!ok) {
Ipc::UnmapAndFreeUserRange(sharedPml4, base, mappedPages); Memory::UnmapAndFreeUserRange(sharedPml4, base, mappedPages);
ReleaseUserHeapRange(primarySlot_, base, numPages * 0x1000ULL); ReleaseUserHeapRange(primarySlot_, base, numPages * 0x1000ULL);
Kt::KernelLogStream(Kt::ERROR, "Sched") Kt::KernelLogStream(Kt::ERROR, "Sched")
<< "Thread TLS allocation failed"; << "Thread TLS allocation failed";
@@ -711,7 +745,7 @@ namespace Sched {
void* stackMem = Memory::g_pfa->ReallocConsecutive(nullptr, StackPages); void* stackMem = Memory::g_pfa->ReallocConsecutive(nullptr, StackPages);
if (stackMem == nullptr) { if (stackMem == nullptr) {
if (threadTlsPages != 0) { if (threadTlsPages != 0) {
Ipc::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages); Memory::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages);
ReleaseUserHeapRange(primarySlot_, threadTlsBase, ReleaseUserHeapRange(primarySlot_, threadTlsBase,
threadTlsPages * 0x1000ULL); threadTlsPages * 0x1000ULL);
} }
@@ -737,7 +771,7 @@ namespace Sched {
schedLock.Release(); schedLock.Release();
Memory::g_pfa->Free(stackMem, StackPages); Memory::g_pfa->Free(stackMem, StackPages);
if (threadTlsPages != 0) { if (threadTlsPages != 0) {
Ipc::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages); Memory::UnmapAndFreeUserRange(sharedPml4, threadTlsBase, threadTlsPages);
ReleaseUserHeapRange(primarySlot_, threadTlsBase, ReleaseUserHeapRange(primarySlot_, threadTlsBase,
threadTlsPages * 0x1000ULL); threadTlsPages * 0x1000ULL);
} }
@@ -885,7 +919,7 @@ namespace Sched {
uint64_t base = thr.fsBase - blockSize; uint64_t base = thr.fsBase - blockSize;
uint64_t pages = (blockSize + 16 + 0xFFF) / 0x1000; uint64_t pages = (blockSize + 16 + 0xFFF) / 0x1000;
thr.fsBase = 0; thr.fsBase = 0;
Ipc::UnmapAndFreeUserRange(primary.pml4Phys, base, pages); Memory::UnmapAndFreeUserRange(primary.pml4Phys, base, pages);
ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL); ReleaseUserHeapRange(primarySlot_, base, pages * 0x1000ULL);
} }
@@ -1418,6 +1452,11 @@ namespace Sched {
return &processTable[primary]; return &processTable[primary];
} }
bool HasCapability(uint64_t capability) {
Process* proc = GetCurrentProcessPtr();
return proc != nullptr && (proc->effectiveCaps & capability) == capability;
}
Process* GetCurrentThreadPtr() { Process* GetCurrentThreadPtr() {
auto* cpu = Smp::GetCurrentCpuData(); auto* cpu = Smp::GetCurrentCpuData();
int slot = cpu->currentSlot; int slot = cpu->currentSlot;
+10 -1
View File
@@ -86,6 +86,9 @@ namespace Sched {
char environment[EnvironmentBytes]; // NUL-separated NAME=VALUE entries char environment[EnvironmentBytes]; // NUL-separated NAME=VALUE entries
uint32_t environmentLength; uint32_t environmentLength;
char user[32]; // Owner user name (inherited from parent on spawn) 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) int sessionId; // Process-session leader PID (inherited on spawn)
char cwd[256]; // Absolute current working directory char cwd[256]; // Absolute current working directory
@@ -144,7 +147,9 @@ namespace Sched {
void Initialize(); void Initialize();
int Spawn(const char* vfsPath, const char* args = nullptr, bool startReady = true, 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); int StartProcess(int pid);
void Schedule(); void Schedule();
@@ -172,6 +177,10 @@ namespace Sched {
// Always returns the slot that owns per-process state -- never a sibling thread. // Always returns the slot that owns per-process state -- never a sibling thread.
Process* GetCurrentProcessPtr(); 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). // Get a pointer to the currently running thread's slot (may be a sibling).
Process* GetCurrentThreadPtr(); Process* GetCurrentThreadPtr();
+126
View File
@@ -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"]
+67
View File
@@ -250,6 +250,63 @@ namespace montauk::abi {
static constexpr uint64_t SYS_LOG_WRITE = 176; // (logMessage) -> 0 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_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; 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 // Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
// a pending action and exits; login.elf reads it, runs the shutdown stages, // a pending action and exits; login.elf reads it, runs the shutdown stages,
// then issues the matching SYS_SHUTDOWN / SYS_RESET. // 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 { enum PowerRequestAction : int {
POWER_REQ_QUERY = 0, // read-and-clear the pending action POWER_REQ_QUERY = 0, // read-and-clear the pending action
POWER_REQ_SHUTDOWN = 1, POWER_REQ_SHUTDOWN = 1,
POWER_REQ_REBOOT = 2, 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; static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
@@ -729,6 +793,9 @@ namespace montauk::abi {
char name[64]; char name[64];
uint64_t heapUsed; // Distance from UserHeapBase to high-water mark uint64_t heapUsed; // Distance from UserHeapBase to high-water mark
uint64_t cpuTimeMs; // accumulated scheduler runtime uint64_t cpuTimeMs; // accumulated scheduler runtime
uint64_t permittedCaps;
uint64_t effectiveCaps;
uint64_t delegableCaps;
}; };
struct MemStats { struct MemStats {
+14 -1
View File
@@ -10,6 +10,7 @@
#include <montauk/syscall.h> #include <montauk/syscall.h>
#include <montauk/string.h> #include <montauk/string.h>
#include <Api/Syscall.hpp> #include <Api/Syscall.hpp>
#include <montauk/capabilities.h>
namespace gui { 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); terminal_init_cells(t, cols, rows, TERM_MAX_SCROLLBACK);
t->cursor_visible = true; 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) if (t->child_pid > 0)
montauk::childio_settermsz(t->child_pid, cols, rows); montauk::childio_settermsz(t->child_pid, cols, rows);
} }
+8 -6
View File
@@ -208,6 +208,8 @@ extern "C" {
#define MTK_SYS_USB_BULK_IN_START 182 #define MTK_SYS_USB_BULK_IN_START 182
#define MTK_SYS_USB_BULK_IN_STOP 183 #define MTK_SYS_USB_BULK_IN_STOP 183
#define MTK_SYS_USB_BULK_IN_READ 184 #define MTK_SYS_USB_BULK_IN_READ 184
#define MTK_SYS_SPAWN_CAPS 185
#define MTK_SYS_SPAWN_REDIR_CAPS 186
/* @SYSCALLS-END */ /* @SYSCALLS-END */
#define MTK_SOCK_TCP 1 #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); return (int)_mtk_syscall1(MTK_SYS_SETUNIXTIME, (long)unix_seconds);
} }
static inline void mtk_settz(int offset_minutes) { static inline int mtk_settz(int offset_minutes) {
_mtk_syscall1(MTK_SYS_SETTZ, (long)offset_minutes); return (int)_mtk_syscall1(MTK_SYS_SETTZ, (long)offset_minutes);
} }
static inline int mtk_gettz(void) { static inline int mtk_gettz(void) {
@@ -883,12 +885,12 @@ static inline int mtk_audio_ctl(int handle, int cmd, int value) {
Power management Power management
==================================================================== */ ==================================================================== */
static inline void mtk_reset(void) { static inline int mtk_reset(void) {
_mtk_syscall0(MTK_SYS_RESET); return (int)_mtk_syscall0(MTK_SYS_RESET);
} }
static inline void mtk_shutdown(void) { static inline int mtk_shutdown(void) {
_mtk_syscall0(MTK_SYS_SHUTDOWN); return (int)_mtk_syscall0(MTK_SYS_SHUTDOWN);
} }
/* ==================================================================== /* ====================================================================
+216
View File
@@ -0,0 +1,216 @@
/*
* capabilities.h
* Shared reader for the capability grant table (0:/config/capabilities.toml)
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <montauk/syscall.h>
#include <montauk/toml.h>
#include <montauk/config.h>
#include <montauk/heap.h>
/*
* Launchers (init, the desktop, the shell) look up the authority a program
* should receive here instead of each carrying its own compiled-in table.
*
* This file is advisory, never authoritative. Every grant still goes
* through SYS_SPAWN_CAPS and is validated in the kernel against the
* caller's own delegable set, so nothing written here can produce authority
* the kernel has not already delegated to the launcher. A missing,
* truncated or hostile file can only ever result in a program receiving
* less authority than intended. That is why the table can live in
* userspace TOML: the kernel enumerates protected paths, userspace
* interprets policy.
*
* Grants are keyed on the resolved binary path, which is what makes the
* table safe to hand to init: pointing a privileged service entry at a
* different executable looks up the new path, finds no entry, and grants
* nothing. The kernel write-protects 0:/apps and 0:/os so the path cannot
* be made to refer to a substituted image.
*/
namespace montauk {
namespace caps {
inline constexpr const char* GRANT_CONFIG = "capabilities";
inline constexpr const char* GRANT_PREFIX = "grant.";
inline constexpr int MAX_SCAN_PROCS = 256;
struct CapName {
const char* name;
uint64_t bit;
};
// Names as they appear in the config file. Kept in the same order as the
// CAP_* bit definitions in Api/Syscall.hpp.
inline constexpr CapName NAMES[] = {
{"process_admin", montauk::abi::CAP_PROCESS_ADMIN},
{"power_request", montauk::abi::CAP_POWER_REQUEST},
{"power_control", montauk::abi::CAP_POWER_CONTROL},
{"suspend", montauk::abi::CAP_SUSPEND},
{"storage_admin", montauk::abi::CAP_STORAGE_ADMIN},
{"raw_storage", montauk::abi::CAP_RAW_STORAGE},
{"network_admin", montauk::abi::CAP_NETWORK_ADMIN},
{"set_time", montauk::abi::CAP_SET_TIME},
{"user_admin", montauk::abi::CAP_USER_ADMIN},
{"display_admin", montauk::abi::CAP_DISPLAY_ADMIN},
{"device_admin", montauk::abi::CAP_DEVICE_ADMIN},
{"log_read", montauk::abi::CAP_LOG_READ},
{"system_image", montauk::abi::CAP_SYSTEM_IMAGE},
};
inline uint64_t bit_for_name(const char* name) {
if (name == nullptr || name[0] == '\0') return 0;
// "all" means "everything this launcher may pass on", which the
// caller-delegable clamp in for_binary() then narrows. It excludes
// CAP_SYSTEM_IMAGE: authority to rewrite a program image is never
// something a wildcard should hand out, only an explicit name.
if (montauk::streq(name, "all"))
return montauk::abi::CAP_ALL & ~montauk::abi::CAP_SYSTEM_IMAGE;
for (const auto& entry : NAMES) {
if (montauk::streq(entry.name, name)) return entry.bit;
}
// Unknown names are ignored rather than rejected. Failing closed
// costs a program some authority; failing open would hand out
// authority nobody asked for.
return 0;
}
// Read an array-of-strings key into a capability mask. A missing key is
// an empty mask, which is the correct default for an absent grant.
inline uint64_t mask_from_key(const montauk::toml::Doc& doc, const char* key) {
montauk::toml::Value* arr = doc.get_array(key);
if (arr == nullptr) return 0;
uint64_t mask = 0;
for (int i = 0; i < arr->array.count; i++) {
montauk::toml::Value* item = arr->array.items[i];
if (item == nullptr || item->type != montauk::toml::Type::String) continue;
mask |= bit_for_name(item->str);
}
return mask;
}
// Append `suffix` to the "grant.<id>." stem of `path_key`.
// Returns false if the key is not of that shape or does not fit.
inline bool build_sibling_key(const char* path_key, const char* suffix,
char* out, int outSz) {
int prefixLen = 0;
for (; GRANT_PREFIX[prefixLen]; prefixLen++) {
if (path_key[prefixLen] != GRANT_PREFIX[prefixLen]) return false;
}
// Copy through the final '.' so "grant.foo.path" yields "grant.foo.".
int lastDot = -1;
for (int i = 0; path_key[i]; i++) {
if (path_key[i] == '.') lastDot = i;
}
if (lastDot < prefixLen) return false;
int n = 0;
for (; n <= lastDot && n < outSz - 1; n++) out[n] = path_key[n];
for (int i = 0; suffix[i] && n < outSz - 1; i++) out[n++] = suffix[i];
out[n] = '\0';
return true;
}
// Look up the grant declared for `binary_path`. Returns false when the
// path has no entry, which is the common case and means "no authority".
inline bool lookup(const char* binary_path,
montauk::abi::SpawnCapabilities& out) {
out = {0, 0, 0};
if (binary_path == nullptr || binary_path[0] == '\0') return false;
montauk::toml::Doc doc = montauk::config::load(GRANT_CONFIG);
bool found = false;
for (int i = 0; i < doc.entries.count && !found; i++) {
montauk::toml::Value* entry = doc.entries.items[i];
if (entry == nullptr || entry->key == nullptr) continue;
if (entry->type != montauk::toml::Type::String) continue;
char sibling[128];
if (!build_sibling_key(entry->key, "path", sibling, sizeof(sibling))) continue;
if (!montauk::streq(sibling, entry->key)) continue;
if (!montauk::streq(entry->str, binary_path)) continue;
build_sibling_key(entry->key, "effective", sibling, sizeof(sibling));
uint64_t effective = mask_from_key(doc, sibling);
build_sibling_key(entry->key, "delegable", sibling, sizeof(sibling));
uint64_t delegable = mask_from_key(doc, sibling);
build_sibling_key(entry->key, "permitted", sibling, sizeof(sibling));
uint64_t permitted = mask_from_key(doc, sibling);
// A grant that does not name `permitted` owns exactly what it can
// use or pass on. Declaring it separately is only needed by a
// supervisor that holds authority in reserve (login).
if (permitted == 0) permitted = effective | delegable;
out.permitted = permitted;
out.effective = effective;
out.delegable = delegable;
found = true;
}
doc.destroy();
return found;
}
// The calling process's own capability masks.
//
// There is no syscall to ask "what am I?", so this scans the process table
// for our own PID. The buffer is heap-allocated because ProcInfo is large
// enough that MAX_SCAN_PROCS of them would be a ~29 KB stack frame.
inline bool self(montauk::abi::SpawnCapabilities& out) {
out = {0, 0, 0};
auto* table = (montauk::abi::ProcInfo*)montauk::malloc(
sizeof(montauk::abi::ProcInfo) * MAX_SCAN_PROCS);
if (table == nullptr) return false;
int count = montauk::proclist(table, MAX_SCAN_PROCS);
int self_pid = montauk::getpid();
bool found = false;
for (int i = 0; i < count; i++) {
if (table[i].pid != self_pid) continue;
out.permitted = table[i].permittedCaps;
out.effective = table[i].effectiveCaps;
out.delegable = table[i].delegableCaps;
found = true;
break;
}
montauk::mfree(table);
return found;
}
inline uint64_t self_delegable() {
montauk::abi::SpawnCapabilities mine;
return self(mine) ? mine.delegable : 0;
}
// Build a spawn request for `binary_path`, clamped to what the caller may
// actually delegate. The kernel enforces the same bound; clamping here
// means a launcher that holds less authority than the table declares
// degrades to a reduced grant instead of failing the spawn outright.
inline montauk::abi::SpawnCapabilities for_binary(const char* binary_path,
uint64_t caller_delegable) {
montauk::abi::SpawnCapabilities caps{0, 0, 0};
// A caller with nothing to delegate cannot produce a non-empty grant,
// so skip the file read entirely. This is the common case: every
// unprivileged session, on every launch.
if (caller_delegable == 0) return caps;
if (!lookup(binary_path, caps)) return caps;
caps.permitted &= caller_delegable;
caps.effective &= caps.permitted;
caps.delegable &= caps.permitted;
return caps;
}
} // namespace caps
} // namespace montauk
+29 -7
View File
@@ -120,6 +120,13 @@ namespace montauk {
inline int spawn(const char* path, const char* args = nullptr) { inline int spawn(const char* path, const char* args = nullptr) {
return (int)syscall2(montauk::abi::SYS_SPAWN, (uint64_t)path, (uint64_t)args); 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) { inline int chdir(const char* path) {
return (int)syscall1(montauk::abi::SYS_CHDIR, (uint64_t)path); return (int)syscall1(montauk::abi::SYS_CHDIR, (uint64_t)path);
} }
@@ -391,7 +398,10 @@ namespace montauk {
} }
// Timezone offset (total minutes from UTC) // 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); } inline int gettz() { return (int)syscall0(montauk::abi::SYS_GETTZ); }
// Random number generation // Random number generation
@@ -400,14 +410,12 @@ namespace montauk {
} }
// Power management // Power management
[[noreturn]] inline void reset() { inline int reset() {
syscall0(montauk::abi::SYS_RESET); return (int)syscall0(montauk::abi::SYS_RESET);
__builtin_unreachable();
} }
[[noreturn]] inline void shutdown() { inline int shutdown() {
syscall0(montauk::abi::SYS_SHUTDOWN); return (int)syscall0(montauk::abi::SYS_SHUTDOWN);
__builtin_unreachable();
} }
inline int suspend() { inline int suspend() {
@@ -423,6 +431,13 @@ namespace montauk {
return (int)syscall1(montauk::abi::SYS_POWER_REQUEST, (uint64_t)(int64_t)action); 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 // Mouse
inline void mouse_state(montauk::abi::MouseState* out) { syscall1(montauk::abi::SYS_MOUSESTATE, (uint64_t)out); } 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) { 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) { 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); 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) { 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); return (int)syscall3(montauk::abi::SYS_CHILDIO_READ, (uint64_t)childPid, (uint64_t)buf, (uint64_t)maxLen);
} }
+10
View File
@@ -219,6 +219,16 @@ namespace user {
return false; 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 ---- // ---- User management ----
inline bool create_user(const char* username, const char* display_name, inline bool create_user(const char* username, const char* display_name,
+8 -2
View File
@@ -129,8 +129,14 @@
fontscale [n] Get or set terminal font scale -- see fontscale(1) fontscale [n] Get or set terminal font scale -- see fontscale(1)
lua Lua interpreter lua Lua interpreter
tcc TinyCC (in-system C compiler) tcc TinyCC (in-system C compiler)
reset Reboot the system reset / reboot Request a supervised system reboot
shutdown Shut down the system 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 .SS Network commands
ping <host> Send ICMP echo requests -- see ping(1) ping <host> Send ICMP echo requests -- see ping(1)
+147 -35
View File
@@ -3,7 +3,7 @@
syscalls - overview of MontaukOS system calls syscalls - overview of MontaukOS system calls
.SH DESCRIPTION .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 140-148 reserved) for userspace programs. Syscalls use the x86-64
SYSCALL instruction with the following register convention: SYSCALL instruction with the following register convention:
@@ -20,6 +20,66 @@
montauk:: namespace. This page groups syscalls the same way the montauk:: namespace. This page groups syscalls the same way the
kernel source does (one subsystem header per group). 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 .SH PROCESS MANAGEMENT
.B SYS_EXIT (0) .B SYS_EXIT (0)
Terminate the calling process. Terminate the calling process.
@@ -39,9 +99,18 @@
.B SYS_SPAWN (20) .B SYS_SPAWN (20)
Spawn a new process from an ELF binary on the VFS. The child inherits 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); 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) .B SYS_WAITPID (23)
Block until the given process has exited. Returns 0-255 for a normal 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 exit, 256 plus the signal number if it was killed or crashed, or 0 if
@@ -64,11 +133,13 @@
.B SYS_PROCLIST (61) .B SYS_PROCLIST (61)
List running processes (pid, parent, state, name, heap usage, 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); int montauk::proclist(montauk::abi::ProcInfo* buf, int max);
.B SYS_KILL (62) .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); int montauk::kill(int pid);
.B SYS_SETSESSION (174) .B SYS_SETSESSION (174)
@@ -79,6 +150,7 @@
.B SYS_KILLSESSION (175) .B SYS_KILLSESSION (175)
Terminate all live processes in a process session. Returns the number of Terminate all live processes in a process session. Returns the number of
members signalled; repeat until zero to wait for complete teardown. members signalled; repeat until zero to wait for complete teardown.
Requires CAP_PROCESS_ADMIN.
int montauk::killsession(int sessionId); int montauk::killsession(int sessionId);
.B SYS_CHDIR (96) .B SYS_CHDIR (96)
@@ -93,7 +165,8 @@
.B SYS_SETUSER (92) .B SYS_SETUSER (92)
Associate a process with a logged-in user name (used by login/session 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); int montauk::setuser(int pid, const char* name);
.B SYS_GETUSER (93) .B SYS_GETUSER (93)
@@ -256,12 +329,14 @@
.B SYS_SETUNIXTIME (153) .B SYS_SETUNIXTIME (153)
Set the system wall clock from a UTC Unix timestamp. Returns 0 on 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); int montauk::set_unix_time(int64_t unixSeconds);
.B SYS_SETTZ (90) .B SYS_SETTZ (90)
Set the system-wide timezone offset, in minutes from UTC. Set the system-wide timezone offset, in minutes from UTC. Requires
void montauk::settz(int offset_minutes); CAP_SET_TIME.
int montauk::settz(int offset_minutes);
.B SYS_GETTZ (91) .B SYS_GETTZ (91)
Get the current timezone offset, in minutes from UTC. Get the current timezone offset, in minutes from UTC.
@@ -328,7 +403,8 @@
void montauk::get_netcfg(montauk::abi::NetCfg* out); void montauk::get_netcfg(montauk::abi::NetCfg* out);
.B SYS_SETNETCFG (38) .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); int montauk::set_netcfg(const montauk::abi::NetCfg* cfg);
.B SYS_NETSTATUS (125) .B SYS_NETSTATUS (125)
@@ -346,6 +422,7 @@
.B SYS_WIFI_SCAN (158) .B SYS_WIFI_SCAN (158)
Perform a channel scan and block until it finishes or timeoutMs Perform a channel scan and block until it finishes or timeoutMs
elapses. Returns the number of results, or -1 if no adapter is ready. 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 montauk::wifi_scan(montauk::abi::WifiNetwork* buf,
int maxCount, uint32_t timeoutMs); int maxCount, uint32_t timeoutMs);
@@ -355,16 +432,18 @@
.B SYS_WIFI_CONNECT (160) .B SYS_WIFI_CONNECT (160)
Join a network and block until the link is up or the attempt fails. 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, int montauk::wifi_connect(const char* ssid,
const char* password); const char* password);
.B SYS_WIFI_DISCONNECT (161) .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(); int montauk::wifi_disconnect();
.B SYS_WIFI_SCAN_START (162) .B SYS_WIFI_SCAN_START (162)
Start a non-blocking channel scan. Returns 0 if started, 1 if a scan 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); int montauk::wifi_scan_start(uint32_t timeoutMs);
.B SYS_WIFI_RESULTS (163) .B SYS_WIFI_RESULTS (163)
@@ -374,7 +453,7 @@
.B SYS_WIFI_CONNECT_ASYNC (164) .B SYS_WIFI_CONNECT_ASYNC (164)
Start a non-blocking network join. Observe SYS_WIFI_INFO for progress 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, int montauk::wifi_connect_async(const char* ssid,
const char* password); const char* password);
@@ -450,11 +529,13 @@
int maxCount); int maxCount);
.B SYS_DISPLAYSETMODE (156) .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); int montauk::display_set_mode(int modeIndex);
.B SYS_DISPLAYBRIGHTNESS (157) .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); int montauk::display_brightness(int percent = -1);
.SH TERMINAL .SH TERMINAL
@@ -478,15 +559,18 @@
.SH POWER MANAGEMENT .SH POWER MANAGEMENT
.B SYS_RESET (26) .B SYS_RESET (26)
Reboot the system. Reboot the system. Requires CAP_POWER_CONTROL. A successful call does not
[[noreturn]] void montauk::reset(); return; an unauthorized call returns SYS_ERR_PERMISSION.
int montauk::reset();
.B SYS_SHUTDOWN (27) .B SYS_SHUTDOWN (27)
Shut down the system. Shut down the system. Requires CAP_POWER_CONTROL. A successful call does
[[noreturn]] void montauk::shutdown(); not return; an unauthorized call returns SYS_ERR_PERMISSION.
int montauk::shutdown();
.B SYS_SUSPEND (89) .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(); int montauk::suspend();
.B SYS_POWER_REQUEST (135) .B SYS_POWER_REQUEST (135)
@@ -494,8 +578,16 @@
posts a pending action (POWER_REQ_SHUTDOWN / POWER_REQ_REBOOT) posts a pending action (POWER_REQ_SHUTDOWN / POWER_REQ_REBOOT)
then exits; login.elf reads it with POWER_REQ_QUERY then exits; login.elf reads it with POWER_REQ_QUERY
(read-and-clear), runs the shutdown stages, and finally calls (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(int action);
int montauk::power_request_pending();
.B SYS_POWERINFO (149) .B SYS_POWERINFO (149)
Get the CPU power/thermal snapshot (HWP state, throttling, Get the CPU power/thermal snapshot (HWP state, throttling,
@@ -507,7 +599,7 @@
.SH KERNEL LOG .SH KERNEL LOG
.B SYS_LOG (46) .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); int64_t montauk::read_log(char* buf, uint64_t size);
.B SYS_LOG_WRITE (176) .B SYS_LOG_WRITE (176)
@@ -520,9 +612,20 @@
to the framebuffer console. to the framebuffer console.
.B SYS_SPAWN_REDIR (49) .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); 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) .B SYS_CHILDIO_READ (50)
Read buffered output produced by a redirected child. Read buffered output produced by a redirected child.
int montauk::childio_read(int childPid, char* buf, int maxLen); int montauk::childio_read(int childPid, char* buf, int maxLen);
@@ -625,37 +728,41 @@
int montauk::partlist(montauk::abi::PartInfo* buf, int max); int montauk::partlist(montauk::abi::PartInfo* buf, int max);
.B SYS_DISKREAD (71) .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, int64_t montauk::disk_read(int blockDev, uint64_t lba,
uint32_t sectorCount, void* buf); uint32_t sectorCount, void* buf);
.B SYS_DISKWRITE (72) .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, int64_t montauk::disk_write(int blockDev, uint64_t lba,
uint32_t sectorCount, const void* buf); uint32_t sectorCount, const void* buf);
.B SYS_GPTINIT (73) .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); int montauk::gpt_init(int blockDev);
.B SYS_GPTADD (74) .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); int montauk::gpt_add(const montauk::abi::GptAddParams* params);
.B SYS_FSMOUNT (75) .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); int montauk::fs_mount(int partIndex, int driveNum);
.B SYS_FSFORMAT (76) .B SYS_FSFORMAT (76)
Format a partition with a filesystem (FS_TYPE_FAT32 or 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); int montauk::fs_format(const montauk::abi::FsFormatParams* params);
.B SYS_FS_SYNC (134) .B SYS_FS_SYNC (134)
Flush all block-device write caches and cleanly unmount Flush all block-device write caches and cleanly unmount
disk-backed volumes ahead of power-off. Returns the number of disk-backed volumes ahead of power-off. Returns the number of
volumes unmounted. Part of the graceful shutdown sequence volumes unmounted. Part of the graceful shutdown sequence
(see SYS_POWER_REQUEST). (see SYS_POWER_REQUEST). Requires CAP_STORAGE_ADMIN.
int montauk::fs_sync(); int montauk::fs_sync();
.SH AUDIO .SH AUDIO
@@ -708,16 +815,18 @@
.SH BLUETOOTH .SH BLUETOOTH
.B SYS_BTSCAN (84) .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, int montauk::bt_scan(montauk::abi::BtScanResult* buf, int maxCount,
uint32_t timeoutMs); uint32_t timeoutMs);
.B SYS_BTCONNECT (85) .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); int montauk::bt_connect(const uint8_t* bdAddr);
.B SYS_BTDISCONNECT (86) .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); int montauk::bt_disconnect(const uint8_t* bdAddr);
.B SYS_BTLIST (87) .B SYS_BTLIST (87)
@@ -731,7 +840,8 @@
.B SYS_BTSETADDR (137) .B SYS_BTSETADDR (137)
Change the adapter's BD_ADDR (6-byte buffer, byte 0 is the Change the adapter's BD_ADDR (6-byte buffer, byte 0 is the
least-significant octet). Volatile -- apply after the last 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); int montauk::bt_set_addr(const uint8_t* bdAddr);
.B SYS_BTBONDS (138) .B SYS_BTBONDS (138)
@@ -739,7 +849,8 @@
int montauk::bt_bonds(montauk::abi::BtBondInfo* buf, int maxCount); int montauk::bt_bonds(montauk::abi::BtBondInfo* buf, int maxCount);
.B SYS_BTFORGET (139) .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); int montauk::bt_forget(const uint8_t* bdAddr);
.SH GENERIC USB INTERFACES .SH GENERIC USB INTERFACES
@@ -759,7 +870,8 @@
.B SYS_USB_CLAIM (179) .B SYS_USB_CLAIM (179)
Exclusively claim an unbound interface. Returns a generation-checked handle 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); int montauk::usb_claim(uint8_t slotId, uint8_t interfaceNumber);
.B SYS_USB_CLOSE (180) .B SYS_USB_CLOSE (180)
@@ -6,33 +6,31 @@
#include "apps_common.hpp" #include "apps_common.hpp"
static void spawn_app(const char* path, const char* args = nullptr) { static void spawn_app(DesktopState* ds, const char* path, const char* args = nullptr) {
if (path && path[0]) { if (path && path[0]) desktop_spawn_app(ds, path, args);
montauk::spawn(path, args);
}
} }
void open_terminal(DesktopState* ds) { void open_terminal(DesktopState* ds) {
const char* home = (ds && ds->home_dir[0]) ? ds->home_dir : nullptr; 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 open_calculator(DesktopState* ds) {
(void)ds; (void)ds;
spawn_app("0:/apps/calculator/calculator.elf"); spawn_app(ds, "0:/apps/calculator/calculator.elf");
} }
void open_texteditor(DesktopState* ds) { void open_texteditor(DesktopState* ds) {
(void)ds; (void)ds;
spawn_app("0:/apps/texteditor/texteditor.elf"); spawn_app(ds, "0:/apps/texteditor/texteditor.elf");
} }
void open_syslog(DesktopState* ds) { void open_syslog(DesktopState* ds) {
(void)ds; (void)ds;
spawn_app("0:/apps/klog/syslog.elf"); spawn_app(ds, "0:/apps/syslog/syslog.elf");
} }
void open_wordprocessor(DesktopState* ds) { void open_wordprocessor(DesktopState* ds) {
(void)ds; (void)ds;
spawn_app("0:/apps/wordprocessor/wordprocessor.elf"); spawn_app(ds, "0:/apps/wordprocessor/wordprocessor.elf");
} }
+9 -3
View File
@@ -25,6 +25,12 @@ inline void* operator new(unsigned long, void* p) { return p; }
using namespace gui; 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 // Minimal snprintf
// ============================================================================ // ============================================================================
@@ -261,8 +267,8 @@ void open_sleep_dialog(DesktopState* ds);
// user returns to the login screen, where the shutdown stages run (Bluetooth // user returns to the login screen, where the shutdown stages run (Bluetooth
// teardown, filesystem flush) before the final ACPI power-off / reset. Pass // teardown, filesystem flush) before the final ACPI power-off / reset. Pass
// montauk::abi::POWER_REQ_SHUTDOWN or montauk::abi::POWER_REQ_REBOOT. // montauk::abi::POWER_REQ_SHUTDOWN or montauk::abi::POWER_REQ_REBOOT.
[[noreturn]] inline void desktop_request_power(int action) { inline void desktop_request_power(int action) {
montauk::power_request(action); if (montauk::power_request(action) == 0)
montauk::exit(0); montauk::exit(0);
} }
bool desktop_poll_external_windows(DesktopState* ds); bool desktop_poll_external_windows(DesktopState* ds);
+15 -2
View File
@@ -5,6 +5,7 @@
*/ */
#include "desktop_internal.hpp" #include "desktop_internal.hpp"
#include <montauk/capabilities.h>
namespace { namespace {
@@ -24,6 +25,18 @@ static void sort_item_indices(DesktopState* ds, int* indices, int count) {
} // namespace } // 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, int desktop_list_item_indices(DesktopState* ds,
DesktopItemSection section, DesktopItemSection section,
int* out, int* out,
@@ -54,9 +67,9 @@ bool desktop_launch_item(DesktopState* ds, const DesktopItem* item) {
case DESKTOP_ITEM_LAUNCH_EXECUTABLE: case DESKTOP_ITEM_LAUNCH_EXECUTABLE:
if (!item->binary_path[0]) return false; if (!item->binary_path[0]) return false;
if (item->launch_with_home) { 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: default:
return false; return false;
} }
+2 -2
View File
@@ -381,9 +381,9 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
} else if (!row.is_category) { } else if (!row.is_category) {
if (row.external) { if (row.external) {
if (row.launch_with_home) { if (row.launch_with_home) {
montauk::spawn(row.binary_path, ds->home_dir); desktop_spawn_app(ds, row.binary_path, ds->home_dir);
} else { } else {
montauk::spawn(row.binary_path); desktop_spawn_app(ds, row.binary_path);
} }
} else { } else {
desktop_launch_builtin(ds, row.app_id); desktop_launch_builtin(ds, row.app_id);
+13
View File
@@ -834,6 +834,19 @@ void gui::desktop_run(DesktopState* ds) {
if (now >= nextClockPollMs) { if (now >= nextClockPollMs) {
nextClockPollMs = now + 1000; 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(); uint64_t clockToken = desktop_clock_token();
if (clockToken != lastClockToken) { if (clockToken != lastClockToken) {
lastClockToken = clockToken; lastClockToken = clockToken;
+1 -1
View File
@@ -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) { if (ds->wifi_dhcp_pending && info.connected && ds->cached_net_cfg.ipAddress == 0) {
ds->wifi_dhcp_pending = false; ds->wifi_dhcp_pending = false;
ds->wifi_dhcp_waiting = true; 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..."); wifi_set_status(ds, "Requesting an address...");
changed = true; changed = true;
} else if (ds->wifi_dhcp_pending && info.connected) { } else if (ds->wifi_dhcp_pending && info.connected) {
+6 -1
View File
@@ -7,6 +7,7 @@
#include <montauk/syscall.h> #include <montauk/syscall.h>
#include <montauk/config.h> #include <montauk/config.h>
#include <montauk/capabilities.h>
#include <libc/stdio.h> #include <libc/stdio.h>
// ---- ANSI color codes ---- // ---- 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); snprintf(msg, sizeof(msg), "Starting %s", name);
log_info(msg); 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) { if (pid < 0) {
snprintf(msg, sizeof(msg), "Failed to start %s", name); snprintf(msg, sizeof(msg), "Failed to start %s", name);
log_err(msg); log_err(msg);
+17 -3
View File
@@ -52,16 +52,30 @@ void launch_session(LoginState* ls) {
montauk::memset(ls->password, 0, sizeof(ls->password)); montauk::memset(ls->password, 0, sizeof(ls->password));
ls->password_len = 0; 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; int pid;
if (ls->session_mode == SESSION_CONSOLE) { if (ls->session_mode == SESSION_CONSOLE) {
char console_args[96]; char console_args[96];
build_console_args(ls, console_args, (int)sizeof(console_args)); 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 { } 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) { if (pid >= 0) {
montauk::setuser(pid, ls->username);
montauk::waitpid(pid); montauk::waitpid(pid);
if (ls->session_mode == SESSION_DESKTOP) { if (ls->session_mode == SESSION_DESKTOP) {
terminate_desktop_session(pid); terminate_desktop_session(pid);
+8 -6
View File
@@ -250,10 +250,12 @@ void perform_graceful_shutdown(LoginState* ls, int action) {
// ==== Stage 4: dispatch the ACPI power-off / reset ==== // ==== Stage 4: dispatch the ACPI power-off / reset ====
show_stage(ls, heading, rebooting ? "Restarting now..." : "Powering off..."); show_stage(ls, heading, rebooting ? "Restarting now..." : "Powering off...");
if (rebooting) { int rc = rebooting ? montauk::reset() : montauk::shutdown();
montauk::reset(); // A successful power-control syscall never returns. If it does return,
} else { // keep the trusted supervisor alive and make the authorization failure
montauk::shutdown(); // visible instead of falling through an unreachable-code assumption.
} show_stage(ls, heading, rc == montauk::abi::SYS_ERR_PERMISSION
__builtin_unreachable(); ? "Power control permission denied."
: "Power control failed.");
for (;;) montauk::sleep_ms(1000);
} }
+9 -2
View File
@@ -25,9 +25,16 @@ static void maybe_run_setup_session() {
montauk::user::set_session(user); montauk::user::set_session(user);
doc.destroy(); 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) { if (pid >= 0) {
montauk::setuser(pid, user);
montauk::waitpid(pid); montauk::waitpid(pid);
terminate_desktop_session(pid); terminate_desktop_session(pid);
} }
+7 -1
View File
@@ -951,7 +951,13 @@ static void render() {
} }
static void launch_dhcp() { 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) { if (pid >= 0) {
set_status("DHCP client started"); set_status("DHCP client started");
} else { } else {
+20 -1
View File
@@ -7,6 +7,7 @@
#include <montauk/syscall.h> #include <montauk/syscall.h>
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/heap.h> #include <montauk/heap.h>
#include <montauk/capabilities.h>
#include <gui/mtk.hpp> #include <gui/mtk.hpp>
extern "C" { extern "C" {
@@ -392,8 +393,26 @@ static bool refresh_state(bool force) {
return true; 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() { 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 >= 0
&& g_pm.selected < g_pm.proc_count && g_pm.selected < g_pm.proc_count
&& g_pm.procs[g_pm.selected].pid != 0 && g_pm.procs[g_pm.selected].pid != 0
+6 -1
View File
@@ -8,5 +8,10 @@
extern "C" void _start() { extern "C" void _start() {
montauk::print("Rebooting...\n"); 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);
} }
+3 -2
View File
@@ -18,6 +18,7 @@ endif
PROG_INC := ../../include PROG_INC := ../../include
LINK_LD := ../../link.ld LINK_LD := ../../link.ld
BINDIR := ../../bin BINDIR := ../../bin
LIBDIR := ../../lib
OBJDIR := obj OBJDIR := obj
# ---- C++ compiler flags ---- # ---- C++ compiler flags ----
@@ -63,9 +64,9 @@ TARGET := $(BINDIR)/os/shell.elf
all: $(TARGET) all: $(TARGET)
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBDIR)/libc/liblibc.a
mkdir -p $(BINDIR)/os mkdir -p $(BINDIR)/os
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) -o $@ $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@
$(OBJDIR)/%.o: %.cpp shell.h Makefile $(OBJDIR)/%.o: %.cpp shell.h Makefile
mkdir -p $(OBJDIR) mkdir -p $(OBJDIR)
+1
View File
@@ -68,6 +68,7 @@ void cmd_help() {
montauk::print(" fontscale [n] Set terminal font scale (1-8)\n"); montauk::print(" fontscale [n] Set terminal font scale (1-8)\n");
montauk::print(" reset Reboot the system\n"); montauk::print(" reset Reboot the system\n");
montauk::print(" shutdown Shut down the system\n"); montauk::print(" shutdown Shut down the system\n");
montauk::print(" suspend Suspend the system\n");
montauk::print("\n"); montauk::print("\n");
montauk::print("Network commands:\n"); montauk::print("Network commands:\n");
montauk::print(" ping <ip> Send ICMP echo requests\n"); montauk::print(" ping <ip> Send ICMP echo requests\n");
+11 -1
View File
@@ -41,7 +41,17 @@ static void print_exit_code(int code) {
static bool try_exec(const char* path, const char* args) { static bool try_exec(const char* path, const char* args) {
if (!file_exists(path)) return false; 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; if (pid < 0) return false;
print_exit_code(montauk::waitpid(pid)); print_exit_code(montauk::waitpid(pid));
return true; return true;
+26
View File
@@ -13,6 +13,7 @@ int current_drive = 0;
int last_exit = 0; int last_exit = 0;
char session_user[32] = ""; char session_user[32] = "";
char session_home[64] = ""; char session_home[64] = "";
uint64_t shell_delegable_caps = 0;
void sync_cwd() { void sync_cwd() {
char abs[128]; char abs[128];
@@ -46,6 +47,8 @@ void read_session() {
scopy(session_home, "0:/users/", sizeof(session_home)); scopy(session_home, "0:/users/", sizeof(session_home));
scat(session_home, session_user, sizeof(session_home)); scat(session_home, session_user, sizeof(session_home));
shell_delegable_caps = montauk::caps::self_delegable();
} }
// ---- Command history ---- // ---- Command history ----
@@ -184,6 +187,29 @@ static int process_command(const char* line) {
if (streq(cmd, "true")) { return 0; } if (streq(cmd, "true")) { return 0; }
if (streq(cmd, "false")) { return 1; } 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")) { if (streq(cmd, "pwd")) {
sync_cwd(); sync_cwd();
char path[128]; char path[128];
+3 -1
View File
@@ -10,6 +10,7 @@
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/heap.h> #include <montauk/heap.h>
#include <montauk/config.h> #include <montauk/config.h>
#include <montauk/capabilities.h>
using montauk::slen; using montauk::slen;
using montauk::streq; using montauk::streq;
@@ -55,6 +56,7 @@ extern int current_drive;
extern int last_exit; extern int last_exit;
extern char session_user[32]; extern char session_user[32];
extern char session_home[64]; extern char session_home[64];
extern uint64_t shell_delegable_caps;
// ---- Inline path helpers ---- // ---- Inline path helpers ----
@@ -144,4 +146,4 @@ constexpr const char* shell_builtins[] = {
"true", "true",
"false", "false",
"exit" "exit"
}; };
+11 -2
View File
@@ -10,6 +10,15 @@ extern "C" void _start() {
montauk::print("Shutting down...\n"); montauk::print("Shutting down...\n");
// This low-level utility bypasses the login graceful-shutdown view, so flush // This low-level utility bypasses the login graceful-shutdown view, so flush
// pending writes and unmount disk-backed volumes here before powering off. // pending writes and unmount disk-backed volumes here before powering off.
montauk::fs_sync(); int sync_rc = montauk::fs_sync();
montauk::shutdown(); 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);
} }