Skip to content

Commit 05d232a

Browse files
authored
docs: add HOST_FUNCTIONS.md for guest host surface (#78)
* docs: add HOST_FUNCTIONS.md for guest host surface Document __dispatch tools, CLI flags, limits, and security model. Closes #42. Signed-off-by: Akrm Al-Hakimi <alhakimiakrmj@gmail.com> * docs: move host_functions to docs/, fix links, clarify net limits and fs_list, add end-to-end call example Signed-off-by: akrm al-hakimi <alhakimiakrmj@gmail.com> --------- Signed-off-by: Akrm Al-Hakimi <alhakimiakrmj@gmail.com> Signed-off-by: akrm al-hakimi <alhakimiakrmj@gmail.com>
1 parent 05c4d64 commit 05d232a

1 file changed

Lines changed: 266 additions & 0 deletions

File tree

docs/host_functions.md

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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

Comments
 (0)