"Ą Ę Ś Ł Ż" drew as "?????" and the obvious diagnosis -- the font has no glyphs for them -- was wrong twice over. `struct vtcell.ch` was **one byte** holding a CP437 index, and the console mapped UTF-8 down to it on the way in. U+0104 does not fit in a byte. No font could have fixed that; the cell had to get wider first. So: `ch` is a uint32 codepoint, the mapping on the way in is gone, and the cell draws through kernel/dev/ttf.c against JetBrains Mono linked into the image. Cell size comes from the font's measured advance instead of a hardcoded 8x16. The grid costs 864 KB of BSS instead of 432 KB, which is the honest price of holding a codepoint per cell for six terminals at 240x75. The rasteriser needed three fixes before it could be trusted with this. **21 KB of locals on a 4 KB kernel stack.** `ttf_render` held an outline (512 points as three arrays) and a raster (1024 edges) as automatics. KSTACKSIZE is 4096. They walked off the bottom of the stack into whatever was underneath, which surfaced as user processes faulting on addresses they never touched and then a panic inside freevm() on an overwritten page directory. Nothing pointed anywhere near a font. **And then again, recursively.** load_outline() held a 4736-byte outline as a local *and recursed* for composite glyphs. Composite glyphs are exactly Ą, Ę, Ś, Ł, Ż -- so the second crash arrived the moment the first fix let Unicode reach the screen, looking like a fresh bug. Now a static outline per recursion level with a depth cap of 4 (the font's deepest real nesting is 2), and the leaf parse split into load_simple() so its 512-byte flag array is not multiplied by the depth. Worst chain measured from the object file: ~2.3 KB. Neither showed in tools/ttftest, which has an 8 MB stack. That is the standing hazard of one source file built for host and kernel: the host is most forgiving about exactly the resource the kernel is strictest about. **Component transforms were being skipped**, on the stated theory that a wrong transform is worse than none. It is not. `‘` in this font is the comma glyph with a scale of -1 -- a 180-degree rotation -- and the offset that ships with it only lands correctly after the rotation. Without it the mark went twelve pixels into a ten-pixel cell and was clipped away entirely, so the character read as missing from the font rather than as misplaced. F2Dot14 2x2 now, with SCALED_COMPONENT_OFFSET honoured. The glyph cache was ASCII-only, so every accented letter re-rasterised on every repaint: a screenful of Polish did hundreds of bezier flattenings per scroll where the same screenful of English did none. 95 reserved ASCII slots plus 224 shared slots keyed by codepoint, 64 KB pool. Verified 13680 cached renders byte-identical to direct ones across collisions and a pool flush. tools/ttftest speaks UTF-8 now. Iterating bytes meant it could only ever ask for ASCII, and ASCII is entirely simple glyphs -- it could not reach the code path that mattered. Also: resolvconf's success line moves behind /proc/bootmode, same rule as vgpud. A quiet boot opened with "resolvconf: nameserver 10.0.2.3" where the one line was supposed to be "Welcome to SchOS!". Failures stay loud. Verified on screen, not just on the host: Ą Ę Ś Ł Ż ć ń ó ź, Ω λ π Œ Ð Æ ß, ∞ ‘x’ → ≤ all render, panic=0 across four boots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
189 lines
6.1 KiB
C
189 lines
6.1 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// resolvconf -- put the nameserver where the resolver will look.
|
|
//
|
|
// SchOS has no DNS client, and the interesting part is that it does not need
|
|
// one. musl contains a complete resolver: `getaddrinfo` reads
|
|
// /etc/resolv.conf, sends a query over an ordinary UDP socket and parses the
|
|
// reply, and every one of those pieces already worked here. The single missing
|
|
// fact was *which* nameserver -- which arrives in a DHCP reply the kernel
|
|
// handled and never told anyone about.
|
|
//
|
|
// That is what made `wget` look broken. It was not wget, and it was not the
|
|
// core utilities: nothing on this system could turn a name into an address, so
|
|
// every program that took a hostname failed the same way and each one looked
|
|
// like its own separate bug.
|
|
//
|
|
// resolvconf wait for a nameserver, write it, exit
|
|
// resolvconf -n write whatever there is now, do not wait
|
|
// resolvconf -p print what the kernel has, change nothing
|
|
//
|
|
// The kernel publishes /proc/resolv.conf and stops there, deliberately: it
|
|
// renders resolv.conf's exact syntax so this is a copy rather than a
|
|
// translation, and *whether* /etc/resolv.conf should be overwritten is a
|
|
// policy question. A machine with a hand-written one should keep it, which is
|
|
// why -f exists and why it is not the default.
|
|
|
|
#include "types.h"
|
|
#include "user.h"
|
|
#include "fcntl.h"
|
|
|
|
#define SRC "/proc/resolv.conf"
|
|
#define DST "/etc/resolv.conf"
|
|
|
|
// DHCP takes a few seconds on a quiet network and longer on a busy one. Two
|
|
// minutes is past any of that and short enough that a machine with no DHCP
|
|
// server does not hold a boot job open for ever.
|
|
#define WAIT_SECONDS 120
|
|
|
|
static void
|
|
puts_(const char *s)
|
|
{
|
|
int n = 0;
|
|
while (s[n]) n++;
|
|
write(1, (void *)s, n);
|
|
}
|
|
|
|
// Whether this boot asked to be told what is happening; see /proc/bootmode and
|
|
// kinfo() in kernel/core/console.c for the same distinction one level down.
|
|
//
|
|
// Success here is progress, not news. Names starting to work is the expected
|
|
// outcome of a normal boot, and printing it unconditionally meant a quiet boot
|
|
// still opened with a line about the nameserver -- the one line that was
|
|
// supposed to be "Welcome to SchOS!". Failures stay on puts_(), because a
|
|
// machine where nothing can resolve a hostname should say so at any verbosity.
|
|
static int verbose_boot;
|
|
|
|
static void
|
|
read_bootmode(void)
|
|
{
|
|
char b[16];
|
|
int fd = open("/proc/bootmode", O_RDONLY);
|
|
int n;
|
|
|
|
if (fd < 0)
|
|
return;
|
|
n = read(fd, b, sizeof(b) - 1);
|
|
close(fd);
|
|
if (n > 0) { b[n] = 0; verbose_boot = (b[0] == 'v'); }
|
|
}
|
|
|
|
static int
|
|
same(const char *a, const char *b, int n)
|
|
{
|
|
for (int i = 0; i < n; i++)
|
|
if (a[i] != b[i])
|
|
return 0;
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
slurp(const char *path, char *buf, int cap)
|
|
{
|
|
int fd = open((char *)path, O_RDONLY);
|
|
int n, len = 0;
|
|
|
|
if (fd < 0)
|
|
return -1;
|
|
while (len < cap - 1 && (n = read(fd, buf + len, cap - 1 - len)) > 0)
|
|
len += n;
|
|
close(fd);
|
|
buf[len] = 0;
|
|
return len;
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
char buf[512];
|
|
int i, n, fd, wait = 1, force = 0, print = 0;
|
|
|
|
read_bootmode();
|
|
|
|
for (i = 1; i < argc; i++) {
|
|
if (argv[i][0] != '-' || argv[i][2] != 0)
|
|
goto usage;
|
|
switch (argv[i][1]) {
|
|
case 'n': wait = 0; break;
|
|
case 'f': force = 1; break;
|
|
case 'p': print = 1; wait = 0; break;
|
|
default:
|
|
usage:
|
|
puts_("usage: resolvconf [-n] [-f] [-p]\n"
|
|
" -n do not wait for DHCP\n"
|
|
" -f overwrite an existing " DST "\n"
|
|
" -p print what the kernel has and exit\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Wait by polling rather than by blocking on the file: /proc has no
|
|
// readiness to report and inventing one for this would be a worse
|
|
// interface than a loop that ends.
|
|
for (i = 0; ; i++) {
|
|
n = slurp(SRC, buf, sizeof(buf));
|
|
if (n < 0) {
|
|
puts_("resolvconf: no " SRC " -- this kernel has no network\n");
|
|
return 1;
|
|
}
|
|
if (n > 0 || !wait || i >= WAIT_SECONDS)
|
|
break;
|
|
sleep(100); // one second, at 100 Hz
|
|
}
|
|
|
|
if (print) {
|
|
if (n > 0) write(1, buf, n);
|
|
else puts_("resolvconf: the kernel has no nameserver yet\n");
|
|
return n > 0 ? 0 : 1;
|
|
}
|
|
|
|
if (n == 0) {
|
|
puts_("resolvconf: no nameserver after ");
|
|
puts_(wait ? "two minutes" : "no wait");
|
|
puts_(" -- is there a DHCP server?\n");
|
|
return 1;
|
|
}
|
|
|
|
// An existing file is somebody's decision until told otherwise.
|
|
//
|
|
// Saying so every boot is the wrong volume for it. On a machine that has
|
|
// been up before, /etc/resolv.conf always exists and always says the same
|
|
// thing, so this printed a line about having done nothing on every single
|
|
// start -- which is exactly the kind of message that teaches people to
|
|
// stop reading their own boot.
|
|
//
|
|
// Quiet when the file already says what this would have written. It is
|
|
// still worth a word when the two *disagree*, because then the nameserver
|
|
// has changed and the machine is using the old one.
|
|
if (!force) {
|
|
char old[512];
|
|
int on = slurp(DST, old, sizeof(old));
|
|
if (on > 0) {
|
|
if (on == n && same(old, buf, n))
|
|
return 0; // already right; nothing to say
|
|
puts_("resolvconf: " DST " differs from what DHCP gave;"
|
|
" -f to replace it\n");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
fd = open(DST, O_WRONLY | O_CREAT | O_TRUNC);
|
|
if (fd < 0) {
|
|
puts_("resolvconf: cannot write " DST "\n");
|
|
return 1;
|
|
}
|
|
if (write(fd, buf, n) != n) {
|
|
puts_("resolvconf: short write to " DST "\n");
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
close(fd);
|
|
|
|
// Worth one line under -v, because it is the moment names start working
|
|
// and somebody watching a verbose boot may be waiting to know it.
|
|
if (verbose_boot) {
|
|
puts_("resolvconf: ");
|
|
write(1, buf, n);
|
|
}
|
|
return 0;
|
|
}
|