Pseudo-terminals, and what tmux and htop needed around them

- /dev/ptmx and /dev/pts/N: Unix98 pairs with a full line discipline,
  window sizes, job control and hangup (kernel/dev/pty.c).
- The console speaks scroll regions, insert/delete line, SU/SD, RI/IND/NEL,
  DECSC/DECRC; swallows charset designations and private CSI sequences;
  and LF only returns the carriage under ONLCR.
- setsid() drops the controlling terminal, so a pane's pty becomes its ctty.
- pipe2() honours O_NONBLOCK and O_CLOEXEC (libevent's signal pipe).
- File modes are stored and enforced: chmod, fchmod(at), umask, creation
  modes; /tmp is 1777.
- /proc/stat has real user/system/idle; /proc/<pid>/task; kthread flag.
- SO_PEERCRED, IOV_MAX 1024.
- /proc/strace: name a program and its syscalls go to the kernel log.
- World gains htop, btop, tmux, mc, ncdu, fastfetch, games, X toys, NetSurf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-25 02:11:10 +01:00
commit fd9d392416
28 changed files with 1577 additions and 47 deletions

View file

@ -18,6 +18,7 @@
#include "fs/schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */
#include "gfx/gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */
#include "dev/vt.h" /* the terminals the TIOC and VT_ ioctls act on */
#include "dev/pty.h"
#include "dev/pciuser.h"
#include "dev/keymap.h" /* KB_*LAYOUT — the keyboard layout catalogue */
#include "net/netuser.h"
@ -83,6 +84,10 @@ linux_tty_rdev(uint32 major, uint32 minor)
if (major == 1 && minor >= 11 && minor <= 16) return linux_makedev(4, minor - 10);
if (major == 1 && minor == 10) return linux_makedev(5, 0);
if (major == 1 && minor == 1) return linux_makedev(5, 1);
// Pseudo-terminals, as Linux numbers them: ptmx (5, 2), slaves 136.
if (major == PTMX_MAJOR) return linux_makedev(5, 2);
if (major == PTS_MAJOR) return linux_makedev(136, minor);
if (major == PTM_MAJOR) return linux_makedev(128, minor);
return linux_makedev(major, minor);
}
@ -112,6 +117,10 @@ stati_linux(struct inode *ip, struct linux_stat *st)
case T_SOCK: type_bits = LINUX_S_IFSOCK; perm_bits = 0755; break;
default: type_bits = LINUX_S_IFREG; perm_bits = (ip->rwmode & 2) ? 0755 : 0555; break;
}
// A filesystem that keeps real permission bits -- SchFS does -- reports
// those instead. tmux, ssh and gpg refuse a directory others can read.
if (ip->mode)
perm_bits = ip->mode & 07777;
st->st_mode = type_bits | perm_bits;
// Which device this node actually is. Left at zero until now, so every
@ -264,9 +273,21 @@ path_toolong(const char *path)
return 0;
}
// The mode argument of a Linux open/mkdir, or -1 from a native program, whose
// calls have no such argument and leave whatever was in the register.
static int
linux_arg_mode(int n)
{
int mode;
if (!myproc()->syscall_map)
return -1;
argint(n, &mode);
return mode & 07777;
}
static struct inode*
createat_x(int dirfd, char *path, short type, short major, short minor,
int *err)
int *err, int mode)
{
struct inode *ip, *dp;
char name[VFS_NAME_MAX + 1];
@ -320,6 +341,14 @@ createat_x(int dirfd, char *path, short type, short major, short minor,
ilock(ip);
ip->major = major; ip->minor = minor; ip->nlink = 1;
ip->showmode = 1;
// The permission bits asked for, less the umask -- or, from a caller that
// has none to give, what open() and mkdir() would ask for. A symlink's
// are 0777 on Linux whatever the umask, and nothing reads them.
if (type == T_SYMLINK)
ip->mode = 0777;
else
ip->mode = (uint)(mode >= 0 ? mode : type == T_DIR ? 0777 : 0666)
& 07777 & ~myproc()->umask;
iupdate(ip);
if (type == T_DIR) {
dp->nlink++;
@ -343,7 +372,7 @@ createat_x(int dirfd, char *path, short type, short major, short minor,
static struct inode*
create(char *path, short type, short major, short minor)
{
return createat_x(AT_FDCWD, path, type, major, minor, 0);
return createat_x(AT_FDCWD, path, type, major, minor, 0, -1);
}
// -----------------------------------------------------------------------
@ -571,12 +600,16 @@ addr_t sys_pipe(void)
return 0;
}
// The flags were ignored, and they are the point of pipe2: libevent makes its
// signal pipe with O_NONBLOCK and drains it until EAGAIN, so the drain blocked
// on the read after the last byte and tmux's client stopped dead.
addr_t sys_pipe2(void)
{
addr_t fdarray;
struct file *rf, *wf;
int fd0, fd1;
int fd0, fd1, flags;
if (argaddr(0, &fdarray) < 0) return -1;
argint(1, &flags);
int *fds = (int*)user_ptr(fdarray, 2 * sizeof(int));
if (!fds || pipealloc(&rf, &wf) < 0) return -1;
fd0 = fd1 = -1;
@ -585,6 +618,11 @@ addr_t sys_pipe2(void)
fileclose(rf); fileclose(wf);
return -1;
}
rf->nonblock = wf->nonblock = (flags & O_NONBLOCK) != 0;
if (flags & O_CLOEXEC) {
myproc()->ofile_flags[fd0] |= FD_CLOEXEC;
myproc()->ofile_flags[fd1] |= FD_CLOEXEC;
}
fds[0] = fd0; fds[1] = fd1;
return 0;
}
@ -642,7 +680,8 @@ addr_t sys_open(void)
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
} else
ip = createat_x(AT_FDCWD, path,
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &cerr);
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &cerr,
linux_arg_mode(2));
// A refusal because the filesystem is read-only is not "no such file".
// A shell prints the same sentence for every create failure, so
// returning ENOENT here made writing to a snapshot look like a missing
@ -660,6 +699,13 @@ addr_t sys_open(void)
if (ip->type == T_DIR && (omode & O_ACCMODE) != O_RDONLY) { iunlockput(ip); end_op(); return -EISDIR; }
if ((omode & O_TRUNC) && ip->type == T_FILE) itrunc(ip);
}
// /dev/ptmx: every open is a new pseudo-terminal, and the file holds its
// master rather than the ptmx node. See kernel/dev/pty.c.
if (ip->type == T_DEV && ip->major == PTMX_MAJOR) {
struct inode *m = ptmx_open(ip);
if (!m) { iunlockput(ip); end_op(); return -ENOSPC; }
ip = m;
}
if (!(f = filealloc()) || (fd = fdalloc(f)) < 0) {
if (f) fileclose(f);
iunlockput(ip); end_op(); return -EMFILE;
@ -699,7 +745,8 @@ addr_t sys_openat(void)
if ((ip = open_existing(dirfd, path, omode)) != 0) {
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
} else if (!(ip = createat_x(dirfd, path, T_FILE, 0, 0, &cerr))) {
} else if (!(ip = createat_x(dirfd, path, T_FILE, 0, 0, &cerr,
linux_arg_mode(3)))) {
end_op();
return cerr ? -cerr : -ENOENT;
}
@ -717,6 +764,13 @@ addr_t sys_openat(void)
// depending on which entry point libc happened to call.
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
itrunc(ip);
// /dev/ptmx: every open is a new pseudo-terminal, and the file holds its
// master rather than the ptmx node. See kernel/dev/pty.c.
if (ip->type == T_DEV && ip->major == PTMX_MAJOR) {
struct inode *m = ptmx_open(ip);
if (!m) { iunlockput(ip); end_op(); return -ENOSPC; }
ip = m;
}
if (!(f = filealloc()) || (fd = fdalloc(f)) < 0) {
if (f) fileclose(f);
iunlockput(ip); end_op(); return -EMFILE;
@ -905,7 +959,8 @@ addr_t sys_mkdir(void)
return -EEXIST;
}
int cerr = 0;
if (!(ip = createat_x(AT_FDCWD, path, T_DIR, 0, 0, &cerr))) {
if (!(ip = createat_x(AT_FDCWD, path, T_DIR, 0, 0, &cerr,
linux_arg_mode(1)))) {
end_op();
return cerr ? -cerr : -ENOENT;
}
@ -948,7 +1003,7 @@ do_mknod(int dirfd, char *path, uint32 mode, uint64 dev)
if (ex) { iput(ex); end_op(); return -EEXIST; }
int cerr = 0;
struct inode *ip = createat_x(dirfd, path, type, (short)major,
(short)minor, &cerr);
(short)minor, &cerr, (int)(mode & 07777));
if (!ip) { end_op(); return cerr ? -cerr : -EEXIST; }
iunlockput(ip); end_op();
return 0;
@ -1275,7 +1330,7 @@ do_symlink(int newdirfd, char *target, char *linkpath)
return -EEXIST;
}
int cerr = 0;
if (!(ip = createat_x(newdirfd, linkpath, T_SYMLINK, 0, 0, &cerr))) {
if (!(ip = createat_x(newdirfd, linkpath, T_SYMLINK, 0, 0, &cerr, -1))) {
end_op();
return cerr ? -cerr : -ENOENT;
}
@ -1503,6 +1558,9 @@ addr_t sys_ioctl(void)
if (is_dev && f->ip->major == AUDIO_MAJOR)
return dsp_ioctl((int)request, argp);
if (is_dev && (f->ip->major == PTM_MAJOR || f->ip->major == PTS_MAJOR))
return pty_ioctl(f->ip, (uint)request, argp);
int is_tty = (is_dev && f->ip->major == 1);
if (!is_tty) {
// -ENOTTY, not 0.
@ -1776,8 +1834,50 @@ addr_t sys_ioctl(void)
// createat() — every *at() syscall below uses them for dirfd resolution.)
// chmod / fchmod / chown / fchown / lchown — stubs (no permission model yet).
addr_t sys_chmod(void) { return 0; }
addr_t sys_fchmod(void) { return 0; }
// The new bits go in the inode and through iupdate to the filesystem. One
// that keeps none (FAT) holds them only while the inode is cached, which is
// as much as it can; there is no failure to report.
static int
chmod_inode(struct inode *ip, int mode)
{
if (fs_readonly(ip->dev))
return -EROFS;
ilock(ip);
ip->mode = (uint)mode & 07777;
if (ip->mode == 0)
ip->mode = 01000000; // "no bits", as distinct from "never set"
iupdate(ip);
iunlock(ip);
return 0;
}
addr_t sys_chmod(void)
{
char *path;
int mode, err, r;
struct inode *ip;
if (argstr(0, &path) < 0) return -EFAULT;
argint(1, &mode);
begin_op();
if (!(ip = at_nameif(AT_FDCWD, path, &err))) { end_op(); return -err; }
r = chmod_inode(ip, mode);
iput(ip);
end_op();
return r;
}
addr_t sys_fchmod(void)
{
struct file *f;
int mode, r;
if (argfd(0, 0, &f) < 0) return -EBADF;
argint(1, &mode);
if (f->type != FD_INODE || !f->ip) return 0;
begin_op();
r = chmod_inode(f->ip, mode);
end_op();
return r;
}
addr_t sys_chown(void) { return 0; }
addr_t sys_fchown(void) { return 0; }
addr_t sys_lchown(void) { return 0; }
@ -2054,7 +2154,8 @@ addr_t sys_mkdirat(void)
return -EEXIST;
}
int cerr = 0;
if (!(ip = createat_x(dirfd, path, T_DIR, 0, 0, &cerr))) {
if (!(ip = createat_x(dirfd, path, T_DIR, 0, 0, &cerr,
linux_arg_mode(2)))) {
end_op();
return cerr ? -cerr : -ENOENT;
}
@ -2183,7 +2284,21 @@ addr_t sys_readlinkat(void)
return got;
}
addr_t sys_fchmodat(void) { return 0; } // stub
addr_t sys_fchmodat(void)
{
int dirfd, mode, err, r;
char *path;
struct inode *ip;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -EFAULT;
argint(2, &mode);
begin_op();
if (!(ip = at_nameif(dirfd, path, &err))) { end_op(); return -err; }
r = chmod_inode(ip, mode);
iput(ip);
end_op();
return r;
}
addr_t sys_fchownat(void) { return 0; } // stub
addr_t sys_faccessat(void)

View file

@ -752,6 +752,23 @@ SYSCALL_DEFINE(getsockopt)
argaddr(3, &uval);
argaddr(4, &ulenp);
// SO_PEERCRED: who is on the other end of a Unix socket. tmux asks its
// client this and, told nothing, refuses it as a stranger.
if (level == 1 && name == 17) {
struct { int pid; uint uid, gid; } cr;
int want;
if (sock_peercred(s, &cr.pid, &cr.uid, &cr.gid) < 0)
return -ENOTCONN;
if (!uval || !ulenp || get_user(want, ulenp) < 0)
return -EFAULT;
if (want < (int)sizeof(cr))
return -EINVAL;
if (copy_to_user(uval, &cr, sizeof(cr)) < 0)
return -EFAULT;
want = sizeof(cr);
return put_user(want, ulenp) < 0 ? -EFAULT : 0;
}
if ((r = sockopt_get(s, level, name, &val)) < 0)
return r;
return opt_out(uval, ulenp, val);
@ -769,11 +786,11 @@ SYSCALL_DEFINE(getsockopt)
// docs/plan/foundations.md item 1, and docs/plan/hybrid.md mechanism 1.
// ---------------------------------------------------------------------------
// Linux caps an iovec at UIO_MAXIOV = 1024. Thirty-two is the cap here for one
// reason: each entry is validated and copied individually, and a caller that
// needs more than thirty-two separate buffers in one message is describing a
// problem this kernel has not met. EMSGSIZE is Linux's answer for too many.
#define SCH_IOV_MAX 32
// Linux caps an iovec at UIO_MAXIOV = 1024, and so does this. It was 32, and
// tmux met it on its first message: imsg hands sendmsg every queued buffer at
// once, one iovec each, and a client announcing its environment has dozens.
// Nothing is sized by the count -- each entry is fetched and copied on its own.
#define SCH_IOV_MAX 1024
// The control buffer is copied onto the kernel stack, and that stack is 4 KB
// (param.h, KSTACKSIZE). 256 bytes holds CMSG_SPACE(SCM_MAXFD * 4) six times

View file

@ -272,7 +272,14 @@ sys_setgid(void)
return 0;
}
addr_t sys_umask(void) { return 022; }
addr_t sys_umask(void)
{
int mask;
argint(0, &mask);
uint old = myproc()->umask;
myproc()->umask = (uint)mask & 0777;
return old;
}
addr_t
sys_setreuid(void)

View file

@ -50,6 +50,26 @@ fetchstr(addr_t addr, char **pp)
return len;
}
// /proc/strace. Which argument of a Linux syscall is a path, by number, so
// the trace can show the name rather than an address: that is most of what
// one wants from a trace of a program that will not start.
static int
path_arg(uint64 nr)
{
switch (nr) {
case 2: case 4: case 6: case 21: case 59: case 76: case 80: case 82:
case 83: case 84: case 85: case 86: case 87: case 88: case 89: case 90:
case 92: case 137: case 161:
return 0;
case 257: case 258: case 260: case 262: case 263: case 264: case 267:
case 268: case 269: case 280: case 316: case 332: case 437: case 439:
return 1;
}
return -1;
}
static void strace_call(struct trapframe *tf);
static addr_t
fetcharg(int n)
{
@ -117,6 +137,10 @@ static addr_t (*syscalls[NSYSCALLS])(void) = {
void
syscall(struct trapframe *tf)
{
if (myproc()->strace) {
strace_call(tf);
return;
}
kstack_check_canary("syscall entry");
myproc()->tf = tf;
uint64 orig = myproc()->tf->rax;
@ -139,3 +163,63 @@ syscall(struct trapframe *tf)
kstack_check_canary("syscall exit");
}
// syscall() for a traced process: the same dispatch, with a line in the log
// for each call -- the Linux number, four arguments, a path where there is
// one, and what came back. The path is copied before the call runs, since
// execve replaces the memory it is in.
static void
strace_call(struct trapframe *tf)
{
struct proc *p = myproc();
uint64 orig = tf->rax, num = orig;
uint64 a0 = tf->rdi, a1 = tf->rsi, a2 = tf->rdx, a3 = tf->r10;
char path[64];
int pa = p->syscall_map ? path_arg(orig) : -1;
path[0] = 0;
if (pa >= 0) {
addr_t u = pa == 0 ? a0 : a1;
int i;
for (i = 0; i < (int)sizeof(path) - 1; i++) {
char c;
if (copy_from_user(&c, u + i, 1) < 0 || c == 0)
break;
path[i] = c;
}
path[i] = 0;
}
// The result line is written when the call returns, so a call that never
// does would leave no trace at all. Everything that can wait gets a line
// on the way in as well.
switch (orig) {
case 0: case 2: case 7: case 23: case 35: case 43: case 45: case 47:
case 59: case 61: case 202: case 232: case 257: case 270: case 271:
case 281: case 288:
if (p->syscall_map)
kinfo("strace %d %s: %d(%lx, %lx, %lx, %lx)%s%s%s ...\n",
p->pid, p->name, (int)orig, a0, a1, a2, a3,
pa >= 0 ? " \"" : "", path, pa >= 0 ? "\"" : "");
}
kstack_check_canary("syscall entry");
p->tf = tf;
if (p->syscall_map)
num = (num < (uint64)p->map_size) ? p->syscall_map[num] : 0;
if (num > 0 && num < NSYSCALLS && syscalls[num])
tf->rax = syscalls[num]();
else
tf->rax = -ENOSYS;
// A successful execve has replaced the program: its name is the new one.
kinfo("strace %d %s: %s%d(%lx, %lx, %lx, %lx)%s%s%s = %ld\n",
p->pid, p->name, p->syscall_map ? "" : "native ", (int)orig,
a0, a1, a2, a3, pa >= 0 ? " \"" : "", path, pa >= 0 ? "\"" : "",
(long)tf->rax);
if (p->killed)
exit(W_EXITCODE(0, p->killsig ? p->killsig : SIGKILL));
deliver_signals(tf);
kstack_check_canary("syscall exit");
}