diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 4d43800..7651bc4 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 149 +#define MONTAUK_BUILD_NUMBER 150 diff --git a/kernel/src/Terminal/Terminal.cpp b/kernel/src/Terminal/Terminal.cpp index f98c57a..9493114 100644 --- a/kernel/src/Terminal/Terminal.cpp +++ b/kernel/src/Terminal/Terminal.cpp @@ -205,12 +205,10 @@ namespace Kt { void Putchar(char c) { if (g_kernelLogDepth > 0) { - if (c == '\n') { - RingBufferAppend('\r'); - RingBufferAppend('\n'); - } else { - RingBufferAppend(c); - } + // Keep the log as canonical text. CRLF is only needed by the + // framebuffer terminal below; storing it in the ring makes file + // consumers treat one logical newline as two line breaks. + RingBufferAppend(c); if (g_suppressKernelLog) { return; diff --git a/template/sysroot/include/Api/Syscall.hpp b/template/sysroot/include/Api/Syscall.hpp index 36e946d..e481169 100644 --- a/template/sysroot/include/Api/Syscall.hpp +++ b/template/sysroot/include/Api/Syscall.hpp @@ -210,9 +210,6 @@ namespace montauk::abi { // Absolute path of the running executable (for argv[0]). static constexpr uint64_t SYS_GETEXECPATH = 151; // (index, flags) -> new front index; index=-1 queries support (1/0); flags bit0 = wait vsync - static constexpr uint64_t SYS_GETENVIRON = 171; - static constexpr uint64_t SYS_SETENVIRON = 172; - static constexpr uint64_t SYS_SPAWN_ENV = 173; // Path metadata (size, timestamps, mode). (const char* path, FileStat* out) -> 0, -1 on error/unsupported. static constexpr uint64_t SYS_STAT = 152; @@ -233,6 +230,14 @@ namespace montauk::abi { static constexpr uint64_t SYS_WIFI_RESULTS = 163; // (WifiNetwork*, maxCount) -> count, no radio work static constexpr uint64_t SYS_WIFI_CONNECT_ASYNC = 164; // (ssid, password) -> 0 accepted, <0 on error static constexpr uint64_t SYS_NETIFS = 165; // (NetIfInfo*, maxCount) -> count + static constexpr uint64_t SYS_GETCHAR_NB = 166; // () -> ascii, 0 if nothing pending; never blocks + static constexpr uint64_t SYS_UTIME = 167; + static constexpr uint64_t SYS_MMAP_ANON = 168; + static constexpr uint64_t SYS_MUNMAP = 169; + static constexpr uint64_t SYS_MPROTECT = 170; + static constexpr uint64_t SYS_GETENVIRON = 171; + static constexpr uint64_t SYS_SETENVIRON = 172; + static constexpr uint64_t SYS_SPAWN_ENV = 173; static constexpr uint64_t SYS_LOG_WRITE = 176; // (componentName, logMessage) -> 0 @@ -724,6 +729,8 @@ namespace montauk::abi { uint64_t codeSegment; uint64_t flags; uint64_t stackSegment; + uint64_t stackWords[8]; + uint8_t stackWordCount; uint8_t pfPresent : 1; uint8_t pfWrite : 1; uint8_t pfUser : 1; diff --git a/template/sysroot/include/gui/desktop.hpp b/template/sysroot/include/gui/desktop.hpp index 4cc7907..0783747 100644 --- a/template/sysroot/include/gui/desktop.hpp +++ b/template/sysroot/include/gui/desktop.hpp @@ -10,8 +10,10 @@ #include "gui/svg.hpp" #include "gui/window.hpp" #include "gui/widgets.hpp" +#include "gui/mtk/widgets.hpp" #include "gui/terminal.hpp" #include +#include namespace gui { @@ -172,8 +174,8 @@ struct DesktopState { int desktop_item_count; int desktop_item_capacity; - bool ctx_menu_open; - int ctx_menu_x, ctx_menu_y; + // Right-click menu on the desktop background (MTK context menu). + mtk::ContextMenuState ctx_menu; bool net_popup_open; montauk::abi::NetCfg cached_net_cfg; @@ -209,18 +211,11 @@ struct DesktopState { bool wifi_autoconnect_done; // saved-network join already tried bool wifi_joining; // a join we started is in flight bool wifi_dhcp_pending; // ask for a lease once the link is up + bool wifi_dhcp_waiting; // dhcp.elf running, no address yet char wifi_joining_ssid[WIFI_SSID_CAP]; char wifi_status[96]; // last result line in the popup uint64_t wifi_status_time; - // Passphrase prompt, shown when a selected network needs a key. - bool wifi_prompt_open; - char wifi_prompt_ssid[WIFI_SSID_CAP]; - char wifi_prompt_password[WIFI_PSK_CAP]; - int wifi_prompt_len; - bool wifi_prompt_reveal; - bool wifi_prompt_remember; - uint8_t wifi_prompt_security; bool vol_popup_open; Rect vol_icon_rect; @@ -240,6 +235,12 @@ struct DesktopState { uint64_t thermal_last_poll; Rect temp_icon_rect; + // Keyboard layouts. The desktop translates hardware scan codes before + // dispatching events, so embedded and external apps see the same layout. + montauk::keyboard::State keyboard; + Rect keyboard_layout_rect; + uint64_t keyboard_last_poll; + int screen_w, screen_h; uint32_t* background_cache; int background_cache_pitch; @@ -275,5 +276,6 @@ void desktop_draw_panel(DesktopState* ds); void desktop_draw_window(DesktopState* ds, int idx); void desktop_handle_mouse(DesktopState* ds); void desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key); +bool desktop_refresh_keyboard_layouts(DesktopState* ds, bool force); } // namespace gui diff --git a/template/sysroot/include/gui/mtk/widgets.hpp b/template/sysroot/include/gui/mtk/widgets.hpp index b3effcc..f4ca3b0 100644 --- a/template/sysroot/include/gui/mtk/widgets.hpp +++ b/template/sysroot/include/gui/mtk/widgets.hpp @@ -40,6 +40,8 @@ struct ContextMenuItem { const char* label; int id; bool enabled; + const char* shortcut; + bool separator_after; }; enum ContextMenuResult : int { @@ -210,8 +212,8 @@ inline void draw_context_menu(Canvas& c, Rect menu = context_menu_rect(state, item_count, c.w, c.h, menu_w, item_h); - c.fill_rect(menu.x + 2, menu.y + 2, menu.w, menu.h, - Color::from_rgb(0x80, 0x80, 0x80)); + c.fill_rounded_rect(menu.x + 2, menu.y + 2, menu.w, menu.h, 4, + Color::from_rgb(0x80, 0x80, 0x80)); c.fill_rounded_rect(menu.x, menu.y, menu.w, menu.h, 4, colors::MENU_BG); c.rect(menu.x, menu.y, menu.w, menu.h, theme.border); @@ -226,6 +228,19 @@ inline void draw_context_menu(Canvas& c, Color label_color = items[i].enabled ? theme.text : theme.text_muted; context_menu_draw_label(c, menu.x + 12, item.y + (item.h - fh) / 2, items[i].label, label_color, font, font_size); + if (items[i].shortcut && items[i].shortcut[0]) { + int shortcut_w = font && font->valid && font_size > 0 + ? font->measure_text(items[i].shortcut, font_size) + : text_width(items[i].shortcut); + context_menu_draw_label(c, menu.x + menu.w - 12 - shortcut_w, + item.y + (item.h - fh) / 2, + items[i].shortcut, theme.text_muted, + font, font_size); + } + if (items[i].separator_after) { + c.hline(menu.x + 10, item.y + item.h - 1, + menu.w - 20, theme.border); + } } } @@ -380,7 +395,7 @@ inline void text_input_delete_range(char* text, int* len, int start, int end) { inline bool text_input_char_allowed(char ch, TextInputCharFilter filter = nullptr, void* userdata = nullptr) { unsigned char u = (unsigned char)ch; - if (u < 0x20 || u >= 0x7F) return false; + if (u < 0x20 || u == 0x7F) return false; return filter == nullptr || filter(ch, userdata); } diff --git a/template/sysroot/include/gui/widgets.hpp b/template/sysroot/include/gui/widgets.hpp index 076846f..5aa203d 100644 --- a/template/sysroot/include/gui/widgets.hpp +++ b/template/sysroot/include/gui/widgets.hpp @@ -240,7 +240,8 @@ struct TextBox { cursor--; text[text_len] = '\0'; } - } else if (key.ascii >= 32 && key.ascii < 127) { + } else if ((unsigned char)key.ascii >= 32 + && (unsigned char)key.ascii != 127) { // Printable character if (text_len < 254) { for (int i = text_len; i > cursor; i--) { diff --git a/template/sysroot/include/libc/dirent.h b/template/sysroot/include/libc/dirent.h index 5e90839..0c2f08a 100644 --- a/template/sysroot/include/libc/dirent.h +++ b/template/sysroot/include/libc/dirent.h @@ -29,6 +29,11 @@ struct dirent *readdir(DIR *dirp); int closedir(DIR *dirp); void rewinddir(DIR *dirp); +int scandir(const char *dirp, struct dirent ***namelist, + int (*filter)(const struct dirent *), + int (*compar)(const struct dirent **, const struct dirent **)); +int alphasort(const struct dirent **a, const struct dirent **b); + #ifdef __cplusplus } #endif diff --git a/template/sysroot/include/libc/math.h b/template/sysroot/include/libc/math.h index 6f2b779..72f42a4 100644 --- a/template/sysroot/include/libc/math.h +++ b/template/sysroot/include/libc/math.h @@ -11,6 +11,24 @@ extern "C" { #define INFINITY __builtin_inff() #define NAN __builtin_nanf("") +/* + * X/Open math constants. Not in ISO C, but ported code assumes them freely + * (NetSurf's about:chart uses M_PI and M_PI_2). + */ +#define M_E 2.7182818284590452354 +#define M_LOG2E 1.4426950408889634074 +#define M_LOG10E 0.43429448190325182765 +#define M_LN2 0.69314718055994530942 +#define M_LN10 2.30258509299404568402 +#define M_PI 3.14159265358979323846 +#define M_PI_2 1.57079632679489661923 +#define M_PI_4 0.78539816339744830962 +#define M_1_PI 0.31830988618379067154 +#define M_2_PI 0.63661977236758134308 +#define M_2_SQRTPI 1.12837916709551257390 +#define M_SQRT2 1.41421356237309504880 +#define M_SQRT1_2 0.70710678118654752440 + /* C99 floating-point classification. */ #define FP_NAN 0 #define FP_INFINITE 1 diff --git a/template/sysroot/include/libc/montauk.h b/template/sysroot/include/libc/montauk.h index b9654b7..496ac6f 100644 --- a/template/sysroot/include/libc/montauk.h +++ b/template/sysroot/include/libc/montauk.h @@ -73,11 +73,6 @@ extern "C" { #define MTK_SYS_MOUSESTATE 47 #define MTK_SYS_SETMOUSEBOUNDS 48 #define MTK_SYS_SPAWN_REDIR 49 -#define MTK_SYS_GETENVIRON 171 -#define MTK_SYS_SETENVIRON 172 -#define MTK_SYS_SPAWN_ENV 173 -#define MTK_SYS_SETSESSION 174 -#define MTK_SYS_KILLSESSION 175 #define MTK_SYS_CHILDIO_READ 50 #define MTK_SYS_CHILDIO_WRITE 51 #define MTK_SYS_CHILDIO_WRITEKEY 52 @@ -194,6 +189,16 @@ extern "C" { #define MTK_SYS_WIFI_RESULTS 163 #define MTK_SYS_WIFI_CONNECT_ASYNC 164 #define MTK_SYS_NETIFS 165 +#define MTK_SYS_GETCHAR_NB 166 +#define MTK_SYS_UTIME 167 +#define MTK_SYS_MMAP_ANON 168 +#define MTK_SYS_MUNMAP 169 +#define MTK_SYS_MPROTECT 170 +#define MTK_SYS_GETENVIRON 171 +#define MTK_SYS_SETENVIRON 172 +#define MTK_SYS_SPAWN_ENV 173 +#define MTK_SYS_SETSESSION 174 +#define MTK_SYS_KILLSESSION 175 #define MTK_SYS_LOG_WRITE 176 /* @SYSCALLS-END */ diff --git a/template/sysroot/include/libc/stdio.h b/template/sysroot/include/libc/stdio.h index a3c9dfd..e11c199 100644 --- a/template/sysroot/include/libc/stdio.h +++ b/template/sysroot/include/libc/stdio.h @@ -95,6 +95,17 @@ int ungetc(int c, FILE *stream); char *fgets(char *s, int size, FILE *stream); int fputs(const char *s, FILE *stream); +/* MontaukOS extension: non-blocking readiness probe for stdin. + Returns 1 when a complete line is buffered, so a following fgets/fgetc on + stdin returns without blocking; 0 otherwise. Partially typed input is kept. + This is what a select()-style poll over stdin has to be built on -- stdio + descriptors are not waitable kernel objects. */ +int montauk_stdin_ready(void); + +/* Blocks until a complete line is buffered on stdin, then returns 1. Same + buffer as montauk_stdin_ready(); nothing is consumed either way. */ +int montauk_stdin_wait(void); + void perror(const char *s); FILE *tmpfile(void); char *tmpnam(char *s); diff --git a/template/sysroot/include/libc/string.h b/template/sysroot/include/libc/string.h index ba815d6..c570dab 100644 --- a/template/sysroot/include/libc/string.h +++ b/template/sysroot/include/libc/string.h @@ -25,6 +25,7 @@ char *strncpy(char *dest, const char *src, size_t n); char *strcat(char *dest, const char *src); char *strncat(char *dest, const char *src, size_t n); char *strdup(const char *s); +char *strndup(const char *s, size_t n); char *strchr(const char *s, int c); char *strrchr(const char *s, int c); char *strpbrk(const char *s, const char *accept); diff --git a/template/sysroot/include/libc/sys/mman.h b/template/sysroot/include/libc/sys/mman.h index b5aa231..9c589fe 100644 --- a/template/sysroot/include/libc/sys/mman.h +++ b/template/sysroot/include/libc/sys/mman.h @@ -10,10 +10,8 @@ extern "C" { #endif /* - * Anonymous-memory mmap over SYS_ALLOC. SYS_ALLOC returns - * page-aligned process memory, which is exactly what callers like - * GCC's page allocator need. File-backed mappings are not - * supported and fail with ENODEV. + * Demand-paged anonymous memory backed by the kernel's VMA subsystem. + * File-backed and shared mappings are not supported and fail cleanly. */ #define PROT_NONE 0 diff --git a/template/sysroot/include/libc/unistd.h b/template/sysroot/include/libc/unistd.h index 2217bbd..b779f90 100644 --- a/template/sysroot/include/libc/unistd.h +++ b/template/sysroot/include/libc/unistd.h @@ -23,6 +23,8 @@ int read(int fd, void *buf, size_t count); int write(int fd, const void *buf, size_t count); int close(int fd); long lseek(int fd, long offset, int whence); +ssize_t pread(int fd, void *buf, size_t count, off_t offset); +ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset); int chdir(const char *path); char *getcwd(char *buf, size_t size); int access(const char *path, int mode); @@ -59,6 +61,8 @@ long sysconf(int name); extern char **environ; unsigned int sleep(unsigned int seconds); +int usleep(unsigned long usec); +int ftruncate(int fd, long length); #ifdef __cplusplus } diff --git a/template/sysroot/include/montauk/config.h b/template/sysroot/include/montauk/config.h index eda5df4..3df51c2 100644 --- a/template/sysroot/include/montauk/config.h +++ b/template/sysroot/include/montauk/config.h @@ -1,7 +1,8 @@ /* * config.h * Config file manager for MontaukOS programs - * Loads, modifies, and saves TOML config files from 0:/config/ + * Loads, modifies, and saves TOML config files from 0:/config/ and + * 0:/users//config/, and reads OS data tables from 0:/os/data/ * Copyright (c) 2026 Daniel Hammer */ @@ -187,29 +188,13 @@ namespace config { // ---- File operations ---- // Ensure the config directory exists - inline void ensure_dir() { - montauk::fmkdir(CONFIG_DIR); - } - - // Build full path: "0:/config/.toml" - inline void build_path(char* out, int outSz, const char* name) { - int p = 0; - const char* dir = CONFIG_DIR; - while (*dir && p < outSz - 2) out[p++] = *dir++; - out[p++] = '/'; - while (*name && p < outSz - 6) out[p++] = *name++; - // Append ".toml" - const char* ext = ".toml"; - while (*ext && p < outSz - 1) out[p++] = *ext++; - out[p] = '\0'; - } - - // Load a config file by name (without extension). - // Returns an initialized Doc (empty if file doesn't exist). - inline toml::Doc load(const char* name) { - char path[128]; - build_path(path, sizeof(path), name); + // ---- Shared file I/O (absolute path in, Doc out) ---- + // Only the path builders below know where each class of file lives. These + // two do the actual work for system config, per-user config and OS data. + // Read and parse a TOML file at an absolute path. + // Returns an initialized Doc (empty if the file is missing or empty). + inline toml::Doc load_path(const char* path) { int handle = montauk::open(path); if (handle < 0) { toml::Doc doc; @@ -235,15 +220,9 @@ namespace config { return doc; } - // Save a Doc to disk as a TOML file. - // Creates the file if it doesn't exist. - // Returns 0 on success, negative on error. - inline int save(const char* name, toml::Doc* doc) { - ensure_dir(); - - char path[128]; - build_path(path, sizeof(path), name); - + // Serialize a Doc and write it to an absolute path. The caller creates the + // parent directory. Returns 0 on success, negative on error. + inline int save_path(const char* path, toml::Doc* doc) { char* text = serialize(doc); int textLen = montauk::slen(text); @@ -262,6 +241,42 @@ namespace config { return ret < 0 ? ret : 0; } + inline void ensure_dir() { + montauk::fmkdir(CONFIG_DIR); + } + + // Build full path: "0:/config/.toml" + inline void build_path(char* out, int outSz, const char* name) { + int p = 0; + const char* dir = CONFIG_DIR; + while (*dir && p < outSz - 2) out[p++] = *dir++; + out[p++] = '/'; + while (*name && p < outSz - 6) out[p++] = *name++; + // Append ".toml" + const char* ext = ".toml"; + while (*ext && p < outSz - 1) out[p++] = *ext++; + out[p] = '\0'; + } + + // Load a config file by name (without extension). + // Returns an initialized Doc (empty if file doesn't exist). + inline toml::Doc load(const char* name) { + char path[128]; + build_path(path, sizeof(path), name); + return load_path(path); + } + + // Save a Doc to disk as a TOML file. + // Creates the file if it doesn't exist. + // Returns 0 on success, negative on error. + inline int save(const char* name, toml::Doc* doc) { + ensure_dir(); + + char path[128]; + build_path(path, sizeof(path), name); + return save_path(path, doc); + } + // ---- Per-user config ---- // Build path: "0:/users//config/.toml" @@ -298,30 +313,7 @@ namespace config { inline toml::Doc load_user(const char* username, const char* name) { char path[192]; build_user_path(path, sizeof(path), username, name); - - int handle = montauk::open(path); - if (handle < 0) { - toml::Doc doc; - doc.init(); - return doc; - } - - uint64_t size = montauk::getsize(handle); - if (size == 0) { - montauk::close(handle); - toml::Doc doc; - doc.init(); - return doc; - } - - char* text = (char*)montauk::malloc(size + 1); - montauk::read(handle, (uint8_t*)text, 0, size); - montauk::close(handle); - text[size] = '\0'; - - toml::Doc doc = toml::parse(text); - montauk::mfree(text); - return doc; + return load_path(path); } // Save a per-user config file @@ -330,21 +322,7 @@ namespace config { char path[192]; build_user_path(path, sizeof(path), username, name); - - char* text = serialize(doc); - int textLen = montauk::slen(text); - - montauk::fdelete(path); - int handle = montauk::fcreate(path); - if (handle < 0) { - montauk::mfree(text); - return -1; - } - - int ret = montauk::fwrite(handle, (const uint8_t*)text, 0, textLen); - montauk::close(handle); - montauk::mfree(text); - return ret < 0 ? ret : 0; + return save_path(path, doc); } // Delete a config file. Returns 0 on success. @@ -426,4 +404,36 @@ namespace config { } } // namespace config + +// ---- Read-only OS data tables ---- +// Reference data that ships with the OS and is never written back: keyboard +// layouts, time zone tables and the like. This lives under 0:/os because it is +// OS payload, not configuration -- 0:/config is for state a user or admin +// edits. There is deliberately no save() here, so the read-only nature of the +// directory is enforced by the API rather than by convention. +namespace data { + + static constexpr const char* DATA_DIR = "0:/os/data"; + + // Build full path: "0:/os/data/.toml" + inline void build_path(char* out, int outSz, const char* name) { + int p = 0; + const char* dir = DATA_DIR; + while (*dir && p < outSz - 2) out[p++] = *dir++; + out[p++] = '/'; + while (*name && p < outSz - 6) out[p++] = *name++; + const char* ext = ".toml"; + while (*ext && p < outSz - 1) out[p++] = *ext++; + out[p] = '\0'; + } + + // Load an OS data table by name (without extension). + // Returns an initialized Doc (empty if the file is missing). + inline toml::Doc load(const char* name) { + char path[128]; + build_path(path, sizeof(path), name); + return config::load_path(path); + } + +} // namespace data } // namespace montauk diff --git a/template/sysroot/include/montauk/heap.h b/template/sysroot/include/montauk/heap.h index 5ed05ce..dd8cccb 100644 --- a/template/sysroot/include/montauk/heap.h +++ b/template/sysroot/include/montauk/heap.h @@ -1,290 +1,39 @@ /* * heap.h - * Userspace heap allocator for MontaukOS programs - * Copyright (c) 2025 Daniel Hammer + * Unified userspace heap API for MontaukOS programs + * Copyright (c) 2025-2026 Daniel Hammer */ #pragma once -#include -#include +#include +#include + +// The allocator lives in libc. Keeping these declarations here lets +// freestanding C++ programs use the Montauk API without pulling in all of +// , while ensuring C, C++, and libraries share one heap. +extern "C" { + void* malloc(std::size_t size); + void free(void* ptr); + void* realloc(void* ptr, std::size_t size); + void* calloc(std::size_t count, std::size_t size); +} namespace montauk { -namespace heap_detail { - - static constexpr uint64_t HEADER_MAGIC = 0x5A484541; // "ZHEA" - static constexpr uint64_t FREED_MAGIC = 0xDEADFEEE; - - struct Header { - uint64_t magic; - uint64_t size; // user-requested size - } __attribute__((packed)); - - struct FreeNode { - uint64_t size; // total size of this free block (including node) - FreeNode* next; - }; - - // Segregated free lists: power-of-2 size classes for blocks <= 4096 bytes. - // Blocks larger than 4096 go to the overflow list. - static constexpr int NUM_BUCKETS = 8; - static constexpr uint64_t BUCKET_SIZES[NUM_BUCKETS] = { - 32, 64, 128, 256, 512, 1024, 2048, 4096 - }; - - // Per-process heap state — must be `inline` (not `static`) so that all - // translation units in a multi-TU program share a single heap. - inline FreeNode* g_buckets[NUM_BUCKETS] = {}; - inline FreeNode g_overflow{0, nullptr}; - inline bool g_initialized = false; - - // Process-wide heap lock. Userspace threads share the heap, so the - // public malloc/mfree/realloc entry points must serialize access to - // g_buckets/g_overflow. Kept inline here (not in thread.h) because - // thread.h depends on heap.h, and the internal helpers below are not - // reentrant into the public API, so a plain spinlock suffices. - inline volatile uint32_t g_heap_lock = 0; - - static inline void heap_lock_acquire() { - while (__atomic_exchange_n(&g_heap_lock, 1, __ATOMIC_ACQUIRE) != 0) { - syscall0(montauk::abi::SYS_YIELD); - } - } - static inline void heap_lock_release() { - __atomic_store_n(&g_heap_lock, 0, __ATOMIC_RELEASE); - } - - static inline Header* get_header(void* block) { - return (Header*)((uint8_t*)block - sizeof(Header)); - } - - // Determine which bucket a block size belongs to, or -1 for overflow - static inline int bucket_index(uint64_t blockSize) { - if (blockSize <= 32) return 0; - if (blockSize <= 64) return 1; - if (blockSize <= 128) return 2; - if (blockSize <= 256) return 3; - if (blockSize <= 512) return 4; - if (blockSize <= 1024) return 5; - if (blockSize <= 2048) return 6; - if (blockSize <= 4096) return 7; - return -1; - } - - // Insert into overflow list (sorted by address, with adjacent-block coalescing) - static inline void insert_overflow(void* ptr, uint64_t size) { - auto* node = (FreeNode*)ptr; - node->size = size; - - FreeNode* prev = &g_overflow; - FreeNode* cur = g_overflow.next; - while (cur != nullptr && cur < node) { - prev = cur; - cur = cur->next; - } - - bool merged_prev = false; - if (prev != &g_overflow && - (uint8_t*)prev + prev->size == (uint8_t*)node) { - prev->size += size; - node = prev; - merged_prev = true; - } - - if (cur != nullptr && - (uint8_t*)node + node->size == (uint8_t*)cur) { - node->size += cur->size; - node->next = cur->next; - if (!merged_prev) prev->next = node; - } else if (!merged_prev) { - node->next = cur; - prev->next = node; - } - } - - // Take a block of at least `needed` bytes from the overflow list. - // Splits remainder back into overflow if worthwhile. - static inline void* take_from_overflow(uint64_t needed) { - FreeNode* prev = &g_overflow; - FreeNode* cur = g_overflow.next; - - while (cur != nullptr) { - if (cur->size >= needed) { - uint64_t blockSize = cur->size; - prev->next = cur->next; - - if (blockSize > needed + sizeof(FreeNode) + 16) { - insert_overflow((uint8_t*)cur + needed, blockSize - needed); - } - return (void*)cur; - } - prev = cur; - cur = cur->next; - } - return nullptr; - } - - // Next slab size for heap growth. The kernel tracks a finite number - // of SYS_ALLOC records per process (MaxHeapAllocs), so growing once - // per large allocation exhausts them under allocation-heavy loads - // (the native ld ran out mid-link). Doubling slabs keep the syscall - // count logarithmic in total heap size. - inline uint64_t g_grow_slab = 16 * 0x1000; - - static inline bool grow(uint64_t bytes) { - uint64_t want = (bytes + 0xFFF) & ~0xFFFULL; - if (want < 0x4000) want = 0x4000; - - uint64_t slab = (want > g_grow_slab) ? want : g_grow_slab; - if (g_grow_slab < 4 * 1024 * 1024) g_grow_slab *= 2; - - void* mem = montauk::alloc(slab); - if (mem == nullptr && slab > want) { - // Big slab refused (low memory): retry with the exact need. - slab = want; - mem = montauk::alloc(slab); - } - if (mem == nullptr) return false; - insert_overflow(mem, slab); - return true; - } - - // Refill a small-block bucket by carving a page-sized chunk from overflow - static inline bool refill_bucket(int idx) { - uint64_t bsize = BUCKET_SIZES[idx]; - uint64_t chunk = (bsize < 4096) ? 4096 : bsize; - - void* block = take_from_overflow(chunk); - if (block == nullptr) { - if (!grow(chunk)) return false; - block = take_from_overflow(chunk); - if (block == nullptr) return false; - } - - uint64_t count = chunk / bsize; - for (uint64_t i = 0; i < count; i++) { - auto* node = (FreeNode*)((uint8_t*)block + i * bsize); - node->size = bsize; - node->next = g_buckets[idx]; - g_buckets[idx] = node; - } - return true; - } - -} // namespace heap_detail - - // ---- Public API ---- inline void* malloc(uint64_t size) { - using namespace heap_detail; - - // Guard against overflow: size + Header must not wrap - if (size > UINT64_MAX - sizeof(Header) - 15) - return nullptr; - - heap_lock_acquire(); - - if (!g_initialized) { - grow(16 * 0x1000); // seed with 64 KiB - g_initialized = true; - } - - uint64_t needed = size + sizeof(Header); - needed = (needed + 15) & ~15ULL; - - int idx = bucket_index(needed); - - if (idx >= 0) { - // Small allocation — use segregated bucket (O(1)) - if (g_buckets[idx] == nullptr && !refill_bucket(idx)) { - heap_lock_release(); - return nullptr; - } - - FreeNode* node = g_buckets[idx]; - g_buckets[idx] = node->next; - - Header* header = (Header*)node; - header->magic = HEADER_MAGIC; - header->size = size; - heap_lock_release(); - return (void*)((uint8_t*)header + sizeof(Header)); - } - - // Large allocation — search overflow list - void* block = take_from_overflow(needed); - if (block == nullptr) { - if (!grow(needed)) { heap_lock_release(); return nullptr; } - block = take_from_overflow(needed); - if (block == nullptr) { heap_lock_release(); return nullptr; } - } - - Header* header = (Header*)block; - header->magic = HEADER_MAGIC; - header->size = size; - heap_lock_release(); - return (void*)((uint8_t*)header + sizeof(Header)); + return ::malloc((std::size_t)size); } inline void mfree(void* ptr) { - using namespace heap_detail; - - if (ptr == nullptr) return; - - Header* header = get_header(ptr); - - heap_lock_acquire(); - - if (header->magic == FREED_MAGIC) { heap_lock_release(); return; } // double-free - if (header->magic != HEADER_MAGIC) { heap_lock_release(); return; } // corrupt - header->magic = FREED_MAGIC; - - uint64_t blockSize = header->size + sizeof(Header); - blockSize = (blockSize + 15) & ~15ULL; - - int idx = bucket_index(blockSize); - - if (idx >= 0) { - // Small block — push onto bucket (O(1)) - auto* node = (FreeNode*)header; - node->size = BUCKET_SIZES[idx]; - node->next = g_buckets[idx]; - g_buckets[idx] = node; - } else { - // Large block — sorted insert with coalescing - insert_overflow((void*)header, blockSize); - } - heap_lock_release(); + ::free(ptr); } inline void* realloc(void* ptr, uint64_t size) { - if (ptr == nullptr) return malloc(size); + return ::realloc(ptr, (std::size_t)size); + } - // Read old size under the lock to avoid racing with another - // thread that might be freeing/recycling this header. - heap_detail::heap_lock_acquire(); - auto* header = heap_detail::get_header(ptr); - uint64_t old = header->size; - - uint64_t oldBlock = (old + sizeof(heap_detail::Header) + 15) & ~15ULL; - int idx = heap_detail::bucket_index(oldBlock); - if (idx >= 0) oldBlock = heap_detail::BUCKET_SIZES[idx]; - - uint64_t newNeed = (size + sizeof(heap_detail::Header) + 15) & ~15ULL; - if (newNeed <= oldBlock) { - header->size = size; - heap_detail::heap_lock_release(); - return ptr; - } - heap_detail::heap_lock_release(); - - void* newBlock = malloc(size); - if (newBlock == nullptr) return nullptr; - - uint64_t copySize = (old < size) ? old : size; - memcpy(newBlock, ptr, copySize); - - mfree(ptr); - return newBlock; + inline void* calloc(uint64_t count, uint64_t size) { + return ::calloc((std::size_t)count, (std::size_t)size); } } // namespace montauk diff --git a/template/sysroot/include/montauk/keyboard.h b/template/sysroot/include/montauk/keyboard.h new file mode 100644 index 0000000..74bb23a --- /dev/null +++ b/template/sysroot/include/montauk/keyboard.h @@ -0,0 +1,326 @@ +/* + * keyboard.h + * Keyboard layout registry, per-user selection, and scan-code translation + * Copyright (c) 2026 Daniel Hammer + */ + +#pragma once + +#include +#include + +namespace montauk::keyboard { + +inline constexpr int MAX_LAYOUTS = 8; +inline constexpr int MAX_KEYS = 64; + +// One overridden key. Characters are Windows-1252 bytes to match the +// single-byte GUI text stack; 0 means "no override, keep the kernel's value". +struct KeyMap { + uint8_t scancode; + uint8_t base; + uint8_t shift; + uint8_t altgr; +}; + +struct Layout { + char id[8]; + char name[48]; + char short_name[8]; + KeyMap keys[MAX_KEYS]; + int key_count; // 0 = passthrough (the kernel's US table) +}; + +struct Registry { + Layout items[MAX_LAYOUTS]; + int count; +}; + +struct State { + Registry registry; + bool enabled[MAX_LAYOUTS]; + int active; +}; + +inline void build_key(char* out, int cap, const char* prefix, + const char* id, const char* suffix = nullptr) { + int pos = 0; + const char* parts[3] = {prefix, id, suffix}; + for (int part = 0; part < 3; part++) { + const char* text = parts[part]; + if (!text) continue; + while (*text && pos < cap - 1) out[pos++] = *text++; + } + out[pos] = '\0'; +} + +// Read an array of byte values from the layout table. +// Returns the element count, or -1 if the key is missing or malformed. +inline int read_bytes(const toml::Doc& doc, const char* key, + uint8_t* out, int cap) { + toml::Value* arr = doc.get_array(key); + if (!arr) return -1; + if (arr->array.count > cap) return -1; + for (int i = 0; i < arr->array.count; i++) { + toml::Value* value = arr->array.items[i]; + if (!value || value->type != toml::Type::Int) return -1; + if (value->ival < 0 || value->ival > 0xFF) return -1; + out[i] = (uint8_t)value->ival; + } + return arr->array.count; +} + +// Parse one [layouts.] table. Returns false if the layout declares key +// overrides but they are inconsistent, in which case the caller skips it: a +// malformed table must never produce a half-applied layout. +inline bool load_layout(Layout* out, const char* id, const toml::Doc& doc) { + if (!out) return false; + *out = {}; + montauk::strncpy(out->id, id, sizeof(out->id)); + + char key[64]; + build_key(key, sizeof(key), "layouts.", id, ".name"); + montauk::strncpy(out->name, doc.get_string(key, id), sizeof(out->name)); + build_key(key, sizeof(key), "layouts.", id, ".short_name"); + montauk::strncpy(out->short_name, doc.get_string(key, id), + sizeof(out->short_name)); + + uint8_t scancodes[MAX_KEYS]; + uint8_t base[MAX_KEYS]; + uint8_t shift[MAX_KEYS]; + uint8_t altgr[MAX_KEYS] = {}; + + build_key(key, sizeof(key), "layouts.", id, ".scancodes"); + int count = read_bytes(doc, key, scancodes, MAX_KEYS); + if (count < 0) { + // No override table at all: a passthrough layout such as "en". + out->key_count = 0; + return true; + } + + build_key(key, sizeof(key), "layouts.", id, ".base"); + if (read_bytes(doc, key, base, MAX_KEYS) != count) return false; + build_key(key, sizeof(key), "layouts.", id, ".shift"); + if (read_bytes(doc, key, shift, MAX_KEYS) != count) return false; + build_key(key, sizeof(key), "layouts.", id, ".altgr"); + int altgr_count = read_bytes(doc, key, altgr, MAX_KEYS); + if (altgr_count >= 0 && altgr_count != count) return false; + + for (int i = 0; i < count; i++) { + out->keys[i].scancode = scancodes[i]; + out->keys[i].base = base[i]; + out->keys[i].shift = shift[i]; + out->keys[i].altgr = altgr[i]; + } + out->key_count = count; + return true; +} + +// The compiled-in base layout. The kernel's scancode table is already US +// English, so this overrides nothing; it exists so the registry is never +// empty and input keeps working even with no data file on disk. +inline void add_base_layout(Registry* registry) { + if (!registry || registry->count >= MAX_LAYOUTS) return; + Layout& layout = registry->items[registry->count++]; + layout = {}; + montauk::strcpy(layout.id, "en"); + montauk::strcpy(layout.name, "English (US)"); + montauk::strcpy(layout.short_name, "en"); + layout.key_count = 0; +} + +inline int find_layout(const Registry& registry, const char* id) { + for (int i = 0; i < registry.count; i++) + if (montauk::streq(registry.items[i].id, id)) return i; + return -1; +} + +inline Registry load_registry() { + Registry registry = {}; + toml::Doc doc = montauk::data::load("keyboard-layouts"); + + toml::Value* order = doc.get_array("registry.layouts"); + if (order) { + for (int i = 0; i < order->array.count; i++) { + toml::Value* value = order->array.items[i]; + if (!value || value->type != toml::Type::String || !value->str) + continue; + if (registry.count >= MAX_LAYOUTS) break; + if (find_layout(registry, value->str) >= 0) continue; + Layout candidate; + if (!load_layout(&candidate, value->str, doc)) continue; + registry.items[registry.count++] = candidate; + } + } + doc.destroy(); + + // Guarantee a working layout even if the data file is missing, malformed, + // or simply omits "en". + if (find_layout(registry, "en") < 0) { + if (registry.count >= MAX_LAYOUTS) registry.count = MAX_LAYOUTS - 1; + for (int i = registry.count; i > 0; i--) + registry.items[i] = registry.items[i - 1]; + registry.count++; + Registry base = {}; + add_base_layout(&base); + registry.items[0] = base.items[0]; + } + return registry; +} + +// Re-read only the user's selection, leaving the registry alone. The registry +// is read-only OS data that cannot change while the machine is running, so +// callers polling for layout changes should use this rather than load_user: +// it reads one small file instead of re-parsing the whole layout table. +inline void refresh_selection(State* state, const char* username) { + if (!state) return; + for (int i = 0; i < state->registry.count; i++) state->enabled[i] = false; + + toml::Doc doc = config::load_user(username, "keyboard"); + for (int i = 0; i < state->registry.count; i++) { + char key[48]; + build_key(key, sizeof(key), "layouts.", state->registry.items[i].id); + state->enabled[i] = doc.get_bool(key, i == 0); + } + const char* active_id = doc.get_string("selection.active", "en"); + state->active = find_layout(state->registry, active_id); + doc.destroy(); + + if (state->registry.count == 0) return; + if (state->active < 0 || !state->enabled[state->active]) { + state->active = 0; + while (state->active < state->registry.count + && !state->enabled[state->active]) + state->active++; + } + if (state->active >= state->registry.count) { + state->active = 0; + state->enabled[0] = true; + } +} + +inline State load_user(const char* username) { + State state = {}; + state.registry = load_registry(); + refresh_selection(&state, username); + return state; +} + +inline bool save_user(const char* username, const State& state) { + toml::Doc doc; + doc.init(); + for (int i = 0; i < state.registry.count; i++) { + char key[48]; + build_key(key, sizeof(key), "layouts.", state.registry.items[i].id); + config::set_bool(&doc, key, state.enabled[i]); + } + int active = state.active >= 0 && state.active < state.registry.count + ? state.active : 0; + config::set_string(&doc, "selection.active", state.registry.items[active].id); + int result = config::save_user(username, "keyboard", &doc); + doc.destroy(); + return result == 0; +} + +// NOTE: layouts are per-user by design. The login screen runs before there is +// a user, so it stays US-English until it grows its own layout switcher; there +// is deliberately no machine-wide "current layout" for it to read, because a +// wrong guess there is unrecoverable (you cannot type your password to fix it). + +inline int next_enabled(const State& state, int current) { + if (state.registry.count <= 0) return 0; + for (int step = 1; step <= state.registry.count; step++) { + int candidate = (current + step) % state.registry.count; + if (state.enabled[candidate]) return candidate; + } + return current; +} + +// Caps Lock is derived rather than declared per key: it applies only where +// base and shift are a Windows-1252 lower/upper letter pair. That covers the +// accented letters (aa 0xE5 / AA 0xC5) without wrongly upper-casing keys such +// as 2 / " where the shifted value is unrelated punctuation. +inline constexpr bool is_letter_pair(uint8_t base, uint8_t shift) { + if (base == 0 || shift == 0) return false; + if (base < 0x61) return false; + if (base > 0x7A && base < 0xE0) return false; + if (base == 0xF7) return false; // division sign sits inside the range + return shift == (uint8_t)(base - 0x20); +} + +static_assert(is_letter_pair(0xE5, 0xC5)); // aa / AA +static_assert(is_letter_pair(0xF8, 0xD8)); // oe / OE +static_assert(is_letter_pair(0xE6, 0xC6)); // ae / AE +static_assert(is_letter_pair('a', 'A')); +static_assert(!is_letter_pair('2', '"')); +static_assert(!is_letter_pair(0xF7, 0xD7)); // divide / multiply + +inline void translate(const State& state, abi::KeyEvent* key) { + if (!key) return; + if (state.active < 0 || state.active >= state.registry.count) return; + const Layout& layout = state.registry.items[state.active]; + + // Extended keys carry a main-block scancode with the E0 prefix stripped + // (keypad "/" arrives as 0x35, the same as the main "/"), so translating + // them would turn keypad "/" into whatever the layout puts on that key. + if (key->extended) return; + + uint8_t scancode = key->scancode & 0x7F; + for (int i = 0; i < layout.key_count; i++) { + const KeyMap& mapping = layout.keys[i]; + if (mapping.scancode != scancode) continue; + + uint8_t out; + if (key->altgr) { + out = mapping.altgr; + } else { + bool upper = key->shift; + if (is_letter_pair(mapping.base, mapping.shift)) + upper = key->shift != key->capslock; + out = upper ? mapping.shift : mapping.base; + } + if (out != 0) { + key->ascii = (char)out; + // An AltGr key that produced a character is text, not a shortcut. + // Apps gate insertion on !alt (and alt is LeftAlt||RightAlt), so + // leaving it set would silently swallow every AltGr character. + if (key->altgr) key->alt = false; + } + return; + } +} + +// ---- Direct keyboard readers ---- +// The desktop translates events before routing them to windows, but programs +// that read the kernel buffer themselves (terminal, login) never pass through +// it and would otherwise always get the US layout. They translate through this +// instead. The registry is parsed once; only the small per-user selection file +// is re-read, at most once a second, so switching layout in the panel reaches +// them shortly afterwards without re-parsing the whole layout table. + +struct DirectInput { + State state; + bool loaded; + uint64_t last_poll; +}; + +inline DirectInput& direct_input() { + static DirectInput input; // zero-initialised POD, so no guard variable + return input; +} + +inline void translate_direct(abi::KeyEvent* key, const char* username) { + DirectInput& input = direct_input(); + uint64_t now = montauk::get_milliseconds(); + if (!input.loaded) { + input.state = load_user(username); + input.loaded = true; + input.last_poll = now; + } else if (now - input.last_poll >= 1000) { + refresh_selection(&input.state, username); + input.last_poll = now; + } + translate(input.state, key); +} + +} // namespace montauk::keyboard diff --git a/template/sysroot/include/montauk/syscall.h b/template/sysroot/include/montauk/syscall.h index 203e050..9771295 100644 --- a/template/sysroot/include/montauk/syscall.h +++ b/template/sysroot/include/montauk/syscall.h @@ -197,6 +197,8 @@ namespace montauk { inline bool is_key_available() { return (bool)syscall0(montauk::abi::SYS_ISKEYAVAILABLE); } inline void getkey(montauk::abi::KeyEvent* out) { syscall1(montauk::abi::SYS_GETKEY, (uint64_t)out); } inline char getchar() { return (char)syscall0(montauk::abi::SYS_GETCHAR); } + // Returns 0 rather than blocking when no character is pending. + inline char getchar_nb() { return (char)syscall0(montauk::abi::SYS_GETCHAR_NB); } inline uint64_t input_wait(uint64_t observedSerial, uint64_t timeoutMs) { return (uint64_t)syscall2(montauk::abi::SYS_INPUT_WAIT, observedSerial, timeoutMs); } @@ -432,6 +434,10 @@ namespace montauk { return syscall2(montauk::abi::SYS_LOG, (uint64_t)buf, size); } + inline int64_t write_log(const char* userspaceComponent, const char* logMessage) { + return syscall2(montauk::abi::SYS_LOG_WRITE, (uint64_t)userspaceComponent, (uint64_t)logMessage); + } + // I/O redirection 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); diff --git a/template/sysroot/include/montauk/thread.h b/template/sysroot/include/montauk/thread.h index 57b3777..d45085a 100644 --- a/template/sysroot/include/montauk/thread.h +++ b/template/sysroot/include/montauk/thread.h @@ -44,8 +44,22 @@ namespace montauk { ThreadEntry user_entry; void* user_arg; void* stack_base; + int tid; + ThreadCtx* next; }; + inline ThreadCtx* g_thread_records = nullptr; + inline volatile uint32_t g_thread_records_lock = 0; + + inline void records_lock() { + while (__atomic_exchange_n(&g_thread_records_lock, 1, __ATOMIC_ACQUIRE) != 0) + montauk::yield(); + } + + inline void records_unlock() { + __atomic_store_n(&g_thread_records_lock, 0, __ATOMIC_RELEASE); + } + // Userspace trampoline: bridges from the raw entry the kernel jumps // to into the typed entry, then funnels into SYS_THREAD_EXIT. We // route the exit through libc rather than relying on a kernel-side @@ -53,21 +67,24 @@ namespace montauk { // memory on this path. // // The thread's stack itself is intentionally not freed here: we are - // still running on it. It is reclaimed when the process exits, or - // the joiner may free it explicitly after thread_join. + // still running on it. It is reclaimed by a successful thread_join, + // or as part of whole-process teardown if the thread is never joined. [[noreturn]] inline void thread_trampoline(detail::ThreadCtx* ctx) { + // A sibling CPU can start the thread before thread_spawn has + // returned its TID. Wait until the parent has published the record + // needed by thread_join to reclaim this stack. + while (__atomic_load_n(&ctx->tid, __ATOMIC_ACQUIRE) == 0) + montauk::yield(); int code = ctx->user_entry(ctx->user_arg); - montauk::mfree(ctx); thread_exit(code); } } // Spawn a new thread that begins executing `entry(arg)`. Returns the // new TID on success, or -1 on failure. The thread's stack is - // allocated from the user heap; it is leaked on thread exit (the - // thread itself cannot free the stack it is running on). The kernel - // reclaims it on process exit. Callers that need to spawn many short- - // lived threads should pool stacks themselves. + // allocated from the user heap. The exiting thread cannot free the stack + // it is running on, so thread_join reclaims both it and the trampoline + // context after the kernel has reaped the sibling. inline int thread_spawn(ThreadEntry entry, void* arg, uint64_t stack_bytes = 0) { if (entry == nullptr) return -1; @@ -84,6 +101,8 @@ namespace montauk { ctx->user_entry = entry; ctx->user_arg = arg; ctx->stack_base = stack; + ctx->tid = 0; + ctx->next = nullptr; uint64_t stack_top = ((uint64_t)stack + stack_bytes) & ~0xFULL; int tid = (int)syscall3(montauk::abi::SYS_THREAD_SPAWN, @@ -94,6 +113,11 @@ namespace montauk { montauk::mfree(stack); return -1; } + detail::records_lock(); + ctx->next = detail::g_thread_records; + detail::g_thread_records = ctx; + __atomic_store_n(&ctx->tid, tid, __ATOMIC_RELEASE); + detail::records_unlock(); return tid; } @@ -101,8 +125,22 @@ namespace montauk { // success (with the thread's exit code in *out_code if non-null) or // -1 if `tid` is not a joinable sibling. inline int thread_join(int tid, int* out_code = nullptr) { - return (int)syscall2(montauk::abi::SYS_THREAD_JOIN, - (uint64_t)tid, (uint64_t)out_code); + int result = (int)syscall2(montauk::abi::SYS_THREAD_JOIN, + (uint64_t)tid, (uint64_t)out_code); + if (result == 0) { + detail::records_lock(); + detail::ThreadCtx** link = &detail::g_thread_records; + while (*link != nullptr && (*link)->tid != tid) + link = &(*link)->next; + detail::ThreadCtx* ctx = *link; + if (ctx != nullptr) *link = ctx->next; + detail::records_unlock(); + if (ctx != nullptr) { + montauk::mfree(ctx->stack_base); + montauk::mfree(ctx); + } + } + return result; } // Return the calling thread's TID (== getpid() for the main thread). diff --git a/template/sysroot/include/montauk/wifi.h b/template/sysroot/include/montauk/wifi.h index f909857..69adc5a 100644 --- a/template/sysroot/include/montauk/wifi.h +++ b/template/sysroot/include/montauk/wifi.h @@ -16,6 +16,12 @@ * the kernel derives the PMK from it (or takes a 64-character hex string as * a raw PSK). Anyone who can read 0:/config can read the keys. * + * No default copy of this file ships in the image, deliberately. Every other + * config an app writes (bluetooth.toml, display.toml, session.toml) is + * created on demand for the same reason: a shipped copy is laid down again + * by anything that refreshes the system files, and it would overwrite the + * networks the user had saved. + * * Copyright (c) 2026 Daniel Hammer */ diff --git a/template/sysroot/lib/crt1.o b/template/sysroot/lib/crt1.o index ef5b27c..715568c 100644 Binary files a/template/sysroot/lib/crt1.o and b/template/sysroot/lib/crt1.o differ diff --git a/template/sysroot/lib/liblibc.a b/template/sysroot/lib/liblibc.a index a0e88d4..f69128e 100644 Binary files a/template/sysroot/lib/liblibc.a and b/template/sysroot/lib/liblibc.a differ