Supported features
paludarium aims to run real programs with the same results as real Linux. Everything listed here is checked by differential tests against a real x86-64 Linux.
Instructions
- Integer instructions and flags: arithmetic, logic, shifts, bit operations,
lock-prefixed atomics,REPstring instructions (including restart after an interruption),CMPXCHG8B/CMPXCHG16B, BMI (ADCX/ADOX,ANDN,BZHI,MULX,PEXT,RORX,SHLX/SHRX),MOVBE,ENTER/LEAVEand more. - Hints:
PAUSEand thePREFETCHNTA/PREFETCHT0/T1/T2prefetches, which change nothing and never fault. - SSE: the 145 SSE-family instructions real programs (probe, aube and pitchfork) use, the
MFENCE/LFENCE/SFENCEfences among them, and cryptographic instructions such as AES, PCLMUL and SHA. Floating point is computed in software. - Instructions are decoded with iced-x86.
CPUIDdoes not reveal the host's values and does not advertise features that are not implemented.- AVX, AVX-512, x87 and MMX are out of scope.
When a guest reaches an instruction it cannot run, it gets SIGILL, as on real Linux. Page faults raise SIGSEGV, and integer division by zero SIGFPE.
System calls
Only implemented system calls are handled, through an explicit table. Everything else returns ENOSYS; nothing is passed to the host. The table below names every implemented call, which lint:all checks against the kernel.
| Group | System calls |
|---|---|
| Processes and threads | clone, clone3, fork, vfork, execve, wait4, waitid, pidfd_open, pidfd_send_signal, exit, exit_group, getpid, getppid, gettid, set_tid_address, futex, sched_getaffinity, sched_yield, setpgid/getpgid/getpgrp, setsid/getsid, getpriority/setpriority, prlimit64/getrlimit/setrlimit, prctl |
| Memory | mmap (anonymous, and files private or shared), munmap, mremap, mprotect, madvise, msync, brk, arch_prctl |
| System information | uname, sysinfo, getuid, geteuid, getgid, getegid, getgroups, getrusage, times |
| Signals | rt_sigaction, rt_sigprocmask, rt_sigreturn, sigaltstack, kill, tkill, tgkill, setitimer, rt_sigtimedwait, rt_sigqueueinfo, rt_tgsigqueueinfo, rt_sigpending, rt_sigsuspend, pause, restart_syscall |
| Time | clock_gettime, clock_getres, nanosleep, clock_nanosleep, timer_create/timer_settime/timer_gettime/timer_getoverrun/timer_delete |
| Files | open/openat/creat, read, write, readv/writev, pread64/pwrite64, preadv/pwritev, copy_file_range, sendfile, close, close_range, lseek, stat/fstat/lstat/newfstatat, statx, getdents64, mkdir/mkdirat, rmdir, unlink/unlinkat, rename/renameat, link/linkat, symlink/symlinkat, readlink/readlinkat, truncate/ftruncate, fallocate, memfd_create, flock, fcntl, dup/dup2/dup3, pipe/pipe2, chdir/fchdir, getcwd, umask, chmod/fchmod/fchmodat, chown/fchown/lchown/fchownat, utimensat, fsync/fdatasync, sync, fadvise64, access/faccessat/faccessat2, statfs/fstatfs |
| I/O | poll, ppoll, select, pselect6, ioctl (TCGETS, TIOCGWINSZ, FIOCLEX, FIONCLEX, FIONBIO, FIOASYNC, FIOQSIZE, FIGETBSZ, SIOCATMARK, FIONREAD/SIOCINQ, SIOCOUTQ) |
| Events | eventfd2, timerfd_create/timerfd_settime/timerfd_gettime, epoll_create/epoll_create1, epoll_ctl, epoll_wait, epoll_pwait |
| Sockets | socket (AF_INET and AF_INET6 with a network, else EAFNOSUPPORT), connect, getsockname, getpeername, bind, listen, accept and accept4 (on the guest's own loopback; listen and accept for TCP), socketpair (AF_UNIX, SOCK_STREAM, SOCK_SEQPACKET and SOCK_DGRAM), sendto, recvfrom, sendmsg and recvmsg (passing descriptors with SCM_RIGHTS, credentials with SCM_CREDENTIALS and the sender's pidfd with SCM_PIDFD), setsockopt and getsockopt (SOL_SOCKET), shutdown |
| Randomness | getrandom |
pread64,pwrite64,preadvandpwritevleave the file position where it was, and a pipe or socket givesESPIPE; in anO_APPENDfile a positioned write still appends, as on Linux.copy_file_rangecopies between two regular files on one mount (EXDEVacross mounts), stopping at the input's end, with Linux's checks in order (EISDIR,EINVAL,EBADF,EOVERFLOW, andEINVALfor an overlap in one file).sendfilereads a regular file into any writable file, a pipe taking at most its capacity per call. Both update the given offsets or the file positions;sendfilewrites its offset back even after an error.statxreports the fields astathas (STATX_BASIC_STATS) and the mount'sst_devas its device numbers, with Linux's flag and mask checks (EINVAL).AT_EMPTY_PATHnames the descriptor, or the working directory forAT_FDCWD, and as since Linux 6.11 a null path with it does too.fallocatechecks its arguments as Linux'svfs_fallocatedoes, in order (EINVAL,EOPNOTSUPP,EBADF, thenESPIPE,EISDIRorENODEVfor a file that is not regular, andEFBIG). Files then behave as on tmpfs: plain allocation allocates the pages of the range and grows the file unlessFALLOC_FL_KEEP_SIZEis given,FALLOC_FL_PUNCH_HOLEfrees the pages the range covers and sets the rest of it to zeros (also in anO_APPENDfile), and the other modes giveEOPNOTSUPP. Growing pastRLIMIT_FSIZEraisesSIGXFSZand fails withEFBIG, and anfallocatepast the 4 GiB the in-memory filesystem has fails withENOSPCand allocates nothing (writes are not bounded this way; see Limitations).Files of the in-memory filesystem keep their contents in 4 KiB pages, as tmpfs does: a write allocates the pages it touches, and the rest of a file is a hole that reads as zeros and takes no memory, so a file grown by
ftruncatecosts nothing until it is written.st_blockscounts the allocated pages, and shrinking a file frees the pages past its end.lseekwithSEEK_DATAandSEEK_HOLEfinds the written pages and the holes; a pagefallocateallocated is a hole until it is written, and the end of the file is one. An offset at or past the end, or a negative one, givesENXIO.Files of a mounted host directory take
SEEK_DATA,SEEK_HOLEandst_blocksfrom the host on Linux and macOS (st_blockson every Unix host). Where the host's filesystem cannot tell where its holes are, and on other hosts, the whole file is data, as Linux'sgeneric_file_llseekhas it for a filesystem without holes.memfd_createmakes an unlinked in-memory file (mode 0777, or 0666 withMFD_NOEXEC_SEAL, and no links), with Linux's flag and name checks (EINVAL,EFAULT);MFD_HUGETLBgivesENOSYS, as from a kernel without hugetlbfs. Its seals, throughfcntl'sF_ADD_SEALSandF_GET_SEALS, start asF_SEAL_SEALunlessMFD_ALLOW_SEALINGis given (F_SEAL_EXECwithMFD_NOEXEC_SEAL), and restrict the file as tmpfs does:F_SEAL_WRITEandF_SEAL_FUTURE_WRITErefuse writes, punch holes and writable shared mappings,F_SEAL_SHRINKandF_SEAL_GROWthe size (a write stops at the last page that ends within the file), andF_SEAL_EXECthe execute bits, all withEPERM. Memfds share a filesystem of their own, as Linux's internal tmpfs mount, socopy_file_rangebetween one and a file on a mount givesEXDEV. Other files have no seals (EINVAL), even on the in-memory/tmp, where a real tmpfs file reportsF_SEAL_SEAL. As on Linux,F_SEAL_WRITEgivesEBUSYwhile a shared mapping that may write shows the memfd, and a shared mapping of a memfd sealed against writes cannot become writable (EACCES).F_SEAL_EXECon an executable memfd, which seals writes too, givesEBUSYthen as well, as since Linux 7.1; earlier kernels add the seals.close_rangecloses the open descriptors fromfirsttolast, or withCLOSE_RANGE_CLOEXECmarks them close-on-exec.CLOSE_RANGE_UNSHAREfirst gives the thread its own copy of a table it shares withCLONE_FILESthreads, which keep their descriptors. Unknown flags andfirstabovelastgiveEINVAL.futexhandlesWAIT,WAKE,WAIT_BITSETandWAKE_BITSET.futex,epollandeventfdare implemented inside paludarium, not with the host's equivalents, so they behave the same on macOS, Windows and in WebAssembly.Without a network,
socket()returnsEAFNOSUPPORTforAF_INETandAF_INET6.pollwaits likeepoll_wait: until a descriptor is ready, the timeout in milliseconds passes (a negative one never does) or a signal arrives (EINTR). The host's standard input is readable only with bytes to read or once it has ended (a closedGuest.stdinin WebAssembly, the end of the descriptor on a Unix host), as a Linux terminal or pipe is. At its end it is also hung up (POLLHUP), andreadreturns 0.poll,ppoll,select,pselect6andepoll, which can watch it, all see it so. A host that cannot tell (Windows) keeps it always readable. The output streams are always writable. A pipe's read end is readable only with unread data, and only hung up once its writers are gone, as Linux'spipe_pollreports.ppoll,selectandpselect6wait the same way. They write the time left back to their timeout (not a zero one), as Linux does, andppollandpselect6take a temporary signal mask, which a handler runs with.selectfails withEBADFfor a closed descriptor in its sets, and looks at no more than 1024 descriptors (FD_SETSIZE), where Linux cutsnfdsto the size of its descriptor table.A pipe holds 64 KiB, as on Linux, and
fcntl'sF_SETPIPE_SZandF_GETPIPE_SZresize and report it likepipe_set_size: a power of two of at least a page,EBUSYbelow the unread data andEINVALpast 2³¹.A
SOCK_DGRAMpair keeps message boundaries likeSOCK_SEQPACKET, but has no end of file: when the peer closes, reads wait and the next send getsECONNREFUSED, thenENOTCONN. OnlySOCK_STREAMraisesSIGPIPEwithEPIPE, as on Linux.recvmsgandrecvfromtake their flags as Linux's AF_UNIX sockets do:MSG_PEEKleaves the data, and gives copies of the descriptors it passes;MSG_WAITALLfills a stream's buffer, unless the stream ends, descriptors come or a signal arrives after some data, but a peek returns what it finds without waiting for more;MSG_TRUNCreturns a message's whole length.sendmsgandsendtotakeMSG_NOSIGNALandMSG_DONTWAIT.MSG_OOBon a datagram or packet socket isEOPNOTSUPP. The other flags have no effect, as on Linux.A stream write goes in parts, as Linux's
unix_stream_sendmsgsends it: each at most 36544 bytes (a page's head and 32 KiB of pages), or half the send buffer less 64 bytes: the doubled valueSO_SNDBUFsets and reads back, soSO_SNDBUF10000 makes parts of 9936 bytes. The descriptors go with the first part, so a read that takes them ends there, and each part signals theO_ASYNCowner. Datagram and packet messages go whole.A stream has out-of-band data as Linux 7.0's AF_UNIX keeps it. A
MSG_OOBsend makes its last byte the out-of-band byte (an empty one isEOPNOTSUPP), after the rest of its data, and an earlier byte not yet read becomes data. Reads stop before the mark, whichSIOCATMARKreports, whilepoll,select's exception set andepollshowPOLLPRI.recvwithMSG_OOBtakes the byte (EINVALwhen there is none), andMSG_PEEKleaves it; a read that reaches the mark with nothing read yet drops the byte and reads on, or, withSO_OOBINLINE, reads it as data (recvwithMSG_OOBis thenEINVAL). The descriptors a one-byte out-of-band send passes are closed, not delivered. The byte sendsSIGURGto the receiving end's owner (see signal-driven I/O below).Signal-driven I/O, as on Linux.
fcntl'sF_SETOWNgives any open file an owner:- a process or, negative, a process group;
- with
F_SETOWN_EX, also a single thread.
ESRCHwhen the owner does not exist.F_GETOWNandF_GETOWN_EXread it back, andF_SETSIGpicks the signal. Only pipes and sockets keepO_ASYNC(F_SETFLdrops it on other files, andFIOASYNCsetting it there isENOTTY). With it, a pipe or socket sends its ownerSIGIOfrom the kernel. WithF_SETSIG, it sends that signal instead, withsi_code(POLL_IN,POLL_OUT,POLL_ERR,POLL_PRIorPOLL_HUP),si_band, and the descriptorO_ASYNCwas set through assi_fd.- Pipes. Every write signals the reader as it ends, even one that finds the pipe full, and also before each wait. Every read signals the writer as it ends, with data or not. The close of either end signals the other.
- Sockets. A send signals the peer. On a stream or packet socket, so do a write shutdown and the close, which hangs up; a datagram socket's do not. A send that found the buffer full signals once when reads leave three quarters of the buffer free, and so does a full datagram or packet socket that
poll,selectorepoll_ctlasked to take a write, orepoll_waitdid, as Linux's asks only what is on its ready list: an interest it listed when added, after a wakeup, or after a level-triggered report (an edge-triggered one is not listed again). Only what happens whileO_ASYNCis on counts. An out-of-band byte also sends aPOLL_PRIsignal. - TCP and UDP sockets (Network), as Linux's TCP signals:
POLL_OUTwhen a connection opens,POLL_ERRwhen it fails,POLL_INwhen data arrives, and for the peer's endPOLL_IN(POLL_HUPonce this side has shut down too) and thenPOLL_IN. A send orpollthat found the buffer full makes room signalPOLL_OUTonce. A DNS stub's answer signalsPOLL_INwhen it is ready, and so does a datagram from a guest socket when it arrives; the loopback's ICMP answer to a connected UDP socket signalsPOLL_ERR. - A waiting reader. A stream whose read waits gets no signal for the data, end or hang-up that ends the wait, as Linux holds it back; of a send in parts, only the first. For the peer's shutdown or close Linux holds it back only when the reader has not run yet (it wakes the reader before it looks), so there it sometimes signals. A datagram or packet socket's reader gets it.
setsockoptandgetsockopttake theSOL_SOCKEToptions as Linux'ssk_setsockoptandsk_getsockoptdo, with their defaults and checks: anintvalue of fewer than 4 bytes isEINVAL, a value read back is cut to the given length, another level isEOPNOTSUPP, and an unknown option isENOPROTOOPT.SO_TYPE,SO_DOMAIN,SO_PROTOCOL,SO_ERROR,SO_ACCEPTCONN,SO_PEERCREDandSO_PEERPIDFD(the process that made the pair) can only be read.SO_SNDBUFandSO_RCVBUFare doubled and kept at Linux's floors and ceiling, andSO_LINGERtakes astruct linger.SO_PASSCREDmakes messages carry credentials, as Linux's AF_UNIX does. A message sent while either end has it carries the sender's process ID, user and group, or thoseSCM_CREDENTIALSgives. A receiver with it gets them in anSCM_CREDENTIALSmessage, before any descriptors; a message sent without them reads as process 0 with user and group 65534. Such a receiver's stream reads,readandMSG_WAITALLtoo, do not read writes of other credentials together.SCM_CREDENTIALSmust hold onestruct ucred(EINVALotherwise), neither ID may be -1 (EINVAL), and only then is the process looked up (ESRCHwhen there is none).SO_PASSPIDFDmakes messages carry credentials asSO_PASSCREDdoes, and a receiver with it gets a close-on-exec pidfd of the sending process in anSCM_PIDFDmessage, after any descriptors, as Linux 7.0 orders them. The pidfd is installed only when that whole message fits in the control buffer; otherwise the message is lost (MSG_CTRUNC) and no descriptor is taken. A message sent while neither end passed credentials carries none.SO_PEERPIDFDreads a new pidfd of the process that made the pair, installed even when the length given holds only part of its number; it can only be read (setsockoptisENOPROTOOPT).SO_RCVTIMEOandSO_SNDTIMEO(and their_NEWforms) limit how long a blockingrecv,read,sendorwriteon the socket waits, as on Linux: past the limit it givesEAGAIN, or what it moved, andMSG_WAITALLstops there too. A zerotimevalwaits for ever and a negative one not at all; microseconds outside a second areEDOM. The value is kept in ticks of a 1000 Hz clock, so it reads back rounded up to a millisecond. A signal that ends such a wait givesEINTR, which noSA_RESTARTrestarts, as Linux'ssock_intr_errno.SO_RCVLOWATmakes a blocking streamrecvorreadwait until it has that many bytes or its buffer is full, as Linux'ssock_rcvlowatdoes. It returns less when the stream ends, its time is up, or descriptors come. Peeks, nonblocking reads,poll, and datagram and packet sockets are not held back. 0 counts as 1 and a negative value asINT_MAX.SO_PEEK_OFFsets where aMSG_PEEKstarts, as Linux'ssk_peek_off: each peek moves it on by what it read, and each read moves it back by what it took, not below 0. On a datagram or packet socket it passes over whole messages, and over an empty one already peeked at, andMSG_TRUNCgives what is left of the message from there. It takes any value; a negative one (by default -1) is none.SO_TIMESTAMPandSO_TIMESTAMPNS, and their_NEWforms, timestamp messages as Linux's AF_UNIX does. A datagram or packet socket with one gets each message's real time in a control message, before any credentials and descriptors: atimevalforSO_TIMESTAMP, atimespecforSO_TIMESTAMPNS, cut to the control buffer as other control messages are. That time is when the message was sent, if the receiver took timestamps then, or else when it is first read or peeked at. A stream gets none.getsockoptreads each one as set only by the form that set it, and turning any off keeps the_NEWchoice until one is turned on again.A
MAP_SHAREDmapping of a file shows the file's page cache, as on Linux: every shared mapping of the file, in every process, has the same pages. A store to them is whatreadand the file's private mappings see, andwrite,ftruncateand punching a hole change what the mappings show; a store past the end of the file stays out of it. Such a mapping may write, and become writable withmprotect, only when the file is open for reading and writing (EACCESotherwise), as Linux has it.A
MAP_PRIVATEmapping of a file shows the same page cache in the pages it has not written, so it sees later changes to the file there, and its first write to a page copies the page for its process alone, as on Linux; aforked child shares the pages neither process has written. A truncation drops the copies it leaves wholly past the end of the file, so that they read the file again when it grows, as Linux drops them.A page of a file mapping, private or shared, wholly past the end of the file raises
SIGBUS(BUS_ADRERR, with the address) when it is touched, as on Linux; a system call given such memory fails withEFAULT. The end follows the file: a page past it becomes readable when the file grows, and a page read before raisesSIGBUSonce the file shrinks past it. A page the file only partly covers reads zeros after its end.madvise:MADV_DONTNEEDempties private pages. Anonymous ones then read as zeros, and those of a file mapping read the file again, as it is then, as Linux faults them in from the file; this holds aftermremapmoves the mapping and in aforked child. The program's own segments are laid out as Linux lays them out: the pages that hold file data are a file mapping and read the executable again, also those of a read-only segment (code) thatmprotectmade writable and that was written, while the rest (.bss) is anonymous memory and reads as zeros. Shared memory keeps its own contents.MADV_FREEkeeps the contents, as Linux does without memory pressure. The access-pattern hints are accepted and other advice givesEINVAL.mremapgrows a mapping in place when the pages after it are free, and otherwise moves it with its contents underMREMAP_MAYMOVE;MREMAP_FIXEDandMREMAP_DONTUNMAPwork too. Shared memory stays shared with aforked process when it moves. A file mapping grows over more of the file, whose pages it reads when they are first touched, and moving a range that spans several mappings withMREMAP_FIXED, which recent Linux allows, givesEFAULT.msyncchecks its arguments and the range, and withMS_SYNCorMS_ASYNCwrites the stores of shared file mappings back to their files.sched_getaffinitylets every guest thread run on each of the host's CPUs, so the guest sees as many CPUs as the host has (one in WebAssembly).unamereports Linux on x86_64 with the host namepaludarium; likeCPUID, it does not reveal the host. The guest runs as root (user and group 0), and every file belongs to root untilchowngives it another owner or group, which the in-memory filesystem keeps. As on Linux,chowntakes the set-user-ID bit from a file that is not a directory, and the set-group-ID bit when the group may execute it, even when the owner and group stay. What is made in a set-group-ID directory takes the directory's group, and a new directory the bit as well.fchmodandfchownalso change the mode and owner thatfstatreports for a pipe made bypipeorpipe2, whose two ends share them, and for each end of a socket pair. Standard input, output and error, which the host provides, giveEPERM.chmodandutimensatwork in the in-memory filesystem and in mounted host directories; on a Windows host,chmodonly sets or clears the read-only attribute.statreports only the modification time, with access and change times of 0; the host still records both times it is given for a file in a mounted directory. The umask is shared by threads, asCLONE_FSshares it on Linux.fsyncandfdatasyncsucceed on files and directories and giveEINVALon pipes, sockets and terminals;fadvise64is accepted as a hint.statfsreports the in-memory filesystem as a 4 GiB tmpfs whose used space and inodes are what it holds, and a mounted host directory with the host filesystem's own numbers./proc/self/mountinfo,/proc/self/mountsand/proc/mountslist the mounts (tmpfsfor the root,hostfsfor each--mount, without host paths). Each mount has its ownst_dev, as its line inmountinfosays, sodflists every mount.The only other
/procentries are this process's own:/proc/self(also as/proc/<pid>), a directory thatreadlinkresolves to the PID and that its entries can be opened relative to,/proc/self/statusand/proc/self/fd. That file has Linux's fields in Linux's order (umask, IDs, threads, signal masks, capabilities and so on) without the memory and CPU ones, which nothing measures./proc/self/fd(mode 0500) lists.,..and a link per open descriptor, named by its number as Linux reads one (no leading zero). Each link'slstatshows the descriptor's access in its user bits and size 64, andstatfollows it to the file.readlinkgives Linux's names: the file's path with.,..and symbolic links resolved (/proc/<pid>/…for this process's entries), followed by a space and(deleted)once nothing names it,pipe:[<inode>],socket:[<inode>],anon_inode:[eventfd],[eventpoll],[timerfd]or[pidfd](which Linux 6.9 and later name from pidfs instead), and/memfd:<name> (deleted); pipes and sockets have inode numbers of their own for it. Opening a link opens the file again by its path, or gives a memfd a new handle on the same file and seals; a pipe and the standard streams give the same handle for the access they have. A socket givesENXIO, as on Linux; so do, here, a removed file, the other end of a pipe and the files of no type, which Linux can open again./dev(mode 0755) holds the memory devices and the links Linux systems keep there, and lists only these: a file a guest makes under/devcan be opened and stat'ed but is not listed. Each entry has one inode number, whichstatandgetdentsboth report./devopens only for reading (EISDIR), and a device not withO_DIRECTORY(ENOTDIR).null,zero,full,randomandurandomare character devices (mode 0666) with Linux's numbers inst_rdev(1:3, 1:5, 1:7, 1:8, 1:9):nullreads nothing,zeroandfullread zeros,randomandurandomread the host's random bytes, andfullrefuses writes withENOSPCwhile the others take them. A mapping ofzerois anonymous memory, shared withMAP_SHARED.fd,stdin,stdoutandstderrare links (0777) to/proc/self/fdand its first three entries, followed byopen,statand paths through them (/dev/fd/3);O_NOFOLLOWon one givesELOOP, also withO_PATH, where Linux opens the link itself (not supported here). Files on host mounts report no device number.CPU time is the host thread's CPU time (user and system together) from a guest thread's first system call, where the host can measure it (Linux, macOS, Windows); elsewhere, as in WebAssembly, it is wall time instead.
CLOCK_THREAD_CPUTIME_IDreads the thread's, andCLOCK_PROCESS_CPUTIME_IDthe sum of the process's threads, each as of its latest system call. A child starts from zero, and its time joins its parent's children's when it is waited for.getrusage,timesand the rusage ofwait4andwaitidreport it all as user time, as the emulator's work for system calls runs in the host's user space; memory use is not measured.clock_gettimealso readsCLOCK_MONOTONIC_RAW, the coarse clocks,CLOCK_BOOTTIME(the host never suspends), the alarm clocks andCLOCK_TAI(with a TAI offset of 0).clock_getresgives 1 ns, and 4 ms for the coarse clocks.As root,
accessallows reading and writing, and executing when the file has an execute bit or is a directory;getgroupsreturns no groups.sysinforeports a fixed 4 GiB of free memory and no swap, without revealing the host's; the uptime is the monotonic clock's.A timerfd runs on
CLOCK_REALTIMEorCLOCK_MONOTONIC(CLOCK_BOOTTIMEasCLOCK_MONOTONIC), with absolute and periodic settings. A read returns the expirations since the last one, andpollandepollsee it readable once it has expired.TFD_TIMER_CANCEL_ON_SETis accepted, but the guest cannot set the clock, so it never cancels; the alarm clocks giveEPERM.POSIX timers run on
CLOCK_REALTIMEorCLOCK_MONOTONICand notify with a signal to the process (SIGEV_SIGNAL, withSI_TIMERsiginfo and overruns), a signal to one of its threads (SIGEV_THREAD_ID), or not at all (SIGEV_NONE). The kernel takesSIGEV_THREADasSIGEV_SIGNAL, as Linux does; the C library builds the thread onSIGEV_THREAD_ID. A thread's timer signal adds to its overrun only while the thread has not taken it yet, and is lost if the thread has exited. Timers on the CPU-time clocks giveEINVAL. Timers are not inherited byforkand are deleted byexecve.rt_sigtimedwaitalso takes blocked signals pending for the whole process.clone3checks itsstruct clone_argsas Linux does (E2BIGfor a longer struct whose extra part is not zero) and then runs as thecloneit describes;CLONE_CLEAR_SIGHANDresets handled signals to their default.set_tidandCLONE_INTO_CGROUPgiveEINVAL.cloneitself reads only the low 32 bits of its flags, as in Linux, soCLONE_CLEAR_SIGHANDisclone3's alone.prctlsupportsPR_SET_NAME/PR_GET_NAME(a name of up to 15 bytes per thread, taken from the program's file name atexecveand inherited by new threads and processes),PR_SET_PDEATHSIG(the signal is sent when the parent exits),PR_SET_DUMPABLE,PR_SET_CHILD_SUBREAPER(not inherited onfork, kept onexecve) andPR_SET_NO_NEW_PRIVSwith theirPR_GET_*; other options giveEINVAL.When a process exits, its children go to the nearest ancestor that set
PR_SET_CHILD_SUBREAPERand has not exited, or else to the first process, which acts as the init of a PID namespace, as Linux'sforget_original_parentdoes.getppidthen reports the new parent, which can wait for them, and their exit signal becomesSIGCHLD. A child that has already exited is reported to the new parent at once (and reaped at once if that parent ignoresSIGCHLD).When the first process exits, every other process gets
SIGKILLfrom the kernel, as when the init of a PID namespace exits (Linux'szap_pid_ns_processes), so the run ends with it.When an exit orphans a process group, so that no member keeps a parent in another group of the same session, and a member of the group is stopped, every member gets
SIGHUPand thenSIGCONTfrom the kernel (SI_KERNEL), as Linux'skill_orphaned_pgrpsends them. The first process's parent is outside the emulator and counts as such a parent while the first process runs. The first process itself links a group of its children as any parent does: Linux leaves out only the children of the global init, and the first process acts as the init of a PID namespace, as in a container.killreaches a process by its PID or by the ID of one of its threads, the caller's process group with 0, another group with-pgid, and every process but the first one and the caller with -1, as Linux'skill_something_infodoes. A process that has exited counts until it is reaped. With no process to send to,killgivesESRCH, even for an invalid signal; otherwise an invalid signal givesEINVAL. A signal fromkillorpidfd_send_signalgives its sender's PID insi_pid, and thePR_SET_PDEATHSIGsignal the PID of the parent that exited.rt_sigqueueinfo(behindsigqueue) sends a process the caller's siginfo as it is, andrt_tgsigqueueinfosends it to one thread, so the signal carries its value; each realtime signal sent is kept. As in Linux, only a thread signalling itself may give asi_codethat the kernel,killortkillwould give (EPERM, checked before the target), and asi_codewhose layout Linux does not know must leave the rest of the siginfo zero (E2BIG).Resource limits start at Linux's defaults (
RLIMIT_NOFILE1024/4096,RLIMIT_STACK8 MiB/unlimited, ...), are inherited onforkand can be read and changed; the hardRLIMIT_NOFILEgoes up to 1048576 (Linux'sfs.nr_open), or to a lower ceiling the embedder sets. Three are enforced, as in Linux:RLIMIT_NOFILE: a new descriptor past its soft limit givesEMFILE(andopencreates nothing),dup2/dup3onto a number past itEBADF, andF_DUPFDfrom one past itEINVAL.RLIMIT_FSIZE: a write to a regular file (write,writev,pwriteandpwritev, which withO_APPENDstart at the end, andcopy_file_rangeandsendfile) stops at the soft limit; one that starts at or past it, and atruncate/ftruncateorfallocatethat grows the file past it, raiseSIGXFSZand fail withEFBIG.RLIMIT_CORE: a process that a core-dumping signal (SIGSEGV,SIGABRT, ...) ends writes an ELF core namedcorein its working directory (mode 0600 less the umask, replacing an old one), as Linux'sdo_coredumpdoes with the defaultcore_pattern, when the soft limit is at least a page andPR_SET_DUMPABLEhas not turned dumping off. The core has each thread's registers (NT_PRSTATUS), the process (NT_PRPSINFO), the fatal signal (NT_SIGINFO), the auxiliary vector and the memory.waitthen reportsCLD_DUMPEDand the core flag. A core larger than the limit is cut there and still reported as dumped, as Linux reports a core once it has started to write it.
Process groups and sessions are kept the same way, and
wait4andwaitidcan wait for a group. The first process's session started outside the emulator, sogetsidreports it as 0, as Linux reports an ID outside a PID namespace.setsidfails withEPERMin a process whose PID names a group, and makes any other process the leader of a new session and group.setpgidchecks the session as Linux does: a session leader, a child in another session and a group in another session giveEPERM, and a child that has runexecvegivesEACCES. A group exists while any member is left, an exited child not yet reaped included.Nice values are kept per thread, as on Linux: a new thread or process starts with its creator's, and
execvekeeps the caller's.getpriorityreports the highest priority among the threads it finds, as20 - nice, andsetprioritysets them all, cut to -20..19; the guest is root, so it may lower them too.PRIO_PROCESSnames one thread (0 is the caller),PRIO_PGRPevery thread of a process group andPRIO_USERevery thread of user 0. An exited process not yet reaped is still found. No thread givesESRCH, and an unknownwhichEINVAL. The values are reported but do not change scheduling.waitidfollows Linux's checks and copy-out: its siginfo fields are zero when no child is reported, even on an error, and an exited child cannot be waited for withoutWEXITEDorWCONTINUED(ECHILD). A stop and a continue are reported once each to a wait withWUNTRACED(WSTOPPED) orWCONTINUED(WNOWAITleaves them), andSIGCHLDtells the parent unless it setSA_NOCLDSTOP. The siginfo ofSIGCHLDcarries the child's PID, itsCLD_*code (CLD_EXITED,CLD_KILLED,CLD_STOPPED,CLD_CONTINUED), its status and its CPU time assi_utime;si_uidis 0, and aSIGCHLDsent while one is pending is dropped, as Linux does. A child of a process that ignoresSIGCHLDor setSA_NOCLDWAITis reaped as it exits, as Linux'sdo_notify_parentdoes when its exit signal isSIGCHLD: a wait blocks until such children have exited, then fails withECHILD, their CPU time does not join the parent's children's, and withSIGCHLDignored noSIGCHLDis queued, even while blocked. A zombie from before stays for a wait. A pidfd frompidfd_openbecomes readable when its process exits (and hung up once it is reaped; an automatically reaped child shows both at once), soepollcan wait for it;waitid(P_PIDFD)on a nonblocking one givesEAGAIN.pidfd_send_signaltakes no siginfo and no scope flags, andPIDFD_THREADstill names the whole process.Invalid pointers return
EFAULT.
Network
A guest has no network unless the embedder allows destinations (Config::with_network, the command's --allow-net, or the JavaScript network option, which in a browser goes through a relay). An allow-list entry is a host name (github.com), every subdomain of one (*.github.com), an IPv4 address or an IPv6 one ([::1]), each with a port or for every port (github.com:443).
- TCP client sockets:
socket(AF_INETorAF_INET6, SOCK_STREAM), withSOCK_NONBLOCKandSOCK_CLOEXEC, connects through the host, or to a server of the guest's own on its loopback.connect: a blocking one waits for the connection; a nonblocking one returnsEINPROGRESS, the socket then becomes writable, andSO_ERRORgives the result once. A second call isEALREADY, 0 once connected, thenEISCONN; a failed one gives its error, orECONNABORTEDonceSO_ERRORtook it. An IPv6 socket reaches IPv4 addresses as::ffff:a.b.c.d.- A destination the policy does not allow is
ENETUNREACH, except port 53, which is the DNS stub (see below), and a loopback address where no guest socket listens, which is refused (ECONNREFUSED, orEINPROGRESSand thenSO_ERRORfor a nonblocking socket). - Data:
read,write,send/recv,sendto/recvfromandsendmsg/recvmsg, withMSG_PEEK,MSG_WAITALL,MSG_DONTWAITandMSG_NOSIGNAL. A write goes on until all is sent. A connection the peer reset gives its error once, then the end of input. Sending on a socket that never connected, or aftershutdown(SHUT_WR), isEPIPEwithSIGPIPE; receiving on one that never connected isENOTCONN. - Readiness:
poll,selectandepollsee what Linux'stcp_pollreports: hung up and writable before connecting, nothing while connecting,POLLINwith data or at the end of input (withPOLLRDHUP),POLLOUTwhile the send buffer has room,POLLHUPonce both directions are shut, andPOLLERRwhile an error waits. shutdown,getsockname(the host's address and port once connected),getpeername,bind,FIONREAD/SIOCINQandSIOCOUTQ.- Options:
SO_TYPE,SO_DOMAIN,SO_PROTOCOL,SO_ERROR,SO_ACCEPTCONN,SO_KEEPALIVE,SO_REUSEADDR,SO_REUSEPORT,SO_SNDBUF,SO_RCVBUF,SO_RCVTIMEO,SO_SNDTIMEO,SO_LINGER,SO_PRIORITY,SO_BROADCAST,SO_OOBINLINE,TCP_NODELAY(which reaches the host),TCP_KEEPIDLE,TCP_KEEPINTVL,TCP_KEEPCNT,TCP_QUICKACK,TCP_USER_TIMEOUT,IP_TOSandIPV6_V6ONLY, with Linux's defaults and range checks.
- Name resolution: a built-in DNS stub answers the UDP queries a guest's resolver sends to port 53, at any address: the server of
/etc/resolv.conf, or its default. It answersAandAAAAquestions for an allowed name with the addresses the host resolves (for 60 seconds), andNXDOMAINfor any other name. A connection to an address the stub gave for an allowed name is allowed as that name is, for 10 minutes after it last gave it. A datagram answer holds at most 512 bytes; one that cannot hold every address is marked truncated (TC), and the stub answers the query again over TCP: a TCP connection to port 53, at any address, reaches the stub too, which takes queries framed with their two-byte lengths and answers each in turn, in full. - Only global addresses for names: the stub leaves out, and a name rule does not reach, the loopback, private (RFC 1918 and unique local), link-local, shared (RFC 6598), multicast and reserved addresses (also as the IPv4 address in a NAT64 or 6to4 one), so that a name whose DNS points there (DNS rebinding) cannot reach the host's own machine or networks; a name left with none of its addresses is
NXDOMAIN.localhostand its subdomains may reach the loopback. To reach another such address, allow the address itself (127.0.0.1:8080).
Loopback servers
A TCP socket binds, listens and accepts on the guest's own loopback as on Linux, and a UDP socket binds and exchanges datagrams there. A connection from a guest socket to a loopback address where one listens reaches it in the kernel, whatever the policy says, and never leaves the guest: neither the host nor another machine reaches a guest's listener. Where none listens, an allowed address goes to the host as before, and any other is refused. Any network has its loopback, even one that allows nothing (allow: [] in JavaScript, NetworkPolicy::new() in Rust); the command makes one only with an --allow-net.
bindtakes a loopback address (127.0.0.0/8,::1, or IPv4 ones IPv4-mapped on an IPv6 socket) or the unspecified one, with a port or, for port 0, an ephemeral one from 32768–60999, with Linux's checks in its order:EADDRNOTAVAILfor another address,EINVALonce bound,EADDRINUSEfor a port another socket holds on an address both cover (a listener's, a connection it accepted, a client's), unless both setSO_REUSEADDRand the other does not listen, or both setSO_REUSEPORT. An IPv6 socket on::holds the port for IPv4 too, unlessIPV6_V6ONLY.listenkeeps up tobacklog+ 1 connections waiting (at most 4096,net.core.somaxconn); an unbound socket takes an ephemeral port on every address. A listener is readable while a connection waits (forpoll,select,epoll, andPOLL_INwithO_ASYNC), showsSO_ACCEPTCONN, isEINVALtoFIONREAD,EISCONNtoconnect, and stops listening onshutdown(SHUT_RD)or anAF_UNSPECconnect.connectto a listener returns once its queue has the connection, before anyaccept; a nonblocking one isEINPROGRESS, as on Linux. One that finds the queue full waits untilacceptmakes room.acceptandaccept4(SOCK_NONBLOCK,SOCK_CLOEXEC) give the first connection waiting and its peer's address; they wait unless the listener is nonblocking, untilSO_RCVTIMEO(EAGAIN). The accepted socket takes the listener's options.- Resets, as on Linux: closing a socket with unread input or a zero
SO_LINGERtime, disconnecting withAF_UNSPEC, or closing a listener with connections it has not handed out resets the peer (ECONNRESETafter the bytes it holds); bytes sent to a peer that has closed are answered with a reset, so the next send isEPIPE. - UDP binds as TCP does, on ports of its own, where
SO_REUSEADDRon both shares a port. A datagram to a loopback address reaches the guest socket bound there, as Linux'scompute_scorepicks it: a connected one only from its peer, one bound to that address or connected, or of IPv4, first, and among equals the last to bind. A socket that sends unbound takes an ephemeral port on every address; one that connects takes one on the loopback's source address and receives only from its peer, andAF_UNSPECdisconnects it, keeping only the port and address the guest bound itself. A datagram holds up to 65507 bytes over IPv4 and 65527 over IPv6 (EMSGSIZE), may be empty, and is cut to the reader's buffer (MSG_TRUNC);FIONREADgives the first one's length. Where no socket takes it, the loopback's ICMP answer reaches a socket connected there: its next send or receive isECONNREFUSED, once, asSO_ERRORandPOLLERRshow. A datagram to port 53 that no guest socket takes goes to the DNS stub, as before. - UDP
shutdownmarks the socket, connected or not (one that is not isENOTCONNall the same, as Linux'sinet_shutdowngives it). Shut for reading, a receive that would wait ends at once with 0, while datagrams still come and are read first, and a nonblocking one isEAGAIN; shut for sending, a send isEPIPE, with noSIGPIPE.pollthen showsPOLLINandPOLLRDHUP, andPOLLHUPonce both ways are shut.
Threads and processes
- Guest threads created with
clonerun on host threads natively, and on Workers in WebAssembly. fork/vfork-styleclonewithexecvestarts another program from the virtual filesystem, andwait4collects its exit. Rust'sstd::process::Commandworks, also when it forks instead of usingposix_spawn(for example withpre_exec).- A process made by
clonewithCLONE_VM | CLONE_SIGHANDbut withoutCLONE_THREADshares its signal dispositions with its parent: asigactionin one is seen by the other.execvegives the process its own copy again, as in Linux.
Signals
- Registration, masks, delivery (the Linux x86-64 signal frame and
rt_sigreturn),sigaltstack, queued real-time signals, and stop and continue withSIGSTOP. A blocking call that a stop interrupts runs again after the continue, as Linux restarts it when no handler runs:read,write,readv,writev,pread64,pwrite64,preadv,pwritev,wait4,waitid,pause,rt_sigsuspend,flock,sendto,recvfrom,sendmsg,recvmsg,ppoll,select,pselect6,pollwith no timeout and afutexwait with none.nanosleep, a relativeclock_nanosleep,pollwith a timeout and a timedfutexwait resume throughrestart_syscallwith the deadline they had (thefutexwait checks its word again first,EAGAINonce it changed), and an absoluteclock_nanosleepruns again; a handler ends the restart, so they giveEINTRthen. A handler set withSA_RESTARTruns the calls that a stop runs again (fromreadto thefutexwait with no timeout) again when it returns, as on Linux, exceptpause,rt_sigsuspend,poll,ppoll,select,pselect6and a socket call whose waitSO_RCVTIMEOorSO_SNDTIMEOlimits, which giveEINTR.epoll_waitandrt_sigtimedwaitgiveEINTR, as on Linux.rt_sigsuspendwaits with its mask in place of the signal mask until a handler has run, which runs with that mask; the original mask is back after it. - A
sigactionthat makes a signal ignored (SIG_IGN, orSIG_DFLforSIGCHLD,SIGCONT,SIGURGandSIGWINCH, which are ignored by default) discards its pending instances in the process and in every thread, as Linux'sdo_sigactiondoes. A blocked signal sent after that stays pending.
Filesystem
- By default every run gets its own isolated in-memory filesystem, with
/tmp. - Creating, reading and writing files, hard links, symbolic links,
flock,rename, directory listing and thestatfamily. Open files stay usable afterunlink. - Files can be placed before the start.
- A host directory can be mounted when you ask for it (
--mounton the command line,Config::mountsin Rust). Every operation stays inside that directory;..and symbolic links cannot lead outside.
Programs verified
- Hello world in C and Rust, and small test programs for each instruction.
- probe: a Rust program that checks eight thread and filesystem features (a tokio timer, a UNIX socket pair, rayon parallelism, Mutex and Condvar, file operations, hard links, symbolic links and
flock). - aube: a Node.js package manager written in Rust (v2.8.3).
--version,install,install --frozen-lockfileandlistgive the same output and exit status as on real Linux. - ripgrep: a text search tool written in Rust (15.2.0), which runs its searches on several threads. Nine searches, including a regular expression replacement, a parallel search, one through a memory-mapped file and two errors, give the same output and exit status as on real Linux.
--versionis not compared, because it reports the SIMD featuresCPUIDshows, which paludarium limits on purpose.
All of them give the same results as a real x86-64 Linux on Linux, macOS and Windows, and on Node.js, Chromium, Firefox and Safari.