SchOS/kernel/fs/file.c
Nexoniarz 8351e0323b kernel: HTTPS through wget, half-close, and /proc/<pid>/fd
busybox wget hands TLS to ssl_client over a socketpair and then does
shutdown(SHUT_WR). That cut the AF_UNIX link both ways, so ssl_client's
reply was a SIGPIPE and every https:// download hung. SHUT_WR is now a
half-close: the peer reads EOF and can still answer. TCP sockets get
shutdown() at all (tcp_shutdown for the FIN); it used to do nothing.

- exec keeps ignored signals and the signal mask, as execve(2) says.
- /proc/<pid>/fd: a link per descriptor -- the path it was opened by,
  socket:[n], pipe:[n], anon_inode:[eventpoll]. /proc/self too. The
  global inode bit moves above pid << 8; at 0x10000 it was /proc/256.
- fstat on a socket or pipe answers S_IFSOCK / S_IFIFO instead of EINVAL.
- poll, select and epoll wake as soon as a pipe, socket or input device
  changes, not on the next tick.
- Pipes are 64 KB of lazily allocated pages, copied in blocks, with
  PIPE_BUF atomicity: 300 MB through `yes | head | wc` in 7 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 20:28:03 +02:00

436 lines
12 KiB
C

#include "core/types.h"
#include "core/defs.h"
#include "uapi/stat.h"
#include "core/param.h"
#include "fs/fs.h"
#include "core/spinlock.h"
#include "core/sleeplock.h"
#include "fs/file.h"
#include "fs/fsstat.h"
#include "uapi/posix.h"
#include "uapi/errno.h"
#include "core/kmem.h"
extern struct devsw devsw[];
// Open file descriptions, shared by every process. NFILE of them in the
// kernel image and more from a slab cache once those are all open; a closed
// one goes on the free list. This was NFILE and then ENFILE, which a few
// processes each holding a hundred descriptors -- an editor, a build, a
// shell -- were already a third of the way to.
struct {
struct spinlock lock;
struct file file[NFILE];
struct file *free;
struct kmem_cache *more;
uint n;
uint64 serial; // the last ino handed out
} ftable;
void
fileinit(void)
{
initlock(&ftable.lock, "ftable");
for(int i = NFILE - 1; i >= 0; i--){
ftable.file[i].fnext = ftable.free;
ftable.free = &ftable.file[i];
}
ftable.n = NFILE;
}
uint
file_slots(void)
{
return ftable.n;
}
struct file*
filealloc(void)
{
struct file *f;
acquire(&ftable.lock);
if((f = ftable.free) != 0){
ftable.free = f->fnext;
} else {
// The slab allocator takes only its own lock and kalloc's.
if(ftable.more == 0)
ftable.more = kmem_cache_create("file", sizeof(struct file));
if(ftable.more == 0 || (f = kmem_cache_alloc(ftable.more)) == 0){
release(&ftable.lock);
return 0;
}
memset(f, 0, sizeof *f);
ftable.n++;
}
f->ref = 1;
f->ino = ++ftable.serial;
f->path = 0;
FSSTAT_TAKE(fsstat.file_used, fsstat.file_peak); // 0 -> 1
// Cleared here rather than left to the caller: every creator sets
// readable/writable/off, but only open() sets nonblock, so a struct file
// recycled from a non-blocking device would hand its flag to the next
// pipe or socket that happened to land in this slot.
f->nonblock = 0;
release(&ftable.lock);
return f;
}
struct file*
filedup(struct file *f)
{
acquire(&ftable.lock);
if(f->ref < 1)
panic("filedup");
f->ref++;
release(&ftable.lock);
return f;
}
void
fileclose(struct file *f)
{
struct file ff;
acquire(&ftable.lock);
if(f->ref < 1)
panic("fileclose");
if(--f->ref > 0){
release(&ftable.lock);
return;
}
// Its flock() and OFD locks go with it, before the slot can be handed to
// someone else: they are keyed by this address.
lock_close_file(f);
ff = *f;
f->ref = 0;
f->path = 0;
f->type = FD_NONE;
f->fnext = ftable.free;
ftable.free = f;
FSSTAT_DROP(fsstat.file_used); // 1 -> 0
release(&ftable.lock);
if(ff.path)
kmfree(ff.path);
if(ff.type == FD_MEMFD)
shm_detach(ff.memfd_seg); // the last mapper frees the pages
else if(ff.type == FD_EVENT)
ev_close(ff.ev);
else if(ff.type == FD_PIPE)
pipeclose(ff.pipe, ff.writable);
else if(ff.type == FD_SOCKET)
sockclose(ff.sock);
else if(ff.type == FD_INODE){
// Give a device a chance to put the hardware back before the inode goes.
// Done here rather than in exit() because a program that closes the
// framebuffer without exiting has finished with it just as thoroughly.
if(ff.ip && ff.ip->type == T_DEV && ff.ip->major >= 0 &&
ff.ip->major < NDEV && devsw[ff.ip->major].close)
devsw[ff.ip->major].close(ff.ip);
begin_op();
iput(ff.ip);
end_op();
}
}
// What /proc/<pid>/fd/N points at, in Linux's spelling: the path, or
// "socket:[ino]", "pipe:[ino]", "anon_inode:[eventpoll]". Returns the length.
// ftable.lock is what keeps `path` alive: fileclose() takes it from the file
// under the lock and frees it after.
int
file_describe(struct file *f, char *buf, int cap)
{
const char *pre = 0, *s = 0;
int n = 0;
acquire(&ftable.lock);
if(f->ref > 0){
switch(f->type){
case FD_INODE: s = f->path; if(!s) pre = "inode:["; break;
case FD_SOCKET: pre = "socket:["; break;
case FD_PIPE: pre = "pipe:["; break;
case FD_MEMFD: s = "/memfd: (deleted)"; break;
case FD_EVENT: s = ev_kind_name(f->ev); break;
default: break;
}
}
if(pre){
char num[24];
int k = 0;
uint64 v = f->type == FD_INODE && f->ip ? f->ip->inum : f->ino;
do { num[k++] = '0' + v % 10; v /= 10; } while(v);
while(*pre && n < cap) buf[n++] = *pre++;
while(k && n < cap) buf[n++] = num[--k];
if(n < cap) buf[n++] = ']';
} else if(s){
while(*s && n < cap) buf[n++] = *s++;
}
release(&ftable.lock);
return n;
}
int
filestat(struct file *f, struct stat *st)
{
// A memfd has no inode, so the size is the only field with an answer. It is
// the field that matters: mmap length and ftruncate round-trips are checked
// against it, and a caller that fstat()s to find out how big the buffer is
// gets the truth.
if(f->type == FD_MEMFD){
memset(st, 0, sizeof(*st));
st->type = T_FILE;
st->size = memfd_size(f->memfd_seg);
st->nlink = 1;
return 0;
}
if(f->type == FD_INODE){
ilock(f->ip);
stati(f->ip, st);
iunlock(f->ip);
return 0;
}
// An eventfd, timerfd, signalfd or epoll set: Linux calls these anonymous
// inodes, and fstat succeeds on them with nothing much to say. A program
// that fstat()s every descriptor it is handed should not fail on one.
if(f->type == FD_EVENT){
memset(st, 0, sizeof(*st));
st->nlink = 1;
return 0;
}
return -1;
}
int
fileread(struct file *f, char *addr, int n)
{
int r;
if(f->readable == 0)
return -1;
if(f->type == FD_EVENT)
return ev_read(f, addr, n);
// A memfd is a file: read() and write() move bytes in and out of the shared
// pages, and the descriptor carries its own offset like any other. mmap is
// what it is usually for, but a descriptor that refuses read() is a surprise
// nobody needs.
if(f->type == FD_MEMFD){
r = memfd_rw(f->memfd_seg, 1, (uint64)addr, f->off, n, 0);
if(r > 0)
f->off += r;
return r;
}
// O_NONBLOCK on a pipe or a socket. Reported here rather than inside
// piperead/sockread so that the blocking implementations stay unchanged:
// this is a check of the same condition their sleep loops wait on, made one
// level up. Writes still block -- fcntl documents that.
if(f->type == FD_PIPE){
// POLLHUP counts as readable here, and leaving it out was a bug: a pipe
// whose writers have all gone is not "nothing yet", it is end of file, and
// the answer to a read is 0. Told EAGAIN instead, a caller that polls and
// reads spins forever waiting for an end that has already happened.
// sockreadable() has always included the equivalent condition.
if(f->nonblock &&
!(pipe_poll(f->pipe, f->readable, f->writable) & (POLLIN | POLLHUP)))
return -EAGAIN;
return piperead(f->pipe, addr, n);
}
if(f->type == FD_SOCKET){
if(f->nonblock && !sockreadable(f->sock))
return -EAGAIN;
return sockread(f->sock, addr, n);
}
if(f->type == FD_INODE){
ilock(f->ip);
if(f->ip->type == 3){
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].read){
iunlock(f->ip);
return -1;
}
// O_NONBLOCK. Without this a game that opens an input device
// non-blocking and drains it once per frame stops dead on the first
// frame -- it blocks in read() waiting for a key, and the result looks
// exactly like a hung renderer rather than a blocking read.
if(f->nonblock && devsw[f->ip->major].ready &&
!devsw[f->ip->major].ready(f->ip)){
iunlock(f->ip);
return -EAGAIN;
}
r = devsw[f->ip->major].read(f->ip, f->off, addr, n);
if(r > 0)
f->off += r;
iunlock(f->ip);
return r;
}
if((f->ip->rwmode & 2) == 0){
cprintf("SchOS: permission denied (read)\n");
iunlock(f->ip);
return -1;
}
// The "recycle bin" check that used to sit here is gone.
//
// It made a file that still existed, still had a name and still had blocks
// refuse every read with a printf -- a state no POSIX program is prepared
// for. It was reachable only through `delete`, a NO_LINUX syscall from the
// third-party filesystem this file derives from, which the decision to keep
// only Linux numbering had already orphaned.
//
// The on-disk `showmode` field stays for now: removing it changes the inode
// layout, and this whole file is being replaced by SchFS. It is simply
// never consulted.
if((r = readi(f->ip, 0, (uint64)addr, f->off, n)) > 0)
f->off += r;
iunlock(f->ip);
return r;
}
panic("fileread");
}
int
filewrite(struct file *f, char *addr, int n)
{
int r;
if(f->writable == 0)
return -1;
if(f->type == FD_EVENT)
return ev_write(f, addr, n);
if(f->type == FD_MEMFD){
r = memfd_rw(f->memfd_seg, 1, (uint64)addr, f->off, n, 1);
if(r > 0)
f->off += r;
return r;
}
if(f->type == FD_PIPE)
return pipewrite(f->pipe, addr, n);
if(f->type == FD_SOCKET)
return sockwrite(f->sock, addr, n);
if(f->type == FD_INODE){
ilock(f->ip);
if(f->ip->type == 3){
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].write){
iunlock(f->ip);
return -1;
}
r = devsw[f->ip->major].write(f->ip, f->off, addr, n);
if(r > 0)
f->off += r;
iunlock(f->ip);
return r;
}
if((f->ip->rwmode & 1) == 0){
cprintf("SchOS: permission denied (write)\n");
iunlock(f->ip);
return -1;
}
// Recycle-bin check removed here too; see the note in fileread().
iunlock(f->ip);
// How much may go in one call is the filesystem's business. v1 is bounded
// by its log; SchFS is not bounded at all and would rather have the lot,
// because it makes one transaction out of whatever it is given.
uint max = f->ip->ops->max_write;
int i = 0;
while(i < n){
int n1 = n - i;
if(max && (uint)n1 > max)
n1 = (int)max;
begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, (uint64)(addr + i), f->off, n1)) > 0)
f->off += r;
iunlock(f->ip);
end_op();
if(r < 0)
break;
i += r;
// Short means full. v1 and SchFS never wrote short, which is why this
// was a panic; FAT does, when the volume or its root directory fills.
// write(2) reports what went in, and the next call gets the error.
if(r != n1)
break;
}
if(i > 0)
return i;
return r < 0 ? r : -1;
}
panic("filewrite");
}
// poll(2) readiness for one open file. Returns a POLL* bitmask restricted
// to the requested events (plus error/hangup conditions, which POSIX says
// are always reported). Pipes and the console tty have real state; other
// inodes (regular files, block-ish devices) are always ready, per POSIX.
int
file_poll(struct file *f, int events)
{
if(f->type == FD_EVENT)
return ev_poll(f, events);
if(f->type == FD_PIPE)
return pipe_poll(f->pipe, f->readable, f->writable) &
(events | POLLERR | POLLHUP);
if(f->type == FD_SOCKET){
int rev = 0;
if(sockreadable(f->sock))
rev |= POLLIN;
// A stream socket is writable whenever it has a peer. The buffer can
// still be full, in which case the write blocks -- same latitude poll()
// has always had, and the same answer a pipe gives.
if(sock_hup(f->sock))
rev |= POLLHUP;
else
rev |= POLLOUT;
return rev & (events | POLLERR | POLLHUP);
}
if(f->type == FD_INODE){
// An input device is only readable when an event is waiting. Reporting it
// always-ready -- which is what the generic answer below does -- turns a
// poll loop into a busy loop that blocks in read().
if(f->ip && f->ip->type == T_DEV && f->ip->major == INPUT_MAJOR){
int rev = 0;
if((events & POLLIN) && f->readable && input_ready(f->ip->minor))
rev |= POLLIN;
if(events & POLLOUT)
rev |= POLLOUT;
return rev;
}
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))
rev |= POLLIN;
if((events & POLLOUT) && f->writable)
rev |= POLLOUT;
return rev;
}
return events & (POLLIN | POLLOUT);
}
return POLLNVAL;
}