SchOS/kernel/net/net.c
Nexoniarz 54b25708c3 Demand paging, the RTL8168H, links that come up late, and Ctrl+Alt+F1..F12
- Anonymous memory can be reserved without being backed: large mappings,
  MAP_NORESERVE and PROT_NONE are recorded and each page appears, zeroed,
  on first touch -- from trap() for the program, from uva2ka() for the
  kernel on its behalf. WebKit reserves 4 GB at start and touches a sliver;
  backing it all failed with ENOMEM and Epiphany aborted.
- r8169: the 8168G generation and later (the laptop's 8168H, XID 541) are
  taken from the firmware's out-of-band mode, get their PLL powered, the
  receive gate opened, and their PHY reached through the OCP window. The
  PHY's power-down bit is cleared; the old code kept it while restarting
  autonegotiation. Untested on the hardware so far.
- The network thread follows each device's link into lwIP. The link was
  read once at attach, 0.02 s into boot; real autonegotiation takes
  seconds, so on hardware the interface stayed down for good.
- Twelve terminals, switched with Ctrl+Alt+F1..F12 by default: the keys X
  uses, so the console and the desktop agree. AltGr+F-key is gone from
  the defaults (chvt -c altgr+F brings it back).
- SO_PASSCRED on Unix sockets, with SCM_CREDENTIALS delivered by recvmsg.
  WebKit sets it on its IPC sockets and aborted when refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 11:27:48 +01:00

579 lines
19 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// The network thread, and the one place the TCP/IP stack is started.
//
// lwIP with NO_SYS=1 has no threads of its own, so something has to feed it
// received frames and run its timers. That something is a kernel thread rather
// than a driver's interrupt handler, and the reason is the lock: a call into
// lwIP can reach the driver, the driver can sleep, and an interrupt handler
// cannot. So the interrupt keeps doing what it always did -- clear the cause
// register -- and this thread polls.
//
// Nothing here names a driver. Interfaces come from the netdev class
// (include/dev/netdev.h): every bound network device is attached, and a machine
// with two NICs gets two interfaces without a line changing in this file.
//
// Polling costs a wakeup every few milliseconds when the link is idle. The
// alternative, waking this thread from the interrupt, is a worthwhile change
// later; it is deliberately not the first version, because a receive path that
// is woken from an interrupt is much harder to debug than one that is asked.
#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 "dev/device.h"
#include "dev/netdev.h"
#include "lwip/opt.h"
#include "lwip/init.h"
#include "lwip/timeouts.h"
#include "lwip/netif.h"
#include "lwip/dhcp.h"
#include "lwip/ip4_addr.h"
#include "lwip/dns.h"
// Wait until the next timer tick. `ticks` advances at 100 Hz, so this is a
// sleep of up to 10 ms -- and, more importantly, it is the kernel's only timed
// sleep: there is no higher-resolution timer to wait on. The lwIP lock must not
// be held across it, or nothing else could touch the stack while the link is
// idle, which is most of the time.
static void
wait_tick(void)
{
acquire(&tickslock);
uint start = ticks;
while (ticks == start)
sleep(&ticks, &tickslock);
release(&tickslock);
}
// ---------------------------------------------------------------------------
// Waking on a received frame
//
// The thread used to wait for the clock and nothing else, so a frame arriving
// just after a poll sat in the ring for up to 10 ms. That was deliberate at the
// time -- a receive path woken by an interrupt is much harder to debug than one
// that is asked, and the stack had to be proven first -- and it is now the
// largest fixed latency in the system.
//
// The wait stays on `ticks`, rather than moving to a channel of its own, and
// that is the whole design. lwIP's timers must still run when the link is
// silent, so the clock has to remain a floor under the wait; a second channel
// would mean sleeping on two things at once, which sleep() cannot do. Joining
// the tick's existing wakeup costs nothing and keeps one wait, one predicate.
//
// `rx_signal` lives under tickslock for the same reason it is not its own lock:
// sleep() releases tickslock atomically, so a driver that sets the flag and
// wakes under that lock cannot lose the wakeup by arriving in the window
// between the predicate test and the sleep. A private lock would not be held
// across the sleep, and that window would be back.
//
// Called from interrupt context. acquire() disables interrupts, so a CPU
// holding tickslock cannot be interrupted into this; another CPU spins briefly.
// That is the same arrangement the timer handler itself uses.
// ---------------------------------------------------------------------------
static int rx_signal;
// Counted so the mechanism is observable rather than assumed: if `rx` stays
// zero while traffic flows, the interrupt is not arriving and the thread is
// running on the clock exactly as it did before.
static uint64 wake_rx, wake_tick;
void
netdev_rx_signal(void)
{
acquire(&tickslock);
rx_signal = 1;
wakeup(&ticks);
release(&tickslock);
}
void
net_wake_counts(uint64 *rx, uint64 *tick)
{
if (rx) *rx = wake_rx;
if (tick) *tick = wake_tick;
}
// Sleep until there is something to do: a frame has been signalled, or the
// clock has moved and lwIP's timers are due.
static void
wait_work(void)
{
acquire(&tickslock);
uint start = ticks;
while (ticks == start && !rx_signal)
sleep(&ticks, &tickslock);
if (rx_signal) {
wake_rx++;
rx_signal = 0;
} else {
wake_tick++;
}
release(&tickslock);
}
static int net_started;
// Set once DHCP has bound, so `ifconfig`-ish output and /proc/net have
// something true to say rather than reporting 0.0.0.0 forever.
static int net_bound;
// Set once lwIP is initialised and this thread is running its timers. Distinct
// from net_bound, and the distinction is not academic: inet_new() used to
// require an *address* before it would hand out a socket, so every AF_INET
// socket() failed until DHCP answered -- reported as ENFILE, which sends you to
// look at the file table. It is also simply wrong for 127.0.0.1, which needs no
// lease and no NIC.
//
// Creating a socket is not the same as having somewhere to send: a bind or a
// connect with no route fails on its own, with an error that says so.
static int net_ready;
int
net_is_up(void)
{
return net_bound;
}
int
net_is_ready(void)
{
return net_ready;
}
// An interface's address and netmask, in network byte order, for callers that
// have no business including lwIP's headers -- kernel/net/netlink.c is the one
// that needs them. Zero when nothing is bound yet, which is a real answer: an
// interface without a lease has no address to report.
uint
netif_addr4(struct netif *nif)
{
return nif ? ip4_addr_get_u32(netif_ip4_addr(nif)) : 0;
}
uint
netif_mask4(struct netif *nif)
{
return nif ? ip4_addr_get_u32(netif_ip4_netmask(nif)) : 0;
}
// Network to host and back, for a 32-bit quantity. The kernel is little-endian
// and the wire is big-endian; lwIP's own macros are not reachable outside the
// files that include its headers.
uint
ntohl_u32(uint v)
{
return ((v & 0xFF) << 24) | ((v & 0xFF00) << 8) |
((v >> 8) & 0xFF00) | ((v >> 24) & 0xFF);
}
uint
htonl_u32(uint v)
{
return ntohl_u32(v);
}
// Attach an interface that appeared after boot.
//
// netinit() walks the devices that exist when it runs and starts DHCP on each.
// An interface created later -- by a process claiming /dev/netdev, which is
// how an LDLS-hosted driver arrives -- misses that entirely, and would come up
// with no address and no way to ever get one.
//
// It lives here rather than in kernel/dev/netuser.c because this is the only
// file allowed to include lwIP's headers; reaching into the stack from a
// device driver would put them on every kernel file's include path. See the
// note on /proc/netif in kernel/dev/procfs.c, which is the same rule.
int
net_attach_late(struct device *d)
{
int idx;
if (!net_ready)
return -1; // netinit() has not run; it will pick this up
lwip_sys_lock();
idx = net_eth_attach(d);
if (idx >= 0)
dhcp_start(net_eth_netif(idx));
lwip_sys_unlock();
if (idx >= 0)
cprintf("net: %s attached as interface %d, asking for an address\n",
d->name, idx);
return idx;
}
// Which interfaces have had their address reported. File scope rather than
// local to net_thread(), because an interface that goes away has to be able to
// be announced again if one comes back in its slot.
static int announced[NETIF_MAX];
static void
net_unannounce(int idx)
{
if (idx >= 0 && idx < NETIF_MAX)
announced[idx] = 0;
}
// The other direction: a device the kernel no longer drives.
//
// Called when /dev/pci hands a NIC to a process. Same rule about lwIP headers
// as net_attach_late above -- the caller is a device file and must not see
// them.
int
net_detach(struct device *d)
{
int idx;
if (!net_ready)
return -1;
lwip_sys_lock();
idx = net_eth_detach(d);
lwip_sys_unlock();
if (idx >= 0) {
net_unannounce(idx);
cprintf("net: %s is gone; interface %d removed\n", d->name, idx);
}
return idx;
}
static void
net_thread(void)
{
// lwIP's own initialisation has to happen in this thread, not in main():
// it allocates from lwIP's pools and registers timers, and doing that
// before there is a process context means doing it somewhere that cannot
// sleep if the allocator ever needs to.
lwip_sys_init();
// From here a socket can be created: the pools exist and the timers are
// about to run. An address may still be minutes away, or never arrive.
net_ready = 1;
lwip_sys_lock();
lwip_init();
// Every bound network device becomes an interface. DHCP is started on each
// of them: which one answers is the network's business, not ours, and an
// interface left unconfigured because it happened to be second would be a
// rule nobody could see.
int attached = 0;
for (int i = 0; i < netdev_count(); i++) {
struct device *d = netdev_at(i);
int idx = net_eth_attach(d);
if (idx < 0)
continue;
dhcp_start(net_eth_netif(idx));
attached++;
kinfo("net: %s attached as interface %d\n", d->name, idx);
}
if (attached == 0) {
lwip_sys_unlock();
cprintf("net: no network device; the thread has nothing to drive\n");
// Not a panic and not an exit: a kernel thread must never return, and
// a machine with no NIC is an ordinary machine.
for (;;)
wait_tick();
}
lwip_sys_unlock();
kinfo("net: lwIP %d.%d.%d up on %d interface%s, requesting an address\n",
LWIP_VERSION_MAJOR, LWIP_VERSION_MINOR, LWIP_VERSION_REVISION,
attached, attached == 1 ? "" : "s");
// One bit per interface, so a second one binding later is announced too.
uint link_checked = 0;
for (;;) {
int moved;
lwip_sys_lock();
moved = net_eth_poll();
sys_check_timeouts();
// A quarter of a second is quick enough to notice a cable, and a
// register read per interface that often costs nothing.
if (ticks - link_checked >= 25) {
link_checked = ticks;
net_eth_link_check();
}
// Loopback has no driver and no interrupt: a packet sent to 127.0.0.1
// sits on the loopif's queue until something drains it. With NO_SYS=1
// that something is this thread.
netif_poll_all();
// Report each address once, when it first arrives. Anyone who has spent
// an evening wondering whether the guest has an address at all wants
// this on the console rather than in a file they have to know about.
// Over every interface there is now, not over the ones there were at
// boot: an interface can arrive later. /dev/netdev makes one whenever a
// process becomes a network device, and until this counted them an
// address on `ldls0` was never reported at all.
for (int i = 0; i < net_eth_count() && i < NETIF_MAX; i++) {
struct netif *nif = net_eth_netif(i);
if (nif == 0 || net_eth_device(i) == 0)
continue;
if (announced[i] || ip4_addr_isany_val(*netif_ip4_addr(nif)))
continue;
const ip4_addr_t *a = netif_ip4_addr(nif);
const ip4_addr_t *m = netif_ip4_netmask(nif);
const ip4_addr_t *gw = netif_ip4_gw(nif);
// klog, not cprintf: a lease arrives seconds after the shell
// prompt does, so on the display this lands in the middle of
// whatever the user is doing. /proc/netif is where a person looks
// for an address; the serial line is where the log belongs.
klog("net: %s %d.%d.%d.%d/%d.%d.%d.%d via %d.%d.%d.%d\n",
net_eth_device(i)->name,
ip4_addr1(a), ip4_addr2(a), ip4_addr3(a), ip4_addr4(a),
ip4_addr1(m), ip4_addr2(m), ip4_addr3(m), ip4_addr4(m),
ip4_addr1(gw), ip4_addr2(gw), ip4_addr3(gw), ip4_addr4(gw));
announced[i] = 1;
net_bound = 1;
}
lwip_sys_unlock();
// Busy while there is traffic, idle otherwise. Sleeping even 1 ms with
// frames waiting would cap throughput at one burst per tick.
if (moved == 0)
wait_work();
}
}
/* ------------------------------------------------------------------ */
/* /proc/netif */
/* ------------------------------------------------------------------ */
//
// Named netif rather than net, and that is deliberate: on Linux /proc/net is a
// *directory* of a dozen files, and putting a plain file there would mean
// anything reaching for the familiar /proc/net/dev got ENOTDIR from a path that
// looks like it exists. This is one interface's state, so it is named for what
// it holds and leaves the Linux name free for the day there is a directory to
// put under it.
//
// No snprintf in the kernel, so this appends by hand in the same style as
// kernel/dev/pci.c.
static int
put_str(char *out, const char *s)
{
int n = 0;
while (s[n]) { out[n] = s[n]; n++; }
return n;
}
static int
put_u(char *out, uint64 v)
{
char tmp[24];
int i = 0, n = 0;
if (v == 0) { out[0] = '0'; return 1; }
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (char)(v % 10); v /= 10; }
while (i--) out[n++] = tmp[i];
return n;
}
static int
put_ip4(char *out, const ip4_addr_t *a)
{
int n = 0;
n += put_u(out + n, ip4_addr1(a)); out[n++] = '.';
n += put_u(out + n, ip4_addr2(a)); out[n++] = '.';
n += put_u(out + n, ip4_addr3(a)); out[n++] = '.';
n += put_u(out + n, ip4_addr4(a));
return n;
}
static int
put_mac(char *out, const unsigned char *m)
{
static const char d[] = "0123456789abcdef";
int n = 0;
for (int i = 0; i < 6; i++) {
if (i) out[n++] = ':';
out[n++] = d[m[i] >> 4];
out[n++] = d[m[i] & 0xF];
}
return n;
}
// One "name value" pair per line, which is the shape /proc/schfs and
// /proc/fsstat already use here -- a reader that can scan one can scan all of
// them.
static int
put_field(char *out, const char *name)
{
int n = put_str(out, name);
while (n < 12)
out[n++] = ' ';
return n;
}
// One block of fields per interface, separated by a blank line. A reader that
// can parse one can parse any number, which is the whole reason it is not a
// single flat list of values: with two NICs a flat list has no way to say which
// address belongs to which.
// /proc/resolv.conf -- the nameservers, in the format a resolver expects.
//
// SchOS has no DNS client of its own and does not need one: musl contains a
// complete resolver, and it works over ordinary UDP sockets, which this system
// has. The only thing it was missing was the one fact a process cannot find
// out for itself -- *which* nameserver -- because that arrives in a DHCP reply
// the kernel handled.
//
// So this is the mechanism and nothing more. It renders resolv.conf's exact
// syntax so that putting it where the resolver looks is a copy rather than a
// translation, and deciding whether to do that is userspace's business; see
// cmd/resolvconf.
//
// Empty until DHCP answers, which is the honest report: a file saying nothing
// is right, and a file naming a server that does not exist is not.
int
net_resolv(char *buf, int cap)
{
int len = 0, i;
if (cap < 64 * DNS_MAX_SERVERS)
return 0;
if (!net_started)
return 0;
lwip_sys_lock();
for (i = 0; i < DNS_MAX_SERVERS; i++) {
const ip_addr_t *a = dns_getserver(i);
if (a == 0 || ip4_addr_isany_val(*ip_2_ip4(a)))
continue;
// put_field pads to a column, which is right for /proc/netif and
// wrong here: this file is not for looking at, it is for copying to
// where a resolver reads it.
len += put_str(buf + len, "nameserver ");
len += put_ip4(buf + len, ip_2_ip4(a));
buf[len++] = '\n';
}
lwip_sys_unlock();
return len;
}
int
net_info(char *buf, int cap)
{
int len = 0;
// Room for the largest block, so the loop below never needs a bounds check
// in the middle of writing one.
if (cap < 256 * (NETIF_MAX + 1))
return 0;
if (!net_started || net_eth_count() == 0) {
len += put_field(buf + len, "state");
len += put_str(buf + len, netdev_count() ? "down\n" : "no interface\n");
return len;
}
for (int i = 0; i < net_eth_count(); i++) {
struct netif *nif = net_eth_netif(i);
struct device *dev = net_eth_device(i);
if (i)
buf[len++] = '\n';
lwip_sys_lock();
len += put_field(buf + len, "interface");
len += put_str(buf + len, dev->name);
buf[len++] = '\n';
len += put_field(buf + len, "driver");
len += put_str(buf + len, dev->driver ? dev->driver : "-");
buf[len++] = '\n';
len += put_field(buf + len, "mac");
len += put_mac(buf + len, nif->hwaddr);
buf[len++] = '\n';
// Per interface, not per system: with two NICs one can be configured
// while the other is still asking.
len += put_field(buf + len, "state");
len += put_str(buf + len,
ip4_addr_isany_val(*netif_ip4_addr(nif)) ? "no address\n"
: "up\n");
len += put_field(buf + len, "address");
len += put_ip4(buf + len, netif_ip4_addr(nif));
buf[len++] = '\n';
len += put_field(buf + len, "netmask");
len += put_ip4(buf + len, netif_ip4_netmask(nif));
buf[len++] = '\n';
len += put_field(buf + len, "gateway");
len += put_ip4(buf + len, netif_ip4_gw(nif));
buf[len++] = '\n';
len += put_field(buf + len, "mtu");
len += put_u(buf + len, nif->mtu);
buf[len++] = '\n';
lwip_sys_unlock();
uint64 rx, tx;
net_eth_counts(i, &rx, &tx);
len += put_field(buf + len, "rx_frames");
len += put_u(buf + len, rx);
buf[len++] = '\n';
len += put_field(buf + len, "tx_frames");
len += put_u(buf + len, tx);
buf[len++] = '\n';
}
// How the network thread is being woken, for the whole stack rather than
// per interface. `wake_rx` staying at zero while frames arrive means the
// receive interrupt is not reaching us and the thread is running on the
// clock exactly as it did before -- which works, and is 10 ms slower.
{
uint64 wrx, wtick;
net_wake_counts(&wrx, &wtick);
len += put_field(buf + len, "wake_rx");
len += put_u(buf + len, wrx);
buf[len++] = '\n';
len += put_field(buf + len, "wake_tick");
len += put_u(buf + len, wtick);
buf[len++] = '\n';
}
return len;
}
// Start the stack. Called from main() once processes exist.
void
netinit(void)
{
if (net_started)
return;
// Before the check below, not after: the lock it initialises has to be
// valid on a machine with no NIC too. Nothing can reach a wait there --
// inet_new refuses while the stack is down -- but a lock whose validity
// depends on that reasoning holding forever is not worth the two lines
// saved.
inet_init();
if (netdev_count() == 0) {
// Silent: a machine with no network device is not misconfigured, and
// this runs on every boot.
return;
}
net_started = 1;
if (kthread_create(net_thread, "netd") < 0)
cprintf("net: could not start the network thread\n");
}