SchOS/kernel/dev/netuser.c
Nexoniarz 26bca020b6 licence: SchOS is GPL-2.0-or-later, no longer Apache-2.0
Nexoniarz is the sole author and SchOS was never published under
Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text,
every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE,
README.md, docs/licensing.md and the site say so. projects/schfs-linux
follows the rest of the tree.

The GPL gives copyleft: a fork has to ship its source. It also lifts the
old "Linux drivers, never" rule, since GPL-2.0-only code combines with
GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still
has to pick one version, so docs/licensing.md now keeps Apache-2.0 and
GPLv3 code out of the kernel to keep Linux code possible there. BSD
4-clause code goes from "avoid" to "no", because the advertising clause
is GPL-incompatible.

LDLS still ships no .ko. That boundary is now project policy rather than
a legal requirement, and ldls.md, devices.md and licensing.md section 6
say so. refmod declares license=GPL, and ldls-check passes (27 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:48:45 +02:00

414 lines
11 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// /dev/netdev -- a network interface whose backend is a process.
// See include/netuser.h.
//
// The shape is `/dev/pci`'s, deliberately, and for the same reasons. One
// device, one owner, claimed and released, and the kernel takes it back when
// the process dies -- because the process it is handed to is by assumption one
// that may crash, which is the entire argument for it being a process.
//
// What is different is the direction. `/dev/pci` hands a device *to* a
// process; this lets a process *be* a device. The kernel is the consumer here
// and the process is the driver, so the queues run the other way and the
// vtable is ours rather than theirs.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "errno.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "uaccess.h"
#include "device.h"
#include "netdev.h"
#include "sbuf.h"
#include "dbg.h"
#include "netuser.h"
struct frame {
int len;
uint8 data[NETUSER_MTU];
};
// Two rings. `tx` is what the stack has handed down and the process has not
// yet read; `rx` is what the process has written and the stack has not yet
// polled. Both are single-producer/single-consumer in practice -- one process
// on one side, the network thread on the other -- but the lock is taken
// anyway, because "in practice" is not a property the scheduler guarantees.
static struct {
struct spinlock lock;
int inited;
struct proc *owner;
struct device *dev;
uint8 mac[6];
struct frame tx[NETUSER_QLEN];
uint tx_head, tx_tail; // monotonic, indexed mod QLEN
struct frame rx[NETUSER_QLEN];
uint rx_head, rx_tail;
struct netuser_stat st;
} nu;
static void
netuser_once(void)
{
if (!nu.inited) {
initlock(&nu.lock, "netuser");
nu.inited = 1;
}
}
// --- the vtable the kernel sees ---------------------------------------------
// Queue a frame for the process to read.
//
// Returns -1 when the ring is full, which netdev.h defines as congestion
// rather than failure -- the caller is expected to come back. That matters
// here more than it does for real hardware: the "wire" is a process that may
// not have been scheduled yet, and treating a full ring as an error would drop
// packets every time the machine was busy.
static int
netuser_transmit(struct device *d, const void *frame, int len)
{
int rv = 0;
(void)d;
if (len <= 0 || len > NETUSER_MTU)
return -1;
acquire(&nu.lock);
if (!nu.owner) {
release(&nu.lock);
return -1;
}
if (nu.tx_head - nu.tx_tail >= NETUSER_QLEN) {
nu.st.tx_dropped++;
rv = -1;
} else {
struct frame *f = &nu.tx[nu.tx_head % NETUSER_QLEN];
memmove(f->data, frame, len);
f->len = len;
nu.tx_head++;
nu.st.tx_frames++;
nu.st.tx_bytes += len;
}
release(&nu.lock);
if (rv == 0)
wakeup(&nu.tx_head);
return rv;
}
// Take a frame the process wrote, if there is one. Never blocks: net_eth_poll
// calls this in a loop and decides for itself when to stop.
static int
netuser_receive(struct device *d, void *out, int cap)
{
int len = 0;
(void)d;
acquire(&nu.lock);
if (nu.rx_head != nu.rx_tail) {
struct frame *f = &nu.rx[nu.rx_tail % NETUSER_QLEN];
len = f->len;
if (len > cap)
len = cap;
memmove(out, f->data, len);
nu.rx_tail++;
}
release(&nu.lock);
return len;
}
static const unsigned char *
netuser_mac(struct device *d)
{
(void)d;
return nu.mac;
}
// Up exactly while somebody is holding the descriptor.
//
// This is the honest answer and it is also the useful one: a link that stays
// up after its driver died is a link that swallows every packet sent to it.
static int
netuser_link_up(struct device *d)
{
(void)d;
return nu.owner != 0;
}
static const struct netdev_ops netuser_ops = {
.transmit = netuser_transmit,
.receive = netuser_receive,
.mac = netuser_mac,
.link_up = netuser_link_up,
};
// --- claiming and releasing --------------------------------------------------
static void
netuser_drop(void)
{
// The device stays registered. Removing it would mean unpicking it from
// lwIP's interface list, which nothing in that stack supports -- and leaving
// it with link_up() answering false is the same thing from the stack's point
// of view, without inventing a teardown path nobody else has.
nu.owner = 0;
nu.tx_head = nu.tx_tail = 0;
nu.rx_head = nu.rx_tail = 0;
nu.st.attached = 0;
}
static int
do_create(uint64 argp)
{
struct netuser_create c;
struct device *d;
int first = 0;
if (copy_from_user(&c, argp, sizeof(c)) < 0)
return -EFAULT;
netuser_once();
acquire(&nu.lock);
if (nu.owner) {
release(&nu.lock);
return -EBUSY; // one interface, one process
}
nu.owner = myproc();
for (int i = 0; i < 6; i++)
nu.mac[i] = c.mac[i];
nu.tx_head = nu.tx_tail = 0;
nu.rx_head = nu.rx_tail = 0;
nu.st.attached = 1;
d = nu.dev;
release(&nu.lock);
// Registered once and reused. A process that comes back after crashing gets
// the same interface rather than a second one, which is what makes restart
// invisible to anything above -- the same property H2 built for the GPU.
if (!d) {
c.name[sizeof(c.name) - 1] = 0;
d = device_add(c.name[0] ? c.name : "eth", DEVBUS_PLATFORM, DEVCLASS_NET,
"netuser");
if (!d) {
acquire(&nu.lock);
netuser_drop();
release(&nu.lock);
return -ENOSPC;
}
device_set_desc(d, "a process, acting as a network interface");
d->ops = (void *)&netuser_ops;
d->priv = 0;
nu.dev = d;
first = 1;
}
if (first) {
// Hand it to the stack. net_attach_late() adds it to lwIP's interface list
// *and* starts DHCP on it, which netinit()'s boot-time walk cannot do for
// something that did not exist yet -- an interface that only appears once
// a program has started would otherwise come up with no address and no way
// to ever get one.
if (net_attach_late(d) < 0) {
cprintf("netuser: the network stack would not take the interface\n");
acquire(&nu.lock);
netuser_drop();
release(&nu.lock);
return -ENOSPC;
}
cprintf("netuser: %s is a process now, %x:%x:%x:%x:%x:%x\n", d->name,
nu.mac[0], nu.mac[1], nu.mac[2], nu.mac[3], nu.mac[4], nu.mac[5]);
}
return 0;
}
static int
do_release(void)
{
netuser_once();
acquire(&nu.lock);
if (nu.owner == myproc())
netuser_drop();
release(&nu.lock);
wakeup(&nu.tx_head);
return 0;
}
// Called from exit(), beside pciuser_release_all(), and for the same reason: a
// driver that crashed must not leave an interface that claims to be up.
void
netuser_release_all(struct proc *p)
{
netuser_once();
acquire(&nu.lock);
if (nu.owner == p)
netuser_drop();
release(&nu.lock);
wakeup(&nu.tx_head);
}
int
netuser_ioctl(uint64 request, uint64 argp)
{
switch (request) {
case NETUSERIOC_CREATE: return do_create(argp);
case NETUSERIOC_RELEASE: return do_release();
case NETUSERIOC_STAT: {
struct netuser_stat s;
netuser_once();
acquire(&nu.lock);
s = nu.st;
release(&nu.lock);
if (copy_to_user(argp, &s, sizeof(s)) < 0)
return -EFAULT;
return 0;
}
default:
return -ENOTTY;
}
}
// --- the descriptor ----------------------------------------------------------
// A frame the stack wants sent. Blocks until there is one.
int
netuser_read(struct inode *ip, uint64 off, char *dst, int n)
{
struct frame *f;
int len;
(void)off;
netuser_once();
if (nu.owner != myproc())
return -EACCES;
iunlock(ip);
acquire(&nu.lock);
while (nu.tx_head == nu.tx_tail) {
if (myproc()->killed || nu.owner != myproc()) {
release(&nu.lock);
ilock(ip);
return -1;
}
sleep(&nu.tx_head, &nu.lock);
}
f = &nu.tx[nu.tx_tail % NETUSER_QLEN];
len = f->len;
if (len > n)
len = n;
memmove(dst, f->data, len);
nu.tx_tail++;
release(&nu.lock);
ilock(ip);
return len;
}
// A frame that arrived. Handed to the stack at its next poll.
int
netuser_write(struct inode *ip, uint64 off, char *src, int n)
{
(void)ip; (void)off;
netuser_once();
if (nu.owner != myproc())
return -EACCES;
if (n <= 0 || n > NETUSER_MTU)
return -EINVAL;
acquire(&nu.lock);
if (nu.rx_head - nu.rx_tail >= NETUSER_QLEN) {
// Dropped, and counted. The alternative is blocking the driver until the
// stack catches up, which on a receive path means the driver stops reading
// its hardware and the hardware drops it instead -- the same packet lost,
// further from anything that can say so.
nu.st.rx_dropped++;
release(&nu.lock);
return n;
}
{
struct frame *f = &nu.rx[nu.rx_head % NETUSER_QLEN];
memmove(f->data, src, n);
f->len = n;
nu.rx_head++;
nu.st.rx_frames++;
nu.st.rx_bytes += n;
}
release(&nu.lock);
return n;
}
// Whether a read would return without blocking, so O_NONBLOCK and poll() work
// without the driver growing a flags argument. See struct devsw.
int
netuser_ready(struct inode *ip)
{
int r;
(void)ip;
netuser_once();
acquire(&nu.lock);
r = (nu.tx_head != nu.tx_tail);
release(&nu.lock);
return r;
}
// --- what the debugger can be told -------------------------------------------
static void
netuser_dump(struct sbuf *out, const char *arg)
{
(void)arg;
netuser_once();
acquire(&nu.lock);
if (!nu.dev) {
sb_puts(out, " (no process has ever claimed /dev/netdev)\n");
release(&nu.lock);
return;
}
sb_puts(out, " interface ");
sb_puts(out, nu.dev->name);
sb_puts(out, nu.owner ? " up, held by pid " : " DOWN, nobody holds it");
if (nu.owner)
sb_putu(out, nu.owner->pid);
sb_putc(out, '\n');
sb_puts(out, " mac ");
for (int i = 0; i < 6; i++) {
sb_puthex(out, nu.mac[i], 2);
if (i < 5) sb_putc(out, ':');
}
sb_putc(out, '\n');
sb_puts(out, "\n tx "); sb_putu(out, nu.st.tx_frames);
sb_puts(out, " frames, "); sb_putu(out, nu.st.tx_bytes);
sb_puts(out, " bytes, "); sb_putu(out, nu.st.tx_dropped);
sb_puts(out, " dropped, "); sb_putu(out, nu.tx_head - nu.tx_tail);
sb_puts(out, " waiting to be read\n");
sb_puts(out, " rx "); sb_putu(out, nu.st.rx_frames);
sb_puts(out, " frames, "); sb_putu(out, nu.st.rx_bytes);
sb_puts(out, " bytes, "); sb_putu(out, nu.st.rx_dropped);
sb_puts(out, " dropped, "); sb_putu(out, nu.rx_head - nu.rx_tail);
sb_puts(out, " waiting for the stack\n");
release(&nu.lock);
}
void
netuser_init(void)
{
netuser_once();
dbg_register("netuser", netuser_dump,
"the interface backed by a process, and what has moved");
}