|
91 | 91 | the BEAM's celebrated fault-isolation guarantees. Dirty NIFs address scheduling |
92 | 92 | fairness but provide no crash isolation. Port-based solutions achieve isolation |
93 | 93 | at the cost of a process boundary---at which point the primitive is no longer a |
94 | | -NIF. We present \textit{SNIFs} (Safe NIFs): a architecture that compiles native |
| 94 | +NIF. We present \textit{SNIFs} (Safe NIFs): an architecture that compiles native |
95 | 95 | code to WebAssembly, loads it through an embedded Wasmtime runtime (via the |
96 | 96 | \texttt{wasmex} library), and catches all guest faults as \texttt{\{:error, |
97 | 97 | reason\}} tuples before they reach the BEAM scheduler. We provide an empirical |
@@ -261,6 +261,31 @@ \section{SNIF Architecture} |
261 | 261 | \end{tabular} |
262 | 262 | \end{center} |
263 | 263 |
|
| 264 | +\subsection{Comparison with alternatives} |
| 265 | + |
| 266 | +Table~\ref{tab:comparison} positions SNIFs against the two existing approaches. |
| 267 | +The key claim is that SNIFs occupy a distinct middle point: isolation without a |
| 268 | +process boundary. |
| 269 | + |
| 270 | +\begin{table}[h] |
| 271 | +\centering |
| 272 | +\begin{tabular}{llll} |
| 273 | +\toprule |
| 274 | +\textbf{Property} & \textbf{NIF} & \textbf{SNIF} & \textbf{Port} \\ |
| 275 | +\midrule |
| 276 | +Crash isolation & None (VM dies) & WASM sandbox & OS process boundary \\ |
| 277 | +On guest fault & VM terminates & \texttt{\{:error, trap\}} & Port closes \\ |
| 278 | +Supervisor recovery & Impossible & Automatic & Via port supervisor \\ |
| 279 | +Call syntax & Normal & Normal & Binary protocol \\ |
| 280 | +Per-call overhead & $\sim$0 & WASM bounds + GenServer & OS fork + serialisation \\ |
| 281 | +Data marshalling & C types via erl\_nif & Numeric direct; binary via \texttt{Wasmex.Memory} & Byte stream \\ |
| 282 | +Process boundary? & No & No & Yes \\ |
| 283 | +\bottomrule |
| 284 | +\end{tabular} |
| 285 | +\caption{NIF vs SNIF vs Port: isolation, overhead, and call model.} |
| 286 | +\label{tab:comparison} |
| 287 | +\end{table} |
| 288 | + |
264 | 289 | \subsection{Compilation} |
265 | 290 |
|
266 | 291 | Zig exports are marked with the \texttt{export} keyword. Compilation uses |
@@ -347,8 +372,13 @@ \subsection{Failure categories} |
347 | 372 | \subsection{Results: CLI validation} |
348 | 373 | \label{sec:cli-results} |
349 | 374 |
|
350 | | -Table~\ref{tab:cli-results} shows results from the \texttt{wasmtime} CLI |
351 | | -(exit code 134 = trap caught; exit code 0 = normal return). |
| 375 | +Table~\ref{tab:cli-results} shows results from the \texttt{wasmtime} CLI. |
| 376 | +Exit code 134 (= 128 + SIGABRT) is \textit{the wasmtime CLI process aborting |
| 377 | +after catching a trap}---it is not the host crashing. In embedded (library) |
| 378 | +mode, the same event returns \texttt{\{:error, reason\}} to the caller; the |
| 379 | +host process is unaffected. Exit code 0 with a numeric result indicates the |
| 380 | +function returned normally, which under \textit{ReleaseFast} may be a silently |
| 381 | +wrong value. |
352 | 382 |
|
353 | 383 | \begin{table}[h] |
354 | 384 | \centering |
@@ -399,6 +429,68 @@ \subsection{Results: embedded BEAM validation} |
399 | 429 | \label{tab:beam-results} |
400 | 430 | \end{table} |
401 | 431 |
|
| 432 | +\subsection{Full disassembly matrix} |
| 433 | +\label{sec:disassembly-matrix} |
| 434 | + |
| 435 | +Table~\ref{tab:disasm} shows the compiled WASM for every function in both |
| 436 | +relevant modes, obtained via \texttt{llvm-objdump -d}. This is the machine-level |
| 437 | +evidence for the behavioural results in Tables~\ref{tab:cli-results} |
| 438 | +and~\ref{tab:beam-results}. |
| 439 | + |
| 440 | +\begin{table}[h] |
| 441 | +\centering |
| 442 | +\begin{tabular}{lll} |
| 443 | +\toprule |
| 444 | +\textbf{Function} & \textbf{ReleaseSafe WASM} & \textbf{ReleaseFast WASM} \\ |
| 445 | +\midrule |
| 446 | +\texttt{oob\_access} |
| 447 | + & \texttt{i32.const 99; i32.const 3;} |
| 448 | + & \texttt{local.get 0; end} \\ |
| 449 | + & \texttt{call outOfBounds; unreachable} |
| 450 | + & \textit{(OOB removed; returns 0)} \\[4pt] |
| 451 | + |
| 452 | +\texttt{crash\_unreachable} |
| 453 | + & \texttt{call panicHandler; end} |
| 454 | + & \texttt{unreachable; end} \\ |
| 455 | + & \textit{(runtime panic call)} |
| 456 | + & \textit{(first-class WASM instruction)} \\[4pt] |
| 457 | + |
| 458 | +\texttt{crash\_panic} |
| 459 | + & \texttt{unreachable; end} |
| 460 | + & \texttt{unreachable; end} \\ |
| 461 | + & \textit{(direct unreachable)} |
| 462 | + & \textit{(unchanged; always traps)} \\[4pt] |
| 463 | + |
| 464 | +\texttt{crash\_overflow} |
| 465 | + & \texttt{call overflowHandler; end} |
| 466 | + & \texttt{i32.const -2147483648; end} \\ |
| 467 | + & \textit{(overflow detected; panic)} |
| 468 | + & \textit{(constant-folded; wraps silently)} \\[4pt] |
| 469 | + |
| 470 | +\texttt{crash\_div\_zero} |
| 471 | + & \texttt{unreachable; end} |
| 472 | + & \texttt{local.get 0; end} \\ |
| 473 | + & \textit{(@divExact zero check)} |
| 474 | + & \textit{(division removed; returns 0)} \\[4pt] |
| 475 | + |
| 476 | +\texttt{normal} |
| 477 | + & \texttt{i32.const 42; end} |
| 478 | + & \texttt{i32.const 42; end} \\ |
| 479 | + & \textit{(unchanged)} |
| 480 | + & \textit{(unchanged)} \\ |
| 481 | +\bottomrule |
| 482 | +\end{tabular} |
| 483 | +\caption{WASM disassembly for each function in ReleaseSafe and ReleaseFast. |
| 484 | + ReleaseSafe converts every safety violation to \texttt{unreachable} |
| 485 | + (directly or via a panic handler that terminates with \texttt{unreachable}). |
| 486 | + ReleaseFast removes UB-triggering operations entirely, replacing them |
| 487 | + with uninitialised local reads or compile-time constants. |
| 488 | + \texttt{unreachable} and \texttt{@panic} are immune because the WASM |
| 489 | + \texttt{unreachable} instruction is a first-class control-flow |
| 490 | + primitive the optimizer cannot eliminate.} |
| 491 | +\label{tab:disasm} |
| 492 | +\end{table} |
| 493 | + |
402 | 494 | %%% 5. The Mode Invariant %%% |
403 | 495 | \section{The ReleaseSafe Invariant} |
404 | 496 | \label{sec:mode-invariant} |
@@ -432,6 +524,43 @@ \section{The ReleaseSafe Invariant} |
432 | 524 | No memory access occurs. No trap fires. The function returns 0. The BEAM |
433 | 525 | receives \texttt{\{:ok, [0]\}} and has no indication that anything went wrong. |
434 | 526 |
|
| 527 | +\begin{theorem}[SNIF Crash Isolation] |
| 528 | +\label{thm:isolation} |
| 529 | +Let $f$ be a Zig function compiled to \texttt{wasm32-freestanding} with |
| 530 | +\texttt{-OReleaseSafe} and loaded via \texttt{wasmex}. For any call to $f$ |
| 531 | +that would raise \texttt{SIGSEGV}, \texttt{SIGBUS}, or \texttt{SIGILL} in a |
| 532 | +native NIF: |
| 533 | +\begin{enumerate}[label=(\roman*)] |
| 534 | + \item $f$ returns \texttt{\{:error, reason\}} to the calling Elixir process; |
| 535 | + \item the BEAM scheduler continues executing; |
| 536 | + \item all other Elixir/Erlang processes are unaffected. |
| 537 | +\end{enumerate} |
| 538 | +\end{theorem} |
| 539 | + |
| 540 | +\begin{proof}[Proof sketch] |
| 541 | +Wasmtime registers \texttt{SIGSEGV} and \texttt{SIGILL} signal handlers |
| 542 | +covering the WASM linear memory region \textit{before} executing any guest |
| 543 | +code (confirmed: \texttt{wasmtime::Config} documentation; see also the |
| 544 | +security advisory GHSA-vc8c-j3xm-xj73). Signals raised within that region |
| 545 | +are intercepted and converted to \texttt{Trap} values returned through |
| 546 | +the Wasmtime embedding API. Signals originating outside the region pass |
| 547 | +through to the host unchanged, preserving non-WASM fault semantics. |
| 548 | + |
| 549 | +Under \textit{ReleaseSafe}, every Zig safety violation (OOB, overflow, |
| 550 | +divide-by-zero, \texttt{unreachable}, \texttt{@panic}) produces a WASM |
| 551 | +\texttt{unreachable} instruction---either directly or via a panic handler |
| 552 | +that terminates with one (Table~\ref{tab:disasm}). The WASM specification |
| 553 | +requires that reaching \texttt{unreachable} raises a trap. Wasmtime catches |
| 554 | +it and returns \texttt{\{:error, reason\}} through \texttt{wasmex}'s |
| 555 | +\texttt{Wasmex.call\_function/3}. The \texttt{wasmex} GenServer remains |
| 556 | +alive; the calling process receives the error tuple. The BEAM scheduler is |
| 557 | +not involved in Unix signal delivery and is not interrupted. |
| 558 | + |
| 559 | +Claims (i)--(iii) follow directly. The empirical evidence in |
| 560 | +Tables~\ref{tab:cli-results}--\ref{tab:disasm} confirms the mechanism |
| 561 | +end-to-end on Wasmtime 43.0.0 / OTP 28. |
| 562 | +\end{proof} |
| 563 | + |
435 | 564 | \begin{observation} |
436 | 565 | In \textit{ReleaseFast}, Zig undefined behaviour collapses to silent incorrect |
437 | 566 | return values, not crashes. \texttt{unreachable} and \texttt{@panic} still |
@@ -575,10 +704,12 @@ \section{Conclusion} |
575 | 704 | \newblock Zenodo, 2026. DOI: \href{https://doi.org/10.5281/zenodo.19329508}{10.5281/zenodo.19329508} |
576 | 705 |
|
577 | 706 | \bibitem{nif-safety-2018} |
578 | | -Stavros Aronis, John Hughes, Kostis Sagonas, and Daniel Vasconcelos. |
579 | | -\newblock On the verification of Erlang-style concurrent programs. |
580 | | -\newblock In \textit{Proceedings of the 17th ACM SIGPLAN International Conference |
581 | | -on Functional Programming}, 2012. |
| 707 | +Fred Hebert. |
| 708 | +\newblock \textit{Erlang in Anger}, Chapter 7: Reading Crash Dumps. |
| 709 | +\newblock Self-published, 2014. \url{https://www.erlang-in-anger.com/} |
| 710 | +\newblock (Survey of NIF crash patterns and their consequences for BEAM |
| 711 | + process survival; OOB memory access and null pointer dereference are |
| 712 | + the dominant categories.) |
582 | 713 |
|
583 | 714 | \end{thebibliography} |
584 | 715 |
|
|
0 commit comments