feat: POSIX libc surface, native binutils (as/ld/ar run on MontaukOS)

Grow the Montauk libc enough to cross-build binutils 2.43.1 with
--host=x86_64-montauk. gas, ld, ar, nm, objcopy, objdump, readelf,
ranlib, strip and friends now link as native ET_EXEC Montauk binaries
(staged stripped in toolchain/native/, not yet shipped in the image).

New libc surface: full Linux errno and signal sets, O_* flags with
real O_EXCL/O_APPEND semantics, unlink/rmdir/dup/dup2/getpid/_exit,
kill (SYS_KILL), fcntl, fileno/fdopen, putc/getchar/rewind,
ctime/asctime, strtoll/strtoull/atoll, bsearch, mkstemp/mktemp,
realpath (lexical, drive-prefix aware), mbstowcs/mblen, chmod/fchmod/
umask/utime no-ops (VFS has no modes or settable times), full struct
stat with fake inodes, sscanf field widths (bounded conversions),
SCN*/PRI* completions, wait/waitpid over SYS_WAITPID, and new headers
sys/wait.h, sys/param.h, utime.h, wchar.h, memory.h.

fork/exec/pipe are declared but fail with ENOSYS: binutils never
spawns, and real process plumbing is the posix_spawn milestone, which
needs kernel support (exit status reporting, fd redirection).

TCC's montauk_compat.h shims (unlink, chmod, execvp, realpath, fdopen,
strtoll, strtoull) are retired in favor of the libc versions.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-07-14 10:55:39 +02:00
co-authored by Claude Fable 5
parent 648f7111c0
commit bd9158fdcc
24 changed files with 875 additions and 136 deletions
+521 -4
View File
@@ -20,6 +20,7 @@
#include <time.h>
#include <errno.h>
#include <sys/stat.h>
#include <utime.h>
#include <sys/time.h>
#include <unistd.h>
@@ -110,6 +111,9 @@ static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) {
#define SYS_FRENAME 94
#define SYS_GETCWD 95
#define SYS_CHDIR 96
#define SYS_GETPID 3
#define SYS_DUPHANDLE 98
#define SYS_KILL 62
/* ========================================================================
errno
@@ -2035,8 +2039,12 @@ int sscanf(const char *str, const char *fmt, ...) {
while (*fmt) {
if (*fmt == '%') {
fmt++;
int width = 0;
while (isdigit((unsigned char)*fmt))
width = width * 10 + (*fmt++ - '0');
int is_long = 0;
if (*fmt == 'l') { is_long = 1; fmt++; }
if (*fmt == 'l') { fmt++; } /* ll behaves as l on LP64 */
if (*fmt == 'd' || *fmt == 'i') {
while (isspace((unsigned char)*s)) s++;
@@ -2045,7 +2053,8 @@ int sscanf(const char *str, const char *fmt, ...) {
else if (*s == '+') s++;
if (!isdigit((unsigned char)*s)) goto done;
long val = 0;
while (isdigit((unsigned char)*s))
int n = 0;
while (isdigit((unsigned char)*s) && (width == 0 || n++ < width))
val = val * 10 + (*s++ - '0');
if (neg) val = -val;
if (is_long) *va_arg(ap, long *) = val;
@@ -2055,7 +2064,8 @@ int sscanf(const char *str, const char *fmt, ...) {
while (isspace((unsigned char)*s)) s++;
if (!isdigit((unsigned char)*s)) goto done;
unsigned long val = 0;
while (isdigit((unsigned char)*s))
int n = 0;
while (isdigit((unsigned char)*s) && (width == 0 || n++ < width))
val = val * 10 + (unsigned long)(*s++ - '0');
if (is_long) *va_arg(ap, unsigned long *) = val;
else *va_arg(ap, unsigned int *) = (unsigned int)val;
@@ -2065,7 +2075,8 @@ int sscanf(const char *str, const char *fmt, ...) {
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2;
if (!isxdigit((unsigned char)*s)) goto done;
unsigned long val = 0;
while (isxdigit((unsigned char)*s)) {
int n = 0;
while (isxdigit((unsigned char)*s) && (width == 0 || n++ < width)) {
int d;
if (*s >= '0' && *s <= '9') d = *s - '0';
else if (*s >= 'a' && *s <= 'f') d = *s - 'a' + 10;
@@ -2080,7 +2091,9 @@ int sscanf(const char *str, const char *fmt, ...) {
while (isspace((unsigned char)*s)) s++;
char *dst = va_arg(ap, char *);
if (*s == '\0') goto done;
while (*s && !isspace((unsigned char)*s))
int n = 0;
while (*s && !isspace((unsigned char)*s)
&& (width == 0 || n++ < width))
*dst++ = *s++;
*dst = '\0';
matched++;
@@ -2152,6 +2165,10 @@ int open(const char *path, int flags, ...) {
if (wants_create) {
h = (int)_zos_syscall1(SYS_FCREATE, (long)path);
}
} else if (wants_create && (flags & O_EXCL)) {
_zos_syscall1(SYS_CLOSE, (long)h);
errno = EEXIST;
return -1;
} else if (wants_trunc) {
_zos_syscall1(SYS_CLOSE, (long)h);
_zos_syscall1(SYS_FDELETE, (long)path);
@@ -2160,6 +2177,9 @@ int open(const char *path, int flags, ...) {
if (h >= 0 && h < _FD_POS_MAX) {
_fd_pos[h] = 0;
if (flags & O_APPEND) {
_fd_pos[h] = (unsigned long)_zos_syscall1(SYS_GETSIZE, (long)h);
}
}
if (h < 0) {
errno = ENOENT;
@@ -2267,6 +2287,16 @@ int stat(const char *path, struct stat *buf) {
buf->st_mode = S_IFREG;
buf->st_size = (unsigned long)_zos_syscall1(SYS_GETSIZE, (long)h);
_zos_syscall1(SYS_CLOSE, (long)h);
/* No inodes in the VFS API: derive a stable id from the path
so that distinct paths compare as distinct files. */
unsigned long ino = 5381;
for (const char *p = path; *p; p++) {
ino = ino * 33 + (unsigned char)*p;
}
buf->st_ino = ino | 1;
buf->st_nlink = 1;
buf->st_blksize = 4096;
buf->st_blocks = (buf->st_size + 511) / 512;
return 0;
}
@@ -2288,6 +2318,12 @@ int fstat(int fd, struct stat *buf) {
memset(buf, 0, sizeof(*buf));
buf->st_mode = S_IFREG;
buf->st_size = (unsigned long)_zos_syscall1(SYS_GETSIZE, (long)fd);
/* Handles do not map back to paths, so fake a per-handle inode in a
range disjoint from the path hashes used by stat(). */
buf->st_ino = 0x4000000000000000UL + (unsigned long)fd;
buf->st_nlink = 1;
buf->st_blksize = 4096;
buf->st_blocks = (buf->st_size + 511) / 512;
return 0;
}
@@ -3110,3 +3146,484 @@ unsigned int sleep(unsigned int seconds) {
_zos_syscall1(SYS_SLEEP_MS, (long)seconds * 1000L);
return 0;
}
/* ========================================================================
POSIX additions for hosted programs (native binutils/gcc ports)
======================================================================== */
/* Byte-oriented C locale: every byte is one character, nothing is an
invalid sequence. */
void rewind(FILE *stream) {
if (stream == NULL || stream->is_std) {
return;
}
stream->pos = 0;
stream->eof = 0;
stream->error = 0;
stream->ungetc_buf = -1;
}
char *asctime(const struct tm *tm) {
static char buf[26];
if (tm == NULL) {
return NULL;
}
strftime(buf, sizeof(buf), "%a %b %d %H:%M:%S %Y\n", tm);
return buf;
}
char *ctime(const time_t *timep) {
if (timep == NULL) {
return NULL;
}
return asctime(localtime(timep));
}
int fchmod(int fd, mode_t mode) {
(void)fd; (void)mode;
return 0; /* no permission bits in the VFS */
}
int utime(const char *path, const struct utimbuf *times) {
(void)times;
if (path == NULL) {
errno = EINVAL;
return -1;
}
return 0; /* file times cannot be set through the VFS */
}
void *bsearch(const void *key, const void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *)) {
size_t lo = 0;
size_t hi = nmemb;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
const void *elem = (const char *)base + mid * size;
int c = compar(key, elem);
if (c == 0) {
return (void *)elem;
}
if (c < 0) {
hi = mid;
} else {
lo = mid + 1;
}
}
return NULL;
}
size_t mbstowcs(wchar_t *dst, const char *src, size_t n) {
if (src == NULL) {
errno = EINVAL;
return (size_t)-1;
}
if (dst == NULL) {
return strlen(src);
}
size_t i = 0;
for (; i < n && src[i]; i++) {
dst[i] = (wchar_t)(unsigned char)src[i];
}
if (i < n) {
dst[i] = 0;
}
return i;
}
int mblen(const char *s, size_t n) {
if (s == NULL) return 0;
if (n == 0) return -1;
return (*s != '\0') ? 1 : 0;
}
/* Absolute paths on Montauk start with a drive prefix ("0:/...""). A
path is made absolute against the cwd, then . and .. segments are
folded out. No symlinks exist, so this is purely lexical. */
char *realpath(const char *path, char *resolved) {
if (path == NULL) {
errno = EINVAL;
return NULL;
}
char abs[1024];
int has_drive = 0;
for (const char *p = path; *p; p++) {
if (*p == ':') { has_drive = 1; break; }
if (*p == '/') break;
}
if (has_drive) {
strncpy(abs, path, sizeof(abs) - 1);
abs[sizeof(abs) - 1] = '\0';
} else {
if (getcwd(abs, sizeof(abs)) == NULL) {
errno = ENOENT;
return NULL;
}
size_t cl = strlen(abs);
if (cl > 0 && abs[cl - 1] != '/' && cl + 1 < sizeof(abs)) {
abs[cl++] = '/';
abs[cl] = '\0';
}
strncpy(abs + cl, path, sizeof(abs) - cl - 1);
abs[sizeof(abs) - 1] = '\0';
}
/* Fold . and .. segments in place, keeping the drive prefix. */
char *body = strchr(abs, ':');
body = body ? body + 1 : abs; /* points at "/..." */
char out[1024];
size_t o = 0;
const char *p = body;
while (*p) {
while (*p == '/') p++;
if (*p == '\0') break;
const char *seg = p;
while (*p && *p != '/') p++;
size_t seglen = (size_t)(p - seg);
if (seglen == 1 && seg[0] == '.') {
continue;
}
if (seglen == 2 && seg[0] == '.' && seg[1] == '.') {
while (o > 0 && out[--o] != '/') { }
continue;
}
if (o + seglen + 2 >= sizeof(out)) {
errno = ENAMETOOLONG;
return NULL;
}
out[o++] = '/';
memcpy(out + o, seg, seglen);
o += seglen;
}
if (o == 0) {
out[o++] = '/';
}
out[o] = '\0';
size_t prefix = (size_t)(body - abs);
size_t total = prefix + o + 1;
if (resolved == NULL) {
resolved = (char *)malloc(total);
if (resolved == NULL) {
errno = ENOMEM;
return NULL;
}
}
memcpy(resolved, abs, prefix);
memcpy(resolved + prefix, out, o + 1);
return resolved;
}
int putc(int c, FILE *stream) {
return fputc(c, stream);
}
int getchar(void) {
return fgetc(stdin);
}
int unlink(const char *path) {
if (path == NULL) {
errno = EINVAL;
return -1;
}
if ((int)_zos_syscall1(SYS_FDELETE, (long)path) < 0) {
errno = ENOENT;
return -1;
}
return 0;
}
int rmdir(const char *path) {
return unlink(path);
}
/* Handle validity is probed via SYS_DUPHANDLE since the kernel has no
direct query. Flag state is not tracked per handle; F_GETFL reports
O_RDWR and the set commands succeed as no-ops. */
int fcntl(int fd, int cmd, ...) {
switch (cmd) {
case F_GETFD:
case F_GETFL: {
int probe = (int)_zos_syscall1(SYS_DUPHANDLE, (long)fd);
if (probe < 0) {
errno = EBADF;
return -1;
}
_zos_syscall1(SYS_CLOSE, (long)probe);
return (cmd == F_GETFL) ? O_RDWR : 0;
}
case F_SETFD:
case F_SETFL:
return 0;
case F_DUPFD:
return dup(fd);
default:
errno = EINVAL;
return -1;
}
}
pid_t getpid(void) {
return (pid_t)_zos_syscall0(SYS_GETPID);
}
/* True dup2 needs kernel support for duplicating into a chosen slot.
Best effort: freeing newfd first makes the kernel's lowest-free-slot
allocation usually land there. The oldfd == newfd form (the common
validity probe) is handled exactly. */
int dup2(int oldfd, int newfd) {
if (oldfd < 0 || newfd < 0) {
errno = EBADF;
return -1;
}
if (oldfd == newfd) {
int probe = (int)_zos_syscall1(SYS_DUPHANDLE, (long)oldfd);
if (probe < 0) {
errno = EBADF;
return -1;
}
_zos_syscall1(SYS_CLOSE, (long)probe);
return newfd;
}
_zos_syscall1(SYS_CLOSE, (long)newfd);
int h = (int)_zos_syscall1(SYS_DUPHANDLE, (long)oldfd);
if (h < 0) {
errno = EBADF;
return -1;
}
if (h != newfd) {
_zos_syscall1(SYS_CLOSE, (long)h);
errno = EBUSY;
return -1;
}
if (h < _FD_POS_MAX) {
_fd_pos[h] = (oldfd < _FD_POS_MAX) ? _fd_pos[oldfd] : 0;
}
return newfd;
}
/* SYS_WAITPID blocks until the child exits but does not report its
exit code yet, so successful waits decode as exit status 0. */
pid_t waitpid(pid_t pid, int *status, int options) {
(void)options;
if (pid <= 0) {
errno = ECHILD; /* no process groups / wait-any support */
return -1;
}
_zos_syscall1(SYS_WAITPID, (long)pid);
if (status != NULL) {
*status = 0;
}
return pid;
}
void _exit(int status) {
_zos_syscall1(SYS_EXIT, (long)status);
__builtin_unreachable();
}
/* SYS_KILL terminates the target outright; the signal number is
accepted for API shape but not delivered as a handler. */
int kill(pid_t pid, int sig) {
(void)sig;
if ((int)_zos_syscall1(SYS_KILL, (long)pid) < 0) {
errno = ESRCH;
return -1;
}
return 0;
}
/* The libc environment lives in name/value slots (getenv/setenv), not
NAME=VALUE strings, and exec* is unimplemented, so nothing can
consume a populated environ yet. An empty, valid vector satisfies
POSIX-shaped code until the posix_spawn work builds the real one. */
static char *_environ_empty[] = { NULL };
char **environ = _environ_empty;
/* No fork/exec process model on Montauk (spawn-style only, see
SYS_SPAWN). These exist so POSIX code links; they fail cleanly at
runtime. A posix_spawn implementation is the planned real path. */
pid_t fork(void) {
errno = ENOSYS;
return -1;
}
int execv(const char *path, char *const argv[]) {
(void)path; (void)argv;
errno = ENOSYS;
return -1;
}
int execve(const char *path, char *const argv[], char *const envp[]) {
(void)path; (void)argv; (void)envp;
errno = ENOSYS;
return -1;
}
int execvp(const char *file, char *const argv[]) {
(void)file; (void)argv;
errno = ENOSYS;
return -1;
}
/* Byte pipes exist in the kernel (SYS_STREAM_CREATE), but the libc
read()/write() paths are positional file calls; wiring stream
handles into the fd space is future work alongside posix_spawn. */
int pipe(int fds[2]) {
(void)fds;
errno = ENOSYS;
return -1;
}
pid_t wait(int *status) {
(void)status;
errno = ECHILD; /* wait-any needs kernel support */
return -1;
}
int dup(int fd) {
int h = (int)_zos_syscall1(SYS_DUPHANDLE, (long)fd);
if (h < 0) {
errno = EBADF;
return -1;
}
if (h < _FD_POS_MAX) {
_fd_pos[h] = (fd >= 0 && fd < _FD_POS_MAX) ? _fd_pos[fd] : 0;
}
return h;
}
int fileno(FILE *stream) {
if (stream == NULL) {
errno = EBADF;
return -1;
}
if (stream->is_std) {
return stream->is_std - 1; /* stdin 0, stdout 1, stderr 2 */
}
return stream->handle;
}
FILE *fdopen(int fd, const char *mode) {
if (fd < 0 || mode == NULL) {
errno = EBADF;
return NULL;
}
FILE *f = (FILE *)malloc(sizeof(FILE));
if (f == NULL) {
errno = ENOMEM;
return NULL;
}
f->handle = fd;
f->pos = (fd < _FD_POS_MAX) ? _fd_pos[fd] : 0;
f->size = (unsigned long)_zos_syscall1(SYS_GETSIZE, (long)fd);
f->eof = 0;
f->error = 0;
f->is_std = 0;
f->ungetc_buf = -1;
if (mode[0] == 'a') {
f->pos = f->size;
}
return f;
}
/* long is 64-bit on x86_64-montauk, so the ll variants are aliases. */
long long strtoll(const char *nptr, char **endptr, int base) {
return (long long)strtol(nptr, endptr, base);
}
unsigned long long strtoull(const char *nptr, char **endptr, int base) {
return (unsigned long long)strtoul(nptr, endptr, base);
}
long long atoll(const char *nptr) {
return (long long)atol(nptr);
}
/* The Montauk VFS does not store permission bits; accept and ignore. */
int chmod(const char *path, mode_t mode) {
(void)mode;
if (path == NULL) {
errno = EINVAL;
return -1;
}
return 0;
}
static mode_t _umask_cur = 022;
mode_t umask(mode_t mask) {
mode_t old = _umask_cur;
_umask_cur = mask & 0777;
return old;
}
int lstat(const char *path, struct stat *buf) {
return stat(path, buf); /* no symlinks in the Montauk VFS */
}
char *mktemp(char *template_) {
if (template_ == NULL) {
errno = EINVAL;
return NULL;
}
size_t len = strlen(template_);
if (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0) {
errno = EINVAL;
if (len > 0) template_[0] = '\0';
return template_;
}
static const char cs[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
unsigned long seed = (unsigned long)_zos_syscall0(SYS_GETMILLISECONDS)
^ ((unsigned long)getpid() << 16);
for (int attempt = 0; attempt < 128; attempt++) {
unsigned long v = seed + (unsigned long)attempt * 7919UL;
for (int i = 0; i < 6; i++) {
template_[len - 6 + i] = cs[v % (sizeof(cs) - 1)];
v /= sizeof(cs) - 1;
}
int h = (int)_zos_syscall1(SYS_OPEN, (long)template_);
if (h < 0) {
return template_; /* name is free */
}
_zos_syscall1(SYS_CLOSE, (long)h);
}
template_[0] = '\0';
errno = EEXIST;
return template_;
}
int mkstemp(char *template_) {
if (template_ == NULL) {
errno = EINVAL;
return -1;
}
size_t len = strlen(template_);
if (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0) {
errno = EINVAL;
return -1;
}
static const char cs[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
unsigned long seed = (unsigned long)_zos_syscall0(SYS_GETMILLISECONDS)
^ ((unsigned long)getpid() << 16);
for (int attempt = 0; attempt < 128; attempt++) {
unsigned long v = seed + (unsigned long)attempt * 7919UL;
for (int i = 0; i < 6; i++) {
template_[len - 6 + i] = cs[v % (sizeof(cs) - 1)];
v /= sizeof(cs) - 1;
}
int fd = open(template_, O_RDWR | O_CREAT | O_EXCL);
if (fd >= 0) {
return fd;
}
if (errno != EEXIST) {
return -1;
}
}
errno = EEXIST;
return -1;
}
Binary file not shown.