/* * Copyright (c) 2015, 2016, 2021, 2022 Jonas 'Sortie' Termansen. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * pty.cpp * Pseudoterminals. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pty.h" #include "tty.h" #define ULONG_BIT (sizeof(unsigned long) * CHAR_BIT) #define PTY_LIMIT (1024 * 1024) namespace Sortix { struct PTS::Entry { char name[10 + 1]; ino_t ino; Ref inode; }; Ref pts; static kthread_mutex_t ptynum_lock = KTHREAD_MUTEX_INITIALIZER; static unsigned long* ptynum_bitmap = NULL; static size_t ptynum_bitmap_words = 0; static size_t ptynum_none_below = 0; static int AllocatePTYNumber() { ScopedLock lock(&ptynum_lock); for ( size_t i = ptynum_none_below/ULONG_BIT; i < ptynum_bitmap_words; i++ ) { unsigned long word = ptynum_bitmap[i]; if ( word == ULONG_MAX ) continue; for ( size_t n = 0; n < ULONG_BIT; n++ ) { unsigned long mask = 1UL << n; if ( word & mask ) continue; ptynum_bitmap[i] = word | mask; size_t result = i * ULONG_MAX + n; if ( PTY_LIMIT < result || INT_MAX < result ) { ptynum_none_below = result; return errno = EMFILE, -1; } ptynum_none_below = result + 1; return result; } } size_t new_words = 2 * ptynum_bitmap_words; if ( !new_words ) new_words = 4; if ( PTY_LIMIT / ULONG_BIT < new_words ) new_words = PTY_LIMIT / ULONG_BIT; if ( new_words <= ptynum_bitmap_words ) return errno = EMFILE, -1; unsigned long* new_bitmap = (unsigned long*) reallocarray(ptynum_bitmap, new_words, sizeof(unsigned long)); if ( !new_bitmap ) return -1; for ( size_t i = ptynum_bitmap_words; i < new_words; i++ ) new_bitmap[i] = 0; size_t result = ptynum_bitmap_words * ULONG_BIT; new_bitmap[ptynum_bitmap_words] |= 1UL << 0; ptynum_bitmap = new_bitmap; ptynum_bitmap_words = new_words; ptynum_none_below = result + 1; return result; } static void FreePTYNumber(int ptynum) { assert(0 <= ptynum); ScopedLock lock(&ptynum_lock); assert((size_t) ptynum < ptynum_bitmap_words * ULONG_BIT); size_t word = ptynum / ULONG_BIT; size_t bit = ptynum % ULONG_BIT; unsigned long mask = 1UL << bit; assert(ptynum_bitmap[word] & mask); ptynum_bitmap[word] &= ~mask; if ( (size_t) ptynum < ptynum_none_below ) ptynum_none_below = ptynum; } static ssize_t common_tcgetblob(ioctx_t* ctx, const char* name, void* buffer, size_t count) { if ( !name ) { static const char index[] = "device-path\0filesystem-type\0"; size_t index_size = sizeof(index) - 1; if ( buffer && count < index_size ) return errno = ERANGE, -1; if ( buffer && !ctx->copy_to_dest(buffer, &index, index_size) ) return -1; return (ssize_t) index_size; } else if ( !strcmp(name, "device-path") ) { const char* data = "none"; size_t size = strlen(data); if ( buffer && count < size ) return errno = ERANGE, -1; if ( buffer && !ctx->copy_to_dest(buffer, data, size) ) return -1; return (ssize_t) size; } else if ( !strcmp(name, "filesystem-type") ) { const char* data = "pts"; size_t size = strlen(data); if ( buffer && count < size ) return errno = ERANGE, -1; if ( buffer && !ctx->copy_to_dest(buffer, data, size) ) return -1; return (ssize_t) size; } else return errno = ENOENT, -1; } int common_statvfs(ioctx_t* ctx, struct statvfs* stvfs, dev_t dev) { struct statvfs retstvfs; memset(&retstvfs, 0, sizeof(retstvfs)); retstvfs.f_bsize = 0; retstvfs.f_frsize = 0; retstvfs.f_blocks = 0; retstvfs.f_bfree = 0; retstvfs.f_bavail = 0; retstvfs.f_files = 0; retstvfs.f_ffree = 0; retstvfs.f_favail = 0; retstvfs.f_fsid = dev; retstvfs.f_flag = ST_NOSUID; retstvfs.f_namemax = 10; /* ceil(log(INT_MAX)/log(10)) */ if ( !ctx->copy_to_dest(stvfs, &retstvfs, sizeof(retstvfs)) ) return -1; return 0; } class PTMX : public AbstractInode { public: PTMX(dev_t dev, ino_t ino, mode_t mode, uid_t owner, gid_t group); virtual ~PTMX(); public: virtual Ref factory(ioctx_t* ctx, const char* filename, int flags, mode_t mode); }; PTS::PTS(mode_t mode, uid_t owner, gid_t group) { inode_type = INODE_TYPE_DIR; dev = (dev_t) this; ino = 0; stat_gid = owner; stat_gid = group; type = S_IFDIR; stat_mode = (mode & S_SETABLE) | type; dirlock = KTHREAD_MUTEX_INITIALIZER; entries = NULL; entries_count = 0; entries_allocated = 0; } PTS::~PTS() { } bool PTS::ContainsFile(const char* name) // dirlock held { if ( !strcmp(name, ".") || !strcmp(name, "..") || !strcmp(name, "ptmx") ) return true; for ( size_t i = 0; i < entries_count; i++ ) if ( !strcmp(entries[i].name, name) ) return true; return false; } ssize_t PTS::readdirents(ioctx_t* ctx, struct dirent* dirent, size_t size, off_t start) { static const char* const names[3] = { ".", "..", "ptmx" }; static const ino_t inos[3] = { 0, 0, 1 }; static const unsigned char dtypes[3] = { DT_DIR, DT_DIR, DT_CHR }; struct dirent retdirent; memset(&retdirent, 0, sizeof(retdirent)); retdirent.d_dev = dev; const char* name; ino_t ino; unsigned char dtype; ScopedLock lock(&dirlock); if ( start < 3 ) { name = names[start]; ino = inos[start]; dtype = dtypes[start]; } else { start -= 3; if ( (uintmax_t) entries_count <= (uintmax_t) start ) return 0; name = entries[start].name; ino = entries[start].ino; dtype = DT_CHR; } size_t namelen = strlen(name); retdirent.d_reclen = sizeof(*dirent) + namelen + 1; retdirent.d_namlen = namelen; retdirent.d_ino = ino; retdirent.d_type = dtype; if ( !ctx->copy_to_dest(dirent, &retdirent, sizeof(retdirent)) ) return -1; if ( size < retdirent.d_reclen ) return errno = ERANGE, -1; if ( !ctx->copy_to_dest(dirent->d_name, name, namelen+1) ) return -1; return (ssize_t) retdirent.d_reclen; } Ref PTS::open(ioctx_t* /*ctx*/, const char* filename, int flags, mode_t /*mode*/) { ScopedLock lock(&dirlock); if ( ContainsFile(filename) ) { Ref result; if ( (flags & O_CREATE) && (flags & O_EXCL) ) return errno = EEXIST, Ref(NULL); if ( !strcmp(filename, ".") || !strcmp(filename, "..") ) result = Ref(this); else if ( !strcmp(filename, "ptmx") ) return Ref(new PTMX(dev, 1, 0666, 0, 0)); else { for ( size_t i = 0; !result && i < entries_count; i++ ) if ( !strcmp(filename, entries[i].name) ) result = entries[i].inode; } if ( result ) { if ( (flags & O_CREATE) && (flags & O_EXCL) ) return errno = EEXIST, Ref(NULL); return result; } } if ( !(flags & O_CREATE) ) return errno = ENOENT, Ref(NULL); return errno = EPERM, Ref(NULL); } int PTS::mkdir(ioctx_t* /*ctx*/, const char* filename, mode_t /*mode*/) { ScopedLock lock(&dirlock); if ( ContainsFile(filename) ) return errno = EEXIST, -1; return errno = EPERM, -1; } int PTS::link(ioctx_t* /*ctx*/, const char* filename, Ref /*node*/) { ScopedLock lock(&dirlock); if ( ContainsFile(filename) ) return errno = EEXIST, -1; return errno = EPERM, -1; } int PTS::link_raw(ioctx_t* /*ctx*/, const char* filename, Ref /*node*/) { ScopedLock lock(&dirlock); if ( ContainsFile(filename) ) return errno = EEXIST, -1; return errno = EPERM, -1; } int PTS::unlink(ioctx_t* /*ctx*/, const char* filename) { ScopedLock lock(&dirlock); if ( !ContainsFile(filename) ) return errno = ENOENT, -1; return errno = EPERM, -1; } int PTS::unlink_raw(ioctx_t* /*ctx*/, const char* filename) { ScopedLock lock(&dirlock); if ( !ContainsFile(filename) ) return errno = ENOENT, -1; return errno = EPERM, -1; } int PTS::rmdir(ioctx_t* /*ctx*/, const char* filename) { ScopedLock lock(&dirlock); if ( !ContainsFile(filename) ) return errno = ENOENT, -1; return errno = EPERM, -1; } int PTS::rmdir_me(ioctx_t* /*ctx*/) { return errno = EPERM, -1; } int PTS::symlink(ioctx_t* /*ctx*/, const char* /*oldname*/, const char* filename) { ScopedLock lock(&dirlock); if ( ContainsFile(filename) ) return errno = EEXIST, -1; return errno = EPERM, -1; } int PTS::rename_here(ioctx_t* /*ctx*/, Ref /*from*/, const char* /*oldname*/, const char* /*newname*/) { return errno = EPERM, -1; } ssize_t PTS::tcgetblob(ioctx_t* ctx, const char* name, void* buffer, size_t count) { return common_tcgetblob(ctx, name, buffer, count); } int PTS::statvfs(ioctx_t* ctx, struct statvfs* stvfs) { return common_statvfs(ctx, stvfs, dev); } bool PTS::RegisterPTY(Ref pty, int ptynum) { ino_t ino; if ( __builtin_add_overflow(ptynum, 2, &ino) ) return errno = EMFILE, false; ScopedLock lock(&dirlock); if ( entries_count == entries_allocated ) { size_t new_allocated = 2 * entries_allocated; if ( !new_allocated) new_allocated = 16; struct Entry* new_entries = new Entry[new_allocated]; if ( !new_entries ) return false; for ( size_t i = 0; i < entries_count; i++ ) { new_entries[i] = entries[i]; entries[i].inode.Reset(); } delete[] entries; entries = new_entries; entries_allocated = new_allocated; } struct Entry* entry = &entries[entries_count++]; snprintf(entry->name, sizeof(entry->name), "%i", ptynum); entry->ino = ino; entry->inode = pty; return true; } void PTS::UnregisterPTY(int ptynum) { ino_t ino = (ino_t) 2 + (ino_t) ptynum; ScopedLock lock(&dirlock); bool found = false; for ( size_t i = 0; i < entries_count; i++ ) { if ( entries[i].ino == ino ) { entries[i].inode.Reset(); if ( i + 1 != entries_count ) { entries[i] = entries[entries_count-1]; entries[entries_count-1].inode.Reset(); } entries_count--; found = true; break; } } assert(found); if ( 16 < entries_allocated && entries_count <= entries_allocated / 4 ) { size_t new_allocated = entries_allocated / 2; struct Entry* new_entries = new Entry[new_allocated]; if ( !new_entries ) return; for ( size_t i = 0; i < entries_count; i++ ) { new_entries[i] = entries[i]; entries[i].inode.Reset(); } delete[] entries; entries = new_entries; entries_allocated = new_allocated; } } class PTY : public TTY { public: PTY(dev_t dev, ino_t ino, mode_t mode, uid_t owner, gid_t group, int ptynum); virtual ~PTY(); public: virtual int sync(ioctx_t* ctx); virtual int ioctl(ioctx_t* ctx, int cmd, uintptr_t arg); public: ssize_t master_read(ioctx_t* ctx, uint8_t* buf, size_t count); ssize_t master_write(ioctx_t* ctx, const uint8_t* buf, size_t count); int master_poll(ioctx_t* ctx, PollNode* node); int master_ioctl(ioctx_t* ctx, int cmd, uintptr_t arg); protected: virtual void tty_output(const unsigned char* buffer, size_t length); private: short PollMasterEventStatus(); private: PollChannel master_poll_channel; struct winsize ws; kthread_cond_t output_ready_cond; kthread_cond_t output_possible_cond; size_t output_offset; size_t output_used; static const size_t output_size = 4096; // TODO: This is not safe because ^W can produce unbounded output. static const size_t output_probably_safe = 2048; uint8_t output[output_size]; int ptynum; }; PTY::PTY(dev_t dev, ino_t ino, mode_t mode, uid_t owner, gid_t group, int ptynum) : TTY(dev, ino, mode, owner, group, "") { tio.c_cflag |= CREAD; output_ready_cond = KTHREAD_COND_INITIALIZER; output_possible_cond = KTHREAD_COND_INITIALIZER; output_offset = 0; output_used = 0; memset(&ws, 0, sizeof(ws)); this->ptynum = ptynum; snprintf(ttyname, sizeof(ttyname), "pts/%i", ptynum); } PTY::~PTY() { FreePTYNumber(ptynum); } ssize_t PTY::master_read(ioctx_t* ctx, uint8_t* buf, size_t count) { ScopedLockSignal lock(&termlock); if ( !lock.IsAcquired() ) return errno = EINTR, -1; while ( !output_used ) { if ( ctx->dflags & O_NONBLOCK ) return errno = EWOULDBLOCK, -1; if ( !kthread_cond_wait_signal(&output_ready_cond, &termlock) ) return errno = EINTR, -1; } size_t sofar = 0; while ( output_used && sofar < count ) { size_t limit = output_size - output_offset; size_t possible = limit < output_used ? limit : output_used; size_t left = count - sofar; size_t amount = left < possible ? left : possible; if ( !ctx->copy_to_dest(buf + sofar, output + output_offset, amount) ) return sofar ? (ssize_t) sofar : -1; output_used -= amount; output_offset += amount; if ( output_offset == output_size ) output_offset = 0; sofar += amount; kthread_cond_broadcast(&output_possible_cond); } return (ssize_t) sofar; } // TODO: Have this be non-blocking and have master_poll_channel signal POLLOUT // only when it won't block. ssize_t PTY::master_write(ioctx_t* ctx, const uint8_t* buf, size_t count) { ScopedLockSignal lock(&termlock); if ( !lock.IsAcquired() ) return errno = EINTR, -1; // TODO: Work around deadlock by refusing writes when the buffer is starting // to be too full. This can block / "deadlock" too if the caller // didn't try to read the data written by a previous call. while ( output_probably_safe <= output_used ) { if ( ctx->dflags & O_NONBLOCK ) return errno = EWOULDBLOCK, -1; if ( !kthread_cond_wait_signal(&output_ready_cond, &termlock) ) return errno = EINTR, -1; } size_t sofar = 0; while ( sofar < count ) { uint8_t input[1024]; size_t left = count - sofar; size_t amount = left < sizeof(input) ? left : sizeof(input); if ( !ctx->copy_from_src(input, buf + sofar, amount) ) return sofar ? (ssize_t) sofar : -1; for ( size_t i = 0; i < amount; i++ ) { if ( Signal::IsPending() ) return sofar ? (ssize_t) sofar : (errno = EINTR, -1); // TODO: Unfortunately sequences like ^W can cause an unbounded // number of tty_output data causing a deadlock. ProcessByte(input[i]); sofar++; if ( output_probably_safe <= output_used ) return (ssize_t) sofar; } } return (ssize_t) sofar; } // TODO: This function can deadlock if data is written using master_write, with // tty ECHO on, then it echos the input through this function, but the // output buffer is full, so it blocks. But there only was a single thread // using the pty, which did a write, and now is waiting for itself to // read. Either this is a broken usage of pty's and you must have a // dedicated read thread, or need a dedicated kernel thread for each pty // that buffers up a large amount of input, then processes it at its own // pace. // TODO: Alternatively the master is supposed to use non-blocking writes and // check for pending input as well. This function needs to be changed to // allow non-blocking input as well, needs an ioctx and ability to do // partial work. void PTY::tty_output(const unsigned char* buffer, size_t length) // termlock held { while ( length ) { while ( output_used == output_size ) if ( !kthread_cond_wait_signal(&output_possible_cond, &termlock) ) return; // TODO: Data loss? size_t offset = output_offset + output_used; if ( output_size <= offset ) offset -= output_size; size_t left = output_size - output_used; size_t end = offset + left; if ( output_size < end ) end = output_size; size_t possible = end - offset; size_t amount = length < possible ? length : possible; memcpy(output + offset, buffer, amount); buffer += amount; length -= amount; output_used += amount; kthread_cond_broadcast(&output_ready_cond); master_poll_channel.Signal(POLLIN | POLLRDNORM); } } short PTY::PollMasterEventStatus() { short status = 0; if ( output_used ) status |= POLLIN | POLLRDNORM; if ( output_used < output_probably_safe ) status |= POLLOUT | POLLWRNORM; return status; } int PTY::master_poll(ioctx_t* /*ctx*/, PollNode* node) { ScopedLock lock(&termlock); short ret_status = PollMasterEventStatus() & node->events; if ( ret_status ) { node->master->revents |= ret_status; return 0; } master_poll_channel.Register(node); return errno = EAGAIN, -1; } int PTY::sync(ioctx_t* /*ctx*/) { ScopedLock lock(&termlock); if ( hungup ) return errno = EIO, -1; return 0; } int PTY::ioctl(ioctx_t* ctx, int cmd, uintptr_t arg) { ScopedLock lock(&termlock); if ( hungup ) return errno = EIO, -1; if ( cmd == TIOCGWINSZ ) { struct winsize* user_ws = (struct winsize*) arg; if ( !ctx->copy_to_dest(user_ws, &ws, sizeof(ws)) ) return -1; return 0; } else if ( cmd == TIOCGPTN ) { int* arg_ptr = (int*) arg; if ( !ctx->copy_to_dest(arg_ptr, &ptynum, sizeof(ptynum)) ) return -1; return 0; } lock.Reset(); return TTY::ioctl(ctx, cmd, arg); } int PTY::master_ioctl(ioctx_t* ctx, int cmd, uintptr_t arg) { if ( cmd == TIOCSWINSZ ) { ScopedLock lock(&termlock); const struct winsize* user_ws = (const struct winsize*) arg; if ( !ctx->copy_from_src(&ws, user_ws, sizeof(ws)) ) return -1; return 0; } return ioctl(ctx, cmd, arg); } class MasterNode : public AbstractInode { public: MasterNode(uid_t owner, gid_t group, mode_t mode, Ref pty, int ptynum); virtual ~MasterNode(); virtual ssize_t read(ioctx_t* ctx, uint8_t* buf, size_t count); virtual ssize_t write(ioctx_t* ctx, const uint8_t* buf, size_t count); virtual int poll(ioctx_t* ctx, PollNode* node); virtual int ioctl(ioctx_t* ctx, int cmd, uintptr_t arg); public: Ref pty; int ptynum; }; MasterNode::MasterNode(uid_t owner, gid_t group, mode_t mode, Ref pty, int ptynum) : pty(pty) { inode_type = INODE_TYPE_TTY; this->dev = (dev_t) this; this->ino = (ino_t) this; this->stat_uid = owner; this->stat_gid = group; this->type = S_IFCHR; this->stat_mode = (mode & S_SETABLE) | this->type; this->ptynum = ptynum; } MasterNode::~MasterNode() { pts->UnregisterPTY(ptynum); pty->hup(); } ssize_t MasterNode::read(ioctx_t* ctx, uint8_t* buf, size_t count) { return pty->master_read(ctx, buf, count); } ssize_t MasterNode::write(ioctx_t* ctx, const uint8_t* buf, size_t count) { return pty->master_write(ctx, buf, count); } int MasterNode::poll(ioctx_t* ctx, PollNode* node) { return pty->master_poll(ctx, node); } int MasterNode::ioctl(ioctx_t* ctx, int cmd, uintptr_t arg) { return pty->master_ioctl(ctx, cmd, arg); } PTMX::PTMX(dev_t dev, ino_t ino, mode_t mode, uid_t owner, gid_t group) { inode_type = INODE_TYPE_TTY; this->dev = dev; this->ino = ino; this->type = S_IFFACTORY | S_IFFACTORY_NOSTAT; this->stat_mode = (mode & S_SETABLE) | S_IFCHR; this->stat_uid = owner; this->stat_gid = group; } PTMX::~PTMX() { } Ref PTMX::factory(ioctx_t* ctx, const char* filename, int flags, mode_t mode) { (void) ctx; (void) filename; (void) flags; (void) mode; Process* process = CurrentProcess(); uid_t uid = process->uid; uid_t gid = process->gid; mode_t new_mode = 0620; int ptynum = AllocatePTYNumber(); if ( ptynum < 0 ) return Ref(NULL); Ref slave_inode(new PTY(pts->dev, 2 + (ino_t) ptynum, mode, uid, gid, ptynum)); if ( !slave_inode ) return FreePTYNumber(ptynum), Ref(NULL); if ( !pts->RegisterPTY(slave_inode, ptynum) ) return Ref(NULL); Ref master_inode(new MasterNode(uid, gid, new_mode, slave_inode, ptynum)); if ( !master_inode ) { pts->UnregisterPTY(ptynum); return Ref(NULL); } return master_inode; } int sys_mkpty(int* master_fd_user, int* slave_fd_user, int flags) { int fdflags = 0; if ( flags & O_CLOEXEC ) fdflags |= FD_CLOEXEC; if ( flags & O_CLOFORK ) fdflags |= FD_CLOFORK; flags &= ~(O_CLOEXEC | O_CLOFORK); if ( flags & ~(O_NONBLOCK) ) return errno = EINVAL, -1; Process* process = CurrentProcess(); uid_t uid = process->uid; uid_t gid = process->gid; mode_t mode = 0620; int ptynum = AllocatePTYNumber(); if ( ptynum < 0 ) return -1; Ref slave_inode(new PTY(pts->dev, 2 + ptynum, mode, uid, gid, ptynum)); if ( !slave_inode ) return FreePTYNumber(ptynum), -1; if ( !pts->RegisterPTY(slave_inode, ptynum) ) return -1; Ref master_inode(new MasterNode(uid, gid, mode, slave_inode, ptynum)); if ( !master_inode ) { pts->UnregisterPTY(ptynum); return -1; } Ref master_vnode(new Vnode(master_inode, Ref(NULL), 0, 0)); Ref slave_vnode(new Vnode(slave_inode, Ref(NULL), 0, 0)); master_inode.Reset(); slave_inode.Reset(); if ( !master_vnode || !slave_vnode ) return -1; Ref master_desc( new Descriptor(master_vnode, O_READ | O_WRITE | flags)); Ref slave_desc( new Descriptor(slave_vnode, O_READ | O_WRITE | flags)); master_vnode.Reset(); slave_vnode.Reset(); if ( !master_desc || !slave_desc ) return -1; Ref dtable = process->GetDTable(); int reservation = 0; if ( !dtable->Reserve(2, &reservation) ) return -1; int master_fd = dtable->Allocate(master_desc, fdflags, 0, &reservation); int slave_fd = dtable->Allocate(slave_desc, fdflags, 0, &reservation); assert(0 <= master_fd); assert(0 <= slave_fd); master_desc.Reset(); slave_desc.Reset(); dtable.Reset(); if ( !CopyToUser(master_fd_user, &master_fd, sizeof(int)) || !CopyToUser(slave_fd_user, &slave_fd, sizeof(int)) ) return -1; return 0; } } // namespace Sortix