Skip to content

Commit 000aaed

Browse files
committed
fix(console): fall back to the kernel's 8x16 font on a small framebuffer
The prod cmdline fixes fbcon to TER16x32, so a cramped on-screen framebuffer (a small panel, or a management console's low-resolution GOP mode) can leave the VT with fewer rows and columns than even the dashboard's compact render needs. Nothing checked the real console geometry against that floor before drawing. cryptos-console now reads the VT size at startup and, when it is below the floor RenderDashboard already uses to drop into its compact view, requests the kernel's built-in VGA8x16 font via KDFONTOP before the first draw, then re-reads the size. A failed switch is logged and the original size kept, so the compact view still runs. The ioctl sits behind a small interface so the decision logic is unit-tested with a fake switcher. internal/console now exports NeedsCompact so the console binary can reuse the existing floor instead of duplicating the numbers. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
1 parent 40d5477 commit 000aaed

7 files changed

Lines changed: 279 additions & 1 deletion

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package main
2+
3+
/*
4+
Copyright The CryptOS Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
import (
20+
"log"
21+
22+
"github.com/CryptOS-PKI/cryptos-node/internal/console"
23+
)
24+
25+
// fontSwitcher abstracts the console font switch behind an interface so
26+
// ensureConsoleFits is unit-tested with a fake, never the real KDFONTOP ioctl.
27+
type fontSwitcher interface {
28+
// setDefaultFont switches the console tty to the kernel's built-in
29+
// default font (VGA8x16, CONFIG_FONT_8x16), which leaves more rows and
30+
// columns than the prod cmdline's TER16x32 on the same framebuffer.
31+
setDefaultFont() error
32+
}
33+
34+
// ensureConsoleFits requests the kernel's built-in 8x16 font when cols x rows
35+
// is below the floor the dashboard needs to drop out of its compact render
36+
// (console.NeedsCompact), then re-reads the size with readSize so the caller
37+
// draws at whatever the switch produced. A switch failure is logged and the
38+
// original size is kept unchanged; the compact view still runs at that size.
39+
func ensureConsoleFits(sw fontSwitcher, readSize func() (cols, rows int), cols, rows int) (int, int) {
40+
if !console.NeedsCompact(cols, rows) {
41+
return cols, rows
42+
}
43+
if err := sw.setDefaultFont(); err != nil {
44+
log.Printf("console: font: %dx%d is below the compact floor; VGA8x16 fallback failed: %v", cols, rows, err)
45+
return cols, rows
46+
}
47+
newCols, newRows := readSize()
48+
log.Printf("console: font: %dx%d was below the compact floor; switched to VGA8x16, now %dx%d", cols, rows, newCols, newRows)
49+
return newCols, newRows
50+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//go:build linux
2+
3+
package main
4+
5+
/*
6+
Copyright The CryptOS Authors.
7+
8+
Licensed under the Apache License, Version 2.0 (the "License");
9+
you may not use this file except in compliance with the License.
10+
You may obtain a copy of the License at
11+
12+
http://www.apache.org/licenses/LICENSE-2.0
13+
14+
Unless required by applicable law or agreed to in writing, software
15+
distributed under the License is distributed on an "AS IS" BASIS,
16+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
See the License for the specific language governing permissions and
18+
limitations under the License.
19+
*/
20+
21+
import (
22+
"unsafe"
23+
24+
"golang.org/x/sys/unix"
25+
)
26+
27+
// KDFONTOP and the console_font_op layout (linux/kd.h) are not exposed by
28+
// golang.org/x/sys/unix, so they are declared here.
29+
const (
30+
kdFontOp = 0x4B72 // KDFONTOP
31+
kdFontOpSetDefault = 2 // KD_FONT_OP_SET_DEFAULT
32+
)
33+
34+
// consoleFontOp mirrors struct console_font_op from linux/kd.h for the
35+
// KD_FONT_OP_SET_DEFAULT operation, where data points to the font name and
36+
// the other fields are unused. The pad field matches the compiler-inserted
37+
// padding before the pointer on 64-bit (amd64, arm64), the only targets this
38+
// binary builds for.
39+
type consoleFontOp struct {
40+
op uint32
41+
flags uint32
42+
width uint32
43+
height uint32
44+
charcount uint32
45+
pad uint32
46+
data *byte
47+
}
48+
49+
// vga8x16Name is the kernel's built-in 8x16 font name (CONFIG_FONT_8x16),
50+
// NUL-terminated as KD_FONT_OP_SET_DEFAULT expects.
51+
var vga8x16Name = append([]byte("VGA8x16"), 0)
52+
53+
// kdFontSwitcher switches the console at fd to the kernel's built-in default
54+
// font via the KDFONTOP ioctl.
55+
type kdFontSwitcher struct{ fd int }
56+
57+
func (s kdFontSwitcher) setDefaultFont() error {
58+
op := consoleFontOp{op: kdFontOpSetDefault, data: &vga8x16Name[0]}
59+
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(s.fd), uintptr(kdFontOp), uintptr(unsafe.Pointer(&op)))
60+
if errno != 0 {
61+
return errno
62+
}
63+
return nil
64+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//go:build !linux
2+
3+
package main
4+
5+
/*
6+
Copyright The CryptOS Authors.
7+
8+
Licensed under the Apache License, Version 2.0 (the "License");
9+
you may not use this file except in compliance with the License.
10+
You may obtain a copy of the License at
11+
12+
http://www.apache.org/licenses/LICENSE-2.0
13+
14+
Unless required by applicable law or agreed to in writing, software
15+
distributed under the License is distributed on an "AS IS" BASIS,
16+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
See the License for the specific language governing permissions and
18+
limitations under the License.
19+
*/
20+
21+
import "errors"
22+
23+
// kdFontSwitcher is a no-op off Linux; the console only runs on the node's
24+
// Linux framebuffer console.
25+
type kdFontSwitcher struct{ fd int }
26+
27+
func (s kdFontSwitcher) setDefaultFont() error {
28+
return errors.New("console font fallback is only available on linux")
29+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package main
2+
3+
/*
4+
Copyright The CryptOS Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
import (
20+
"errors"
21+
"testing"
22+
)
23+
24+
// fakeFontSwitcher is a fontSwitcher fake so the fallback decision is tested
25+
// without a real console tty.
26+
type fakeFontSwitcher struct {
27+
calls int
28+
err error
29+
}
30+
31+
func (f *fakeFontSwitcher) setDefaultFont() error {
32+
f.calls++
33+
return f.err
34+
}
35+
36+
func TestEnsureConsoleFitsRequestsTheFallbackBelowTheFloor(t *testing.T) {
37+
sw := &fakeFontSwitcher{}
38+
cols, rows := ensureConsoleFits(sw, func() (int, int) { return 80, 24 }, 38, 11)
39+
if sw.calls != 1 {
40+
t.Fatalf("setDefaultFont calls = %d, want 1", sw.calls)
41+
}
42+
if cols != 80 || rows != 24 {
43+
t.Fatalf("got %dx%d, want the re-read 80x24", cols, rows)
44+
}
45+
}
46+
47+
func TestEnsureConsoleFitsSkipsTheFallbackAtOrAboveTheFloor(t *testing.T) {
48+
sw := &fakeFontSwitcher{}
49+
readSize := func() (int, int) {
50+
t.Fatal("readSize must not be called when the console already fits")
51+
return 0, 0
52+
}
53+
cols, rows := ensureConsoleFits(sw, readSize, 40, 12)
54+
if sw.calls != 0 {
55+
t.Fatalf("setDefaultFont calls = %d, want 0", sw.calls)
56+
}
57+
if cols != 40 || rows != 12 {
58+
t.Fatalf("got %dx%d, want the original 40x12 unchanged", cols, rows)
59+
}
60+
}
61+
62+
func TestEnsureConsoleFitsKeepsTheOriginalSizeOnFallbackError(t *testing.T) {
63+
sw := &fakeFontSwitcher{err: errors.New("ioctl failed")}
64+
readSize := func() (int, int) {
65+
t.Fatal("readSize must not be called when the fallback failed")
66+
return 0, 0
67+
}
68+
cols, rows := ensureConsoleFits(sw, readSize, 38, 11)
69+
if sw.calls != 1 {
70+
t.Fatalf("setDefaultFont calls = %d, want 1", sw.calls)
71+
}
72+
if cols != 38 || rows != 11 {
73+
t.Fatalf("got %dx%d, want the original 38x11 kept", cols, rows)
74+
}
75+
}

‎cmd/cryptos-console/main.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ func main() {
4444

4545
// Query the console size once at startup; the kernel sets a large font so
4646
// the frame fills the whole screen. On any failure this falls back to 80x24.
47-
cols, rows := consoleSize(int(os.Stdout.Fd()))
47+
fd := int(os.Stdout.Fd())
48+
cols, rows := consoleSize(fd)
49+
// On a framebuffer too small for the prod cmdline's TER16x32 font, the VT
50+
// can report fewer rows/cols than even the compact view needs. Fall back
51+
// to the kernel's built-in 8x16 font before the first draw so the real
52+
// console geometry, not just the reported one, has room for it.
53+
cols, rows = ensureConsoleFits(kdFontSwitcher{fd: fd}, func() (int, int) { return consoleSize(fd) }, cols, rows)
4854
ver := buildinfo.Get().Version
4955

5056
// Dial the local socket, retrying on failure so the console comes up even
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package console_test
2+
3+
/*
4+
Copyright The CryptOS Authors.
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
import (
20+
"testing"
21+
22+
"github.com/CryptOS-PKI/cryptos-node/internal/console"
23+
)
24+
25+
// NeedsCompact must agree with the threshold screen.render already uses to
26+
// fall back to its compact render, since a userspace caller outside the
27+
// package (the console font fallback) needs the same floor without
28+
// duplicating the numbers.
29+
func TestNeedsCompactMatchesTheFramedFloor(t *testing.T) {
30+
cases := []struct {
31+
cols, rows int
32+
want bool
33+
}{
34+
{40, 12, false},
35+
{80, 24, false},
36+
{39, 12, true},
37+
{40, 11, true},
38+
{38, 11, true},
39+
}
40+
for _, c := range cases {
41+
if got := console.NeedsCompact(c.cols, c.rows); got != c.want {
42+
t.Errorf("NeedsCompact(%d, %d) = %v, want %v", c.cols, c.rows, got, c.want)
43+
}
44+
}
45+
}

‎internal/console/screen.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,15 @@ const (
209209
minRows = 12
210210
)
211211

212+
// NeedsCompact reports whether cols x rows is below the floor a framed screen
213+
// needs, the same threshold render uses to fall back to the compact view. A
214+
// caller outside the package (the console's font fallback) uses this to
215+
// decide when the real console geometry is too small, without duplicating
216+
// the floor.
217+
func NeedsCompact(cols, rows int) bool {
218+
return cols < minCols || rows < minRows
219+
}
220+
212221
// bodyLines lays the screen's body items out as rows within a frame inner
213222
// columns wide. This is the part of the framed layout that never gets
214223
// dropped or shortened, so its row count against rows-3 (the interior rows

0 commit comments

Comments
 (0)