Path: blob/main/core/kernel/src/wasm/posix/unistd.ts
1068 views
import { notImplemented } from "./util";1import constants from "./constants";2import debug from "debug";3import { constants as wasi_constants } from "wasi-js";45const log = debug("posix:unistd");67export default function unistd(context) {8const { fs, os, process, recv, send, wasi, posix, memory, callWithString } =9context;10// TODO: this doesn't throw an error yet if the target filesystem isn't native.11function toNativeFd(fd: number): number {12// OBVIOUSLY -- these functions won't work if the target13// is in a wasi memfs, since the host posix libc knows nothing about that.14// We do translate file descriptors at least.15// Do we need to check and throw an error if target path isn't native?16// Of course, that will happen anyways since the syscall will immediately17// reject the invalid file descriptor anyways.18const x = wasi.FD_MAP.get(fd);19if (x == null) {20throw Error("invalid file descriptor");21}22return x.real;23}2425// We use the rights from stdin and stdout when making26// a pipe. These can get closed after startup (e.g., in27// the test_subprocess.py cpython tests), so we have to28// make a copy here. This also avoids having to keep a data29// structure in sync with wasi-js.30const STDIN = wasi.FD_MAP.get(0);31const STDOUT = wasi.FD_MAP.get(1);3233const unistd = {34chown: (pathPtr: number, uid: number, gid: number): -1 | 0 => {35const path = recv.string(pathPtr);36fs.chownSync(path, uid, gid);37return 0;38},39lchown: (pathPtr: number, uid: number, gid: number): -1 | 0 => {40const path = recv.string(pathPtr);41fs.lchownSync(path, uid, gid);42return 0;43},4445// int fchown(int fd, uid_t owner, gid_t group);46_fchown: (fd: number, uid: number, gid: number): number => {47if (uid == 0 || gid == 0) {48// if either is 0 = root, we just do nothing.49// TODO: We really need to get rid of anything that involves uid/gid, which50// just doesn't make sense for the model of WASM.51return 0;52}53fs.fchownSync(toNativeFd(fd), uid, gid);54return 0;55},5657getuid: () => process.getuid?.() ?? 0,58getgid: () => process.getgid?.() ?? 0,59_geteuid: () => process.geteuid?.() ?? 0,60getegid: () => process.getegid?.() ?? 0,6162// int getgroups(int gidsetsize, gid_t grouplist[]);63// in WASI, "typedef unsigned gid_t"64getgroups: (gidsetsize, grouplistPtr): number => {65const groups = process.getgroups?.();66if (groups == null) {67return 0; // no groups68}69if (gidsetsize == 0) {70// yep, we end up computing getgroups twice, since the71// posix api is a bit awkward...72return groups.length;73}74const count = Math.min(groups.length, gidsetsize);75if (count == 0) {76return 0;77}78const view = new DataView(memory.buffer);79for (let i = 0; i < count; i++) {80view.setUint32(grouplistPtr + 4 * i, groups[i], true);81}82return count;83},8485getpid: () => process.pid ?? 1,8687getpgid: (pid: number): number => {88return posix.getpgid?.(pid) ?? 1;89},9091// int setpgid(pid_t pid, pid_t pgid);92setpgid: (pid: number, pgid: number): number => {93if (posix.setpgid == null) {94notImplemented("setpgid");95}96posix.setpgid(pid, pgid);97return 0; // success98},99100getpgrp: (): number => {101return posix.getpgrp?.() ?? 1;102},103104nice: (incr: number) => {105const p = os.getPriority?.();106if (p != null) {107os.setPriority?.(p + incr);108}109},110// int getpriority(int which, id_t who);111getpriority: (which: number, who: number): number => {112if (os.getPriority == null) {113// environ with no info about processes (e.g., browser).114return 0;115}116if (which != 0) {117console.warn(118"getpriority can only be implemented in node.js for *process id*"119);120return 0; // minimal info.121}122return os.getPriority?.(who);123},124125// int setpriority(int which, id_t who, int value);126setpriority: (which: number, who: number, value: number): number => {127if (os.setPriority == null) {128// environ with no info about processes (e.g., browser).129return 0;130}131if (which != 0) {132console.warn(133"setpriority can only be implemented in node.js for *process id*"134);135return -1;136}137return os.setPriority?.(who, value);138},139140// int dup(int oldfd);141dup: (oldfd: number): number => {142if (posix.dup == null) {143notImplemented("dup");144}145// Considered in 2022, but closed by node developers: https://github.com/libuv/libuv/issues/3448#issuecomment-1174786218146147const x = wasi.FD_MAP.get(oldfd);148const newfd_real = posix.dup(x.real);149const newfd = wasi.getUnusedFileDescriptor();150wasi.FD_MAP.set(newfd, { ...x, real: newfd_real });151return newfd;152},153154// int dup2(int oldfd, int newfd);155dup2: (oldfd: number, newfd: number): number => {156if (posix.dup2 == null) {157notImplemented("dup2");158}159const x_old = wasi.FD_MAP.get(oldfd);160let x_new;161// I'm not 100% happy with this.162if (wasi.FD_MAP.has(newfd)) {163x_new = wasi.FD_MAP.get(newfd).real ?? newfd;164} else {165x_new = newfd;166}167168const newfd_real = posix.dup2(x_old.real, x_new);169wasi.FD_MAP.set(newfd, { ...x_old, real: newfd_real });170return newfd;171},172173sync: () => {174// nodejs doesn't expose sync, but it does expose fsync for a file descriptor, so we call it on175// all the open file descriptors176if (fs.fsyncSync == null) return;177for (const [_, { real }] of wasi.FD_MAP) {178fs.fsyncSync(real);179}180},181182// In nodejs these set*id function can't be done in a worker thread:183// https://nodejs.org/api/process.html#processsetgidid184// TODO: maybe we should implement these by sending a message to185// the main thread requesting to do them? For now, you'll get186// an error unless you run in a mode without a worker thread.187setuid: () => {188throw Error("setuid is not supported");189},190seteuid: (uid: number) => {191if (posix.seteuid == null) {192notImplemented("seteuid");193}194posix.seteuid(uid);195return 0;196},197setegid: (gid: number) => {198if (posix.setegid == null) {199notImplemented("setegid");200}201posix.setegid(gid);202return 0;203},204setgid: (gid: number) => {205if (process.setgid == null) {206notImplemented("setgid");207}208process.setgid(gid);209return 0;210},211setsid: (sid) => {212if (posix.setsid == null) {213notImplemented("setsid");214}215return posix.setsid(sid);216},217// TODO!218getsid: () => {219notImplemented("getsid");220},221222setreuid: (uid) => {223if (posix.setreuid == null) {224notImplemented("setreuid");225}226posix.setreuid(uid);227return 0;228},229setregid: (gid) => {230if (posix.setregid == null) {231notImplemented("setregid");232}233posix.setregid(gid);234return 0;235},236getppid: () => {237if (posix.getppid == null) {238// in browser -- only one process id:239return unistd.getpid();240}241return posix.getppid();242},243setgroups: () => {244notImplemented("setgroups");245},246247setpgrp: () => {248notImplemented("setpgrp");249},250251tcgetpgrp: () => {252notImplemented("tcgetpgrp");253},254255tcsetpgrp: () => {256notImplemented("tcsetpgrp");257},258259fork: () => {260if (posix.fork == null) {261notImplemented("fork");262}263const pid = posix.fork();264if (pid == 0) {265// we end the event loop in the child, because hopefully usually anything266// that is using fork is about to exec* anyways. It seems that trying267// to actually use the Node.js event loop after forking tends to randomly268// hang, so isn't really viable.269posix.close_event_loop?.();270}271return pid;272},273274fork1: () => {275notImplemented("fork1");276},277278vfork: () => {279// "this system call behaves identically to the fork(2) system call, except without280// calling any handlers registered with pthread_atfork(2)."281return unistd.fork();282},283284forkpty: () => {285notImplemented("forkpty");286},287288getlogin: (): number => {289if (context.state.getlogin_ptr != null) return context.state.getlogin_ptr;290// returns the username of the signed in user; if not available, e.g.,291// in a browser, returns "user".292const username = os.userInfo?.()?.username ?? "user";293return (context.state.getlogin_ptr = send.string(username));294},295296// int gethostname(char *name, size_t len);297gethostname: (namePtr: number, len: number): number => {298if (os.hostname == null) {299throw Error("gethostname not supported on this platform");300}301const name = os.hostname();302send.string(name, { ptr: namePtr, len });303return 0;304},305306// int sethostname(const char *name, size_t len);307sethostname: (namePtr: number, len: number): number => {308if (posix.sethostname == null) {309throw Error("sethostname not supported on this platform");310}311const name = recv.string(namePtr, len);312posix.sethostname(name);313return 0;314},315316// char *ttyname(int fd);317ttyname: (fd: number): number => {318if (posix.ttyname == null) {319throw Error("ttyname_r is not supported on this platform");320}321if (context.state.ttyname_ptr != null) return context.state.ttyname_ptr;322const len = 128;323context.state.ttyname_ptr = send.malloc(len);324send.string(posix.ttyname(fd), { ptr: context.state.ttyname_ptr, len });325return context.state.ttyname_ptr;326},327// int ttyname_r(int fd, char *buf, size_t buflen);328ttyname_r: (fd: number, ptr: number, len: number): number => {329if (posix.ttyname == null) {330throw Error("ttyname_r is not supported on this platform");331}332send.string(posix.ttyname(fd), { ptr, len });333return 0;334},335336alarm: (seconds: number): number => {337if (posix.alarm == null) {338throw Error("alarm is not supported on this platform");339}340return posix.alarm(seconds);341},342343// The following 4 are actually only available on a Linux host,344// though wasi-musl defines them,345// so cpython-wasm thinks they exist.346// For CoWasm, let's just make these no-ops when not available,347// since they are about multiple users, which we shouldn't348// support in WASM.349getresuid: (ruidPtr: number, euidPtr: number, suidPtr: number): number => {350let ruid, euid, suid;351if (posix.getresuid == null) {352ruid = euid = suid = 0;353} else {354({ ruid, euid, suid } = posix.getresuid());355}356const view = new DataView(memory.buffer);357view.setUint32(ruidPtr, ruid, true);358view.setUint32(euidPtr, euid, true);359view.setUint32(suidPtr, suid, true);360return 0;361},362363getresgid: (rgidPtr: number, egidPtr: number, sgidPtr: number): number => {364let rgid, egid, sgid;365if (posix.getresgid == null) {366rgid = egid = sgid = 0;367} else {368({ rgid, egid, sgid } = posix.getresgid());369}370const view = new DataView(memory.buffer);371view.setUint32(rgidPtr, rgid, true);372view.setUint32(egidPtr, egid, true);373view.setUint32(sgidPtr, sgid, true);374return 0;375},376377setresuid: (ruid: number, euid: number, suid: number): number => {378if (posix.setresuid != null) {379posix.setresuid(ruid, euid, suid);380}381return 0;382},383384setresgid: (rgid: number, egid: number, sgid: number): number => {385if (posix.setresgid != null) {386posix.setresgid(rgid, egid, sgid);387}388return 0;389},390391// int execve(const char *pathname, char *const argv[], char *const envp[]);392execve: (pathnamePtr: number, argvPtr: number, envpPtr: number): number => {393if (posix._execve == null) {394notImplemented("execve");395}396const pathname = recv.string(pathnamePtr);397const argv = recv.arrayOfStrings(argvPtr);398const envp = recv.arrayOfStrings(envpPtr);399log("execve", pathname, argv, envp);400posix._execve(pathname, argv, envp);401return 0; // this won't happen because execve takes over, or there's an error402},403404execv: (pathnamePtr: number, argvPtr: number): number => {405if (posix.execv == null) {406notImplemented("execv");407}408const pathname = recv.string(pathnamePtr);409const argv = recv.arrayOfStrings(argvPtr);410log("execv", pathname, argv);411posix.execv(pathname, argv);412return 0; // this won't happen because execv takes over413},414415// execvp is like execv but takes the filename rather than the path.416// int execvp(const char *file, char *const argv[]);417execvp: (filePtr: number, argvPtr: number): number => {418if (posix.execvp == null) {419notImplemented("execvp");420}421const file = recv.string(filePtr);422const argv = recv.arrayOfStrings(argvPtr);423log("execvp", file, argv);424posix.execvp(file, argv);425return 0; // this won't happen because execvp takes over426},427428// execlp is so far only by libedit to launch vim to edit429// the history. So it's safe to just disable. Python doesn't430// use this at all.431execlp: () => {432notImplemented("execlp");433},434435/*436I don't have automated testing for this, since it quits node.437However, here is what works on Linux. There is no fexecve on macos.438>>> import os; a = os.open("/bin/ls",os.O_RDONLY | os.O_CREAT)439>>> os.execve(a,['-l','/'],{})440bin dev home media opt root sbin sys usr441boot etc lib mnt proc run srv tmp var442*/443fexecve: (fd: number, argvPtr: number, envpPtr: number): number => {444if (posix._fexecve == null) {445notImplemented("fexecve");446}447const argv = recv.arrayOfStrings(argvPtr);448const envp = recv.arrayOfStrings(envpPtr);449450posix._fexecve(toNativeFd(fd), argv, envp);451return 0; // this won't happen because execve takes over452},453454// int pipe(int pipefd[2]);455pipe: (pipefdPtr: number): number => {456if (posix.pipe == null) {457notImplemented("pipe");458}459const { readfd, writefd } = posix.pipe();460// readfd and writefd are genuine native file descriptors that we just created.461const wasi_readfd = wasi.getUnusedFileDescriptor();462wasi.FD_MAP.set(wasi_readfd, {463real: readfd,464rights: STDIN.rights, // just use rights for stdin465filetype: wasi_constants.WASI_FILETYPE_SOCKET_STREAM,466});467const wasi_writefd = wasi.getUnusedFileDescriptor();468wasi.FD_MAP.set(wasi_writefd, {469real: writefd,470rights: STDOUT.rights, // just use rights for stdout471filetype: wasi_constants.WASI_FILETYPE_SOCKET_STREAM,472});473474send.i32(pipefdPtr, wasi_readfd);475send.i32(pipefdPtr + 4, wasi_writefd);476return 0;477},478479pipe2: (pipefdPtr: number, flags: number): number => {480if (posix.pipe2 == null) {481notImplemented("pipe2");482}483let nativeFlags = 0;484if (flags & constants.O_NONBLOCK) {485nativeFlags += posix.constants?.O_NONBLOCK ?? 0;486}487// NOTE: wasi defined O_CLOEXEC to be 0, which is super annoying.488// We thus never set it, since otherwise it would always get set.489/* if (flags & constants.O_CLOEXEC) {490nativeFlags += posix.constants?.O_CLOEXEC ?? 0;491}*/492const { readfd, writefd } = posix.pipe2(nativeFlags);493console.warn(494"pipe2 -- TODO: we almost certainly need to abstract these through our WASI fd object!"495);496send.i32(pipefdPtr, readfd);497send.i32(pipefdPtr + 4, writefd);498return 0;499},500501lockf: (fd: number, cmd: number, size: number): number => {502const { lockf } = posix;503if (lockf == null) {504notImplemented("lockf");505}506507let cmdNative: number | undefined = undefined;508for (const x of ["F_ULOCK", "F_LOCK", "F_TLOCK", "F_TEST"]) {509if (cmd == constants[x]) {510cmdNative = posix.constants[x];511break;512}513}514if (cmdNative == null) {515throw Error(`invalid cmd ${cmd}`);516}517lockf(toNativeFd(fd), cmdNative, BigInt(size));518return 0;519},520521pause: (): number => {522const { pause } = posix;523if (pause == null) {524// this could be implemented in case of worker525notImplemented("pause");526}527return pause();528},529530// initgroups in node, so easier...531// int initgroups(const char *user, gid_t group);532initgroups: (userPtr: number, group: number): number => {533const { initgroups } = process;534if (initgroups == null) {535notImplemented("initgroups");536}537const user = recv.string(userPtr);538initgroups(user, group);539return 0;540},541542// int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups);543getgrouplist: (544userPtr: number,545group: number,546groupPtr: number,547ngroupsPtr: number548): number => {549const { getgrouplist } = posix;550const user = recv.string(userPtr);551const ngroups = recv.i32(ngroupsPtr);552let v;553if (getgrouplist == null) {554v = [group];555} else {556v = getgrouplist(user, group);557}558const k = Math.min(v.length, ngroups);559for (let i = 0; i < k; i++) {560send.u32(groupPtr + 4 * i, v[i]);561}562send.i32(ngroupsPtr, v.length);563if (k < v.length) {564return -1;565}566return 0;567},568569// just like chdir, but uses a file descriptor. WASI doesn't have it, so we570// add it.571fchdir: (fd: number): number => {572const dir = wasi.FD_MAP.get(fd)?.path;573if (!dir) {574console.error(`fchdir: invalid file descriptor: ${fd}`);575return -1;576}577return callWithString("chdir", dir);578},579580// This is not a system call exactly. It's used by WASI.581// It is supposed to "Adjust the flags associated with a file descriptor."582// and it doesn't acctually just set them because WASI doesn't583// have a way to get. So what we do is change the three things584// that can be changed and leave everything else alone!585fcntlSetFlags: (fd: number, flags: number): number => {586if (posix.fcntlSetFlags == null || posix.fcntlGetFlags == null) {587notImplemented("fcntlSetFlags");588return 0;589}590const real_fd = wasi.FD_MAP.get(fd)?.real;591if (real_fd == null) {592throw Error("invalid file descriptor");593}594595let current_native_flags = posix.fcntlGetFlags(real_fd);596let new_native_flags = current_native_flags;597for (const name of ["O_NONBLOCK", "O_APPEND"]) {598if (flags & constants[name]) {599// do want name600new_native_flags |= posix.constants[name];601} else {602// do not want name603new_native_flags &= ~posix.constants[name];604}605}606607if (current_native_flags == new_native_flags) {608log("fcntlSetFlags - unchanged");609} else {610log("fcntlSetFlags ", current_native_flags, " to", new_native_flags);611posix.fcntlSetFlags(real_fd, new_native_flags);612}613return 0;614},615};616617return unistd;618}619620621