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

@ -39,6 +39,7 @@ int boot_debug;
// the VBE info block, not 0x7000 (startothers() below uses that address for
// the AP boot trampoline).
#include "arch/schboot.h"
#include "dev/pty.h"
// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
@ -269,6 +270,7 @@ kmain(uint64 fb_addr)
// Before the keyboard and mouse, which start producing events the moment
// their handlers are registered.
inputinit();
pty_init();
if (boot_verbose) cprintf("verbose boot: inputinit done\n");
// Before consoleinit, because the console writes to a terminal and there
// has to be one. vt_init reads the framebuffer geometry, so it also has to

View file

@ -7,6 +7,7 @@
#include "fs/file.h"
#include "arch/x86.h"
#include "uapi/errno.h"
#include "dev/pty.h"
extern int consoleread(struct inode*, uint64, char*, int);
extern int consolewrite(struct inode*, uint64, char*, int);
@ -216,4 +217,10 @@ struct devsw devsw[] = {
// The close hook plays out what is still queued: a program that writes a
// sound and exits expects to hear the end of it.
[12] = { dspread, dspwrite, 0, dspclose },
// Pseudo-terminals, kernel/dev/pty.c. /dev/ptmx (13) is never read or
// written: open() swaps it for a new master (14) before it gets the chance.
// The slaves (15) are /dev/pts/N.
[PTMX_MAJOR] = { 0, 0 },
[PTM_MAJOR] = { ptm_read, ptm_write, ptm_ready, ptm_close, ptm_open },
[PTS_MAJOR] = { pts_read, pts_write, pts_ready, pts_close, pts_open },
};

View file

@ -194,16 +194,36 @@ static void
gen_stat(struct sbuf *sb)
{
// USER_HZ, like times() -- everything that parses /proc/stat divides by 100.
uint t = (uint)((uint64)uptime() * USER_HZ / HZ);
//
// Busy is what processes were charged; idle is the rest of every online
// processor's time. Calling it all user time, as this did, is a machine at
// 100% forever in htop and top. The charge is not kept per processor, so
// each cpuN line is an equal share of the total -- right on average, which
// is all a meter can show anyway.
uint64 n = ncpu_online > 0 ? ncpu_online : 1;
uint64 all = (uint64)uptime() * USER_HZ / HZ * n;
uint64 user, sys;
proc_cputime(&user, &sys);
user = user * USER_HZ / HZ;
sys = sys * USER_HZ / HZ;
if (user + sys > all)
all = user + sys;
uint64 idle = all - user - sys;
// user nice system idle iowait irq softirq steal guest guest_nice
sb_puts(sb, "cpu ");
sb_putu(sb, t); sb_puts(sb, " 0 0 0 0 0 0 0 0 0\n");
sb_putu(sb, user); sb_puts(sb, " 0 ");
sb_putu(sb, sys); sb_putc(sb, ' ');
sb_putu(sb, idle); sb_puts(sb, " 0 0 0 0 0 0\n");
// Online, for the same reason as cpuinfo: top and friends count the cpuN
// lines here to decide how many cores to draw.
for (int i = 0; i < ncpu_online; i++) {
sb_puts(sb, "cpu");
sb_putu(sb, i);
sb_putc(sb, ' ');
sb_putu(sb, t); sb_puts(sb, " 0 0 0 0 0 0 0 0 0\n");
sb_putu(sb, user / n); sb_puts(sb, " 0 ");
sb_putu(sb, sys / n); sb_putc(sb, ' ');
sb_putu(sb, idle / n); sb_puts(sb, " 0 0 0 0 0 0\n");
}
sb_kv(sb, "processes ", (uint64)proc_num(), 0);
sb_kv(sb, "procs_running ", 1, 0);
@ -388,12 +408,42 @@ gen_filesystems(struct sbuf *sb)
#define PROC_BOOTDEBUG 30
#define PROC_SLAB 20
#define PROC_PARTITIONS 21
#define PROC_STRACE 31
// Render global file `id` (a PROC_* constant) into `buf`. Returns its length.
//
// Split out of procread() so the /proc *filesystem* (kernel/fs/procfs.c) can
// use the same generators: that file owns the namespace, this one owns the
// content, and they have no other business with each other.
// /proc/strace: the name of the program to trace, or empty. Read by exec, so
// a change takes effect at the next program started, not in running ones.
static char strace_name[16];
int
strace_wanted(const char *name)
{
return strace_name[0] && strncmp(name, strace_name, sizeof(strace_name)) == 0;
}
int
proc_write_strace(int user_src, uint64 src, uint n)
{
char tmp[16];
uint len = n < sizeof(tmp) - 1 ? n : sizeof(tmp) - 1;
if (user_src) {
if (copy_from_user(tmp, src, len) < 0)
return -EFAULT;
} else {
memmove(tmp, (char *)src, len);
}
while (len > 0 && (tmp[len - 1] == '\n' || tmp[len - 1] == ' '))
len--;
tmp[len] = 0;
memmove(strace_name, tmp, sizeof(strace_name));
return (int)n;
}
int
proc_gen_global(int id, char *buf, int cap)
{
@ -442,6 +492,10 @@ proc_gen_global(int id, char *buf, int cap)
sb_puts(&sb, boot_debug ? "on\n" : "off\n");
break;
case PROC_DEVICES: gen_devices(&sb); break;
case PROC_STRACE:
sb_puts(&sb, strace_name);
sb_putc(&sb, '\n');
break;
// The block layer renders its own table, like PCI and SchFS above.
case PROC_PARTITIONS: sb.len = blkdev_info(buf, cap); break;
default: return 0;

709
kernel/dev/pty.c Normal file
View file

@ -0,0 +1,709 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// Pseudo-terminals.
//
// A terminal emulator, tmux, sshd and script(1) all want the same thing: a
// terminal with a program on one side and *themselves* on the other, instead of
// a keyboard and a screen. Unix98 spells that /dev/ptmx -- each open makes a new
// pair and hands back its master -- and /dev/pts/N, the slave, which the program
// gets as its stdin, stdout and controlling terminal. There were none of these,
// so xterm had nothing to run a shell in and tmux could not start a server.
//
// Between the two sides sits a line discipline, the console's in miniature
// (kernel/core/console.c): what the master writes is the slave's *input* --
// canonical editing, echo, ^C as SIGINT to the foreground group -- and what the
// slave writes is its *output*, NL turned into CR NL, read back by the master.
//
// Every pair shares one lock. The paths are short and a second lock per pair
// would only be one more thing to take in the wrong order.
#include "core/types.h"
#include "core/defs.h"
#include "core/param.h"
#include "core/spinlock.h"
#include "core/sleeplock.h"
#include "core/proc.h"
#include "fs/fs.h"
#include "fs/file.h"
#include "core/uaccess.h"
#include "uapi/posix.h"
#include "uapi/errno.h"
#include "dev/pty.h"
#define PTY_BUF 4096
#define TIOCGPTN 0x80045430
#define TIOCSPTLCK 0x40045431
#define TIOCGPTPEER 0x5441
#define TIOCNOTTY 0x5422
#define TIOCGSID 0x5429
#define TIOCOUTQ 0x5411
#define TIOCPKT 0x5420
#define POLLIN_ 0x001
#define POLLOUT_ 0x004
#define POLLHUP_ 0x010
struct pty {
int used;
int mopen, sopen; // open file descriptions on each side
int slave_seen; // the slave has been opened at least once
struct termios tio;
struct winsize ws;
int fg_pgrp, sid;
// The slave's input, after the line discipline. [r, w) is ready to read;
// [w, e) is the line still being edited in canonical mode.
char in[PTY_BUF];
uint ir, iw, ie;
int eof; // ^D on an empty line: the next read returns 0
int abort_read; // ^C: wake the reader with EINTR
// The slave's output, for the master to read.
char out[PTY_BUF];
uint or, ow;
};
static struct spinlock ptylock;
static struct pty ptys[NPTY];
#define CTRL(x) ((x) & 0x1f)
static void
termios_defaults(struct termios *t)
{
memset(t, 0, sizeof(*t));
t->c_iflag = ICRNL | IUTF8;
t->c_oflag = OPOST | ONLCR;
t->c_cflag = CS8 | CREAD;
t->c_lflag = ISIG | ICANON | ECHO | ECHOE | IEXTEN;
t->c_cc[VINTR] = CTRL('C');
t->c_cc[VQUIT] = CTRL('\\');
t->c_cc[VERASE] = 0x7f;
t->c_cc[VKILL] = CTRL('U');
t->c_cc[VEOF] = CTRL('D');
t->c_cc[VSUSP] = CTRL('Z');
t->c_cc[VWERASE]= CTRL('W');
t->c_cc[VMIN] = 1;
t->c_ispeed = 15;
t->c_ospeed = 15;
}
void
pty_init(void)
{
initlock(&ptylock, "pty");
}
int
pty_alloc(void)
{
acquire(&ptylock);
for (int i = 0; i < NPTY; i++) {
struct pty *p = &ptys[i];
if (p->used)
continue;
memset(p, 0, sizeof(*p));
p->used = 1;
termios_defaults(&p->tio);
p->ws.ws_row = 24;
p->ws.ws_col = 80;
release(&ptylock);
return i;
}
release(&ptylock);
return -1;
}
int
pty_used(int n)
{
return n >= 0 && n < NPTY && ptys[n].used;
}
static struct pty *
pty_of(struct inode *ip)
{
if (ip->minor < 0 || ip->minor >= NPTY)
return 0;
return &ptys[ip->minor];
}
// /dev/ptmx, opened: a new pair, and the master in place of the ptmx inode.
// Called with ip locked, as open() holds it; hands back the master the same way.
struct inode *
ptmx_open(struct inode *ip)
{
int n = pty_alloc();
struct inode *m;
if (n < 0)
return 0;
m = devfs_pty_master(n);
if (m == 0) {
ptys[n].used = 0;
return 0;
}
iunlockput(ip);
ilock(m);
return m;
}
static void
maybe_free(struct pty *p)
{
if (p->mopen == 0 && p->sopen == 0)
p->used = 0;
}
// --- output: the slave writes, the master reads ------------------------------
static int
out_room(struct pty *p)
{
return PTY_BUF - (int)(p->ow - p->or);
}
// One byte of output, with OPOST processing. Caller holds the lock and has made
// sure there is room for two.
static void
out_put(struct pty *p, int c)
{
if ((p->tio.c_oflag & OPOST) && (p->tio.c_oflag & ONLCR) && c == '\n')
p->out[p->ow++ % PTY_BUF] = '\r';
p->out[p->ow++ % PTY_BUF] = (char)c;
}
// Echo goes into the same queue, and is dropped rather than waited for when
// the master has stopped reading: the writer here is whoever typed, and
// blocking it on its own echo would deadlock a master that writes and reads
// from one thread.
static void
echo_put(struct pty *p, int c)
{
if (out_room(p) < 2)
return;
out_put(p, c);
}
static void
echo(struct pty *p, int c)
{
if (c == '\n') {
if (p->tio.c_lflag & (ECHO | ECHONL))
echo_put(p, '\n');
return;
}
if (!(p->tio.c_lflag & ECHO))
return;
if (c < 32 && c != '\t' && c != '\r') {
echo_put(p, '^');
echo_put(p, c + '@');
} else {
echo_put(p, c);
}
}
static void
erase_one(struct pty *p)
{
if (p->ie == p->iw)
return;
p->ie--;
if ((p->tio.c_lflag & ECHO) && (p->tio.c_lflag & ECHOE)) {
echo_put(p, '\b');
echo_put(p, ' ');
echo_put(p, '\b');
}
}
// --- input: the master writes, the slave reads -------------------------------
// One byte through the line discipline. Returns a signal to send to the
// foreground group, or 0. Caller holds the lock.
static int
input_byte(struct pty *p, int c)
{
struct termios *t = &p->tio;
if (t->c_iflag & ISTRIP)
c &= 0x7f;
if (c == '\r') {
if (t->c_iflag & IGNCR)
return 0;
if (t->c_iflag & ICRNL)
c = '\n';
} else if (c == '\n' && (t->c_iflag & INLCR)) {
c = '\r';
}
if (t->c_lflag & ISIG) {
int sig = 0;
if (c == t->c_cc[VINTR]) sig = SIGINT;
else if (c == t->c_cc[VQUIT]) sig = SIGQUIT;
else if (c == t->c_cc[VSUSP]) sig = SIGTSTP;
if (sig && c != 0) {
echo(p, c);
if (!(t->c_lflag & NOFLSH))
p->ie = p->iw = p->ir;
p->abort_read = 1;
return sig;
}
}
if (t->c_lflag & ICANON) {
if (c == t->c_cc[VERASE] || c == CTRL('H')) {
erase_one(p);
return 0;
}
if (c == t->c_cc[VKILL]) {
while (p->ie != p->iw && p->in[(p->ie - 1) % PTY_BUF] != '\n')
erase_one(p);
return 0;
}
if (c == t->c_cc[VWERASE] && (t->c_lflag & IEXTEN)) {
while (p->ie != p->iw && p->in[(p->ie - 1) % PTY_BUF] == ' ')
erase_one(p);
while (p->ie != p->iw && p->in[(p->ie - 1) % PTY_BUF] != ' ')
erase_one(p);
return 0;
}
if (c == t->c_cc[VEOF]) {
if (p->ie == p->iw)
p->eof = 1;
p->iw = p->ie;
return 0;
}
}
if (p->ie - p->ir >= PTY_BUF)
return 0; // full: dropped, as a console does
p->in[p->ie++ % PTY_BUF] = (char)c;
echo(p, c);
if (!(t->c_lflag & ICANON) || c == '\n' || p->ie - p->ir >= PTY_BUF)
p->iw = p->ie;
return 0;
}
int
ptm_write(struct inode *ip, uint64 off, char *src, int n)
{
struct pty *p = pty_of(ip);
char kbuf[128];
int done = 0;
(void)off;
if (p == 0 || !p->used)
return -EIO;
iunlock(ip);
while (done < n) {
int chunk = n - done < (int)sizeof(kbuf) ? n - done : (int)sizeof(kbuf);
if (copy_from_user(kbuf, (addr_t)(src + done), chunk) < 0) {
ilock(ip);
return done ? done : -EFAULT;
}
acquire(&ptylock);
for (int i = 0; i < chunk; i++) {
// Raw-mode input waits for room rather than vanishing: a paste into an
// editor is a burst of thousands of bytes, and dropping the tail of it
// is worse than making the writer wait for the reader.
while (!(p->tio.c_lflag & ICANON) && p->ie - p->ir >= PTY_BUF &&
p->sopen > 0) {
if (myproc()->killed) {
release(&ptylock);
ilock(ip);
return done + i ? done + i : -EINTR;
}
sleep(&p->ie, &ptylock);
}
int sig = input_byte(p, kbuf[i] & 0xff);
if (sig) {
int pg = p->fg_pgrp;
wakeup(&p->ir);
release(&ptylock);
if (pg > 0)
sigsend_pgrp(pg, sig);
acquire(&ptylock);
}
}
wakeup(&p->ir);
wakeup(&p->or); // the echo
release(&ptylock);
done += chunk;
}
poll_kick();
ilock(ip);
return n;
}
int
ptm_read(struct inode *ip, uint64 off, char *dst, int n)
{
struct pty *p = pty_of(ip);
char kbuf[256];
int got = 0;
(void)off;
if (p == 0 || !p->used)
return -EIO;
iunlock(ip);
acquire(&ptylock);
while (p->or == p->ow) {
// Every slave descriptor closed, after there was one: the program on the
// other side has gone. Linux says EIO, and that is what terminal
// emulators and tmux take as "the shell exited".
if (p->slave_seen && p->sopen == 0) {
release(&ptylock);
ilock(ip);
return -EIO;
}
if (myproc()->killed) {
release(&ptylock);
ilock(ip);
return -EINTR;
}
sleep(&p->or, &ptylock);
}
while (got < n && got < (int)sizeof(kbuf) && p->or != p->ow)
kbuf[got++] = p->out[p->or++ % PTY_BUF];
wakeup(&p->ow);
release(&ptylock);
poll_kick();
ilock(ip);
if (copy_to_user((addr_t)dst, kbuf, got) < 0)
return -EFAULT;
return got;
}
int
ptm_ready(struct inode *ip)
{
struct pty *p = pty_of(ip);
return p && (p->or != p->ow || (p->slave_seen && p->sopen == 0));
}
void
ptm_open(struct inode *ip)
{
struct pty *p = pty_of(ip);
if (p) {
acquire(&ptylock);
p->mopen++;
release(&ptylock);
}
}
// The master is gone: the slave side has been hung up on. Its readers see end
// of file, its writers EIO, and its session SIGHUP -- which is how closing an
// xterm window ends the shell inside it.
void
ptm_close(struct inode *ip)
{
struct pty *p = pty_of(ip);
int sid = 0, pg = 0;
if (p == 0)
return;
acquire(&ptylock);
if (p->mopen > 0)
p->mopen--;
if (p->mopen == 0) {
sid = p->sid;
pg = p->fg_pgrp;
wakeup(&p->ir);
wakeup(&p->ow);
wakeup(&p->ie);
}
maybe_free(p);
release(&ptylock);
if (sid > 0)
sigsend_pgrp(sid, SIGHUP);
if (pg > 0 && pg != sid)
sigsend_pgrp(pg, SIGHUP);
poll_kick();
}
// --- the slave ---------------------------------------------------------------
int
pts_read(struct inode *ip, uint64 off, char *dst, int n)
{
struct pty *p = pty_of(ip);
char kbuf[256];
int got = 0, c;
(void)off;
if (p == 0 || !p->used)
return -EIO;
if (n > (int)sizeof(kbuf))
n = sizeof(kbuf);
iunlock(ip);
acquire(&ptylock);
while (got < n) {
while (p->ir == p->iw) {
if (p->eof) {
p->eof = 0;
goto done;
}
if (p->mopen == 0)
goto done; // hung up: end of file
// VMIN 0 is a poll, and a program that asked for one must not block.
if (!(p->tio.c_lflag & ICANON) && p->tio.c_cc[VMIN] == 0)
goto done;
if (got > 0 && !(p->tio.c_lflag & ICANON))
goto done;
if (myproc()->killed || p->abort_read) {
p->abort_read = 0;
release(&ptylock);
ilock(ip);
return got ? got : -EINTR;
}
sleep(&p->ir, &ptylock);
}
c = p->in[p->ir++ % PTY_BUF];
kbuf[got++] = (char)c;
if ((p->tio.c_lflag & ICANON) && c == '\n')
break;
if (!(p->tio.c_lflag & ICANON) &&
got >= (p->tio.c_cc[VMIN] ? p->tio.c_cc[VMIN] : 1) && p->ir == p->iw)
break;
}
done:
wakeup(&p->ie);
release(&ptylock);
poll_kick();
ilock(ip);
if (got && copy_to_user((addr_t)dst, kbuf, got) < 0)
return -EFAULT;
return got;
}
int
pts_write(struct inode *ip, uint64 off, char *src, int n)
{
struct pty *p = pty_of(ip);
char kbuf[128];
int done = 0;
(void)off;
if (p == 0 || !p->used)
return -EIO;
iunlock(ip);
while (done < n) {
int chunk = n - done < (int)sizeof(kbuf) ? n - done : (int)sizeof(kbuf);
if (copy_from_user(kbuf, (addr_t)(src + done), chunk) < 0) {
ilock(ip);
return done ? done : -EFAULT;
}
acquire(&ptylock);
for (int i = 0; i < chunk; i++) {
while (out_room(p) < 2) {
if (p->mopen == 0 || myproc()->killed) {
release(&ptylock);
ilock(ip);
return done + i ? done + i : (p->mopen == 0 ? -EIO : -EINTR);
}
wakeup(&p->or);
sleep(&p->ow, &ptylock);
}
if (p->mopen == 0) {
release(&ptylock);
ilock(ip);
return done + i ? done + i : -EIO;
}
out_put(p, kbuf[i] & 0xff);
}
wakeup(&p->or);
release(&ptylock);
done += chunk;
}
poll_kick();
ilock(ip);
return n;
}
int
pts_ready(struct inode *ip)
{
struct pty *p = pty_of(ip);
return p && (p->ir != p->iw || p->eof || p->mopen == 0);
}
// Opening a terminal makes it the caller's controlling terminal when the
// caller leads a session that has none -- the System V rule, which Linux keeps
// and which xterm relies on: its child calls setsid() and opens the slave, and
// expects ^C to work from then on without a TIOCSCTTY.
void
pts_open(struct inode *ip)
{
struct pty *p = pty_of(ip);
struct proc *me = myproc();
if (p == 0)
return;
acquire(&ptylock);
p->sopen++;
p->slave_seen = 1;
if (me && me->sid == me->pid && me->ctty == 0 && p->sid == 0) {
me->ctty = PTY_CTTY + ip->minor;
p->sid = me->sid;
p->fg_pgrp = me->pgid;
}
release(&ptylock);
}
void
pts_close(struct inode *ip)
{
struct pty *p = pty_of(ip);
if (p == 0)
return;
acquire(&ptylock);
if (p->sopen > 0)
p->sopen--;
if (p->sopen == 0) {
p->sid = 0;
p->fg_pgrp = 0;
wakeup(&p->or); // the master reads EIO now
}
maybe_free(p);
release(&ptylock);
poll_kick();
}
// --- ioctl and poll ----------------------------------------------------------
int
pty_ioctl(struct inode *ip, uint request, addr_t arg)
{
struct pty *p = pty_of(ip);
int master = ip->major == PTM_MAJOR;
int v;
if (p == 0 || !p->used)
return -EIO;
switch (request) {
case TIOCGPTN:
if (!master)
return -ENOTTY;
v = ip->minor;
return put_user(v, arg) < 0 ? -EFAULT : 0;
case TIOCSPTLCK:
case TIOCPKT:
// Slaves are never locked here, so unlockpt() has nothing to undo; packet
// mode is not offered, and nothing in the image asks for it.
return master ? 0 : -ENOTTY;
case TCGETS: {
struct termios t;
acquire(&ptylock);
t = p->tio;
release(&ptylock);
return copy_to_user(arg, &t, sizeof(t)) < 0 ? -EFAULT : 0;
}
case TCSETS: case TCSETSW: case TCSETSF: {
struct termios t;
if (copy_from_user(&t, arg, sizeof(t)) < 0)
return -EFAULT;
acquire(&ptylock);
p->tio = t;
if (request == TCSETSF)
p->ie = p->iw = p->ir;
// Leaving canonical mode hands over the line being edited, as on the
// console: a shell that switches to raw mid-line must not lose it.
if (!(t.c_lflag & ICANON))
p->iw = p->ie;
wakeup(&p->ir);
release(&ptylock);
poll_kick();
return 0;
}
case TCFLSH:
acquire(&ptylock);
if (arg == TCIFLUSH || arg == TCIOFLUSH) {
p->ie = p->iw = p->ir;
p->eof = 0;
}
if (arg == TCOFLUSH || arg == TCIOFLUSH)
p->or = p->ow;
wakeup(&p->ie);
wakeup(&p->ow);
release(&ptylock);
return 0;
case TIOCGWINSZ:
return copy_to_user(arg, &p->ws, sizeof(p->ws)) < 0 ? -EFAULT : 0;
case TIOCSWINSZ: {
struct winsize ws;
int changed, pg;
if (copy_from_user(&ws, arg, sizeof(ws)) < 0)
return -EFAULT;
acquire(&ptylock);
changed = ws.ws_row != p->ws.ws_row || ws.ws_col != p->ws.ws_col;
p->ws = ws;
pg = p->fg_pgrp;
release(&ptylock);
// The program inside learns of a resize the way it would on Linux.
if (changed && pg > 0)
sigsend_pgrp(pg, SIGWINCH);
return 0;
}
case FIONREAD:
v = master ? (int)(p->ow - p->or) : (int)(p->iw - p->ir);
return put_user(v, arg) < 0 ? -EFAULT : 0;
case TIOCOUTQ:
v = master ? 0 : (int)(p->ow - p->or);
return put_user(v, arg) < 0 ? -EFAULT : 0;
case TIOCGPGRP:
v = p->fg_pgrp;
return put_user(v, arg) < 0 ? -EFAULT : 0;
case TIOCSPGRP:
if (copy_from_user(&v, arg, sizeof(v)) < 0)
return -EFAULT;
if (v <= 0)
return -EINVAL;
p->fg_pgrp = v;
return 0;
case TIOCGSID:
if (p->sid == 0)
return -ENOTTY;
v = p->sid;
return put_user(v, arg) < 0 ? -EFAULT : 0;
case TIOCSCTTY:
if (master)
return -EINVAL;
myproc()->ctty = PTY_CTTY + ip->minor;
p->sid = myproc()->sid;
p->fg_pgrp = myproc()->pgid;
return 0;
case TIOCNOTTY:
if (myproc()->ctty == PTY_CTTY + ip->minor)
myproc()->ctty = 0;
return 0;
}
return -ENOTTY;
}
int
pty_poll(struct inode *ip, int events, int readable, int writable)
{
struct pty *p = pty_of(ip);
int rev = 0;
if (p == 0 || !p->used)
return POLLHUP_;
if (ip->major == PTM_MAJOR) {
if (readable && (events & POLLIN_) && p->or != p->ow)
rev |= POLLIN_;
if (p->slave_seen && p->sopen == 0)
rev |= POLLHUP_ | (events & POLLIN_);
} else {
if (readable && (events & POLLIN_) && (p->ir != p->iw || p->eof))
rev |= POLLIN_;
if (p->mopen == 0)
rev |= POLLHUP_ | (events & POLLIN_);
}
if (writable && (events & POLLOUT_))
rev |= POLLOUT_;
return rev;
}

View file

@ -286,6 +286,99 @@ vt_scroll(struct vt *v)
v->cy--;
}
// --- the scrolling region ----------------------------------------------------
//
// tmux, htop, less and every curses program on TERM=linux scroll part of the
// screen: a status line stays put while the rest moves. Without a region the
// whole screen scrolled instead, and a pane's output smeared into its
// neighbours and over the status bar.
static int
region_bot(struct vt *v)
{
if (v->scroll_bot > v->scroll_top && v->scroll_bot < v->rows)
return v->scroll_bot;
return v->rows - 1;
}
static int
region_top(struct vt *v)
{
return v->scroll_top < region_bot(v) ? v->scroll_top : 0;
}
static void
draw_rows(struct vt *v, int r0, int r1)
{
if (!is_active(v))
return;
for (int r = r0; r <= r1; r++)
for (int c = 0; c < v->cols; c++)
show(v, c, r);
}
// Rows top..bot move up n; the n exposed at the bottom are blank.
static void
region_up(struct vt *v, int top, int bot, int n)
{
if (n <= 0 || top > bot)
return;
if (n > bot - top + 1)
n = bot - top + 1;
if (top == 0 && bot == v->rows - 1) {
// The whole screen: the framebuffer can be moved instead of redrawn.
while (n-- > 0) {
memmove(v->cells, cell_at(v, 0, 1),
(uint64)(v->rows - 1) * v->cols * sizeof(struct vtcell));
blank_row(v, v->rows - 1);
if (is_active(v))
gfx_scroll_up(vt_rgb(v->bg));
}
return;
}
memmove(cell_at(v, 0, top), cell_at(v, 0, top + n),
(uint64)(bot - top + 1 - n) * v->cols * sizeof(struct vtcell));
for (int r = bot - n + 1; r <= bot; r++)
blank_row(v, r);
draw_rows(v, top, bot);
}
// Rows top..bot move down n; the n exposed at the top are blank.
static void
region_down(struct vt *v, int top, int bot, int n)
{
if (n <= 0 || top > bot)
return;
if (n > bot - top + 1)
n = bot - top + 1;
memmove(cell_at(v, 0, top + n), cell_at(v, 0, top),
(uint64)(bot - top + 1 - n) * v->cols * sizeof(struct vtcell));
for (int r = top; r < top + n; r++)
blank_row(v, r);
draw_rows(v, top, bot);
}
// Down a line, scrolling the region when the cursor is on its last row. Below
// the region the cursor only moves, and stops at the bottom of the screen.
static void
linefeed(struct vt *v)
{
if (v->cy == region_bot(v))
region_up(v, region_top(v), region_bot(v), 1);
else if (v->cy < v->rows - 1)
v->cy++;
}
// Up a line, the reverse: the region scrolls down from its first row.
static void
reverse_index(struct vt *v)
{
if (v->cy == region_top(v))
region_down(v, region_top(v), region_bot(v), 1);
else if (v->cy > 0)
v->cy--;
}
void
vt_cursor_set(struct vt *v, int col, int row)
{
@ -328,19 +421,23 @@ wrap_now(struct vt *v)
return;
v->wrap_pending = 0;
v->cx = 0;
v->cy++;
while (v->cy >= v->rows)
vt_scroll(v);
linefeed(v);
}
void
vt_putc(struct vt *v, int glyph)
{
switch (glyph) {
// LF is a line feed: down one row, same column. The carriage return a
// shell expects with it is ONLCR's, so it happens only while the terminal
// asks for it. Doing it always broke every program that turns OPOST off and
// moves down with a bare LF -- tmux does, row after row, and each row of a
// right-hand pane landed in column 0, where its ESC [ K erased the left one.
case '\n':
v->wrap_pending = 0;
v->cx = 0;
v->cy++;
if ((v->tio.c_oflag & (OPOST | ONLCR)) == (OPOST | ONLCR))
v->cx = 0;
linefeed(v);
break;
case '\r':
@ -759,6 +856,38 @@ vt_csi(struct vt *v, int final)
case 's': v->saved_cx = col; v->saved_cy = row; break;
case 'u': vt_cursor_set(v, v->saved_cx, v->saved_cy); break;
// DECSTBM: the scrolling region, 1-based and inclusive, and the cursor home.
case 'r': {
int t = v->esc_nparm > 0 && v->esc_parm[0] ? v->esc_parm[0] : 1;
int b = v->esc_nparm > 1 && v->esc_parm[1] ? v->esc_parm[1] : v->rows;
if (b > v->rows) b = v->rows;
if (t < b) {
v->scroll_top = t - 1;
v->scroll_bot = b - 1;
} else {
v->scroll_top = v->scroll_bot = 0;
}
vt_cursor_set(v, 0, 0);
break;
}
// Insert and delete lines at the cursor, within the region: what a curses
// program sends to open or close a gap rather than repaint everything below.
case 'L':
if (row >= region_top(v) && row <= region_bot(v))
region_down(v, row, region_bot(v), n1);
v->wrap_pending = 0;
break;
case 'M':
if (row >= region_top(v) && row <= region_bot(v))
region_up(v, row, region_bot(v), n1);
v->wrap_pending = 0;
break;
// Scroll the region without moving the cursor.
case 'S': region_up(v, region_top(v), region_bot(v), n1); break;
case 'T': region_down(v, region_top(v), region_bot(v), n1); break;
// Cursor visibility (DECTCEM) and the other private modes. There is no
// hardware cursor being drawn, so there is nothing to show or hide --
// swallowing them silently is correct, and far better than printing
@ -781,6 +910,37 @@ vt_feed_1(struct vt *v, int c)
for (int i = 0; i < VT_MAXPARM; i++) v->esc_parm[i] = 0;
return; }
if (c == ']') { v->esc_state = VE_OSC; return; }
v->esc_state = VE_NORMAL;
switch (c) {
case '7': // DECSC
v->saved_cx = v->cx;
v->saved_cy = v->cy;
break;
case '8': // DECRC
vt_cursor_set(v, v->saved_cx, v->saved_cy);
break;
case 'M': // RI: up, scrolling back
v->wrap_pending = 0;
reverse_index(v);
break;
case 'D': // IND: down, scrolling
v->wrap_pending = 0;
linefeed(v);
break;
case 'E': // NEL: CR and IND
v->wrap_pending = 0;
v->cx = 0;
linefeed(v);
break;
case '(': case ')': case '*': case '+':
// A character set designation. The set itself is not offered, but the
// byte naming it must be swallowed: ESC ( B printed a stray "B".
v->esc_state = VE_CHARSET;
break;
}
return;
case VE_CHARSET:
v->esc_state = VE_NORMAL;
return;
@ -798,7 +958,12 @@ vt_feed_1(struct vt *v, int c)
return;
case VE_CSI:
if (c == '?') { v->esc_private = 1; return; }
// Private markers (< = > ?) and intermediates (space to /): sequences
// this terminal does not speak, but must still consume to the final byte.
// Ending at the marker instead printed the rest -- tmux asking for
// extended keys as ESC [ > 4 ; 1 m left "4;1m" on the screen.
if (c >= '<' && c <= '?') { v->esc_private = 1; return; }
if (c >= 0x20 && c <= 0x2F) { v->esc_private = 1; return; }
if (c >= '0' && c <= '9') {
if (v->esc_nparm == 0) v->esc_nparm = 1;
if (v->esc_nparm <= VT_MAXPARM)

View file

@ -38,6 +38,7 @@
#include "fs/file.h"
#include "uapi/errno.h"
#include "fs/blkdev.h"
#include "dev/pty.h"
// Inode numbering. Two directories and a flat table of nodes, so the number is
// either a directory id or an index into devnodes[] offset past them.
@ -49,6 +50,12 @@
// device number, and inode DEVFS_BLK0 plus that number. Not in devnodes[],
// because they come and go: a stick plugged in appears, pulled out vanishes.
#define DEVFS_BLK0 4096ULL
// Pseudo-terminals: the /dev/pts directory, its slaves by pair number, and the
// masters, which have inodes so that an open file can hold one but appear in no
// directory -- the only way to one is opening /dev/ptmx.
#define DEVFS_PTS 3ULL
#define DEVFS_PTS0 8192ULL
#define DEVFS_PTM0 12288ULL
// Which directory a node lives in. Only two exist and a third would want a real
// tree rather than a flag, which is the point at which this file should grow
@ -110,6 +117,8 @@ static const struct devnode devnodes[] = {
// Audio, spelled the way UNIX spells it. See include/dev/audio.h.
{ "dsp", 12, 0, D_ROOT },
{ "mouse", 3, 1, D_ROOT },
// Opening it makes a new pseudo-terminal pair; see kernel/dev/pty.c.
{ "ptmx", PTMX_MAJOR, 0, D_ROOT },
// Raw disks. Created whether or not a drive is present: opening one with no
// disk behind it fails with ENXIO, which is a better answer than a missing
@ -143,9 +152,22 @@ devfs_iload(struct inode *ip)
ip->major = ip->minor = 0;
ip->size = 0;
if (ip->inum == DEVFS_ROOT || ip->inum == DEVFS_INPUT) {
if (ip->inum == DEVFS_ROOT || ip->inum == DEVFS_INPUT ||
ip->inum == DEVFS_PTS) {
ip->type = T_DIR;
ip->nlink = 2;
} else if (ip->inum >= DEVFS_PTS0 && ip->inum < DEVFS_PTS0 + NPTY &&
pty_used((int)(ip->inum - DEVFS_PTS0))) {
ip->type = T_DEV;
ip->nlink = 1;
ip->major = PTS_MAJOR;
ip->minor = (short)(ip->inum - DEVFS_PTS0);
ip->mode = 0620;
} else if (ip->inum >= DEVFS_PTM0 && ip->inum < DEVFS_PTM0 + NPTY) {
ip->type = T_DEV;
ip->nlink = 1;
ip->major = PTM_MAJOR;
ip->minor = (short)(ip->inum - DEVFS_PTM0);
} else if (i >= 0) {
ip->type = T_DEV;
ip->nlink = 1;
@ -196,6 +218,32 @@ devfs_dirlookup(struct inode *dp, char *name, uint *poff)
if (dp->inum == DEVFS_ROOT && streq(name, "input"))
return iget(DEVFS_DEV, DEVFS_INPUT);
if (dp->inum == DEVFS_ROOT && streq(name, "pts"))
return iget(DEVFS_DEV, DEVFS_PTS);
// /dev/pts/N: a pair that exists.
if (dp->inum == DEVFS_PTS) {
int n = 0;
const char *s = name;
if (!*s)
return 0;
for (; *s; s++) {
if (*s < '0' || *s > '9')
return 0;
n = n * 10 + (*s - '0');
if (n >= NPTY)
return 0;
}
return pty_used(n) ? iget(DEVFS_DEV, DEVFS_PTS0 + (uint64)n) : 0;
}
// /dev/tty is the caller's controlling terminal. When that is a pty, it is
// the slave itself -- what a program inside xterm or tmux opens to talk to
// its terminal must be that terminal, not the console.
if (dp->inum == DEVFS_ROOT && streq(name, "tty") && myproc() &&
myproc()->ctty >= PTY_CTTY && myproc()->ctty < PTY_CTTY + NPTY &&
pty_used(myproc()->ctty - PTY_CTTY))
return iget(DEVFS_DEV, DEVFS_PTS0 + (uint64)(myproc()->ctty - PTY_CTTY));
short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT;
for (int i = 0; i < NDEVNODES; i++)
@ -258,7 +306,8 @@ devfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
int used = 0;
uint64 n = *poff;
if (dp->inum != DEVFS_ROOT && dp->inum != DEVFS_INPUT)
if (dp->inum != DEVFS_ROOT && dp->inum != DEVFS_INPUT &&
dp->inum != DEVFS_PTS)
return -1;
if (n == 0) {
@ -271,15 +320,36 @@ devfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
n++;
}
// /dev/input is a directory entry of the root, and it is emitted between the
// dots and the nodes so that its ordinal is stable.
// /dev/pts: the dots, then the pairs that exist. Ordinal 2 + N is pair N,
// whether or not the ones before it exist, so a pair opened between two
// calls does not shift the rest.
if (dp->inum == DEVFS_PTS) {
for (; n >= 2 && n < 2 + NPTY; n++) {
int k = (int)(n - 2);
char nm[4];
if (!pty_used(k))
continue;
nm[0] = k >= 10 ? '0' + k / 10 : '0' + k;
nm[1] = k >= 10 ? '0' + k % 10 : 0;
nm[2] = 0;
if (emit(out, cap, &used, DEVFS_PTS0 + (uint64)k, 2, nm) < 0) goto out;
}
goto out;
}
// /dev/input and /dev/pts are directory entries of the root, emitted
// between the dots and the nodes so that their ordinals are stable.
if (dp->inum == DEVFS_ROOT && n == 2) {
if (emit(out, cap, &used, DEVFS_INPUT, 4, "input") < 0) goto out;
n++;
}
if (dp->inum == DEVFS_ROOT && n == 3) {
if (emit(out, cap, &used, DEVFS_PTS, 4, "pts") < 0) goto out;
n++;
}
short want = dp->inum == DEVFS_INPUT ? D_INPUT : D_ROOT;
uint64 base = dp->inum == DEVFS_ROOT ? 3 : 2;
uint64 base = dp->inum == DEVFS_ROOT ? 4 : 2;
for (int i = 0; i < NDEVNODES; i++) {
if (devnodes[i].dir != want)
@ -339,3 +409,12 @@ devfs_mount(const char *target)
iput(dp);
return r < 0 ? -EBUSY : 0;
}
// The inode of pty master N, for ptmx_open() to hand to the file it opens.
struct inode *
devfs_pty_master(int n)
{
if (n < 0 || n >= NPTY)
return 0;
return iget(DEVFS_DEV, DEVFS_PTM0 + (uint64)n);
}

View file

@ -535,6 +535,8 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
if(*s == '/')
last = s+1;
safestrcpy(myproc()->name, last, sizeof(myproc()->name));
if (strace_wanted(myproc()->name))
myproc()->strace = 1;
safestrcpy(myproc()->exe, path, sizeof(myproc()->exe));
for(int i = 0; i < NOFILE; i++){

View file

@ -10,6 +10,7 @@
#include "uapi/posix.h"
#include "uapi/errno.h"
#include "core/kmem.h"
#include "dev/pty.h"
extern struct devsw devsw[];
@ -422,6 +423,10 @@ file_poll(struct file *f, int events)
return rev;
}
if(f->ip && f->ip->type == T_DEV &&
(f->ip->major == PTM_MAJOR || f->ip->major == PTS_MAJOR))
return pty_poll(f->ip, events, f->readable, f->writable);
if(f->ip && f->ip->type == T_DEV && f->ip->major == CONSOLE){
int rev = 0;
if((events & POLLIN) && f->readable && console_input_ready(f->ip ? f->ip->minor : 1))

View file

@ -624,8 +624,10 @@ ilock(struct inode *ip)
acquiresleep(&ip->lock);
if (ip->valid == 0)
if (ip->valid == 0) {
ip->mode = 0;
ip->ops->iload(ip);
}
}
void

View file

@ -34,6 +34,7 @@
#include "uapi/stat.h"
#include "core/uaccess.h"
#include "uapi/errno.h"
#include "dev/pty.h"
// ---------------------------------------------------------------------------
// Inode numbering
@ -68,7 +69,8 @@
#define PF_EXE 5
#define PF_SCHEDSTAT 6
#define PF_FD 7 // the directory /proc/<pid>/fd
#define PF_MAX 7
#define PF_TASK 8 // the directory /proc/<pid>/task
#define PF_MAX 8
#define PF_FDENT 0x80 // | fd: one link in it
#define INO_PID(ino) ((int)((ino) >> 8))
@ -109,6 +111,9 @@ static const struct global_file globals[] = {
// Disks and partitions, in Linux's format. See blkdev_info().
{ "partitions", 21 },
{ "kmsg", 17 },
// A program's name: from its next exec on, it and everything it starts log
// every syscall. Empty to stop. See strace_call() in kernel/sys/syscall.c.
{ "strace", 31 },
};
#define NGLOBALS ((int)(sizeof(globals) / sizeof(globals[0])))
@ -132,6 +137,11 @@ static const struct global_file pidfiles[] = {
// One symlink per open descriptor, named by its number and pointing at the
// path it was opened by, or at "socket:[ino]" and the like.
{ "fd", PF_FD },
// The process's threads, one directory each, named by thread id. A process
// here is one thread, so task/ holds a single entry, the pid, and it *is*
// /proc/<pid> -- the same inode. htop reads every process through it and
// shows nothing at all without it.
{ "task", PF_TASK },
};
#define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0])))
@ -201,8 +211,16 @@ gen_pid_stat(struct sb *s, const struct procinfo *pi)
// header and nothing else: ps with no arguments shows only the processes on
// *this* terminal, and nothing could ever match. `ps -e` always worked.
sbputc(s, ' ');
sbputi(s, pi->ctty ? ((1 << 8) | (10 + pi->ctty)) : 0);
sbputs(s, " -1 0"); // 8 tpgid, 9 flags
// A pty is Linux's (136, N), the same number stat() gives /dev/pts/N.
if (pi->ctty >= PTY_CTTY)
sbputi(s, (136 << 8) | (pi->ctty - PTY_CTTY));
else
sbputi(s, pi->ctty ? ((1 << 8) | (10 + pi->ctty)) : 0);
// 8 tpgid, 9 flags. PF_KTHREAD (0x00200000) on a kernel thread is how htop
// and ps tell one from a process and hide it; without it acpi and dbgd
// listed as programs of four bytes each.
sbputs(s, " -1 ");
sbputi(s, pi->kthread ? 0x00200000 : 0);
// 10..13: minflt cminflt majflt cmajflt. There is no fault accounting, and
// a fabricated number is worse than none -- top would draw a plausible graph
@ -320,18 +338,20 @@ procfs_iload(struct inode *ip)
ip->valid = 1;
ip->nlink = 1;
ip->major = ip->minor = 0;
// Read-only, with two exceptions: /proc/sysrq is the deliberate write target
// (`echo c > /proc/sysrq` to crash on purpose), and /proc/schinit is where
// PID 1 publishes its job table. Marking everything 2 made filewrite()
// Read-only, with three exceptions: /proc/sysrq is the deliberate write
// target (`echo c > /proc/sysrq` to crash on purpose), /proc/schinit is where
// PID 1 publishes its job table, and /proc/strace takes a name to trace. Marking everything 2 made filewrite()
// refuse with "permission denied", which the USB suite caught immediately --
// three of its checks drive the disk through sysrq.
ip->rwmode = (INO_IS_GLOBAL(ino) &&
((ino & 0xFF) == 10 || (ino & 0xFF) == 16)) ? 3 : 2;
((ino & 0xFF) == 10 || (ino & 0xFF) == 16 ||
(ino & 0xFF) == 31)) ? 3 : 2;
ip->showmode = 1;
ip->atime = ip->mtime = ip->ctime = 0;
if (ino == PROCFS_ROOT ||
(!INO_IS_GLOBAL(ino) && (INO_FILE(ino) == PF_DIR || INO_FILE(ino) == PF_FD))) {
(!INO_IS_GLOBAL(ino) && (INO_FILE(ino) == PF_DIR || INO_FILE(ino) == PF_FD ||
INO_FILE(ino) == PF_TASK))) {
ip->type = T_DIR;
ip->size = 0;
ip->nlink = 2;
@ -460,6 +480,8 @@ procfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
// should be doing, and PID 1 already knows when the table changed.
if (INO_IS_GLOBAL(ip->inum) && (ip->inum & 0xFF) == 16)
return proc_write_schinit(user_src, src, n);
if (INO_IS_GLOBAL(ip->inum) && (ip->inum & 0xFF) == 31)
return proc_write_strace(user_src, src, n);
(void)off;
return -EROFS;
}
@ -495,8 +517,16 @@ procfs_dirlookup(struct inode *dp, char *name, uint *poff)
if (namecmp(name, ".") == 0)
return iget(dp->dev, dp->inum);
if (namecmp(name, "..") == 0) // /proc/<pid>/.. is /proc
return iget(dp->dev, INO_FILE(dp->inum) == PF_FD ? (dp->inum & ~0xFFULL)
: PROCFS_ROOT);
return iget(dp->dev, INO_FILE(dp->inum) == PF_FD ||
INO_FILE(dp->inum) == PF_TASK ? (dp->inum & ~0xFFULL)
: PROCFS_ROOT);
// Inside /proc/<pid>/task/: the one thread, which is the process.
if (dp->inum != PROCFS_ROOT && INO_FILE(dp->inum) == PF_TASK) {
if (name_to_pid(name) != INO_PID(dp->inum))
return 0;
return iget(dp->dev, (dp->inum & ~0xFFULL) | PF_DIR);
}
// Inside /proc/<pid>/fd/: a number that is an open descriptor.
if (dp->inum != PROCFS_ROOT && INO_FILE(dp->inum) == PF_FD) {
@ -631,12 +661,18 @@ procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off)
utoa((uint)i, name);
EMIT((dp->inum & ~0xFFULL) | PF_FDENT | i, DT_LNK_, name);
}
} else if (INO_FILE(dp->inum) == PF_TASK) {
EMIT(dp->inum, DT_DIR_, ".");
EMIT(dp->inum & ~0xFFULL, DT_DIR_, "..");
utoa((uint)INO_PID(dp->inum), name);
EMIT((dp->inum & ~0xFFULL) | PF_DIR, DT_DIR_, name);
} else {
EMIT(dp->inum, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
for (i = 0; i < NPIDFILES; i++)
EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id,
pidfiles[i].id == PF_FD ? DT_DIR_ : pidfiles[i].id == PF_EXE ? DT_LNK_ : DT_REG_,
pidfiles[i].id == PF_FD || pidfiles[i].id == PF_TASK ? DT_DIR_ :
pidfiles[i].id == PF_EXE ? DT_LNK_ : DT_REG_,
pidfiles[i].name);
}

View file

@ -100,6 +100,7 @@ schfs_vfs_iload(struct inode *ip)
ip->nlink = in.nlink > 0x7fffffffu ? 0x7fffffff : (int)in.nlink;
ip->size = in.size;
ip->rwmode = RW;
ip->mode = in.mode & 07777; // 0 on disk: never recorded
ip->supermode = 0;
ip->showmode = 1;
ip->atime = (uint)in.atime;
@ -134,6 +135,8 @@ schfs_vfs_istore(struct inode *ip)
in.mode = ip->type == T_DIR ? 0755 : 0644;
}
if (ip->mode)
in.mode = ip->mode & 07777;
in.type = type_from_vfs(ip->type);
in.rdev_major = (uint16_t)ip->major;
in.rdev_minor = (uint16_t)ip->minor;

View file

@ -125,6 +125,11 @@ alloc_locked(int type)
s->ref = 1;
s->type = type;
s->state = SS_UNBOUND;
if(myproc()){
s->cred_pid = myproc()->pid;
s->cred_uid = myproc()->euid;
s->cred_gid = myproc()->egid;
}
return s;
}
}
@ -852,6 +857,9 @@ sockconnect(struct socket *s, uint dev, uint64 ino)
peer->peer = s;
peer->state = SS_CONNECTED;
peer->cred_pid = srv->cred_pid;
peer->cred_uid = srv->cred_uid;
peer->cred_gid = srv->cred_gid;
s->peer = peer;
s->state = SS_CONNECTED;
@ -1016,3 +1024,21 @@ socksendto(struct socket *s, char *buf, int n, uint addr, ushort port)
return -EAFNOSUPPORT; // an AF_INET destination on an AF_UNIX socket
return sockwrite(s, buf, n);
}
// SO_PEERCRED: the credentials recorded for the far end of a connected Unix
// socket. -1 if there is no far end.
int
sock_peercred(struct socket *s, int *pid, uint *uid, uint *gid)
{
int r = -1;
acquire(&stab.lock);
if(s->domain != AF_INET && s->peer){
*pid = s->peer->cred_pid;
*uid = s->peer->cred_uid;
*gid = s->peer->cred_gid;
r = 0;
}
release(&stab.lock);
return r;
}

View file

@ -25,6 +25,10 @@ struct {
struct proc proc[NPROC];
} ptable;
// CPU time of every process already reaped, so that /proc/stat's busy time
// does not drop each time one is. Under ptable.lock, like the processes.
static uint64 reaped_utime, reaped_stime;
// Kernel stacks, one per ptable slot, statically.
//
// **They used to be a single kalloc() page**, and a measurement said that was
@ -313,6 +317,8 @@ found:
p->pending_signals = 0;
p->alarm_at = p->alarm_every = 0; // a child's timers start disarmed
p->utime = p->stime = p->cutime = p->cstime = 0;
p->strace = 0;
p->umask = 022;
// A recycled slot must not inherit a dead process's place in the queue --
// in either direction. sched_init_proc starts it at min_vruntime, which is
@ -509,6 +515,8 @@ fork(void)
np->fs_base = myproc()->fs_base;
safestrcpy(np->name, myproc()->name, sizeof(myproc()->name));
np->strace = myproc()->strace;
np->umask = myproc()->umask;
// The child inherits the parent's place in the queue, priority and all.
//
@ -746,6 +754,8 @@ waitpid(int pid, int *status, int options, uint64 *ut, uint64 *st)
// of processes rather than one generation deep.
myproc()->cutime += p->utime + p->cutime;
myproc()->cstime += p->stime + p->cstime;
reaped_utime += p->utime;
reaped_stime += p->stime;
// wait4()'s fourth argument wants the *child's* usage, not the
// parent's running total, so it has to be read here -- three lines
// further down the slot is UNUSED and these are zero.
@ -1786,6 +1796,11 @@ proc_setsid(void)
}
myproc()->sid = myproc()->pid;
myproc()->pgid = myproc()->pid;
// A new session starts with no controlling terminal; that is what lets the
// first terminal it opens become one. Keeping the old one meant a tmux pane
// still had tty1 as its /dev/tty, and zsh and htop drew straight onto the
// console underneath tmux instead of into their pane.
myproc()->ctty = 0;
r = myproc()->sid;
release(&ptable.lock);
return r;
@ -1913,6 +1928,7 @@ proc_snapshot(int idx, struct procinfo *out)
out->nivcsw = p->nivcsw;
safestrcpy(out->name, p->name, sizeof(out->name));
safestrcpy(out->exe, p->exe, sizeof(out->exe));
out->kthread = p->kfn != 0;
release(&ptable.lock);
return 0;
}
@ -2012,3 +2028,24 @@ proc_kill_init(void)
if (initproc)
initproc->killed = 1;
}
// Ticks spent in user and kernel mode by every process, living or reaped.
// What /proc/stat's busy columns are made of; the rest of the time was idle.
void
proc_cputime(uint64 *user, uint64 *sys)
{
struct proc *p;
uint64 u, s;
acquire(&ptable.lock);
u = reaped_utime;
s = reaped_stime;
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++)
if (p->state != UNUSED) {
u += p->utime;
s += p->stime;
}
release(&ptable.lock);
*user = u;
*sys = s;
}

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");
}