gfx: the console's typeface is replaceable, and the loader is what replaces it

Gate 0's last-but-one open item. Drop a TrueType file at \schos\console.ttf on
the ESP and the console is drawn with it; with no file, the face linked into
the kernel image, which is what an early panic needs and is not a degraded
mode.

**The loader reads it, and that is the decision worth writing down.** The
kernel opening the file once the root filesystem is mounted is the obvious
design. It is also wrong: the console has been drawing since the kernel's
first line, so a font arriving later changes the cell size, which changes the
column count, which re-lays out every terminal -- and blanks them. The kernel
log would vanish off the screen at the moment the root filesystem mounted,
which is exactly the log a machine being brought up for the first time is
being read for. Since the next thing on the list is real hardware, losing that
was not a trade worth making.

So the choice happens before anything is drawn. Three details make it cheap:

  - **EfiReservedMemoryType**, not EfiLoaderData. The E820 handoff turns
    loader data into usable memory, on the argument that the loader's own
    pages are the kernel's the moment it runs -- true of the loader, not true
    of a buffer it wants the kernel to keep reading from.
  - **Below 4 GB**, because the kernel reaches this through its direct map and
    that is how far the direct map goes. A font above it would need an ioremap
    the console cannot do before there is a console.
  - **Used in place.** No copy, which matters because the kernel has no
    contiguous allocator to copy 112 KB into -- the only one is the DMA arena,
    and that belongs to devices.

Sized without EFI_FILE_INFO and its GUID: seek past the end and ask where that
landed, which the specification requires to clamp.

Every failure falls back and keeps going -- no file, implausible size, no
memory, short read, unparseable file. The last says so in the log, because a
font that silently did not load looks exactly like a font that did.

Verified by booting with JetBrainsMono-ExtraBold.ttf on the ESP and
screendumping the console: visibly the bold face, against the Regular of a
boot with no file. Three boots each way, no traps and no panics, socktest and
memfdtest green in all six.

Nothing ships a console.ttf: the file would be the file already in the kernel,
and two copies of a font are two things that can disagree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-09-23 17:47:45 +02:00
commit d85274dbd9
5 changed files with 215 additions and 5 deletions

View file

@ -301,6 +301,10 @@ copy(void *dst, const void *src, UINTN n)
// Reading the kernel off the ESP
// ---------------------------------------------------------------------------
// The volume this loader came off, kept so that anything else read at boot is
// read from the same place the kernel was. Set by open_kernel().
static EFI_FILE_PROTOCOL *boot_root;
static EFI_FILE_PROTOCOL *
open_kernel(EFI_HANDLE image)
{
@ -323,6 +327,8 @@ open_kernel(EFI_HANDLE image)
st = fs->OpenVolume(fs, &root);
if (EFI_ERROR(st)) die(L"cannot open the boot volume", st);
boot_root = root;
st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH, EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st))
st = root->Open(root, &file, (CHAR16 *)KERNEL_PATH_ALT,
@ -332,6 +338,95 @@ open_kernel(EFI_HANDLE image)
return file;
}
// ---------------------------------------------------------------------------
// The console typeface
//
// See SCHBOOT_FONT_ADDR in include/schboot.h for why this is the loader's job
// and not the kernel's. Everything here is best-effort: a machine with no
// console.ttf on its ESP boots with the font in the kernel image, which is
// the same font, and says nothing about it.
// ---------------------------------------------------------------------------
#define FONT_PATH L"\\schos\\console.ttf"
#define FONT_PATH_ALT L"\\console.ttf"
// Four megabytes. Large enough for any single-weight TrueType face -- the one
// in the kernel image is 112 KB -- and small enough that a wrong file cannot
// eat the machine's memory before the kernel has any.
#define FONT_MAX (4u * 1024u * 1024u)
static void
load_console_font(void)
{
EFI_FILE_PROTOCOL *file = 0;
EFI_STATUS st;
UINT64 size = 0;
EFI_PHYSICAL_ADDRESS at;
UINTN read;
// Absent until proven present: the kernel reads these two fields with no
// other way to know whether the loader wrote them.
*(UINT64 *)SCHBOOT_FONT_ADDR = 0;
*(UINT32 *)SCHBOOT_FONT_LEN = 0;
if (!boot_root)
return;
st = boot_root->Open(boot_root, &file, (CHAR16 *)FONT_PATH,
EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st))
st = boot_root->Open(boot_root, &file, (CHAR16 *)FONT_PATH_ALT,
EFI_FILE_MODE_READ, 0);
if (EFI_ERROR(st) || !file)
return; /* no console.ttf; the built-in one stands */
// The size, without EFI_FILE_INFO and its GUID: seek past the end and ask
// where that landed. The specification requires SetPosition beyond EOF to
// clamp to the end, which is exactly the number wanted.
if (EFI_ERROR(file->SetPosition(file, 0xFFFFFFFFFFFFFFFFULL)) ||
EFI_ERROR(file->GetPosition(file, &size)) ||
EFI_ERROR(file->SetPosition(file, 0))) {
file->Close(file);
return;
}
if (size < 4096 || size > FONT_MAX) {
vprint(L"schos: console.ttf is an implausible size; ignoring it\r\n");
file->Close(file);
return;
}
// Below 4 GB, because the kernel reaches this through its direct map and
// that is how far the direct map goes -- a font above it would need an
// ioremap the console cannot do before there is a console.
//
// Reserved rather than loader data: EfiLoaderData becomes E820_USABLE in
// the handoff, on the argument that the loader's own pages are the
// kernel's the moment it runs. True of the loader; not true of this.
at = 0xFFFFFFFFULL;
st = BS->AllocatePages(AllocateMaxAddress, EfiReservedMemoryType,
(UINTN)((size + 0xFFF) / 0x1000), &at);
if (EFI_ERROR(st)) {
vprint(L"schos: no memory for console.ttf; using the built-in font\r\n");
file->Close(file);
return;
}
read = (UINTN)size;
st = file->Read(file, &read, (void *)(UINTN)at);
file->Close(file);
if (EFI_ERROR(st) || read != size) {
vprint(L"schos: console.ttf would not read; using the built-in font\r\n");
return; /* the pages stay reserved; harmless */
}
*(UINT64 *)SCHBOOT_FONT_ADDR = (UINT64)at;
*(UINT32 *)SCHBOOT_FONT_LEN = (UINT32)size;
vprint(L"schos: console font "); vprint_hex((UINT32)size);
vprint(L" bytes at "); vprint_hex((UINT32)at); vprint(L"\r\n");
}
// Load PT_LOAD segments to p_paddr. Physical addresses, not virtual: the
// kernel is linked at 0xffff800000100000 but must be *placed* at 0x100000, and
// p_paddr is where the linker script says each segment belongs.
@ -1346,6 +1441,12 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
ui_progress_end();
file->Close(file);
// After open_kernel, because that is what finds the volume, and after the
// kernel rather than before it: the font is optional and the kernel is
// not, so a machine that is going to fail should fail on the thing that
// matters.
load_console_font();
vprint(L"schos: kernel entry "); vprint_hex(entry); vprint(L"\r\n");
// Read while boot services still exist and the system table is guaranteed