feat: unify system status codes
This commit is contained in:
1 parent
93894b8fe5
commit
8a3ffb2be1
46 files changed
+1499
-489
No files matched your search
@@ -2,8 +2,31 @@
|
||||
|
||||
All syscalls are available through `#include <montauk/syscall.h>` in the `montauk` namespace. The syscall layer provides `syscall0()` through `syscall6()` primitives using AMD64 calling conventions. All raw syscalls return `int64_t`.
|
||||
|
||||
## Status Codes
|
||||
|
||||
| Code | Value | Meaning |
|
||||
|------|-------|---------|
|
||||
| `Success` | 0 | The call succeeded |
|
||||
| `GenericFailure` | -1 | Unspecified failure |
|
||||
| `PermissionDenied` | -2 | The object belongs to another process or the handle lacks the right |
|
||||
| `CapabilityDenied` | -3 | The caller lacks (or was refused consent for) a required capability |
|
||||
| `SystemIntegrityError` | -4 | The path is protected (ProtectedPaths) and the caller may not modify it |
|
||||
| `BadPointer` | -5 | A user pointer or buffer range is invalid |
|
||||
| `InvalidArgument` | -6 | An argument, id, or handle is invalid |
|
||||
| `UnsupportedOp` | -7 | The operation is not supported here |
|
||||
| `NoDevice` | -8 | The required device is absent or not initialized |
|
||||
| `IoFailure` | -9 | Device I/O failed |
|
||||
| `BadInvocation` | -10 | Unknown syscall number |
|
||||
| `NoSuchPath` | -11 | The path does not exist |
|
||||
| `Busy` | -12 | The resource is in use or its queue is full |
|
||||
| `BufferTooSmall` | -13 | The output buffer is too small; retry with a larger one |
|
||||
| `OutOfMemory` | -14 | Memory could not be allocated |
|
||||
|
||||
The Wi-Fi (`WIFI_ERR_*`) and USB (`USB_ERR_*`) syscalls keep their own error codes.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Status Codes](#status-codes)
|
||||
- [Process Management](#process-management)
|
||||
- [File System](#file-system)
|
||||
- [Memory](#memory)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/statuscodes.h>
|
||||
|
||||
namespace montauk::abi {
|
||||
|
||||
@@ -250,6 +251,122 @@ namespace montauk::abi {
|
||||
|
||||
static constexpr uint64_t SYS_LOG_WRITE = 176; // (logMessage) -> 0
|
||||
static constexpr uint64_t SYS_TERMINAL_ATTACHED = 177; // () -> 1 when connected to a userspace terminal
|
||||
static constexpr uint64_t SYS_SPAWN_CAPS = 185;
|
||||
static constexpr uint64_t SYS_SPAWN_REDIR_CAPS = 186;
|
||||
// As SYS_ALLOC, but commits every page up front instead of faulting them
|
||||
// in one at a time. For buffers the caller is about to touch in full.
|
||||
static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure
|
||||
|
||||
/* Battery.hpp -- ACPI battery status */
|
||||
static constexpr uint64_t SYS_BATTERYINFO = 188; // (BatteryInfo*) -> 0, -1 no battery device
|
||||
|
||||
// The caller's own capability masks.
|
||||
static constexpr uint64_t SYS_GETCAPS = 189; // (SpawnCapabilities*) -> 0, -1 bad pointer
|
||||
|
||||
/* Storage.hpp */
|
||||
static constexpr uint64_t SYS_VOLINFO = 190; // (VolInfo*, maxCount) -> count, -1 bad pointer
|
||||
|
||||
/* Consent.hpp */
|
||||
static constexpr uint64_t SYS_CONSENT_REGISTER = 191; // () -> 0, -13 lacks CAP_COMPOSITOR, -1 another agent is live
|
||||
static constexpr uint64_t SYS_CONSENT_NEXT = 192; // (ConsentRequest*) -> 1 delivered, 0 none queued
|
||||
static constexpr uint64_t SYS_CONSENT_REPLY = 193; // (requestId, CONSENT_ALLOW/DENY) -> 0, -1 stale id
|
||||
|
||||
/* 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;
|
||||
/* Map other processes' windows and deliver input events to them
|
||||
(SYS_WINMAP, SYS_WINSENDEVENT): the desktop compositor's authority.
|
||||
Anything holding it can read every window's pixels and click on any
|
||||
window, including a consent prompt it is itself waiting on, so it is
|
||||
never part of a session mask, never part of "all", and never
|
||||
delegated by the compositor to what it launches. */
|
||||
static constexpr uint64_t CAP_COMPOSITOR = 1ULL << 13;
|
||||
static constexpr uint64_t CAP_ALL = (1ULL << 14) - 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_assert((CAP_ADMIN_SESSION & CAP_COMPOSITOR) == 0,
|
||||
"a session must not delegate the compositor's authority; "
|
||||
"login grants it to the desktop directly");
|
||||
// Capability denial for the Wi-Fi and USB syscalls only, which keep their
|
||||
// own WIFI_ERR_* / USB_ERR_* spaces; everything else returns
|
||||
// CapabilityDenied from StatusCodes.
|
||||
static constexpr int SYS_ERR_PERMISSION = -13;
|
||||
|
||||
// A process holds two independent sets: what it may use (effective) and
|
||||
// what it may pass to children (delegable). A launcher can delegate what
|
||||
// it never uses itself, so neither set bounds the other.
|
||||
struct SpawnCapabilities {
|
||||
uint64_t effective;
|
||||
uint64_t delegable;
|
||||
};
|
||||
|
||||
// Both of the child's sets must come out of the parent's delegable set.
|
||||
constexpr bool ValidCapabilityDelegation(const SpawnCapabilities& child,
|
||||
uint64_t parentDelegable) {
|
||||
return ((child.effective | child.delegable) & ~CAP_ALL) == 0 &&
|
||||
(child.effective & ~parentDelegable) == 0 &&
|
||||
(child.delegable & ~parentDelegable) == 0;
|
||||
}
|
||||
static_assert(ValidCapabilityDelegation(
|
||||
{CAP_NETWORK_ADMIN, 0}, CAP_NETWORK_ADMIN));
|
||||
static_assert(ValidCapabilityDelegation(
|
||||
{0, CAP_NETWORK_ADMIN}, CAP_NETWORK_ADMIN));
|
||||
static_assert(!ValidCapabilityDelegation(
|
||||
{CAP_NETWORK_ADMIN, CAP_NETWORK_ADMIN}, 0));
|
||||
static_assert(!ValidCapabilityDelegation(
|
||||
{CAP_NETWORK_ADMIN | CAP_SET_TIME, 0}, CAP_NETWORK_ADMIN));
|
||||
static_assert(!ValidCapabilityDelegation(
|
||||
{0, CAP_NETWORK_ADMIN | CAP_SET_TIME}, CAP_NETWORK_ADMIN));
|
||||
|
||||
/* Capabilities a user can grant at first use through a consent prompt.
|
||||
A consent grant never exceeds what the process's parent could have
|
||||
delegated to it at spawn (its consent ceiling), is never delegable
|
||||
onwards, and is issued only by the session's consent agent -- so it is
|
||||
deferred delegation, not amplification. */
|
||||
static constexpr uint64_t CAP_CONSENT_ELIGIBLE =
|
||||
CAP_STORAGE_ADMIN | CAP_NETWORK_ADMIN | CAP_SET_TIME |
|
||||
CAP_DISPLAY_ADMIN | CAP_LOG_READ;
|
||||
static_assert((CAP_CONSENT_ELIGIBLE & ~CAP_ADMIN_SESSION) == 0,
|
||||
"consent can only hand out authority an admin session holds");
|
||||
|
||||
static constexpr int CONSENT_DENY = 0;
|
||||
static constexpr int CONSENT_ALLOW = 1;
|
||||
// A request left unanswered this long is denied (and not remembered).
|
||||
static constexpr uint64_t CONSENT_TIMEOUT_MS = 120000;
|
||||
|
||||
struct ConsentRequest {
|
||||
uint64_t id;
|
||||
uint64_t capability; // exactly one CAP_* bit
|
||||
int32_t pid;
|
||||
uint32_t imageProtected; // 1 when writing the image needs CAP_SYSTEM_IMAGE
|
||||
char imagePath[256];
|
||||
};
|
||||
|
||||
|
||||
static constexpr int USB_ERR_INVALID = -1;
|
||||
@@ -264,10 +381,17 @@ namespace montauk::abi {
|
||||
// Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts
|
||||
// a pending action and exits; login.elf reads it, runs the shutdown stages,
|
||||
// then issues the matching SYS_SHUTDOWN / SYS_RESET.
|
||||
//
|
||||
// A request can also be posted from inside the session -- the shell's
|
||||
// shutdown builtin does. login only looks at it once the session leader
|
||||
// exits, so the leader has to notice and stand down: POWER_REQ_PEEK is the
|
||||
// non-destructive read it polls with. Only login consumes (QUERY), so a
|
||||
// leader that peeks cannot swallow the request it is meant to act on.
|
||||
enum PowerRequestAction : int {
|
||||
POWER_REQ_QUERY = 0, // read-and-clear the pending action
|
||||
POWER_REQ_SHUTDOWN = 1,
|
||||
POWER_REQ_REBOOT = 2,
|
||||
POWER_REQ_PEEK = 3, // read the pending action without clearing it
|
||||
};
|
||||
|
||||
static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
|
||||
@@ -530,6 +654,28 @@ namespace montauk::abi {
|
||||
char typeName[24]; // human-readable type name
|
||||
};
|
||||
|
||||
// One mounted volume (returned by SYS_VOLINFO). A volume is a single
|
||||
// mounted filesystem, which is what a drive number names: one physical
|
||||
// disk with three partitions occupies three drive numbers.
|
||||
//
|
||||
// A volume with no backing partition -- the boot ramdisk -- reports
|
||||
// blockDev and partIndex as -1 and leaves the geometry fields zero.
|
||||
struct VolInfo {
|
||||
int32_t drive; // VFS drive number ("N:/")
|
||||
int32_t blockDev; // backing block device index, -1 if none
|
||||
int32_t partIndex; // global GPT partition index, -1 if none
|
||||
uint32_t kind; // BlockDeviceKind: 0=none/ramdisk, 1=SATA, 2=SATAPI, 3=NVMe, 4=USB MSC
|
||||
uint64_t startLba; // partition start LBA, 0 if none
|
||||
uint64_t sectorCount; // partition length in sectors, 0 if none
|
||||
uint32_t sectorSize; // logical sector size in bytes, 0 if none
|
||||
uint32_t _pad0;
|
||||
char label[36]; // filesystem volume label, "" if unset
|
||||
char fsType[12]; // "ext2", "FAT32", "ramdisk", "" if unknown
|
||||
char partName[72]; // GPT partition name, "" if none
|
||||
char model[41]; // backing disk model, "" if none
|
||||
char _pad1[3];
|
||||
};
|
||||
|
||||
struct GptAddParams {
|
||||
int32_t blockDev;
|
||||
uint32_t _pad0;
|
||||
@@ -729,6 +875,8 @@ namespace montauk::abi {
|
||||
char name[64];
|
||||
uint64_t heapUsed; // Distance from UserHeapBase to high-water mark
|
||||
uint64_t cpuTimeMs; // accumulated scheduler runtime
|
||||
uint64_t effectiveCaps;
|
||||
uint64_t delegableCaps;
|
||||
};
|
||||
|
||||
struct MemStats {
|
||||
@@ -738,6 +886,26 @@ namespace montauk::abi {
|
||||
uint64_t pageSize;
|
||||
};
|
||||
|
||||
// Battery status (returned by SYS_BATTERYINFO). A machine with no battery
|
||||
// device fails the call outright; `present` distinguishes a laptop with the
|
||||
// pack removed from one that is running on it.
|
||||
struct BatteryInfo {
|
||||
uint8_t present; // a battery is installed
|
||||
uint8_t charging;
|
||||
uint8_t discharging;
|
||||
uint8_t critical;
|
||||
uint8_t acOnline; // AC adapter connected
|
||||
uint8_t hasAcAdapter; // an AC adapter device exists at all
|
||||
uint8_t percent; // 0..100, 0 when capacity is unknown
|
||||
uint8_t powerUnitMilliAmps; // capacities are mAh/mA, not mWh/mW
|
||||
uint32_t remainingCapacity;
|
||||
uint32_t fullCapacity; // last full charge capacity
|
||||
uint32_t designCapacity;
|
||||
uint32_t presentRate; // mW or mA, 0 when unknown
|
||||
uint32_t voltageMv;
|
||||
int32_t minutesRemaining; // -1 when not estimable (e.g. charging)
|
||||
};
|
||||
|
||||
// Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT)
|
||||
struct CrashReportInfo {
|
||||
int pid;
|
||||
|
||||
@@ -217,6 +217,16 @@ struct DesktopState {
|
||||
uint64_t wifi_status_time;
|
||||
|
||||
|
||||
// Battery indicator. Absent entirely on a machine with no ACPI battery
|
||||
// device, which is how a desktop keeps a clean panel.
|
||||
SvgIcon icon_battery;
|
||||
char battery_icon_name[64]; // which glyph icon_battery holds
|
||||
bool battery_present; // a battery device exists
|
||||
bool battery_popup_open;
|
||||
Rect battery_icon_rect;
|
||||
montauk::abi::BatteryInfo battery_info;
|
||||
uint64_t battery_last_poll;
|
||||
|
||||
bool vol_popup_open;
|
||||
Rect vol_icon_rect;
|
||||
int vol_level; // 0-100
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <Api/Syscall.hpp>
|
||||
#include <montauk/capabilities.h>
|
||||
|
||||
namespace gui {
|
||||
|
||||
@@ -59,32 +60,47 @@ struct TerminalState {
|
||||
int csi_current_param;
|
||||
};
|
||||
|
||||
// ==== Palette ====
|
||||
//
|
||||
// The palette is a mutable global rather than a set of constants because the
|
||||
// terminal lets the user switch themes at runtime. Cells store resolved colors,
|
||||
// so a theme switch also has to rewrite the cells that were painted with the
|
||||
// outgoing palette -- see terminal_remap_palette below.
|
||||
|
||||
struct TermPalette {
|
||||
Color bg;
|
||||
Color fg;
|
||||
Color cursor;
|
||||
Color ansi[16];
|
||||
};
|
||||
|
||||
inline constexpr TermPalette TERM_PALETTE_DEFAULT = {
|
||||
colors::TERM_BG,
|
||||
colors::TERM_FG,
|
||||
colors::TERM_FG,
|
||||
{
|
||||
Color::from_hex(0x000000), Color::from_hex(0xCC0000),
|
||||
Color::from_hex(0x4E9A06), Color::from_hex(0xC4A000),
|
||||
Color::from_hex(0x3465A4), Color::from_hex(0x75507B),
|
||||
Color::from_hex(0x06989A), Color::from_hex(0xD3D7CF),
|
||||
Color::from_hex(0x555753), Color::from_hex(0xEF2929),
|
||||
Color::from_hex(0x8AE234), Color::from_hex(0xFCE94F),
|
||||
Color::from_hex(0x729FCF), Color::from_hex(0xAD7FA8),
|
||||
Color::from_hex(0x34E2E2), Color::from_hex(0xEEEEEC),
|
||||
}
|
||||
};
|
||||
|
||||
inline TermPalette g_term_palette = TERM_PALETTE_DEFAULT;
|
||||
|
||||
// Standard ANSI color palette as ARGB pixels
|
||||
static inline Color term_ansi_color(int idx) {
|
||||
switch (idx) {
|
||||
case 0: return Color::from_hex(0x000000);
|
||||
case 1: return Color::from_hex(0xCC0000);
|
||||
case 2: return Color::from_hex(0x4E9A06);
|
||||
case 3: return Color::from_hex(0xC4A000);
|
||||
case 4: return Color::from_hex(0x3465A4);
|
||||
case 5: return Color::from_hex(0x75507B);
|
||||
case 6: return Color::from_hex(0x06989A);
|
||||
case 7: return Color::from_hex(0xD3D7CF);
|
||||
case 8: return Color::from_hex(0x555753);
|
||||
case 9: return Color::from_hex(0xEF2929);
|
||||
case 10: return Color::from_hex(0x8AE234);
|
||||
case 11: return Color::from_hex(0xFCE94F);
|
||||
case 12: return Color::from_hex(0x729FCF);
|
||||
case 13: return Color::from_hex(0xAD7FA8);
|
||||
case 14: return Color::from_hex(0x34E2E2);
|
||||
case 15: return Color::from_hex(0xEEEEEC);
|
||||
default: return colors::TERM_FG;
|
||||
}
|
||||
if (idx < 0 || idx > 15) return g_term_palette.fg;
|
||||
return g_term_palette.ansi[idx];
|
||||
}
|
||||
|
||||
// ANSI 256-color palette (0-15 = standard, 16-231 = RGB cube, 232-255 = grayscale)
|
||||
static inline Color term_ansi_256_color(int idx) {
|
||||
if (idx < 0) return colors::TERM_FG;
|
||||
if (idx < 0) return g_term_palette.fg;
|
||||
if (idx <= 15) return term_ansi_color(idx);
|
||||
if (idx <= 231) {
|
||||
// 6x6x6 RGB cube: idx 16 = (0,0,0), idx 231 = (5,5,5)
|
||||
@@ -118,6 +134,47 @@ static inline void terminal_invalidate_render_cache(TerminalState* t) {
|
||||
t->render_cursor_visible = false;
|
||||
}
|
||||
|
||||
// Translate one already-resolved cell color from an outgoing palette to the
|
||||
// incoming one. Colors that are not palette entries -- 256-color cube and
|
||||
// grayscale ramp values -- are left exactly as the program asked for them.
|
||||
static inline Color term_remap_color(Color c, const TermPalette& from,
|
||||
const TermPalette& to) {
|
||||
if (term_color_equal(c, from.bg)) return to.bg;
|
||||
if (term_color_equal(c, from.fg)) return to.fg;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
if (term_color_equal(c, from.ansi[i])) return to.ansi[i];
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
// Repaint an existing terminal in a new palette. Cells carry resolved colors,
|
||||
// so switching themes without this leaves all scrollback -- and the shell's
|
||||
// colored prompt -- in the old theme's colors.
|
||||
static inline void terminal_remap_palette(TerminalState* t,
|
||||
const TermPalette& from,
|
||||
const TermPalette& to) {
|
||||
if (!t || !t->cells) return;
|
||||
|
||||
int total = (t->rows + t->max_scrollback) * t->cols;
|
||||
for (int i = 0; i < total; i++) {
|
||||
t->cells[i].fg = term_remap_color(t->cells[i].fg, from, to);
|
||||
t->cells[i].bg = term_remap_color(t->cells[i].bg, from, to);
|
||||
}
|
||||
|
||||
if (t->alt_cells) {
|
||||
int screen = t->rows * t->cols;
|
||||
for (int i = 0; i < screen; i++) {
|
||||
t->alt_cells[i].fg = term_remap_color(t->alt_cells[i].fg, from, to);
|
||||
t->alt_cells[i].bg = term_remap_color(t->alt_cells[i].bg, from, to);
|
||||
}
|
||||
}
|
||||
|
||||
t->current_fg = term_remap_color(t->current_fg, from, to);
|
||||
t->current_bg = term_remap_color(t->current_bg, from, to);
|
||||
terminal_invalidate_render_cache(t);
|
||||
t->dirty = true;
|
||||
}
|
||||
|
||||
static inline Color terminal_erase_bg(TerminalState* t) {
|
||||
return t->current_bg;
|
||||
}
|
||||
@@ -162,8 +219,8 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
|
||||
t->scrollback_lines = 0;
|
||||
t->max_scrollback = max_sb;
|
||||
t->view_offset = 0;
|
||||
t->current_fg = colors::TERM_FG;
|
||||
t->current_bg = colors::TERM_BG;
|
||||
t->current_fg = g_term_palette.fg;
|
||||
t->current_bg = g_term_palette.bg;
|
||||
t->cursor_visible = false;
|
||||
t->alt_screen_active = false;
|
||||
t->reverse_video = false;
|
||||
@@ -201,10 +258,10 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < total_cells; i++) {
|
||||
t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
t->cells[i] = {' ', g_term_palette.fg, g_term_palette.bg};
|
||||
}
|
||||
for (int i = 0; i < screen_cells; i++) {
|
||||
t->alt_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
t->alt_cells[i] = {' ', g_term_palette.fg, g_term_palette.bg};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +269,8 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) {
|
||||
terminal_init_cells(t, cols, rows, TERM_MAX_SCROLLBACK);
|
||||
t->cursor_visible = true;
|
||||
|
||||
t->child_pid = montauk::spawn_redir("0:/os/shell.elf");
|
||||
static constexpr const char* kShellPath = "0:/os/shell.elf";
|
||||
t->child_pid = montauk::caps::launch(kShellPath, {.redirected = true});
|
||||
if (t->child_pid > 0)
|
||||
montauk::childio_settermsz(t->child_pid, cols, rows);
|
||||
}
|
||||
@@ -245,7 +303,7 @@ static inline void terminal_enter_alt_screen(TerminalState* t) {
|
||||
TermCell* screen = term_screen_row(t, 0);
|
||||
for (int i = 0; i < total; i++) {
|
||||
t->alt_cells[i] = screen[i];
|
||||
screen[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
screen[i] = {' ', g_term_palette.fg, g_term_palette.bg};
|
||||
}
|
||||
t->view_offset = 0;
|
||||
t->cursor_x = 0;
|
||||
@@ -424,8 +482,8 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
|
||||
}
|
||||
|
||||
if (code == 0) {
|
||||
t->current_fg = colors::TERM_FG;
|
||||
t->current_bg = colors::TERM_BG;
|
||||
t->current_fg = g_term_palette.fg;
|
||||
t->current_bg = g_term_palette.bg;
|
||||
t->reverse_video = false;
|
||||
} else if (code == 1) {
|
||||
// Bold: map to bright version of current color
|
||||
@@ -473,15 +531,15 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
|
||||
} else if (code >= 100 && code <= 107) {
|
||||
t->current_bg = term_ansi_color(code - 100 + 8);
|
||||
} else if (code == 39) {
|
||||
t->current_fg = colors::TERM_FG;
|
||||
t->current_fg = g_term_palette.fg;
|
||||
} else if (code == 49) {
|
||||
t->current_bg = colors::TERM_BG;
|
||||
t->current_bg = g_term_palette.bg;
|
||||
}
|
||||
}
|
||||
if (t->csi_param_count == 0) {
|
||||
// ESC[m with no params = reset
|
||||
t->current_fg = colors::TERM_FG;
|
||||
t->current_bg = colors::TERM_BG;
|
||||
t->current_fg = g_term_palette.fg;
|
||||
t->current_bg = g_term_palette.bg;
|
||||
t->reverse_video = false;
|
||||
}
|
||||
break;
|
||||
@@ -532,8 +590,8 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
|
||||
for (int j = 0; j < 8; j++) t->csi_params[j] = 0;
|
||||
} else if (ch == 'c') {
|
||||
// Reset terminal
|
||||
t->current_fg = colors::TERM_FG;
|
||||
t->current_bg = colors::TERM_BG;
|
||||
t->current_fg = g_term_palette.fg;
|
||||
t->current_bg = g_term_palette.bg;
|
||||
t->cursor_x = 0;
|
||||
t->cursor_y = 0;
|
||||
t->parse_state = TerminalState::STATE_NORMAL;
|
||||
@@ -627,18 +685,21 @@ static inline void terminal_draw_cursor_pixels(const TermCell& cell,
|
||||
uint32_t* pixels, int pw, int ph,
|
||||
int px, int py, int cell_w, int cell_h,
|
||||
bool use_ttf, GlyphCache* gc) {
|
||||
// The block cursor takes its color from the palette and punches the glyph
|
||||
// out in the background color, so it stays legible on light themes too --
|
||||
// a hardcoded white block disappears on a white background.
|
||||
terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h,
|
||||
colors::WHITE.to_pixel());
|
||||
g_term_palette.cursor.to_pixel());
|
||||
|
||||
if (cell.ch <= 32 && cell.ch >= 0) return;
|
||||
|
||||
if (use_ttf) {
|
||||
int baseline = py + gc->ascent;
|
||||
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
|
||||
px, baseline, (unsigned char)cell.ch, colors::BLACK, gc);
|
||||
px, baseline, (unsigned char)cell.ch, g_term_palette.bg, gc);
|
||||
} else {
|
||||
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
|
||||
colors::BLACK.to_pixel());
|
||||
g_term_palette.bg.to_pixel());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,7 +761,7 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
|
||||
t->render_base_row != base_row ||
|
||||
t->render_term_cols != t->cols || t->render_term_rows != t->rows;
|
||||
|
||||
uint32_t bg_px = colors::TERM_BG.to_pixel();
|
||||
uint32_t bg_px = g_term_palette.bg.to_pixel();
|
||||
if (full_redraw) {
|
||||
int row_bytes = pw * sizeof(uint32_t);
|
||||
for (int i = 0; i < pw; i++) pixels[i] = bg_px;
|
||||
@@ -779,9 +840,9 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
|
||||
|
||||
// Clear new buffers
|
||||
for (int i = 0; i < new_total; i++)
|
||||
new_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
new_cells[i] = {' ', g_term_palette.fg, g_term_palette.bg};
|
||||
for (int i = 0; i < new_rows * new_cols; i++)
|
||||
new_alt[i] = {' ', colors::TERM_FG, colors::TERM_BG};
|
||||
new_alt[i] = {' ', g_term_palette.fg, g_term_palette.bg};
|
||||
|
||||
// Copy content: scrollback + visible screen
|
||||
int old_content = t->scrollback_lines + t->rows;
|
||||
|
||||
@@ -483,10 +483,10 @@ namespace fonts {
|
||||
return f;
|
||||
};
|
||||
|
||||
system_font = load("0:/fonts/Roboto-Medium.ttf");
|
||||
system_bold = load("0:/fonts/Roboto-Bold.ttf");
|
||||
mono = load("0:/fonts/JetBrainsMono-Regular.ttf");
|
||||
mono_bold = load("0:/fonts/JetBrainsMono-Bold.ttf");
|
||||
system_font = load("0:/os/fonts/Roboto-Medium.ttf");
|
||||
system_bold = load("0:/os/fonts/Roboto-Bold.ttf");
|
||||
mono = load("0:/os/fonts/JetBrainsMono-Regular.ttf");
|
||||
mono_bold = load("0:/os/fonts/JetBrainsMono-Bold.ttf");
|
||||
|
||||
return system_font != nullptr;
|
||||
}
|
||||
|
||||
@@ -208,8 +208,38 @@ extern "C" {
|
||||
#define MTK_SYS_USB_BULK_IN_START 182
|
||||
#define MTK_SYS_USB_BULK_IN_STOP 183
|
||||
#define MTK_SYS_USB_BULK_IN_READ 184
|
||||
#define MTK_SYS_SPAWN_CAPS 185
|
||||
#define MTK_SYS_SPAWN_REDIR_CAPS 186
|
||||
#define MTK_SYS_ALLOC_EAGER 187
|
||||
#define MTK_SYS_BATTERYINFO 188
|
||||
#define MTK_SYS_GETCAPS 189
|
||||
#define MTK_SYS_VOLINFO 190
|
||||
#define MTK_SYS_CONSENT_REGISTER 191
|
||||
#define MTK_SYS_CONSENT_NEXT 192
|
||||
#define MTK_SYS_CONSENT_REPLY 193
|
||||
/* @SYSCALLS-END */
|
||||
|
||||
/* Syscall status codes (montauk/statuscodes.h for C++). */
|
||||
/* @STATUS-BEGIN
|
||||
Generated from Common/StatusCodes.hpp by scripts/montauk-syscalls.py.
|
||||
Do not edit by hand; run 'make gen-syscalls' to refresh. */
|
||||
#define MTK_STATUS_SUCCESS (0)
|
||||
#define MTK_STATUS_GENERIC_FAILURE (-1)
|
||||
#define MTK_STATUS_PERMISSION_DENIED (-2)
|
||||
#define MTK_STATUS_CAPABILITY_DENIED (-3)
|
||||
#define MTK_STATUS_SYSTEM_INTEGRITY_ERROR (-4)
|
||||
#define MTK_STATUS_BAD_POINTER (-5)
|
||||
#define MTK_STATUS_INVALID_ARGUMENT (-6)
|
||||
#define MTK_STATUS_UNSUPPORTED_OP (-7)
|
||||
#define MTK_STATUS_NO_DEVICE (-8)
|
||||
#define MTK_STATUS_IO_FAILURE (-9)
|
||||
#define MTK_STATUS_BAD_INVOCATION (-10)
|
||||
#define MTK_STATUS_NO_SUCH_PATH (-11)
|
||||
#define MTK_STATUS_BUSY (-12)
|
||||
#define MTK_STATUS_BUFFER_TOO_SMALL (-13)
|
||||
#define MTK_STATUS_OUT_OF_MEMORY (-14)
|
||||
/* @STATUS-END */
|
||||
|
||||
#define MTK_SOCK_TCP 1
|
||||
#define MTK_SOCK_UDP 2
|
||||
#define MTK_IPC_SIGNAL_READABLE (1u << 0)
|
||||
@@ -683,8 +713,8 @@ static inline int mtk_set_unix_time(int64_t unix_seconds) {
|
||||
return (int)_mtk_syscall1(MTK_SYS_SETUNIXTIME, (long)unix_seconds);
|
||||
}
|
||||
|
||||
static inline void mtk_settz(int offset_minutes) {
|
||||
_mtk_syscall1(MTK_SYS_SETTZ, (long)offset_minutes);
|
||||
static inline int mtk_settz(int offset_minutes) {
|
||||
return (int)_mtk_syscall1(MTK_SYS_SETTZ, (long)offset_minutes);
|
||||
}
|
||||
|
||||
static inline int mtk_gettz(void) {
|
||||
@@ -883,12 +913,12 @@ static inline int mtk_audio_ctl(int handle, int cmd, int value) {
|
||||
Power management
|
||||
==================================================================== */
|
||||
|
||||
static inline void mtk_reset(void) {
|
||||
_mtk_syscall0(MTK_SYS_RESET);
|
||||
static inline int mtk_reset(void) {
|
||||
return (int)_mtk_syscall0(MTK_SYS_RESET);
|
||||
}
|
||||
|
||||
static inline void mtk_shutdown(void) {
|
||||
_mtk_syscall0(MTK_SYS_SHUTDOWN);
|
||||
static inline int mtk_shutdown(void) {
|
||||
return (int)_mtk_syscall0(MTK_SYS_SHUTDOWN);
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
|
||||
@@ -21,6 +21,7 @@ typedef struct {
|
||||
/* Byte-oriented C locale only: one byte, one character. */
|
||||
size_t mbstowcs(wchar_t *dst, const char *src, size_t n);
|
||||
int mblen(const char *s, size_t n);
|
||||
size_t mbrtowc(wchar_t *pwc, const char *s, size_t n, mbstate_t *ps);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
namespace montauk {
|
||||
namespace caps {
|
||||
|
||||
inline constexpr const char* GRANT_CONFIG = "capabilities";
|
||||
inline constexpr const char* GRANT_PREFIX = "grant.";
|
||||
|
||||
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},
|
||||
{"compositor", montauk::abi::CAP_COMPOSITOR},
|
||||
};
|
||||
|
||||
// Never implied by "all": each must be named explicitly in a grant.
|
||||
inline constexpr uint64_t EXPLICIT_ONLY =
|
||||
montauk::abi::CAP_SYSTEM_IMAGE | montauk::abi::CAP_COMPOSITOR;
|
||||
|
||||
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".
|
||||
if (montauk::streq(name, "all"))
|
||||
return montauk::abi::CAP_ALL & ~EXPLICIT_ONLY;
|
||||
for (const auto& entry : NAMES) {
|
||||
if (montauk::streq(entry.name, name)) return entry.bit;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// True when a grant for `path` can apply: the program is installed under
|
||||
// 0:/os or 0:/apps, which cannot be rewritten without system_image. The
|
||||
// kernel enforces this (Fs::IsSystemImage) by starting any other program
|
||||
// without capabilities; this copy of the rule is only for display.
|
||||
inline bool is_installed_path(const char* path) {
|
||||
if (path == nullptr) return false;
|
||||
const char* p = path;
|
||||
uint64_t drive = 0;
|
||||
if (*p < '0' || *p > '9') return false;
|
||||
while (*p >= '0' && *p <= '9') drive = drive * 10 + (uint64_t)(*p++ - '0');
|
||||
if (*p != ':' || drive != 0) return false;
|
||||
p++;
|
||||
static const char* const roots[] = {"/os", "/apps"};
|
||||
for (const char* root : roots) {
|
||||
int i = 0;
|
||||
while (root[i]) {
|
||||
char c = p[i];
|
||||
if (c >= 'A' && c <= 'Z') c = (char)(c + ('a' - 'A'));
|
||||
if (c != root[i]) break;
|
||||
i++;
|
||||
}
|
||||
if (root[i] == '\0' && (p[i] == '\0' || p[i] == '/')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read an array-of-strings key into a capability mask.
|
||||
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};
|
||||
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);
|
||||
|
||||
out.effective = effective;
|
||||
out.delegable = delegable;
|
||||
found = true;
|
||||
}
|
||||
|
||||
doc.destroy();
|
||||
return found;
|
||||
}
|
||||
|
||||
// The calling process's own capability masks.
|
||||
inline bool self(montauk::abi::SpawnCapabilities& out) {
|
||||
out = {0, 0};
|
||||
return montauk::getcaps(&out) == 0;
|
||||
}
|
||||
|
||||
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.
|
||||
inline montauk::abi::SpawnCapabilities for_binary(const char* binary_path,
|
||||
uint64_t caller_delegable) {
|
||||
montauk::abi::SpawnCapabilities caps{0, 0};
|
||||
|
||||
if (caller_delegable == 0) return caps;
|
||||
|
||||
if (!lookup(binary_path, caps)) return caps;
|
||||
|
||||
caps.effective &= caller_delegable;
|
||||
caps.delegable &= caller_delegable;
|
||||
return caps;
|
||||
}
|
||||
|
||||
// The calling process's delegable set, cached.
|
||||
inline uint64_t my_delegable() {
|
||||
static uint64_t cached = 0;
|
||||
static bool loaded = false;
|
||||
if (!loaded) {
|
||||
cached = self_delegable();
|
||||
loaded = true;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
struct LaunchOptions {
|
||||
const char* args = nullptr;
|
||||
// Owner identity for the child. Requires CAP_USER_ADMIN and is
|
||||
// reserved for session creators (init, login);
|
||||
const char* user = nullptr;
|
||||
// True for a console host that reads and writes the child's I/O
|
||||
// through the childio calls rather than sharing its own.
|
||||
bool redirected = false;
|
||||
};
|
||||
|
||||
// Unified launch helper.
|
||||
inline int launch(const char* path, const LaunchOptions& opt = {}) {
|
||||
if (path == nullptr || path[0] == '\0') return -1;
|
||||
|
||||
if (opt.redirected && opt.user != nullptr) return -1;
|
||||
|
||||
montauk::abi::SpawnCapabilities caps = for_binary(path, my_delegable());
|
||||
return opt.redirected
|
||||
? spawn_redir_with_caps(path, opt.args, caps)
|
||||
: spawn_with_caps(path, opt.args, opt.user, caps);
|
||||
}
|
||||
|
||||
} // namespace caps
|
||||
} // namespace montauk
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* consent.h
|
||||
* First-use consent: prompt wording and the stored-decision table
|
||||
* (0:/config/consent.toml).
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/toml.h>
|
||||
#include <montauk/config.h>
|
||||
#include <montauk/capabilities.h>
|
||||
|
||||
namespace montauk {
|
||||
namespace consent {
|
||||
|
||||
inline constexpr const char* STORE_CONFIG = "consent";
|
||||
inline constexpr const char* STORE_PREFIX = "decision.";
|
||||
|
||||
struct CapText {
|
||||
uint64_t bit;
|
||||
const char* action; // completes "<App> wants to ..."
|
||||
const char* icon; // shown in the prompt
|
||||
};
|
||||
|
||||
inline constexpr CapText TEXTS[] = {
|
||||
{montauk::abi::CAP_STORAGE_ADMIN, "format, partition and mount disks",
|
||||
"0:/os/icons/drive-harddisk.svg"},
|
||||
{montauk::abi::CAP_NETWORK_ADMIN, "change network settings and join Wi-Fi networks",
|
||||
"0:/os/icons/network-wired.svg"},
|
||||
{montauk::abi::CAP_SET_TIME, "change the system clock and time zone",
|
||||
"0:/os/icons/preferences-system-time.svg"},
|
||||
{montauk::abi::CAP_DISPLAY_ADMIN, "change the screen resolution and brightness",
|
||||
"0:/os/icons/preferences-desktop-display.svg"},
|
||||
{montauk::abi::CAP_LOG_READ, "read the system log, which can reveal what other apps and devices are doing",
|
||||
"0:/os/icons/utilities-log-viewer.svg"},
|
||||
};
|
||||
|
||||
// Fallback for a capability without its own icon.
|
||||
inline constexpr const char* DEFAULT_ICON = "0:/os/icons/lock.svg";
|
||||
|
||||
inline const char* action_for(uint64_t bit) {
|
||||
for (const auto& t : TEXTS) {
|
||||
if (t.bit == bit) return t.action;
|
||||
}
|
||||
return "use a protected system capability";
|
||||
}
|
||||
|
||||
inline const char* icon_for(uint64_t bit) {
|
||||
for (const auto& t : TEXTS) {
|
||||
if (t.bit == bit) return t.icon;
|
||||
}
|
||||
return DEFAULT_ICON;
|
||||
}
|
||||
|
||||
inline const char* name_for(uint64_t bit) {
|
||||
for (const auto& n : montauk::caps::NAMES) {
|
||||
if (n.bit == bit) return n.name;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// One stored decision, as read from the table. Only programs installed
|
||||
// under 0:/os or 0:/apps are ever stored: those paths cannot be rewritten
|
||||
// without CAP_SYSTEM_IMAGE, so the path alone identifies the program.
|
||||
// The kernel reports this as ConsentRequest::imageProtected.
|
||||
struct Decision {
|
||||
char id[16];
|
||||
char path[256];
|
||||
uint64_t capability;
|
||||
bool allow;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
inline void key(char* out, int cap, const char* id, const char* field) {
|
||||
int n = 0;
|
||||
for (const char* p = STORE_PREFIX; *p && n < cap - 1; p++) out[n++] = *p;
|
||||
for (const char* p = id; *p && n < cap - 1; p++) out[n++] = *p;
|
||||
if (n < cap - 1) out[n++] = '.';
|
||||
for (const char* p = field; *p && n < cap - 1; p++) out[n++] = *p;
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
// Ids of "decision.<id>.path" entries, in file order.
|
||||
inline int collect_ids(const montauk::toml::Doc& doc, char (*ids)[16], int max) {
|
||||
int count = 0;
|
||||
int prefixLen = montauk::slen(STORE_PREFIX);
|
||||
for (int i = 0; i < doc.entries.count && count < max; i++) {
|
||||
montauk::toml::Value* v = doc.entries.items[i];
|
||||
if (v == nullptr || v->key == nullptr) continue;
|
||||
if (!montauk::starts_with(v->key, STORE_PREFIX)) continue;
|
||||
const char* rest = v->key + prefixLen;
|
||||
int dot = 0;
|
||||
while (rest[dot] && rest[dot] != '.') dot++;
|
||||
if (rest[dot] != '.' || !montauk::streq(rest + dot + 1, "path")) continue;
|
||||
if (dot <= 0 || dot >= 16) continue;
|
||||
montauk::memcpy(ids[count], rest, dot);
|
||||
ids[count][dot] = '\0';
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
inline bool read(const montauk::toml::Doc& doc, const char* id, Decision& out) {
|
||||
char k[64];
|
||||
montauk::memset(&out, 0, sizeof(out));
|
||||
montauk::strncpy(out.id, id, sizeof(out.id));
|
||||
key(k, sizeof(k), id, "path");
|
||||
montauk::strncpy(out.path, doc.get_string(k, ""), sizeof(out.path));
|
||||
key(k, sizeof(k), id, "capability");
|
||||
out.capability = montauk::caps::bit_for_name(doc.get_string(k, ""));
|
||||
key(k, sizeof(k), id, "decision");
|
||||
out.allow = montauk::streq(doc.get_string(k, ""), "allow");
|
||||
// "all" or an unknown name is never a valid stored decision.
|
||||
return out.path[0] &&
|
||||
(out.capability & montauk::abi::CAP_CONSENT_ELIGIBLE) != 0 &&
|
||||
(out.capability & (out.capability - 1)) == 0;
|
||||
}
|
||||
|
||||
inline void remove_id(montauk::toml::Doc* doc, const char* id) {
|
||||
static const char* fields[] = {"path", "capability", "decision"};
|
||||
char k[64];
|
||||
for (const char* f : fields) {
|
||||
key(k, sizeof(k), id, f);
|
||||
montauk::config::unset(doc, k);
|
||||
}
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
// Read up to `max` stored decisions. Returns the count.
|
||||
inline int load_all(Decision* out, int max) {
|
||||
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
|
||||
char ids[128][16];
|
||||
int idCount = detail::collect_ids(doc, ids, 128);
|
||||
int count = 0;
|
||||
for (int i = 0; i < idCount && count < max; i++) {
|
||||
if (detail::read(doc, ids[i], out[count])) count++;
|
||||
}
|
||||
doc.destroy();
|
||||
return count;
|
||||
}
|
||||
|
||||
// Stored answer for (path, capability): 1 allow, 0 deny, -1 none.
|
||||
inline int lookup(const char* path, uint64_t capability) {
|
||||
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
|
||||
char ids[128][16];
|
||||
int idCount = detail::collect_ids(doc, ids, 128);
|
||||
int result = -1;
|
||||
for (int i = 0; i < idCount && result < 0; i++) {
|
||||
Decision d;
|
||||
if (!detail::read(doc, ids[i], d)) continue;
|
||||
if (d.capability == capability && montauk::streq(d.path, path)) {
|
||||
result = d.allow ? 1 : 0;
|
||||
}
|
||||
}
|
||||
doc.destroy();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Store an answer, replacing any earlier one for the same path and
|
||||
// capability. Returns 0 on success, negative when the table cannot be
|
||||
// written.
|
||||
inline int record(const char* path, uint64_t capability, bool allow) {
|
||||
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
|
||||
char ids[128][16];
|
||||
int idCount = detail::collect_ids(doc, ids, 128);
|
||||
int nextId = 1;
|
||||
for (int i = 0; i < idCount; i++) {
|
||||
Decision d;
|
||||
bool valid = detail::read(doc, ids[i], d);
|
||||
if (valid && d.capability == capability && montauk::streq(d.path, path))
|
||||
detail::remove_id(&doc, ids[i]);
|
||||
int n = 0;
|
||||
for (const char* p = ids[i]; *p >= '0' && *p <= '9'; p++) n = n * 10 + (*p - '0');
|
||||
if (n >= nextId) nextId = n + 1;
|
||||
}
|
||||
|
||||
char id[16];
|
||||
int len = 0;
|
||||
char rev[16];
|
||||
for (int n = nextId; n > 0 && len < 15; n /= 10) rev[len++] = (char)('0' + n % 10);
|
||||
for (int i = 0; i < len; i++) id[i] = rev[len - 1 - i];
|
||||
id[len] = '\0';
|
||||
|
||||
char k[64];
|
||||
detail::key(k, sizeof(k), id, "path");
|
||||
montauk::config::set_string(&doc, k, path);
|
||||
detail::key(k, sizeof(k), id, "capability");
|
||||
montauk::config::set_string(&doc, k, name_for(capability));
|
||||
detail::key(k, sizeof(k), id, "decision");
|
||||
montauk::config::set_string(&doc, k, allow ? "allow" : "deny");
|
||||
|
||||
int rc = montauk::config::save(STORE_CONFIG, &doc);
|
||||
doc.destroy();
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Drop one stored decision by id; the program is asked again next time.
|
||||
inline int forget(const char* id) {
|
||||
montauk::toml::Doc doc = montauk::config::load(STORE_CONFIG);
|
||||
detail::remove_id(&doc, id);
|
||||
int rc = montauk::config::save(STORE_CONFIG, &doc);
|
||||
doc.destroy();
|
||||
return rc;
|
||||
}
|
||||
|
||||
} // namespace consent
|
||||
} // namespace montauk
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* statuscodes.h
|
||||
* MontaukOS status codes
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// Userspace mirror of kernel/src/Common/StatusCodes.hpp. Also usable from C
|
||||
// (the libc includes it), where the enumerators are unqualified.
|
||||
#ifdef __cplusplus
|
||||
namespace montauk {
|
||||
#endif
|
||||
|
||||
enum StatusCodes {
|
||||
Success = 0,
|
||||
GenericFailure = -1,
|
||||
PermissionDenied = -2,
|
||||
CapabilityDenied = -3,
|
||||
SystemIntegrityError = -4,
|
||||
BadPointer = -5,
|
||||
InvalidArgument = -6,
|
||||
UnsupportedOp = -7,
|
||||
NoDevice = -8,
|
||||
IoFailure = -9,
|
||||
BadInvocation = -10,
|
||||
NoSuchPath = -11,
|
||||
Busy = -12,
|
||||
BufferTooSmall = -13,
|
||||
OutOfMemory = -14
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <Api/Syscall.hpp>
|
||||
#include <montauk/statuscodes.h>
|
||||
|
||||
namespace montauk {
|
||||
|
||||
@@ -120,6 +121,28 @@ namespace montauk {
|
||||
inline int spawn(const char* path, const char* args = nullptr) {
|
||||
return (int)syscall2(montauk::abi::SYS_SPAWN, (uint64_t)path, (uint64_t)args);
|
||||
}
|
||||
inline int spawn_with_caps(const char* path, const char* args,
|
||||
const char* user,
|
||||
const montauk::abi::SpawnCapabilities& capabilities) {
|
||||
return (int)syscall4(montauk::abi::SYS_SPAWN_CAPS, (uint64_t)path,
|
||||
(uint64_t)args, (uint64_t)user,
|
||||
(uint64_t)&capabilities);
|
||||
}
|
||||
// The calling process's own capability masks. Returns 0 on success.
|
||||
inline int getcaps(montauk::abi::SpawnCapabilities* out) {
|
||||
return (int)syscall1(montauk::abi::SYS_GETCAPS, (uint64_t)out);
|
||||
}
|
||||
|
||||
// Consent agent.
|
||||
inline int consent_register() {
|
||||
return (int)syscall0(montauk::abi::SYS_CONSENT_REGISTER);
|
||||
}
|
||||
inline int consent_next(montauk::abi::ConsentRequest* out) {
|
||||
return (int)syscall1(montauk::abi::SYS_CONSENT_NEXT, (uint64_t)out);
|
||||
}
|
||||
inline int consent_reply(uint64_t id, int decision) {
|
||||
return (int)syscall2(montauk::abi::SYS_CONSENT_REPLY, id, (uint64_t)decision);
|
||||
}
|
||||
inline int chdir(const char* path) {
|
||||
return (int)syscall1(montauk::abi::SYS_CHDIR, (uint64_t)path);
|
||||
}
|
||||
@@ -184,6 +207,10 @@ namespace montauk {
|
||||
|
||||
// Memory
|
||||
inline void* alloc(uint64_t size) { return (void*)syscall1(montauk::abi::SYS_ALLOC, size); }
|
||||
// As alloc(), but the kernel commits the whole range immediately. Use it
|
||||
// for a buffer that is about to be written end to end: the lazy path costs
|
||||
// one page fault, one mutex acquire and one VMA walk per 4 KiB.
|
||||
inline void* alloc_eager(uint64_t size) { return (void*)syscall1(montauk::abi::SYS_ALLOC_EAGER, size); }
|
||||
inline void free(void* ptr) { syscall1(montauk::abi::SYS_FREE, (uint64_t)ptr); }
|
||||
|
||||
// Timekeeping
|
||||
@@ -391,7 +418,10 @@ namespace montauk {
|
||||
}
|
||||
|
||||
// Timezone offset (total minutes from UTC)
|
||||
inline void settz(int offset_minutes) { syscall1(montauk::abi::SYS_SETTZ, (uint64_t)(int64_t)offset_minutes); }
|
||||
inline int settz(int offset_minutes) {
|
||||
return (int)syscall1(montauk::abi::SYS_SETTZ,
|
||||
(uint64_t)(int64_t)offset_minutes);
|
||||
}
|
||||
inline int gettz() { return (int)syscall0(montauk::abi::SYS_GETTZ); }
|
||||
|
||||
// Random number generation
|
||||
@@ -400,14 +430,12 @@ namespace montauk {
|
||||
}
|
||||
|
||||
// Power management
|
||||
[[noreturn]] inline void reset() {
|
||||
syscall0(montauk::abi::SYS_RESET);
|
||||
__builtin_unreachable();
|
||||
inline int reset() {
|
||||
return (int)syscall0(montauk::abi::SYS_RESET);
|
||||
}
|
||||
|
||||
[[noreturn]] inline void shutdown() {
|
||||
syscall0(montauk::abi::SYS_SHUTDOWN);
|
||||
__builtin_unreachable();
|
||||
inline int shutdown() {
|
||||
return (int)syscall0(montauk::abi::SYS_SHUTDOWN);
|
||||
}
|
||||
|
||||
inline int suspend() {
|
||||
@@ -423,6 +451,13 @@ namespace montauk {
|
||||
return (int)syscall1(montauk::abi::SYS_POWER_REQUEST, (uint64_t)(int64_t)action);
|
||||
}
|
||||
|
||||
// Non-destructive read of the pending request, for a session leader that
|
||||
// must stand down when something inside its session (the shell's shutdown
|
||||
// builtin, say) asked for power-off. Returns POWER_REQ_QUERY when idle.
|
||||
inline int power_request_pending() {
|
||||
return power_request(montauk::abi::POWER_REQ_PEEK);
|
||||
}
|
||||
|
||||
// Mouse
|
||||
inline void mouse_state(montauk::abi::MouseState* out) { syscall1(montauk::abi::SYS_MOUSESTATE, (uint64_t)out); }
|
||||
inline void set_mouse_bounds(int32_t maxX, int32_t maxY) {
|
||||
@@ -447,6 +482,13 @@ namespace montauk {
|
||||
inline int spawn_redir(const char* path, const char* args = nullptr) {
|
||||
return (int)syscall2(montauk::abi::SYS_SPAWN_REDIR, (uint64_t)path, (uint64_t)args);
|
||||
}
|
||||
inline int spawn_redir_with_caps(
|
||||
const char* path, const char* args,
|
||||
const montauk::abi::SpawnCapabilities& capabilities) {
|
||||
return (int)syscall3(montauk::abi::SYS_SPAWN_REDIR_CAPS,
|
||||
(uint64_t)path, (uint64_t)args,
|
||||
(uint64_t)&capabilities);
|
||||
}
|
||||
inline int childio_read(int childPid, char* buf, int maxLen) {
|
||||
return (int)syscall3(montauk::abi::SYS_CHILDIO_READ, (uint64_t)childPid, (uint64_t)buf, (uint64_t)maxLen);
|
||||
}
|
||||
@@ -484,6 +526,10 @@ namespace montauk {
|
||||
inline int partlist(montauk::abi::PartInfo* buf, int max) {
|
||||
return (int)syscall2(montauk::abi::SYS_PARTLIST, (uint64_t)buf, (uint64_t)max);
|
||||
}
|
||||
// One entry per mounted volume (one filesystem, i.e. one drive number).
|
||||
inline int volinfo(montauk::abi::VolInfo* buf, int max) {
|
||||
return (int)syscall2(montauk::abi::SYS_VOLINFO, (uint64_t)buf, (uint64_t)max);
|
||||
}
|
||||
|
||||
// Raw block device I/O (driver-agnostic)
|
||||
inline int64_t disk_read(int blockDev, uint64_t lba, uint32_t sectorCount, void* buf) {
|
||||
@@ -613,6 +659,13 @@ namespace montauk {
|
||||
return (int)syscall1(montauk::abi::SYS_BTINFO, (uint64_t)buf);
|
||||
}
|
||||
|
||||
// ACPI battery status. Returns 0 on success, or -1 on a machine with no
|
||||
// battery device at all -- which is the signal not to draw an indicator.
|
||||
// A laptop with the pack removed still returns 0, with present == 0.
|
||||
inline int battery_info(montauk::abi::BatteryInfo* out) {
|
||||
return (int)syscall1(montauk::abi::SYS_BATTERYINFO, (uint64_t)out);
|
||||
}
|
||||
|
||||
// Wi-Fi. scan() runs a full channel sweep and blocks until it finishes or
|
||||
// timeoutMs elapses, then fills buf with the networks seen; it returns the
|
||||
// number of entries written, or -1 when no adapter is ready.
|
||||
|
||||
@@ -219,6 +219,16 @@ namespace user {
|
||||
return false;
|
||||
}
|
||||
|
||||
inline bool is_admin(const char* username) {
|
||||
UserInfo users[MAX_USERS];
|
||||
int count = load_users(users, MAX_USERS);
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (montauk::streq(users[i].username, username))
|
||||
return montauk::streq(users[i].role, "admin");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- User management ----
|
||||
|
||||
inline bool create_user(const char* username, const char* display_name,
|
||||
|
||||
Binary file not shown.
Reference in new issue
Block a user