Surfaces

July 16, 2026 ยท View on GitHub

A surface is a framebuffer a capsule owns: a rectangle of ARGB pixels backed by physical frames, registered with the kernel so it can be shared with a compositor and presented to the display. The kernel does not draw; it tracks who owns which surface and mediates the frame sharing. This page documents the surface itself and the registry. The code is src/kernel_core/surface_registry/.

The descriptor and the slot

A capsule describes a surface with a SurfaceDescriptor (types.rs:32) and the registry keeps a Slot (table.rs:27) for each live one:

  SurfaceDescriptor { width, height, stride, format, byte_len, base_va, flags }

  Slot { owner_pid, epoch, refcount, width, height, stride, format,
         flags, byte_len, owner_base_va, frames: Vec<PhysAddr> }

The only pixel format is ARGB8888 (PIXEL_BYTES = 4), and the surface is backed by a vector of physical frames the owner already had mapped. The slot records the owner pid, a refcount for sharing, the geometry, and the frames; owner_base_va is the virtual address the owner registered it at, kept so a self-attach can return it without remapping. Surfaces live in a fixed table of SLOT_CAP = 256 slots, and a surface is at most MAX_PAGES_PER_SURFACE = 8192 pages, which is the same framebuffer-sized ceiling the DMA broker uses (one 4K ARGB surface).

Registration

register_surface (table.rs:46) validates the descriptor and claims a free slot:

  register_surface(owner_pid, desc, frames):
      reject if format != ARGB8888, or width/height == 0
      reject if stride < width * 4                    // stride must cover a row
      reject if frames empty or > MAX_PAGES_PER_SURFACE
      find a free slot, set refcount = 1
      return (sid, handle)

The validation is strict: the format must be the one supported format, the dimensions must be non-zero, and the stride must be at least a full row of pixels, so a surface cannot be registered with geometry that would let a later present read out of bounds. A full table returns OutOfSlots. The call returns a surface id and a handle.

The epoch-guarded handle

A surface handle packs the slot index and an epoch (types.rs:70):

  handle = (slot_index << 32) | epoch

The epoch is what makes a handle safe to hold across a slot being freed and reused. Every operation that takes a handle, share, attach, present, decodes it and checks the epoch against the slot's current epoch, rejecting a mismatch with BadHandle. So if a surface is released and its slot is later reused for a different surface, a stale handle from the old surface does not silently address the new one; it fails. lookup_owned additionally checks the caller is the owner (NotOwner otherwise), so ownership operations cannot be performed on someone else's surface.

Security analysis

A surface is a piece of shareable framebuffer that a capsule registers with the kernel, and the registry is the one place that decides whether a descriptor is well formed and who owns the slot. Three properties draw the bound, and one is honestly outside the registry's reach.

Registration is gated by its own capability. MkSurfaceRegister is dispatched only after the cap table check passes: mk.rs:72 routes register, share, and release through can_surface_create (caps/checks/graphics.rs:25), which requires the GraphicsSurfaceCreate grant (bit 4096, capabilities/types.rs:68) on a valid token. A capsule with no graphics capability cannot mint a surface at all, and the create capability is distinct from the map and present ones, so the three stages of the surface lifecycle are separately grantable.

The descriptor cannot describe an out-of-bounds surface. register_surface (table.rs:46) rejects any format other than ARGB8888, a zero width or height, a stride below width * 4 (PIXEL_BYTES), an empty frame list, and a frame count over MAX_PAGES_PER_SURFACE = 8192 (table.rs:51). The stride check is the load-bearing one: because the stride is validated to cover a full row, a later present cannot be talked into reading past the end of a row into another surface's frames. The syscall layer bounds the request even earlier, capping byte_len at 64 MiB in do_register (surface_handlers.rs:62) so an unbounded length cannot force a huge Vec or a near-infinite translate loop before the registry ever sees it.

Ownership and epoch are checked on every handle. The handle packs a slot index and an epoch (types.rs:70), and lookup_owned (table.rs:84) returns NotOwner if the caller is not the recorded owner and NotFound for an empty slot. The per-slot epoch is bumped on reuse, so a stale handle to a released surface fails with BadHandle rather than aliasing whatever surface later took the slot. A capsule therefore operates only on surfaces it registered, and only while its handle is current.

The honest boundary is that the frames themselves are the owner's own already-mapped pages, translated from base_va in do_register (surface_handlers.rs:69). The registry trusts that those frames belong to the caller because they came out of the caller's own address space through translate_address; it does not, and without an IOMMU cannot, stop a device that is later handed those physical frames from reaching other RAM. The registry bounds the descriptor and the ownership, not the reach of downstream hardware.

Debugging surfaces

Every registry rejection is a RegistryError (types.rs:56) that map_err (surface_ops.rs:51) turns into an errno the caller sees, so a surface that will not register is never silent about why:

  InvalidArg   -> EINVAL   bad format, zero geometry, stride < width*4, or frame count over 8192
  OutOfSlots   -> ENOMEM   the 256-slot table is full
  NotOwner     -> EPERM    share/present on a surface the caller does not own
  NotFound     -> EINVAL   the sid does not name a live slot
  BadHandle    -> EINVAL   the handle epoch does not match the slot (stale handle)
  MapFailed    -> ENOTSUP  attach could not map the frames into the receiver
  NoProc       -> ENOTSUP  no current process during attach

The two most common create-time failures are geometry and translation. An EINVAL out of MkSurfaceRegister with a valid handle means the descriptor failed a register_surface check, and the usual culprit is a stride that does not cover the row or a byte_len over the 64 MiB syscall cap. An EFAULT instead means translate_address (surface_handlers.rs:70) could not resolve one of the base_va pages, so the surface memory the capsule pointed at is not actually mapped in its own address space. On the pid values 0x17, 0x26, 0x27 the handlers also emit [SURFACE] register enter / register ok trace lines (surface_handlers.rs:37), so on the compositor and shell you can see registration reach or miss ok on the serial log.

Source map

  src/kernel_core/surface_registry/types.rs   SurfaceDescriptor, the handle encoding, RegistryError, the caps
  src/kernel_core/surface_registry/table.rs   the slot table, register_surface, lookup_owned
  src/syscall/dispatch/router/surface_handlers.rs  do_register: byte_len cap and frame translation
  src/syscall/dispatch/router/surface_ops.rs  map_err: RegistryError to errno
  src/syscall/contract/cap_table/mk.rs        the per-op capability gate (register -> GraphicsSurfaceCreate)
  src/capabilities/types.rs                   the graphics capability bits

Every reference above is verified against those trees. The cross-address-space frame mapping these surfaces feed is on the sharing page, present and vsync are on the presentation page, and the framebuffer-sized 8192-page ceiling is shared with the DMA broker.