|
| 1 | +# Hostcalls |
| 2 | + |
| 3 | +Hostcalls are how Lucet guests interact with the world outside the WebAssembly |
| 4 | +VM. For example, all functions in [WASI](https://github.com/bytecodealliance/wasmtime/blob/main/docs/WASI-intro.md) are implemented in terms of |
| 5 | +hostcalls that can be exposed to a WebAssembly guest. This chapter discusses |
| 6 | +implementation details of hostcalls as Lucet implements them. |
| 7 | + |
| 8 | +Lucet implements hostcalls as imports of symbols specified by the |
| 9 | +`bindings.json` provided to `lucetc`. This maps namespaced functions provided |
| 10 | +to the WebAssembly module to symbol names the module should import when loaded. |
| 11 | +Functionally, `lucet-runtime` currently relies on the dynamic linker to be able |
| 12 | +to locate and fix up refereneces to these imported functions, and will fail to |
| 13 | +load a module if the dynamic linker can't resolve all imports. |
| 14 | + |
| 15 | +Hostcalls have an important intersection with safety properties Lucet seeks to |
| 16 | +uphold: if a fault occurs in a WebAssembly guest, it should be isolated to that |
| 17 | +guest, and the host should, generally, be able to continue execution. However, |
| 18 | +a fault in a hostcall is a fault _outside_ the WebAssembly guest, back in |
| 19 | +whatever code the host running `lucet-runtime` has provided. A fault here is |
| 20 | +well outside any guarantees WebAssembly can offer, and the only sound option |
| 21 | +Lucet has is to raise that issue in the host, if it has the option, and hope |
| 22 | +the host knows what to do with it. |
| 23 | + |
| 24 | +## Stack overflows |
| 25 | + |
| 26 | +Generally, "kick the problem to the host and hope they know what to do" works. |
| 27 | +For a general memory fault in host code, that gets handled no differently. |
| 28 | +Language-specific features, like Rust panics, work too; if a `ud2` is present |
| 29 | +in the hostcall's body, the `SIGILL` still causes the same kind of panic - not |
| 30 | +a recoverable issue, but Lucet doesn't cause new problems here. |
| 31 | + |
| 32 | +Unfortunately, memory faults like `SIGBUS` and `SIGSEGV` are typically fatal to |
| 33 | +host applications. Memory faults in unpredictable locations even moreso. A |
| 34 | +naive hostcall implementation scheme by calling import functions raises a real |
| 35 | +risk here: if a WebAssembly guest consumes most, but not all, of the Lucet |
| 36 | +guest's stack, _then_ makes a hostcall, the hostcall may consume the rest of |
| 37 | +the guest's stack space and experience a stack overflow. |
| 38 | + |
| 39 | +To mitigate the risk of an unknown WebAssembly guest being able to cause host |
| 40 | +faults essentially on-demand, Lucet guards hostcalls by a trampoline function |
| 41 | +that performs safety checks before actually making the call into host code. |
| 42 | +Currently, there is one check: is there enough guest stack space remaining to |
| 43 | +uphold some guaranteed amount available for hostcalls? |
| 44 | + |
| 45 | +By guaranteeing some minimum available space, the problem of hostcall stack use |
| 46 | +becomes the same as not overflowing stacks generally; if a host expects to |
| 47 | +handle stack overflows in some manner, it probably still can, and if it allows |
| 48 | +the system to do what it will on stack overflows, a hostcall overflowing the |
| 49 | +guaranteed space will still observe a normal stack overflow. Hostcalls must |
| 50 | +conform to the same requirements code would have without Lucet inolved, except |
| 51 | +that the Lucet hostcall stack reservation may be more or less than the system's |
| 52 | +configured thread size. |
| 53 | + |
| 54 | +The good news is that while the hostcall stack reservation is a fixed size, it |
| 55 | +is customizable: the field |
| 56 | +[hostcall_reservation](https://docs.rs/lucet-runtime/0.7.0/lucet_runtime/struct.Limits.html#structfield.heap_memory_size) |
| 57 | +in `Limits` specifies the space Lucet will require to be available, with a |
| 58 | +default of 32KiB. Lucet requires that `hostcall_reservation` is between zero |
| 59 | +and the guest's entire stack. Finally, a `hostcall_reservation` equal to the |
| 60 | +entire guest stack size is allowed, and a de facto denial of hostcalls to the |
| 61 | +guest - a Lucet guest will always have some stack space reserved for the |
| 62 | +runtime-required backstop, so the availability check would always fail. |
| 63 | + |
| 64 | +In terms of implementation, hostcall checks are done in entirely synthetic |
| 65 | +functions generated by lucetc, prefixed with `trampoline_`. The trampoline |
| 66 | +functions themselves are very simple, and have a shape like the following |
| 67 | +Cranelift IR: |
| 68 | +``` |
| 69 | +; A trampoline has the same signature as its hostcall - args plus a vmctx. |
| 70 | +function %trampoline_$HOSTCALL($HOSTCALL_ARGS.., i64 vmctx) -> $HOSTCALL_RESULT { |
| 71 | + gv_vmctx = vmctx |
| 72 | + heap0 = static gv0 |
| 73 | +
|
| 74 | +block0($HOSTCALL_ARGS.., vmctx: i64): |
| 75 | + ; The stack limit is recorded as part of the instance, just before the start of the guest heap. |
| 76 | + stack_limit_addr = heap_addr.i64 heap0, gv_vmctx, -$STACK_LIMIT_OFFSET |
| 77 | + stack_limit = load.i64 stack_limit_addr |
| 78 | + ; Compare the current stack pointer to stack_limit. |
| 79 | + ; The stack pointer is the LHS of this comparison, `stack_limit` the RHS. |
| 80 | + stack_cmp = ifcmp_sp stack_limit |
| 81 | + ; If the limit is greater than or equal to the stack pointer, there is |
| 82 | + ; insufficient space for the hostcall. Branch to the fail block and trap. |
| 83 | + ; |
| 84 | + ; "greater than or equal" may be surprising - it might be more natural to |
| 85 | + ; consider this comparison with arguments reversed; if the stack pointer is |
| 86 | + ; less than the limit, there is insufficient space. |
| 87 | + ; |
| 88 | + ; Even phrased like this, "less than" may be surprising, but is correct since |
| 89 | + ; the stack grows downward. Given an example layout: |
| 90 | + ; 0x0000: start of stack - start of the stack's allocation |
| 91 | + ; 0x2000: hostcall limit - reserve 0x2000 bytes |
| 92 | + ; 0x8000: base of stack - initial guest stack pointer |
| 93 | + ; |
| 94 | + ; and knowledge that the stack grows downward, the space from 0x2000 to |
| 95 | + ; 0x0000 is the reserved space, and a stack pointer below 0x2000 is in the |
| 96 | + ; reserved area, and thus voids the guarantee of reserved space by Lucet. |
| 97 | + brif ugte stack_cmp, stack_check_fail |
| 98 | + jump do_hostcall($HOSTCALL_ARGS.., vmctx) |
| 99 | +
|
| 100 | +do_hostcall($HOSTCALL_ARGS.., vmctx: i64): |
| 101 | + $HOSTCALL_RESULT.. = call $HOSTCALL($HOSTCALL_ARGS.., vmctx) |
| 102 | + return $HOSTCALL_RESULT |
| 103 | +
|
| 104 | +stack_check_fail(): |
| 105 | + ; If the stack check fails, it's raised as a stack overflow in guest code. |
| 106 | + ; This is reasonably close to the actual occurrance, and allows the guest to be |
| 107 | + ; unwound. |
| 108 | + trap stk_ovf |
| 109 | +``` |
| 110 | + |
| 111 | +### Lucet implementation considerations |
| 112 | + |
| 113 | +Why do this trampoline and stack usage test, instead of observing failures in |
| 114 | +guest stack guards? The issue here is twofold: first, we can't robustly |
| 115 | +distinguish a stack overflow from an unlucky errant memory access for other |
| 116 | +reasons. If hosts are built using stack probes, stack overflows will probably |
| 117 | +be observed in places we expect, with patterns we can expect. But this is by no |
| 118 | +means a guarantee, and stack accesses might not be through the stack pointer |
| 119 | +directly (perhaps the address is loaded into an alternate register, and a fault |
| 120 | +occurs without referencing the stack pointer at all). Second, if a hostcall |
| 121 | +fault were to be recovered by Lucet, the runtime may have to unwind a guest that |
| 122 | +already has exhausted its stack space. This would require temporarily making |
| 123 | +the stack guard writable to support instigating a guest unwind, and probably |
| 124 | +motivate a second "real" guard page for safety against unforseen circumstances |
| 125 | +where unwinding might consume significant amounts of stack space. |
| 126 | + |
| 127 | +Given the complexity and fallibility of trying to recover from a stack overflow |
| 128 | +in guest code, we've concluded that pushing the error out of host code, with |
| 129 | +hostcalls constrained to the same kind of boundaries as would otherwise be |
| 130 | +expected, is a reasonable compromise. |
0 commit comments