SchOS/kernel/dev/pty.c
Nexoniarz 020aa6113c Users and permissions: accounts, login, enforced file permissions, setuid, su, passwd, doas
The kernel now checks owner/group/other on open, create, unlink, rename, exec
and path search; honours set-user-id and set-group-id; restricts kill, mount,
reboot and mknod; keeps supplementary groups; and implements chown. devtmpfs
nodes carry owners and modes that chmod/chown keep. Terminals run login, the
image has root and schos, X starts as the ordinary user.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
2026-10-26 13:16:07 +01:00

720 lines
17 KiB
C

// 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;
}
devfs_pty_claim(n, myproc() ? myproc()->euid : 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;
}
if (sig_attention(myproc())) {
release(&ptylock);
int e = sig_blocking_check();
if (e) {
ilock(ip);
return got ? got : e;
}
acquire(&ptylock);
continue;
}
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;
}