diff --git a/_config.yml b/_config.yml index 5d5763f..b32c9f4 100644 --- a/_config.yml +++ b/_config.yml @@ -24,7 +24,7 @@ description: >- # this means to ignore newlines until "baseurl:" Asterinas is an open-source community to advance Rust-based OSes baseurl: "" # the subpath of your site, e.g. /blog url: asterinas.github.io -github_username: asterinas +github_username: asterinas # Build settings theme: minima diff --git a/_layouts/post.html b/_layouts/post.html index 0e6f0e3..e317d0a 100644 --- a/_layouts/post.html +++ b/_layouts/post.html @@ -8,6 +8,19 @@ + +
diff --git a/_posts/2026-07-03-verifying-ostd-soundness.md b/_posts/2026-07-03-verifying-ostd-soundness.md new file mode 100644 index 0000000..c8a51c8 --- /dev/null +++ b/_posts/2026-07-03-verifying-ostd-soundness.md @@ -0,0 +1,215 @@ +--- +layout: post +title: "Proving Soundness for Unsafe Rust: Lessons from an OS Kernel" +date: 2026-07-03 09:00:00 +0800 +author: "Asterinas Team and CertiK" +categories: [formal-verification, rust, kernel] +tags: [unsafe, soundness, verus, ostd, asterinas] +--- + +**TL;DR:** After a year and a half of work, the core memory-management API of the Asterinas kernel is +formally proven **sound** — no safe caller can drive it into undefined behavior, in any order, with any arguments. + +- **What it covers.** The virtual memory subsystem of `mm`, from physical memory management up to virtual address spaces. +- **What it costs.** Roughly 6,000 lines of `unsafe`-heavy kernel code in two person-years. A proof-to-code ratio of about 5:1, compared to the 10:1 or even 20:1 that has historically been necessary. +- **Who should read on.** If you maintain a Rust system with an `unsafe` core, the methodology transfers. Everything is open: [vostd](https://github.com/asterinas/vostd). + +---- + +Rust's `unsafe` mechanism is an escape hatch for low-level code. In kernel development, that hatch is unavoidable. Kernels must manage raw physical addresses, manipulate hardware page tables, and execute direct memory access, operations that cannot be expressed as safe Rust. Outside of those low-level operations, the kernel still benefits from the high-level guarantees that come for free with safe Rust—but only if they still hold in the presence of `unsafe`. + +The standard way to bridge the gap is to carefully encapsulate the `unsafe` code into a safe abstraction, allowing its consumers to ignore the implementation details. But an abstraction is only as good as your confidence in it. How can you actually *trust* that the safe API built around your `unsafe` code is perfectly sound for every possible caller, in every possible state? Is it sufficient annotate every unsafe block with a '`SAFETY: ...`' comment explaining why it will not go wrong? No. Even when such comments are detailed and precise, they cannot possibly exhaustively cover the space of possible states that the library as a whole could reach. + +**[Asterinas](https://github.com/asterinas/asterinas)** exemplifies the importance of sound encapsulation. Its **[framekernel architecture](https://asterinas.github.io/book/kernel/the-framekernel-architecture.html)** isolates the raw dangerous primitives—physical memory management, synchronization, hardware configuration, and so on—from the rest of the kernel, in the Operating System Standard Library ([OSTD](https://asterinas.github.io/book/ostd/index.html)). Kernel developers working on top of OSTD have access to the powerful abstractions and guarantees of safe Rust. But those guarantees can only be trusted if the OSTD interface is sound! A bug in OSTD isn't just a localized issue; it compromises the safety guarantees of the entire rest of the kernel. + +When a small piece of code has an outsized impact on system reliability, it is the perfect candidate for **formal verification** (FV). FV uses a specialized [logic](https://en.wikipedia.org/wiki/Hoare_logic) to create machine-checked proofs that guarantee the code behaves exactly as intended. While writing these specifications and proofs requires significant upfront effort, the payoff is absolute certainty with zero runtime overhead. And as we will show, that *effort is now far more manageable than it used to be*. + +A year ago, *our [Phase I](https://asterinas.github.io/2025/02/13/towards-practical-formal-verification-for-a-general-purpose-os-in-rust.html) groundwork* successfully verified isolated functions within the memory management (`mm`) module in OSTD. While meaningful, these proofs were localized. + +**This post marks the completion of Phase II.** Not only have we verified *more* functions' individual behavior, but we have successfully proven that the public memory-management API is **sound**. Read on to discover what soundness means from a theoretical standpoint, and how we prove that it holds on this library. + +## Verification Methodology + +First, let's start with a simple explanation of how verification works. To summarize: formal verification involves annotating program code with a mathematical specification describing its effect on the overall program state. A complex program's overall behavior arises emergently from disparate pieces of data, often managed by many functions and subsystems. The specification distills that complexity down to a simpler, unified structure, and describes individual functions' behavior in terms of that structure. Then, for each function, a verification tool analyzes the concrete structure of the code and attempts to construct a logical proof that the specification holds on all inputs. Often a verification engineer must add annotations to assist this process. + +Specifications naturally compose vertically, with the verification of one function depending on the verified behavior of other functions that it calls, defining how the state changes at each level of the call stack. This is termed *correctness*. + +Proving soundness also requires horizontal composition in the form of an *invariant*: a property of system states that is guaranteed to always hold. Correctness proofs across the codebase prove that the invariant holds after each call, and therefore no caller can cause a state that would lead to UB. + +## Verus + +Our verification tool of choice is [Verus](https://github.com/verus-lang/verus), which integrates directly with the Rust language. Verus code is Rust code, with additional constructs that allow us to annotate functions with preconditions (boolean formulae that must be true in order to safely call the function) and postconditions (which we would like to prove to hold when the function returns). The Verus compiler converts the pre- and postconditions of each function into a logical representation and searches for a proof that all executions that satisfy the preconditions must, at each function exit, satisfy the postconditions. + +For complex verification, Verus also allows us to add *ghost state* that exists only during the verification. Because ghost variables are not compiled into executable code, they have no performance impact. They are only used to instrument the code to make information about the broader system legible to the verifier. For example, Verus' pointer libraries provide a ghost [`PointsTo