danos/docs/vfs-protocol.md

7.7 KiB

The VFS wire protocol

Status: built and spoken today between runtime.fs (the client) and the VFS server (system/services/vfs), with mounted backends (the FAT server) speaking the same protocol behind the router. The Zig source of truth is system/services/vfs/protocol.zig (the vfs-protocol module), whose unit tests pin the sizes and values below. This page is the language-neutral wire specification of that contract — what a Rust or C client implements (vdso.md explains why the IPC protocols, not the syscall numbers, are danos's public ABI).

Transport

A VFS exchange is one synchronous IPC rendezvous (ipc_call, docs/ipc.md): the client sends one message and blocks; the server replies with one message. The endpoint is found by well-known service id (ipc_lookup, service id 1 = vfs).

  • A message is at most 256 bytes (message_maximum).
  • A request is a fixed 32-byte Request header followed by an inline payload of at most 224 bytes (maximum_payload) — a path, or write bytes. There is no multi-message request: paths and single reads/writes must fit, and larger transfers loop (see read / write).
  • A reply is a fixed 24-byte Reply header followed by an inline payload — read bytes, a FileStatus, or a DirectoryEntry.
  • All integers are little-endian; layouts are C layout for x86-64 (extern struct), offsets given below so nothing need be inferred.

The kernel never parses any of this — it only moves the bytes (docs/syscall.md); files are entirely a user-space affair.

Request header — 32 bytes

offset size field meaning
0 4 operation an Operation value (below)
4 4 padding
8 8 node the server-side open-node id from a prior open; 0 for path-based operations
16 8 offset byte position for read/write; entry index (cursor) for readdir; else 0
24 4 len payload length for path/write operations; requested byte count for read
28 4 flags open flags (below); else 0

Reply header — 24 bytes

offset size field meaning
0 4 status 0 = success, negative = failure (signed)
4 4 padding
8 8 node the new open-node id (for open); else 0
16 4 len reply payload length in bytes
20 4 padding

On failure the router replies status = -1; a mounted backend's negative status is forwarded to the client verbatim. A richer errno vocabulary is future work — clients must treat any negative status as failure, not match on -1.

Operations

Values are append-only and never renumbered (the same evolution rule every danos protocol follows); an unrecognised operation gets a status = -1 reply.

value operation request payload reply
0 open the path (len = its length), flags as below node = open-node id
1 close — (node set) status only
2 read — (node, offset, len = wanted count) len bytes read, payload = the bytes; len 0 at end of file
3 write the bytes (node, offset, len = count) len = bytes accepted (may be short — loop)
4 status — (node set) payload = FileStatus (24 bytes)
5 readdir — (node = a directory, offset = cursor) payload = one DirectoryEntry + name; len 0 at end
6 mount the mount-point path; the backend endpoint rides as the call's capability status only
7 unmount the mount-point path status only
8 mkdir the path status only
9 unlink the path status only
10 rename old path, one 0x00, new path (len = total) status only

Notes per operation:

  • open — paths are absolute (/mnt/usb/notes.txt) or bare names (greeting); bare names resolve in the VFS's flat ramfs, absolute paths route through the mount table (below). The returned node is an id in the router's open table; clients never see a backend's own ids.
  • read / write — a single exchange moves at most 224 bytes (maximum_payload); the client loops, advancing offset by the returned len, until done (read) or the slice is written (write). A write reply shorter than requested is progress, not an error; a len of 0 means no forward progress — stop rather than spin.
  • readdiroffset is a cursor: the entry index, not a byte position. Each call returns exactly one entry; the client increments the cursor by 1. A reply with len 0 is end-of-directory. The directory must have been opened with the directory flag.
  • mount — the one operation that passes a capability: the caller (a filesystem server, e.g. FAT) sends its own request endpoint as the ipc_call capability argument, and the router forwards everything under the mount point to it — speaking this same protocol, with paths rewritten relative to the mount. Prefixes match at path boundaries only (/mnt/usb never captures /mnt/usbextra); the longest matching prefix wins.
  • rename — same-directory rename only (the router requires old and new to resolve under one mount).

Open flags

Bitwise OR in Request.flags, meaningful for open only:

bit name meaning
1 create create the file if it does not exist
2 directory open a directory node for readdir rather than a file
4 truncate truncate an existing file to zero length on open (replace, don't overwrite in place)

FileStatus — 24 bytes (the status reply payload)

offset size field meaning
0 8 size file size in bytes
8 4 kind a NodeKind value
12 4 padding
16 8 mtime modification time, Unix epoch seconds UTC; 0 if the backend keeps none

DirectoryEntry — 16 bytes + name (the readdir reply payload)

offset size field meaning
0 4 kind a NodeKind value
4 4 name_len length of the name that follows
8 8 size the entry's size in bytes
16 name_len name the entry's name, not NUL-terminated

NodeKind

Aligned to the FSH file-type table (docs/danos-file-system-hierarchy-FSH.md):

value kind
0 regular file
1 directory
2 character device
3 block device
4 symbolic link
5 fifo
6 socket

Clients should map unknown values to regular rather than reject — the table can grow.

Lifetimes and trust

Open-node ids live in the server. A client that dies without closing leaks nothing permanently: the VFS subscribes to the kernel's published process-exit events (docs/process-lifecycle.md) and releases a dead client's handles, closing forwarded backend nodes best-effort. Ids are plain integers, not capabilities — the VFS trusts its callers with each other's ids today, which is acceptable while every client is part of the system image and worth revisiting (per-client id namespaces) before third-party binaries arrive.

Evolution rules

What a non-Zig implementation may rely on, and what it must not:

  • Operation values, flag bits, NodeKind values, and struct layouts are append-only and frozen once shipped — the unit tests in protocol.zig pin them exactly so a refactor can't silently move them.
  • The 256-byte message ceiling is a property of the current IPC transport, not a promise; clients should read maximum_payload-shaped limits from the reply lengths they actually get (loop-until-done), not hard-code 224.
  • Negative statuses beyond -1 will appear (an errno vocabulary); success is exactly 0.