Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

consoled

consoled is the ns16550 UART driver. It serves the physical console as /dev/cons, one file over 9P: a write goes out of the UART, and a read returns what was typed, or, when nothing has been, parks until a key arrives rather than blocking or answering nothing. The physical console carries no labels, so any caller may read it and only an unlabelled one may write it.

Purpose

The serial line is the box’s physical console: where the first owner is enrolled, where recovery happens, and where a panic is reported. Its driver must never stop serving because of one client or one broken device: a read with no input must wait without holding the server, and a device that claims data for ever must cost a bounded amount of work. Session consoles are served per SSH channel by sshd, not here.

Interface

/dev/cons

Status: built · partly tested: attacked with a fake UART against the runtime's fake kernel, and in a boot only written to; no test writes as a labelled caller · tested (14)
  • bench:r4-host-tests
  • host:redoubt-consoled::a_session_s_console_opens_beside_the_one_it_replaces_and_a_third_waits_for_one_to_go
  • bench:consoled-build
  • bench:init-boot
  • bench:init-servers
  • host:redoubt-consoled::a_refused_typed_request_leaves_no_handle_behind
  • host:redoubt-consoled::typing_on_the_uart_reaches_a_ninep_reader
  • host:redoubt-consoled::a_read_with_no_input_waits_and_is_freed_when_its_caller_gives_up
  • host:redoubt-consoled::a_multiplexed_read_waits_for_input
  • host:redoubt-consoled::writes_go_out_of_the_uart_in_order
  • host:redoubt-consoled::writes_go_out_inside_the_console_s_hold_a_chunk_at_a_time
  • host:redoubt-consoled::a_flood_of_input_keeps_what_was_typed_first
  • host:redoubt-consoled::the_console_refuses_what_it_is_not
  • host:redoubt-consoled::the_conformance_vectors_run_against_consoled

/dev/cons is served over the 9P server skeleton as one file with nothing below it.

  • A write sends its bytes out of the UART, in order, each line saying who wrote it (started by init), inside the console’s hold 256 bytes at a time, so the kernel’s lines land between those chunks and never inside one; a chunk that leaves a line unfinished gives the hold back saying so, and a line the kernel ended meanwhile starts again at the next write, prefix first (the console’s one writer). A hold the kernel refuses is a write as before.
  • A read returns the input held, from the start of the queue; the offset is ignored, since a console is a stream. With no input it parks its call with no deadline, because it waits on a person, and is served again, unchanged, when a key arrives; a caller that gives up abandons it and it is freed at once (parked calls). A multiplexed connection’s read waits the same way, as a request in the skeleton, and is answered into its completion call when a key arrives (multiplexed connections).
  • One line, one queue. Input goes to whichever waiting read has waited longest. At most MAX_INPUT (1024) bytes are held; a byte beyond that is dropped and counted, so a flood keeps what was typed first.
  • stat reports length 0: a console has no size.
  • What it refuses: opening with OEXEC or OTRUNC, walking, creating and removing, and a write from a labelled caller, through the label check against the console’s empty label set (R69 (no write down onto the console)).
  • Beside 9P and ninep_common, consol’s size and resize (below). Any other typed opcode, ended sent as a call among them, is answered malformed, and the handles it carried are closed.
  • Admission: per (account, label set), at most 2 parked calls (waiting reads and resize calls together), 2 connections and 6 fids, and 80 multiplexed requests and 2 pages they brought (serving), across at most buckets=N of those; init’s own bucket, by an override on its root badge, holds MAX_THREADS connections with 2 fids each instead. A block with no buckets=N, or one the budget cannot hold, and consoled does not start (init). The two kinds of bucket are derived, not chosen:
    • A principal’s bucket holds the consoles the steward mints for that account here: the console principal’s UART session’s, minted through the steward’s own console (a context over ssh gets the console its relay serves and a lease gets none: the steward’s slot table), admitted in the session’s account from its first request (serving), plus one spare for a session ending while its successor starts: 2. Each console is a share of the bucket, and a share takes one more only while it holds less than limit / (n + 1), n the shares holding (a fair share per badge), so the fids are what lets the last console open its CONSOLE_FIDS (2: the root it attaches and the cons file it opens) while the others hold theirs: CONSOLE_FIDS × 3, 6; at 4, the successor of an ended session, whose console holds its two until the steward’s cleanup gives it back, would get one fid of two. The bench holds the caps to at least that need over the tree’s manifests (tools/testbench, the_servers_admit_what_a_manifest_lets_one_account_hold).
    • init’s bucket holds every server’s console: init mints each through its one root badge and starts at most MAX_THREADS - 1 servers, and each of those consoles, a system caller’s whose chain came through init’s badge, is charged to init’s bucket however it is used, the steward’s own among them. init’s badge is always 1 here: init mints its own first badge at each server it calls, the first not handed to anyone at that endpoint, and refuses a manifest handing consoled to any server (started by init).
    • The fit, by the worst case of Admission::with_overrides (init’s bucket at its override’s caps, every other at the defaults): a default bucket at its caps costs 2 parked calls at 64 KiB, 2 connections at 256 bytes and 6 fids at 256, 80 requests at 256 and 2 pages at 4 KiB, 161,792 bytes; init’s the same with MAX_THREADS connections and their fids, 355,584 bytes at 255 (rv64) and 183,552 at 31 (rv32). The 2 MiB budget holds init’s and 10 default buckets on rv64 (1,973,504; an eleventh does not fit) and init’s and 11 on rv32 (1,963,264); the image’s manifest asks for 5. The entry’s pages follow the entry rule (walfsd) with this constant as the budget: 512 of it, 24 of heap and the stack’s bound of 128, 664, so 768 within its 1,024.
    • The count: buckets=N is the manifest’s principal domains plus one for init, whose bucket every system caller’s console shares; the steward’s own console is init’s minting and counts there, not as a bucket of its own.

Two threads and the UART

Status: built · tested: host:redoubt-consoled::a_device_stuck_on_data_ready_does_not_hang_the_server, host:redoubt-consoled::typing_on_the_uart_reaches_a_ninep_reader, host:redoubt-consoled::a_console_with_no_device_does_not_start

  • The serving thread owns the UART and the 9P skeleton, answers writes, answers reads from the input it holds, and parks reads when it holds none, so every other client is still served.
  • The interrupt thread only receives on the interrupt handle and sends one word to the serving thread’s endpoint; it touches no register. The UART’s registers are not Sync, so the split is the type system’s, not a convention, and neither thread needs a lock. The kernel masks the interrupt when it fires and the next receive unmasks it (R5 (interrupts)).
  • The FIFO is drained whole on each wake-up and before a read parks, so two bytes between two interrupts are both read and no byte is left behind with a reader waiting. A drain reads at most the FIFO’s 16 bytes, so a device that says “data ready” for ever costs a bounded number of register reads per call instead of hanging the server (R70 (a stuck UART never hangs the console)).
  • The registers are mapped from the device handle by the runtime and reached through its bounds-checked accessors, every access volatile; the crate forbids unsafe.

The consol protocol

Status: built · tested (8)
  • bench:consol-size
  • bench:steward-ssh-resize
  • host:redoubt-consoled::consol_size_is_the_argument_and_a_resize_waits_until_its_caller_gives_up
  • host:redoubt-consoled::a_console_with_no_size_refuses_consol
  • host:redoubt-consoled::a_refused_typed_request_leaves_no_handle_behind
  • host:redoubt-consoled::a_console_with_no_device_does_not_start
  • host:redoubt-sshd::consol_size_is_the_pty_s_and_a_resize_is_due_when_it_changes
  • host:redoubt-client::a_session_writes_and_reads_the_console

Every server of a /dev/cons (consoled, and sshd for each channel) also serves consol on the same endpoint, through the runtime’s server::consol:

  • size answers the console’s columns and rows at once. consoled takes its size from its arguments, size=COLS,ROWS, each from 1 to 1,024; a malformed size, or two, and it does not start. A UART cannot know the size of the terminal at its far end, so when the manifest names none consoled does not guess: it refuses size and resize (malformed), and its clients’ console is of unknown size, as the image’s is. sshd answers from the channel’s pty request (sshd).
  • resize answers the size when it next changes: it parks like a read, with no deadline, and is freed when its caller gives up (parking a typed call). A UART has no window, so on consoled a resize waits until its caller gives up. A client learns the size of its own console only; another channel’s waiter stays parked and learns nothing.
  • What a waiter costs. A waiting resize is a parked call of its caller’s, in its bucket and share, beside its waiting reads and, on a multiplexed connection, its session, which holds one of the share too (multiplexed connections). On consoled a lone connection’s share is one of the bucket’s 2: a connection with a resize waiting has no room for a waiting read, and one with a multiplexed session, as the shell’s VM has, has no room for a resize, so the VM’s is refused. On sshd the share is two, the session’s and one waiter’s, and the VM’s reads and writes wait inside its session, so its resize starves nothing of its own. One more is refused at once (malformed), so however many waiters a client asks for, what they cost the server is the bucket’s fixed slots and their admitted cost, never memory beyond them.
  • ended is the steward’s word that a minted console’s session is over (started by init); sent as a call it is malformed.

The table: libs/wire/tables/consol.md.

OpcodeKindMessageFieldsReply
16callsize-cols: u16, rows: u16
17callresize-cols: u16, rows: u16
18sendended--
CodeError

Started by init

Status: built · tested (17)
  • bench:init-boot
  • bench:init-servers
  • bench:init-console-forgery
  • bench:console-kernel-line-mid-line
  • bench:bench-init-reporter-forged
  • host:redoubt-consoled::the_prefix_is_the_id_in_sixteen_hex_digits
  • host:redoubt-consoled::a_root_line_is_bare_and_a_minted_one_carries_its_id_on_every_line
  • host:redoubt-consoled::a_line_is_continued_by_its_writer_and_ended_by_any_other
  • host:redoubt-consoled::a_line_the_kernel_ended_starts_again_with_its_prefix
  • host:redoubt-consoled::an_unfinished_line_is_given_back_as_such_and_starts_again_once_the_kernel_ended_it
  • host:redoubt-consoled::whatever_the_uart_takes_no_line_mixes_writers_or_carries_another_s_id
  • host:redoubt-rt::the_file_server_learns_the_id_the_requester_got
  • host:redoubt-consoled::a_console_with_no_device_does_not_start
  • host:redoubt-consoled::init_s_badge_mints_a_console_for_every_server_init_can_start
  • host:redoubt-consoled::every_console_init_mints_attaches_and_opens_in_its_one_bucket
  • bench:init-refuses-consoled-handed
  • host:redoubt-init::no_server_is_handed_a_root_badge_at_consoled

init starts consoled once the manifest and the keys are checked, with the UART’s MMIO region and interrupt as named handles and its endpoint, placed from the boot manifest’s devices list (init). consoled parses no device tree; without both handles it does not start. Only one process ever maps the UART: two would both reach the registers, and two readers of one FIFO would each take half the line. init writes to the UART itself only before consoled starts, and unmaps it first.

Every line says who wrote it. A write through init’s own connection goes out as it is. init holds the only handle to the root, and it never hands the root to a child: its check refuses a manifest that hands any server an endpoint consoled receives on (init). A write through any connection minted under the root starts each of its lines with [con N] , where N is that connection’s id, the one its requester was given, in 16 lowercase hex digits. init prints the id of each child’s connection, bare, when it starts the child (init: started NAME, console N), so a reader of the console can tell init from every program and each program from the others, as the bench’s log server does today (rule F). ended on a minted connection releases it: the steward says so when the console session it started is over. If a write comes from a different connection than the one that left the last line unfinished, consoled ends that line first. The kernel is a writer consoled cannot see, and a program’s line can stay unfinished between two of its writes (a prompt waiting for its answer; a \r sent before the text), when nobody holds the console: so consoled gives the console’s hold back saying the line is unfinished, the kernel ends that line before a line of its own, and says so at consoled’s next take, which starts the program’s line again, prefix first (the console’s one writer). So a line a program writes cannot come out bare, not even after a kernel line broke it, and it cannot carry another connection’s id. A transmitter that takes only part of a prefix leaves the line open, and the rest of the prefix goes out before any more of the writer’s bytes; ended by another writer, such a line holds the start of its writer’s prefix and nothing else.

The rule names the handles NAME and NAME-irq from one devices entry (init); consoled takes uart and uart-irq.

Authority

Status: built · partly tested: no test writes as a labelled caller · tested: host:redoubt-consoled::the_console_refuses_what_it_is_not

  • consoled holds the UART’s registers and interrupt, its endpoint, and the connections it minted. It makes no calls to other servers.
  • Any caller with a connection may read the console; only an unlabelled caller may write it.

Security properties

R69 (no write down onto the console)

Status: built · partly tested: no consoled test writes as a labelled caller; the rule rests on the 9P skeleton’s label check, which is attacked in the serving library’s tests · tested: host:redoubt-rt::labels_are_checked_on_every_request, host:redoubt-rt::every_write_needs_equal_labels

The physical console carries no labels, so a caller with any label cannot write to it: no labelled data reaches a screen someone else may be looking at.

R70 (a stuck UART never hangs the console)

Status: built · tested: host:redoubt-consoled::a_device_stuck_on_data_ready_does_not_hang_the_server, host:redoubt-consoled::a_flood_of_input_keeps_what_was_typed_first

Whatever the UART reports, consoled does a bounded amount of work per call and keeps serving: a drain reads at most the FIFO’s depth, and held input is bounded, dropping new bytes rather than growing.

Failure and restart

Status: built · partly tested: the restart claims are read from the code, not attacked across a restart · tested: host:redoubt-consoled::a_console_with_no_device_does_not_start, host:redoubt-consoled::a_read_with_no_input_waits_and_is_freed_when_its_caller_gives_up

  • No UART or interrupt handle: consoled exits with a code before serving.
  • The hold refused: consoled takes and gives back the console’s hold once before serving. A kernel that refuses it (WrongObject: the UART is not the kernel’s console; Busy) would leave every line open to a kernel line, so consoled exits with code 8 (NO_HOLD) instead.
  • A reader gives up: its parked read is answered and freed at once.
  • consoled restarts: held input is lost, parked reads and resize calls get Dead, and clients ask for fresh connections.

Residual risks

  • One keyboard, one queue. A client reading /dev/cons takes input another was waiting for; a session that must not share a console gets its own from sshd.
  • A parked read, or resize, has no deadline. It holds one of its caller’s open calls and one of consoled’s admission slots until a key arrives (for a resize, never) or its caller gives up.
  • Anyone with a connection reads the console. What is typed on the physical console is visible to every holder of a consoled connection.

Why

  • Park, rather than block or answer empty. A blocked server serves nobody else; an empty answer looks like a closed console; parking costs one slot and nothing more.
  • The interrupt thread touches nothing. Keeping the device on one thread removes every lock and every shared-state bug between the two.
  • Drain at most the FIFO. A 16550 can hold no more; a device claiming otherwise is broken, and bounding the loop turns it into dropped input instead of a hung server.