|
| 1 | +# Host functions and attack surface |
| 2 | + |
| 3 | +This document describes how the **hyperlight-unikraft** host exposes capabilities to a Unikraft guest on the Hyperlight platform, what is enabled by default, and what security boundaries apply. |
| 4 | + |
| 5 | +Implementation lives in [`host/src/lib.rs`](../host/src/lib.rs). Guest-side callers include [`lib/hostfs`](https://github.com/unikraft/unikraft/tree/plat-hyperlight/lib/hostfs) (filesystem) and [`lib/hostsock`](https://github.com/unikraft/unikraft/tree/plat-hyperlight/lib/hostsock) (networking), both on the Unikraft `plat-hyperlight` branch. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## Summary |
| 10 | + |
| 11 | +| Surface | Default | Enable with | |
| 12 | +|---------|---------|-------------| |
| 13 | +| `__dispatch` host function | Always registered at VM boot | (automatic) | |
| 14 | +| `__hl_exit`, `__hl_sleep` | Always available via `__dispatch` | (automatic) | |
| 15 | +| `fs_*` tools | **Off** | `--mount HOST[:GUEST]` (repeatable) | |
| 16 | +| `net_*` tools | **Off** | `--net`, `--net-allow`, or `--net-block` | |
| 17 | +| Inbound listen | **Off** | `--port PORT` (requires network enabled) | |
| 18 | +| Custom tools | **Off** | `--enable-tools` + `SandboxBuilder::tool()` | |
| 19 | + |
| 20 | +With **no flags**, the guest cannot reach the host filesystem or network through dispatch. Only internal plumbing (`__hl_exit`, `__hl_sleep`) is wired. |
| 21 | + |
| 22 | +--- |
| 23 | + |
| 24 | +## Architecture |
| 25 | + |
| 26 | +``` |
| 27 | +Guest (Unikraft) |
| 28 | + ├─ lib/hostfs (when --mount) ──► hyperlight_hcall() ──► __dispatch ──► fs_* handlers ──► FsSandbox ──► host files |
| 29 | + ├─ lib/hostsock (when --net) ──► hyperlight_hcall() ──► __dispatch ──► net_* handlers ──► host sockets |
| 30 | + └─ /dev/hcall or direct hcall ──► hyperlight_hcall() ──► __dispatch ──► named tool in ToolRegistry |
| 31 | + │ |
| 32 | + ▼ |
| 33 | +Hyperlight host (hyperlight-unikraft) |
| 34 | + ToolRegistry::dispatch(payload) → JSON in / JSON out |
| 35 | +``` |
| 36 | + |
| 37 | +There is **one** guest-to-host RPC channel for tools: the Hyperlight host function **`__dispatch`**, registered when the sandbox is created. All tool names are looked up in a host-side `ToolRegistry`. |
| 38 | + |
| 39 | +### End-to-end example: `time.sleep(2)` in a Python guest |
| 40 | + |
| 41 | +1. The Python app calls `time.sleep(2)`. |
| 42 | +2. The Hyperlight Python driver patches the call to open `/dev/hcall` and write JSON: `{"name": "__hl_sleep", "args": {"ns": 2000000000}}`. |
| 43 | +3. Unikraft's Hyperlight platform routes writes to `/dev/hcall` to execute the `__dispatch` host function call. |
| 44 | +4. The host parses the call to `__dispatch`, looks up `__hl_sleep` in the `ToolRegistry`, and calls the handler. |
| 45 | +5. `__hl_sleep` executes, and the response flows back via the normal hyperlight-core host function mechanism (writing the response into shared memory via the input shared buffer). |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +## `__dispatch` wire format |
| 50 | + |
| 51 | +**Request** (UTF-8 JSON bytes, max **64 MiB**): |
| 52 | + |
| 53 | +```json |
| 54 | +{"name": "<tool_name>", "args": <json_value>} |
| 55 | +``` |
| 56 | + |
| 57 | +**Success response:** |
| 58 | + |
| 59 | +```json |
| 60 | +{"result": <json_value>} |
| 61 | +``` |
| 62 | + |
| 63 | +**Error response:** |
| 64 | + |
| 65 | +```json |
| 66 | +{"error": "<message>"} |
| 67 | +``` |
| 68 | + |
| 69 | +Unknown tools, malformed JSON, and handler errors become `{"error": "..."}`. The host does not panic on bad guest input. |
| 70 | + |
| 71 | +**Debug:** set `HL_DISPATCH_DEBUG=1` in the environment to log each request/response on stderr. |
| 72 | + |
| 73 | +--- |
| 74 | + |
| 75 | +## CLI: enabling host capabilities |
| 76 | + |
| 77 | +```bash |
| 78 | +hyperlight-unikraft KERNEL [--initrd CPIO] [options] [-- APP_ARGS...] |
| 79 | +``` |
| 80 | + |
| 81 | +| Flag | Effect | |
| 82 | +|------|--------| |
| 83 | +| `--mount HOST[:GUEST]` | Preopen `HOST` at guest path `GUEST` (default `/host`). Registers all `fs_*` tools. Repeat for multiple mounts. | |
| 84 | +| `--net` | Outbound networking: register `net_*` tools with **allow-all** policy (still blocks loopback and link-local). | |
| 85 | +| `--net-allow HOST_OR_IP` | Allow-list outbound destinations (implies `--net`). Repeatable. | |
| 86 | +| `--net-block HOST_OR_IP` | Block-list; all other destinations allowed (implies `--net`). Mutually exclusive with `--net-allow`. | |
| 87 | +| `--port PORT` | Allow `net_bind` / listen on `PORT` (implies `--net`). Without `--port`, outbound-only: bind is rejected. | |
| 88 | +| `--enable-tools` | Enables custom tool registration. Registers a built-in `echo` tool (used by the `python-tools` example). Library users add their own tools via `SandboxBuilder::tool()`. | |
| 89 | + |
| 90 | +**Mount rules (host-enforced before boot):** |
| 91 | + |
| 92 | +- `GUEST` must be absolute (e.g. `/data`, `/host`). |
| 93 | +- Cannot use reserved guest paths: `/`, `/bin`, `/dev`, `/proc`, `/sys`, `/usr`. |
| 94 | +- Duplicate `GUEST` paths are rejected. |
| 95 | + |
| 96 | +**Initrd metadata** (not dispatch, but host-to-guest config): cmdline (`HLCMDLN`), mount table (`HLHSMNT`), optional wall-clock seed (`HLWALL0`). See `prepend_cmdline_to_initrd()` in `host/src/lib.rs`. |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +## Always-registered internal tools |
| 101 | + |
| 102 | +Registered for every sandbox, regardless of `--mount` / `--net`: |
| 103 | + |
| 104 | +### `__hl_exit` |
| 105 | + |
| 106 | +Guest driver exit hook. |
| 107 | + |
| 108 | +| Arg | Type | Description | |
| 109 | +|-----|------|-------------| |
| 110 | +| `code` | number (optional) | Exit code; default `1` | |
| 111 | + |
| 112 | +**Result:** `{}` |
| 113 | +Host stores the code in an atomic read after the VM run. |
| 114 | + |
| 115 | +### `__hl_sleep` |
| 116 | + |
| 117 | +Sleep on the host thread (used by guest drivers). |
| 118 | + |
| 119 | +| Arg | Type | Description | |
| 120 | +|-----|------|-------------| |
| 121 | +| `ns` | number (optional) | Nanoseconds; capped at **60 s** | |
| 122 | + |
| 123 | +**Result:** `{}` |
| 124 | +Can be cancelled via `SleepCancel` when tearing down the sandbox. |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +## Filesystem tools (`fs_*`) |
| 129 | + |
| 130 | +Registered when at least one `--mount` / `Preopen` is configured. Paths in `args.path` are **guest paths** (e.g. `/host/project/file.txt`). The host routes to the longest matching preopen prefix, then resolves under that host directory via [`FsSandbox`](../host/src/lib.rs). |
| 131 | + |
| 132 | +**Sandbox guarantees:** |
| 133 | + |
| 134 | +- Host directory is canonicalized at mount setup. |
| 135 | +- `..` and symlink chains cannot escape the mount root (hop limit 40). |
| 136 | +- Mount root itself cannot be deleted (`fs_unlink`). |
| 137 | + |
| 138 | +| Tool | Args | Result (success) | |
| 139 | +|------|------|------------------| |
| 140 | +| `fs_read` | `path` | `{"text": "<utf-8 string>"}` — whole file, max **16 MiB** | |
| 141 | +| `fs_write` | `path`, `text`, `append?` | `{"bytes_written": N}` — max **16 MiB** text | |
| 142 | +| `fs_read_bytes` | `path`, `offset?`, `len?` | `{"data": "<base64>", "eof": bool, "bytes_read": N}` — default `len` 65536, max **16 MiB** | |
| 143 | +| `fs_write_bytes` | `path`, `data` (base64), `offset?`, `append?` | `{"bytes_written": N}` — max **16 MiB** decoded | |
| 144 | +| `fs_list` | `path` (required; must match a preopen prefix, e.g. `/host`) | `{"entries": [{"name", "is_dir", "is_file", "is_symlink"}, ...]}` — max **100 000** entries | |
| 145 | +| `fs_stat` | `path` | `{"size", "is_dir", "is_file", "mtime_ns", "atime_ns"}` | |
| 146 | +| `fs_truncate` | `path`, `length` | `{}` — max length **1 GiB** | |
| 147 | +| `fs_mkdir` | `path`, `parents?` | `{}` | |
| 148 | +| `fs_unlink` | `path` | `{}` — file or empty dir; not mount root | |
| 149 | + |
| 150 | +Errors are normalized to Linux-style `std::io::Error` wording where possible so the guest `lib/hostfs` can map them to POSIX errno (see `normalize_fs_error()`). |
| 151 | + |
| 152 | +**Guest integration:** With `CONFIG_LIBHOSTFS`, unmodified POSIX under the mount point uses these same tools via `hostfs_rpc_*` → `hyperlight_hcall()`. See [`lib/hostfs`](https://github.com/unikraft/unikraft/tree/plat-hyperlight/lib/hostfs). |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Network tools (`net_*`) |
| 157 | + |
| 158 | +Registered only when a [`NetworkPolicy`](../host/src/lib.rs) is set (`--net`, `--net-allow`, or `--net-block`). |
| 159 | + |
| 160 | +Sockets are host-side (`socket2`); the guest sees opaque numeric **`fd`** handles (per-sandbox table, max **1024** sockets, **30 s** read/write/connect timeout). |
| 161 | + |
| 162 | +**Common arg:** `addr` + `port` for sockaddr (IPv4/IPv6 string + port). |
| 163 | + |
| 164 | +| Tool | Purpose | |
| 165 | +|------|---------| |
| 166 | +| `net_socket` | `family`, `type`, `protocol` → `{"fd"}` | |
| 167 | +| `net_connect` | Outbound connect (policy-checked) | |
| 168 | +| `net_bind` | Bind; requires `--port` allowlist entry | |
| 169 | +| `net_listen` | Listen after bind | |
| 170 | +| `net_accept` | Accept; returns new `fd` + peer | |
| 171 | +| `net_send` / `net_recv` | Stream/datagram I/O — payload max **1 MiB decoded bytes** (base64-encoded on wire) | |
| 172 | +| `net_sendto` / `net_recvfrom` | Datagram with address (policy on destination) | |
| 173 | +| `net_close` | Close host socket | |
| 174 | +| `net_shutdown` | Shutdown (best-effort) | |
| 175 | +| `net_setsockopt` / `net_getsockopt` | Limited socket options | |
| 176 | +| `net_getpeername` / `net_getsockname` | Peer/local address | |
| 177 | + |
| 178 | +### Network policy |
| 179 | + |
| 180 | +| Policy | Behavior | |
| 181 | +|--------|----------| |
| 182 | +| **AllowAll** (`--net`) | Any outbound IP except **loopback** and **link-local** (blocks cloud metadata-style addresses). | |
| 183 | +| **AllowList** (`--net-allow`) | Only listed IPs/hostnames; hostnames re-resolved at check time; DNS to resolver IPs on port **53** allowed for listed resolvers + common public DNS. | |
| 184 | +| **BlockList** (`--net-block`) | Block listed targets; others allowed (same loopback/link-local deny for all policies). | |
| 185 | + |
| 186 | +**Inbound:** `--port` adds a listen-port allowlist. Without it, `net_bind` fails with "no --port specified" (outbound-only mode). |
| 187 | + |
| 188 | +--- |
| 189 | + |
| 190 | +## Custom tools |
| 191 | + |
| 192 | +**CLI:** `--enable-tools` registers a built-in `echo` tool (returns `args` unchanged) used by the [`python-tools` example](../examples/python-tools). The primary purpose of `--enable-tools` is to demonstrate custom host function registration via the API. |
| 193 | + |
| 194 | +**Library:** |
| 195 | + |
| 196 | +```rust |
| 197 | +Sandbox::builder("kernel") |
| 198 | + .tool("my_tool", |args| Ok(serde_json::json!({"ok": true}))) |
| 199 | + .build()?; |
| 200 | +``` |
| 201 | + |
| 202 | +Custom handlers run with the same JSON request/response envelope as built-in tools. |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | +## Resource limits (host-enforced) |
| 207 | + |
| 208 | +| Limit | Value | |
| 209 | +|-------|-------| |
| 210 | +| Dispatch payload | 64 MiB | |
| 211 | +| `fs_read` / `fs_read_bytes` | 16 MiB per call | |
| 212 | +| `fs_write` / `fs_write_bytes` | 16 MiB per call | |
| 213 | +| `fs_truncate` length | 1 GiB | |
| 214 | +| `fs_list` entries | 100 000 | |
| 215 | +| `net_send` / `net_sendto` | 1 MiB decoded bytes | |
| 216 | +| `__hl_sleep` | 60 s | |
| 217 | +| Open host sockets | 1024 per sandbox | |
| 218 | +| AllowList learned DNS IPs | 256 | |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +## Security and attack surface |
| 223 | + |
| 224 | +**Default posture:** The guest is a micro-VM with no host FS and no host network unless the operator opts in. That matches Hyperlight's embed-in-application threat model: the host application chooses what to expose per sandbox. |
| 225 | + |
| 226 | +**When `--mount` is used:** |
| 227 | + |
| 228 | +- The guest can read/write/delete files under the preopened host trees only. |
| 229 | +- Path traversal and symlink escape are rejected host-side; operators should still mount **non-sensitive** directories and treat the guest as **untrusted**. |
| 230 | +- Large reads/writes are capped to limit guest-driven host memory use. |
| 231 | + |
| 232 | +**When `--net` is used:** |
| 233 | + |
| 234 | +- The guest uses the **host network stack**; policy filters destinations but does not isolate traffic from other host processes. |
| 235 | +- Loopback and link-local connects are denied to reduce access to host services and instance metadata. |
| 236 | +- Allow-list mode still permits DNS to configured resolvers on port 53 so resolvers can be used without listing every CDN IP. |
| 237 | +- Inbound listen requires explicit `--port`; otherwise bind is denied. |
| 238 | + |
| 239 | +**`__dispatch` itself:** |
| 240 | + |
| 241 | +- Always registered: internal tools cannot be disabled without code changes. |
| 242 | +- A compromised guest can invoke any **registered** tool name; do not register powerful custom tools unless needed. |
| 243 | +- Payload size is capped; malformed JSON fails closed with an error response. |
| 244 | + |
| 245 | +**Not exposed via dispatch:** Host shell, arbitrary process spawn, unrestricted host `exec`, or kernel modules — only the tools listed above. |
| 246 | + |
| 247 | +**Operators should:** Use minimal flags, allow-lists over `--net` where possible, mount least-privilege directories, and run guests with the smallest initrd/runtime required. |
| 248 | + |
| 249 | +--- |
| 250 | + |
| 251 | +## Programmatic API |
| 252 | + |
| 253 | +Same behavior as the CLI via [`Sandbox`](../host/src/lib.rs) / [`SandboxBuilder`](../host/src/lib.rs): |
| 254 | + |
| 255 | +```rust |
| 256 | +use hyperlight_unikraft::{AllowList, NetworkPolicy, Preopen, Sandbox}; |
| 257 | + |
| 258 | +let mut sbox = Sandbox::builder("./kernel") |
| 259 | + .initrd_file("./app.cpio") |
| 260 | + .preopen(Preopen::new("./workspace", "/host")?) |
| 261 | + .network(NetworkPolicy::AllowList(AllowList::from_hosts(&["api.example.com"])?)) |
| 262 | + .listen_ports(hyperlight_unikraft::ListenPorts::from_ports([8080])) |
| 263 | + .build()?; |
| 264 | +``` |
| 265 | + |
| 266 | +See crate docs in `host/src/lib.rs` for snapshot/restore and `call_run()`. |
0 commit comments