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>
414 lines
11 KiB
C
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");
|
|
}
|