Mises v0.40.0 is tagged. It adds the three descriptor syscalls thatredirection is made of, which finishes the kernel side of `>` and `|`. Since v0.39 an fd has been a counted reference to an open file description, and a child inherits its parent's whole table across spawn. What was still missing was any way for the parent to change what a number *means* before it spawns, and any description that is a channel between two processes rather than a file.   0x50 dup(fd)              -> newfd   second name, lowest free fd   0x51 dup2(oldfd, newfd)   -> newfd   second name at a chosen fd   0x52 pipe(fds_ptr, flags) -> 0       [read_fd, write_fd], two LE u32 These open a new 0x50..=0x5F "descriptor" band rather than spending the file band's last three numbers. The narrow reason is that 0x10..=0x1F had exactly three left and link(2) still wants one. The real reason is that none of these takes a path: they operate on the fd table and on open file descriptions, which is a different kind of object -- the same argument the mount band and the system-control band each made. The file band was going to overflow regardless, since lseek, fstat, fcntl, ftruncate, ioctl and poll are all fd-shaped and path-free; those are reserved in the new band. The ABI is append-only, so this was settled before any code was written. dup is literally the existing Tables::link. dup2 is the same with a caller-chosen fd, plus a release of what it displaced -- a real close, flush included. Two orderings in it are load-bearing: the new reference is taken before the displaced one is dropped, and old == new returns early having closed nothing but still validating. Get either wrong and dup2 destroys the fd it was asked to preserve. A pipe lives in a new kern::pipe, which is a second leaf lock that the fd layer never nests under: retiring a pipe end wakes the scheduler, so every close path extracts the dying description under the fd lock, drop the guard, and only then retires it. A pipe's readers/writers count DESCRIPTIONS, not fds, which is the whole of end-of-file -- a dup2 or a spawn adds an fd to an existing description and moves neither count. Buffered bytes drain before EOF is reported. A write with no reader is EPIPE; there is no SIGPIPE to send. Userland can now deadlock the machine, which is new. A parent that waits for a child blocked on a pipe nobody drains is the classic pipeline mistake and Mises now has it, for the same reason POSIX does. Tested by user/pipetest and tests/pipe.rs. The probe re-launches itself with a role operand so writer and reader are one binary; the children write to fd 1 and mention no pipe, and the parent re-emits what it read back out *framed*, so "arrived through the pipe" and "went straight to the terminal" are two different strings -- which lets the test require the raw form to be absent. It moves 12 KiB through a 4 KiB pipe with both ends blocking. Six bug-injections were run against it. Three were worth writing down:   - A dup2(fd, fd) check on stderr proves nothing. fds 0/1/2 share one     description, so a close-then-link dup2 drops it from three     references to two and the fd keeps working by accident. Confirmed     by running the injection with both forms present.   - A leaked pipe is invisible to every behavioural assertion. One     whose end counts are right but which is never freed reads, writes,     blocks and reports EOF perfectly. Only a count() hook sees the page     it pins each time.   - The drive-loop bound had to be sized from the measured healthy cost     (4 spins). At the neighbouring tests' 50,000,000 the deadlock     injection became a 60-second QEMU timeout with no output; at     200,000 it fails a named assertion in under a second. Rationale, alternatives and invariants: ADR-0051. `>` and `|` are now entirely user/sh's work, with no further kernel change. There is no O_CLOEXEC yet, so a pipeline must spawn its writer first and drain before it waits; both are documented. Suite: 357 [ok] across 58 test binaries (60 Running lines), 0 failed, 0 warnings.         kaguya