Add main body and diagrams for phase II formal verification blog article#7
Add main body and diagrams for phase II formal verification blog article#7chrihop wants to merge 14 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new blog post draft describing Phase II formal verification progress for OSTD soundness (with diagrams), and introduces an English logo SVG asset.
Changes:
- Add a new post: “Verifying OSTD soundness” describing vertical/horizontal composition and invariants for UB-freedom.
- Add a new SVG asset (
logo_en.svg) underassets/images/.
Reviewed changes
Copilot reviewed 1 out of 6 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
assets/images/logo_en.svg |
Adds a new SVG logo asset to the site’s image bundle. |
_posts/2026-tbd-verifying-ostd-soundness.md |
Adds the main body of the formal verification blog article, including figures/diagrams and example specs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tatetian
left a comment
There was a problem hiding this comment.
Thanks for writing up this post! The core technical content -- proving soundness via defined behavior, vertical composition (soundness-correctness feedback loop), and horizontal composition (invariant preservation) -- is solid.
However, the post needs significant revision before it's ready for a broad audience:
- Blocking: Missing Jekyll front matter and placeholder filename date prevent the post from rendering.
- Title and framing: "Verifying OSTD soundness" frames the post as a project update, but its real value is educational -- a methodology piece on proving soundness for unsafe Rust.
- Audience accessibility: Many Asterinas/OSTD-specific concepts are used without introduction, but the post will reach a broad Rust audience via "This Week in Rust."
- Narrative structure: The Verus section is misplaced and disconnected from the composition narrative; the Vertical Composition subsection ends on a tangent; multiple "soundness" variants create unnecessary confusion.
- Repetition: Every major section restates its core insight 3-5 times across prose, blockquotes, and figure captions -- the single biggest obstacle to the PR description's own "concise requirement."
- Diagrams: Figure 1's text doesn't match the diagram's labels; Figure 3 is unreadable at rendered size.
| @@ -0,0 +1,155 @@ | |||
| # Verifying OSTD soundness | |||
There was a problem hiding this comment.
Missing Jekyll front matter; title needs rethinking: The post has no YAML front matter block. All other posts in this repo use:
---
layout: post
title: "..."
date: 2026-XX-XX HH:MM:SS +0800
author: "CertiK and Hongliang Tian"
---Without this, Jekyll will not process the file as a blog post.
The # Verifying OSTD soundness heading on line 1 must be removed. Jekyll's post layout automatically renders the title from the YAML title field, so keeping the manual heading will produce a duplicate title.
Please ensure that the post can be rendered correctly before marking the PR as "Ready for Review".
The title itself should be reconsidered. "Verifying OSTD soundness" frames this as a project status update, but what it offers is a methodology for proving soundness of unsafe code, using OSTD as the concrete case study. The three main takeaways (prove defined behavior, vertical composition, horizontal composition) describe a strategy that any Rust project with an unsafe core could learn from. As the post only describes a strategy, insufficient information (e.g., specs, models, invariants, and proofs) are provided to convince the readers that we actually complete the verification.
So I think the post's real value is educational, a bridge between Rust/kernel developers and verification experts. The title should lead with that educational angle, especially since this will be promoted on "This Week in Rust" to an audience that has never heard of Asterinas. The content should also be adjusted accordingly.
Suggested alternatives:
- "What It Takes to Formally Verify Rust Soundness: A Kernel Perspective"
- "Proving Soundness for Unsafe Rust: Lessons from a Kernel"
- "From Unsafe to Sound: Verifying a Rust Kernel's Trusted Core"
|
|
||
| So we had better be really certain that OSTD is actually well-encapsulated! And when we need certainty, we turn to formal verification. | ||
|
|
||
| A year ago, we reported our [initial results](https://asterinas.github.io/2025/02/13/towards-practical-formal-verification-for-a-general-purpose-os-in-rust.html) in verifying the `mm` module: a collection of functions from different components of the module, for each of which we verified specific safety concerns. Now we have expanded that effort to all components: |
There was a problem hiding this comment.
Progress characterization undersells the advance: "A collection of functions... Now we have expanded that effort to all components" is vague and doesn't convey the qualitative shift. Phase I verified a small set of internal functions for specific safety concerns in isolation. Phase II verifies the public API surface with composable specifications that provide system-wide soundness guarantees.
That's a fundamentally different kind of result: the difference between "we checked some internal functions don't have UB" and "we proved the public interface is sound against any safe caller." The paragraph should make this leap explicit. For example:
This also sets up the vertical/horizontal composition narrative that follows.
There was a problem hiding this comment.
Rephrased in introduction to make it more clear that this is a qualitative difference, and emphasized it even more in the conclusion.
| From the low-level foundation of `frame` to the "high-level" (for the kernel) abstraction of `vm_space` and `io`, we verify the Rust-soundness of the OSTD interface, as well as correctness properties of each component. (In `vm_space`, the soundness claim is weaker, because an ill-behaved caller can misconfigure userspace memory to cause UB, but that UB cannot affect kernel space.) | ||
|
|
||
| Besides the `mm` verification effort, we have published a paper on concurrent verification of our performant page table locking mechanism. Read more about that [here](https://dl.acm.org/doi/10.1145/3731569.3764836). | ||
|
|
||
| In this post, we will dig into how we define Rust soundness as a formal property, and how individual verified functions compose vertically and horizontally to prove that property, along with useful correctness properties, over this entire subset of OSTD. | ||
|
|
||
| --- | ||
|
|
||
| ## Proving Rust Soundness Positively | ||
|
|
||
| In `unsafe` Rust, the programmer is allowed to write code that may be nonsensical, and the Rust compiler only guarantees that a small subset of those possible programs will have any particular behavior. Code that does not fit into that subset is considered to have undefined behavior (UB). When such code is linked against safe Rust, we can no longer be confident that the safe code will behave as expected, because undefined behavior in the unsafe code could cause errors to crop up at any point in execution. The only way to ensure that this doesn't happen is to prove that the Rust semantics are sound in the context of this specific unsafe code. | ||
|
|
||
| Rust Soundness is the property of such a library that it is never responsible for UB. In Asterinas, since OSTD is the only library that is allowed to contain unsafe Rust, we can be more precise: when linked against any safe caller, a sound library never encounters **UB**. | ||
|
|
||
| There is a saying in the formal methods community, generally attributed to Andrew Appel, of the [Verified Software Toolkit](https://vst.cs.princeton.edu/): | ||
|
|
||
| > *The best way to show that a program has no undefined behavior is to show that it has a particular defined behavior.* | ||
|
|
||
| The reason lies in the structure of language specifications construction: they are designed to express positive statements about defined programs, rather than negative statements about undefined ones. In Rust, especially, the exact range of what is considered UB is not fully established and may change between Rust versions, whereas a narrower sense of "behavior that is defined in unsafe code" remains more stable. Tools like [Miri](https://github.com/rust-lang/miri), which we [also use to check OSTD](https://asterinas.github.io/2025/06/04/kernel-memory-safety-mission-accomplished.html), do a remarkable job of detecting some classes of UB at runtime, but they can only explore the paths they actually execute. They cannot guarantee that the paths they didn't explore are clean. | ||
|
|
||
| > The most rigorous approach is to prove what the program *does*, not what it *doesn't*. Once you establish that a piece of code has a specific, defined behavior, the absence of UB follows as a consequence. | ||
|
|
||
| This reframing shifts the target entirely. Instead of asking "Does this code have UB?", we ask "What does this code do?" If we can answer the second question precisely and completely, the first is answered automatically. And we gain something else in return: *stability*. | ||
|
|
||
| **Enter: [Verus](https://github.com/verus-lang/verus).** Verus is a dialect of Rust intended for formal deductive verification of code. Unlike Rust, Verus has no distinction between safe and unsafe code. Instead, all operations that might cause **UB** in Rust (and many that are defined but might cause unexpected behavior, like integer arithmetic that could overflow or underflow) require the checker to construct a proof that their preconditions are satisfied. | ||
|
|
||
| Verus verifies proofs on a per-function basis, annotating each function with pre- and post-conditions that its SMT solver checks at every exit point. Many of these proofs are discharged automatically from the surrounding code: for instance, when `x` is `unsigned`, | ||
|
|
||
| ```rust | ||
| if x > 0 { x - 1 } else { 0 } | ||
| ``` | ||
|
|
||
| requires no human intervention to prove the subtraction is safe. For operations the solver cannot resolve on its own, the user provides an explicit *witness*, a piece of ghost code that carries a logical ‘fact’ the compiler needs but discards after verification. | ||
|
|
||
| For memory accesses, this witness is a [`PointsTo`](https://asterinas.github.io/vostd/vstd_extra/cast_ptr/struct.PointsTo.html) object, encoding the fact that an address holds a particular value. This means that Verus code can reason explicitly about unsafe code that is still well-defined in the Rust memory model. In the Rust MM, writing through a pointer that was cast from an integer is valid if there is some known object in that location that previously had its address ‘exposed’, but it is incumbent on the programmer to ensure that this is the case, and that the object in question is always the one that was intended. In Verus reading and writing through any pointer requires an explicit proof that the target object exists, in the form of the `PointsTo` token. The programmer's job is to use ghost state to track the value of the tokens, much like the Rust programmer tracks objects in memory, but now with a verifier to check the work. | ||
|
|
||
| Verifying individual functions in isolation, however, is only the beginning. The real challenge lies in how those per-function specifications are composed across the entire codebase. As we will see, it is precisely this composition, both vertical across call stacks and horizontal across calling contexts, that allows local proof obligations to accumulate into a system-wide guarantee. | ||
|
|
||
| > **The key to effective verification is composing those specifications, both vertically and horizontally.** | ||
|
|
||
| ### Vertical Composition: When Soundness Becomes Correctness | ||
|
|
||
| Function specifications are composed vertically, just like the functions themselves. This is straightforward enough, but it has an important implication for the kinds of specifications we prove. While proving soundness in theory only requires showing that each function's behavior has *a* definition, in practice, verifying higher-level functions requires us to be much more precise in specifying the lower-level functions it calls. | ||
|
|
||
| > **A caller's soundness depends on the correctness of its callees.** | ||
|
|
||
| So, as we prove that each function exhibits some defined behavior, we are incentivized to make those specifications as tight as possible. The page table module is illustrative. Proving the correctness of [`Entry::replace`](https://asterinas.github.io/api-docs/0.17.1/src/ostd/mm/page_table/node/entry.rs.html#84), that it modifies exactly one entry in a page table node and leaves the rest unchanged, is a prerequisite for proving both the soundness and the correctness of [`page_table::CursorMut::replace_cur_entry`](https://asterinas.github.io/api-docs/0.17.1/src/ostd/mm/page_table/cursor/mod.rs.html#549). The correctness proof of that function is, in turn, a prerequisite for proving the soundness of [`page_table::CursorMut::map`](https://asterinas.github.io/api-docs/0.17.1/ostd/mm/vm_space/struct.CursorMut.html#method.map). Each layer's proof assumptions *motivate* the precision of the correctness specs in the layers below, and validate the | ||
| assumptions of the ones above. | ||
|
|
||
| > **Correctness and soundness are not parallel goals. One supports the other as you move up the call stack.** | ||
|
|
||
|  | ||
|
|
||
| > Figure 2. Soundness and correctness as a vertical feedback loop. *The soundness obligation of each caller motivates tighter correctness specs in its callees; those correctness specs in turn become the proof prerequisites that discharge the caller's soundness. Correctness at the bottom is soundness at the top.* | ||
|
|
||
| At the top of the stack, the [**public API of OSTD**](https://asterinas.github.io/api-docs/0.17.1/ostd/index.html), we are no longer writing specs for other proofs to consume. This changes what a good specification looks like. Internal specs must track implementation details closely because they are consumed by other proofs. The consumer for an external spec is the reader, attempting to understand exactly what has been verified. For a human audience, the specifications should be more abstract, so that it is clear at a glance that they capture desirable properties of the system. And in being more abstract, they are also represented differently from the code they describe, meaning that if the original code is buggy, the specification is less likely to simply reproduce the bug. | ||
|
|
||
| > Descriptions of what a module *should* do, not of what it *happens* to do. | ||
|
|
||
| For instance, the [specification for a linked list cursor](https://asterinas.github.io/vostd/ostd/specs/mm/frame/linked_list/linked_list_owners/struct.CursorModel.html) is a pair of mathematical sequences, representing the whole list bifurcated by the cursor: | ||
|
|
||
| ```rust | ||
| // Verus mathematical sequence model | ||
| pub ghost struct CursorModel { | ||
| pub ghost fore: Seq<LinkModel>, | ||
| pub ghost rear: Seq<LinkModel>, | ||
| pub ghost list_model: LinkedListModel, | ||
| } | ||
| ``` | ||
|
|
||
| ### Horizontal Composition: Invariants Against Adversarial Calls | ||
|
|
||
| > How do you prove soundness against a caller you do not have? | ||
|
|
||
| Rust Soundness is a property of a partial program. To prove it in its basic form, we would need to quantify over all possible calling contexts that might call into OSTD. That's an infinite set, not very tractable to go through one-by-one. And in the end, Verus will still be verifying the functions one at a time. The solution is to *unwind* the property. We can think of the whole program execution as a loop, like this: | ||
|
|
||
|  | ||
|
|
||
| > Figure 3. Horizontal composition via invariants. *Left: an arbitrary safe-Rust caller may invoke any API function in any order, store returned object handles, and re-dispatch them at any later time, where OSTD must remain UB-free under all such sequences. Middle: equipping each API function with specs and proofs reduces this infinite-caller problem to a per-function obligation, assuming invariants hold on entry, proving they are preserved on exit. Right: since every call is an invariant-preserving step, any sequence of calls, however adversarial, is UB-free.* | ||
|
|
||
| The client calls into OSTD via a function or method, and eventually receives a result. It may do so in any order, with any parameters to the calls. And it can do *nearly* anything with the results that it receives. The one catch is that, being safe Rust, it cannot duplicate those objects. We need to compose the functions horizontally and show that calling them in any order produces defined results. | ||
|
|
||
| We collect the assumptions that our functions rely on into *invariants* --- properties of the overall system state, which must be preserved by every API function call. | ||
|
|
||
| One important invariant is the relation between [`EntryOwner`](https://asterinas.github.io/vostd/ostd/specs/mm/page_table/node/entry_owners/struct.EntryOwner.html) (a piece of "ghost state" that tracks the contents of a page table entry) and the global state of the metadata region, the [`MetaRegionOwners`](https://asterinas.github.io/vostd/ostd/specs/mm/frame/meta_region_owners/struct.MetaRegionOwners.html). This [`relate_region`](https://asterinas.github.io/vostd/ostd/specs/mm/page_table/node/entry_owners/struct.EntryOwner.html#method.relate_region) predicate contains a number of assertions that must hold for entries that represent nodes in the page table: they must have a valid metadata slot, which must have a suitable reference count, and their location within the tree-structure of the page table must be correctly tracked by the proof state. [`expected_raw_count`](https://asterinas.github.io/vostd/ostd/specs/mm/page_table/node/entry_owners/struct.EntryOwner.html#method.expected_raw_count) is either zero or one, depending on whether the entry is currently in-scope or has been "forgotten" and converted into a raw pointer to store in its parent node. Entries that directly map frames have a simpler invariant, and absent entries, none at all. | ||
|
|
||
| ```rust | ||
| impl<C: PageTableConfig> EntryOwner<C> { | ||
| pub open spec fn relate_region(self, regions: MetaRegionOwners) -> bool { | ||
| if self.is_node() { | ||
| let idx = frame_to_index(self.meta_slot_paddr().unwrap()); | ||
| let slot_owner = regions.slot_owners[idx]; | ||
| &&& slot_owner.inner_perms.ref_count.value() != REF_COUNT_UNUSED | ||
| &&& slot_owner.raw_count == self.expected_raw_count() | ||
| &&& slot_owner.self_addr == self.node.unwrap().meta_perm.addr() | ||
| &&& self.node.unwrap().meta_perm.points_to.value().wf(slot_owner) | ||
| &&& slot_owner.path_if_in_pt is Some ==> | ||
| slot_owner.path_if_in_pt.unwrap() == self.path | ||
| } else if self.is_frame() { | ||
| let idx = frame_to_index(self.meta_slot_paddr().unwrap()); | ||
| let slot_owner = regions.slot_owners[idx]; | ||
| slot_owner.path_if_in_pt is Some ==> | ||
| slot_owner.path_if_in_pt.unwrap() == self.path | ||
| } else { | ||
| true | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| This invariant is mapped over every entry in the page table, and it is essential for proving such properties as "a given frame is not mapped multiple times in the same page table"--- if it were, one of the copies would have the wrong path. | ||
|
|
||
| *Invariant preservation* is the bridge between the local and the global: **every public API function must preserve the system invariants, and those invariants are jointly sufficient to guarantee defined behavior at every call site.** Any sequence of calls is a sequence of invariant-preserving steps. For any particular calling context, we could trace its calls and construct that sequence of steps, confident that they will all be defined, as **UB** has nowhere to enter. | ||
|
|
||
| On top of this foundation, our correctness proofs at the API boundary place additional constraints on the caller and, in return, prove more detailed specifications of the functions' behavior. All callers get defined behavior, and reasonable callers get correct behavior. | ||
|
|
||
| In the case of the higher-level [`vm_space`](https://asterinas.github.io/vostd/ostd/mm/vm_space/vm_space_specs/index.html) module, the situation becomes more complicated. We cannot prove that there is no UB for any arbitrary caller, because `vm_space` exposes an interface for the caller to map and unmap frames for userspace, and so can cause UB within userspace memory by mapping frames incoherently. In this case, we can still prove that kernel memory remains sound, as it covers a separate range of virtual addresses that the caller cannot corrupt. In addition to this "weak soundness," we prove correctness properties of VM space given additional constraints on the caller, especially the constraint that it maintains coherent virtual memory mappings in userspace. |
There was a problem hiding this comment.
Confusing "soundness" variants -- simplify to one concept: The post introduces multiple flavors: "soundness," "kernel soundness" (in the Figure 1 table), and "weak soundness" (line 137). This is unnecessarily confusing. There should be only one soundness concept: no UB in the kernel. Userspace is both out of scope and impractical to enforce -- a userspace program can be inherently flawed, and there is no way for the kernel to prevent it from shooting itself in the foot and compromising its own memory safety. That's not the kernel's responsibility.
Rather than framing vm_space as having "weaker" soundness, reframe it as: vm_space is fully sound in the sense that matters -- it guarantees no UB in kernel space. The fact that a caller can misconfigure userspace mappings is expected behavior; it's the userspace program's problem. Drop "weak soundness" entirely and align with the single definition from the Figure 1 table's "Kernel Soundness" row: arbitrary caller, kernel scope, no UB in kernel space.
Additionally, this point is stated twice: the parenthetical at line 21 and the full paragraph at line 137. Remove the parenthetical in the introduction and let the Horizontal Composition section (line 137) handle it, where the reader has the context to understand why.
There was a problem hiding this comment.
Rolled everything into soundness, with a very shallow mention of why soundness requires giving definition to behavior that might be unexpected. For this audience I think that is sufficient.
| **Enter: [Verus](https://github.com/verus-lang/verus).** Verus is a dialect of Rust intended for formal deductive verification of code. Unlike Rust, Verus has no distinction between safe and unsafe code. Instead, all operations that might cause **UB** in Rust (and many that are defined but might cause unexpected behavior, like integer arithmetic that could overflow or underflow) require the checker to construct a proof that their preconditions are satisfied. | ||
|
|
||
| Verus verifies proofs on a per-function basis, annotating each function with pre- and post-conditions that its SMT solver checks at every exit point. Many of these proofs are discharged automatically from the surrounding code: for instance, when `x` is `unsigned`, | ||
|
|
||
| ```rust | ||
| if x > 0 { x - 1 } else { 0 } | ||
| ``` | ||
|
|
||
| requires no human intervention to prove the subtraction is safe. For operations the solver cannot resolve on its own, the user provides an explicit *witness*, a piece of ghost code that carries a logical ‘fact’ the compiler needs but discards after verification. | ||
|
|
||
| For memory accesses, this witness is a [`PointsTo`](https://asterinas.github.io/vostd/vstd_extra/cast_ptr/struct.PointsTo.html) object, encoding the fact that an address holds a particular value. This means that Verus code can reason explicitly about unsafe code that is still well-defined in the Rust memory model. In the Rust MM, writing through a pointer that was cast from an integer is valid if there is some known object in that location that previously had its address ‘exposed’, but it is incumbent on the programmer to ensure that this is the case, and that the object in question is always the one that was intended. In Verus reading and writing through any pointer requires an explicit proof that the target object exists, in the form of the `PointsTo` token. The programmer's job is to use ghost state to track the value of the tokens, much like the Rust programmer tracks objects in memory, but now with a verifier to check the work. | ||
|
|
||
| Verifying individual functions in isolation, however, is only the beginning. The real challenge lies in how those per-function specifications are composed across the entire codebase. As we will see, it is precisely this composition, both vertical across call stacks and horizontal across calling contexts, that allows local proof obligations to accumulate into a system-wide guarantee. | ||
|
|
||
| > **The key to effective verification is composing those specifications, both vertically and horizontally.** |
There was a problem hiding this comment.
Verus material deserves its own section and needs tighter focus on what serves the post's narrative: Two issues:
1. Structure -- Verus is wedged into "Proving Rust Soundness Positively" but is a separate topic. The section is about a conceptual insight (prove defined behavior, not absence of UB). The Verus material is about tooling. Verus deserves its own ## heading (e.g., "Verification with Verus") to make the structure honest.
2. Relevance -- much of the Verus content doesn't connect to the rest of the post:
-
PointsTotokens (line 55): Explained at length, including a deep dive into Rust memory model provenance and exposed-address semantics. ButPointsTonever appears again in the post -- not in the vertical composition example (which usesEntry::replaceandCursorMut::map), nor in the horizontal composition invariant (which usesEntryOwnerandMetaRegionOwners). The reader invests effort understanding it and then it's abandoned. Thepoints-to.pngimage, which illustrates this concept well, is included in the PR but never referenced. IfPointsTostays, use the diagram; if it's cut, remove the image. -
SMT solver and automatic proof discharge (lines 47-51): The
if x > 0 { x - 1 } else { 0 }example illustrates Verus basics but has nothing to do with kernel verification. It doesn't set up any concept used later. -
Witnesses and ghost code (line 53): This concept is important --
EntryOwnerandMetaRegionOwnersin the horizontal composition section are exactly this kind of ghost state, andCursorModelis a ghost struct. But the connection is never made explicit. The reader learns about "ghost code" here and encounters ghost structs later without realizing they're the same concept. -
Missing: how Verus specs compose -- The one thing the Verus section should set up -- that Verus verifies per-function pre/post-conditions, and that a caller's preconditions can be discharged by its callee's postconditions -- is mentioned in passing (line 47) but not emphasized. This is exactly the mechanism that makes vertical composition work, and it deserves to be the centerpiece.
In short, the Verus section explains how Verus works in general rather than the aspects that matter for this post. Consider restructuring to: (a) briefly introduce Verus; (b) focus on per-function specs with pre/post-conditions (the basis for vertical composition); (c) introduce ghost state as the mechanism for tracking invariants (the basis for horizontal composition); and (d) either cut or drastically shorten the PointsTo/provenance material.
Also, line 55's paragraph is very long -- it covers PointsTo, Rust MM provenance semantics, and ghost-state tracking in a single block. If the PointsTo content is kept, break it into shorter paragraphs.
There was a problem hiding this comment.
Verus material is now introduced right away, before moving on to the actual property definition. Vertical composition is explained at this point, because it's so fundamental to Verus' structure, leaving the question of horizontal composition to be explained once we have the property.
| At the top of the stack, the [**public API of OSTD**](https://asterinas.github.io/api-docs/0.17.1/ostd/index.html), we are no longer writing specs for other proofs to consume. This changes what a good specification looks like. Internal specs must track implementation details closely because they are consumed by other proofs. The consumer for an external spec is the reader, attempting to understand exactly what has been verified. For a human audience, the specifications should be more abstract, so that it is clear at a glance that they capture desirable properties of the system. And in being more abstract, they are also represented differently from the code they describe, meaning that if the original code is buggy, the specification is less likely to simply reproduce the bug. | ||
|
|
||
| > Descriptions of what a module *should* do, not of what it *happens* to do. | ||
|
|
||
| For instance, the [specification for a linked list cursor](https://asterinas.github.io/vostd/ostd/specs/mm/frame/linked_list/linked_list_owners/struct.CursorModel.html) is a pair of mathematical sequences, representing the whole list bifurcated by the cursor: | ||
|
|
||
| ```rust | ||
| // Verus mathematical sequence model | ||
| pub ghost struct CursorModel { | ||
| pub ghost fore: Seq<LinkModel>, | ||
| pub ghost rear: Seq<LinkModel>, | ||
| pub ghost list_model: LinkedListModel, | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Tangent that derails Vertical Composition and ends abruptly: The Vertical Composition subsection (lines 61-89) has two parts, and the second doesn't belong:
Lines 61-74 are the actual vertical composition argument: caller soundness requires callee correctness, illustrated with the Entry::replace → CursorMut::replace_cur_entry → CursorMut::map chain, capped by Figure 2. This is strong and self-contained.
Lines 76-89 pivot to a different topic: what makes a good spec at the API boundary (abstract vs. implementation-tracking). This is a spec design philosophy point, not a composition point.
The CursorModel example compounds the problem:
- It has no connection to the vertical composition argument. The reader was following
Entry::replace→CursorMut::mapand suddenly encounters a linked list cursor. - It ends the subsection abruptly. The code block is the last thing before "Horizontal Composition" -- no concluding sentence, no tie-back.
- The reader has no context for why a linked list cursor exists in a memory management module, or what
LinkModelandLinkedListModelrepresent.
Consider either: (a) cutting lines 76-89 entirely and ending the subsection on the strong Figure 2 at line 74, or (b) giving the abstract-spec point its own short subsection with a better-motivated example.
There was a problem hiding this comment.
Dropped the CursorModel example. We don't really need to get into the different kinds of specifications. The rest of this section has been merged into the Verus discussion, where hopefully it flows better.
|
|
||
| *(Foreword: This post summarizes our progress in verifying OSTD and highlights key results from our research papers. This work is carried out in collaboration with [CertiK](https://www.certik.com/).)* | ||
|
|
||
| You can trust Rust. Millions of developers sleep soundly on the promise that Rust is safer than other systems-level programming languages. That's why we chose it for Asterinas -- if any system needs to be trusted, it's a kernel. Our framekernel architecture confines all unsafe code to OSTD, a minimal 15,000-line trusted core. If OSTD is unsound, the entire 100,000-line kernel is unsafe as well. |
There was a problem hiding this comment.
Minor wording nit: In a post about Rust verification, "unsafe" is an overloaded term. The kernel's code doesn't become unsafe Rust -- its safety guarantees are voided. Consider: "...the safety guarantees of the entire 100,000-line kernel are compromised."
There was a problem hiding this comment.
Filename uses placeholder date tbd: The Jekyll filename convention is YYYY-MM-DD-slug.md. The tbd will cause Jekyll to either fail to parse the date or sort the post incorrectly. This should be updated to the intended publication date before merging.
Please ensure that the post can be rendered correctly before marking the PR as "Ready for Review".
There was a problem hiding this comment.
The current draft is 2026-04-01-verifying-ostd-soundness.md because that's when I created it. When we decide on a publication date I will change it.
There was a problem hiding this comment.
Many Asterinas/OSTD-specific concepts are used without introduction: This post will be advertised on "This Week in Rust" and similar forums, so it cannot assume the reader is familiar with Asterinas or OSTD. Every project-specific concept needs a brief definition or explanation on first use. Here is an inventory of terms that are dropped in without context:
-
"Asterinas" (line 5) -- mentioned as "a kernel" but never described further. A one-sentence description (e.g., "Asterinas, a general-purpose OS kernel written in Rust") and a link to the project would orient the reader.
-
"framekernel architecture" (line 5) -- a term specific to Asterinas that most developers won't know. Even a parenthetical ("our framekernel architecture, which isolates all
unsafecode into a single trusted layer") would help. -
"OSTD" (line 3, 5) -- used from the very first sentence of the foreword before the reader knows what it is. The name is never unpacked. What does OSTD stand for? What does it provide -- is it a runtime, a HAL, a stdlib?
-
"
mmmodule" (line 9) -- the reader must guess this means "memory management." Spell it out on first use. -
"frames" (line 17) -- OS kernel developers know what a physical page frame is, but Rust application developers may not. A brief gloss on first use (e.g., "frames (fixed-size blocks of physical memory)") would help.
-
"metadata slots" (line 17) -- an OSTD-internal concept with no context. Why does OSTD track metadata in an unsafe array? What kind of metadata?
-
"TCB" (line 151) -- acronym used without expansion. Spell out "Trusted Computing Base" on first use.
-
"CortenMM" (line 143) -- appears once with no introduction. What is it? A sub-project? A tool? A paper?
-
"KVerus" (line 151) -- described as "our AI-assisted proof tool" but that's vague. Is it open-source? A link or one more sentence of context would help.
-
"
syncmodule" (line 143) -- assumes the reader knows OSTD has a sync module. A brief qualifier (e.g., "thesyncmodule, which provides concurrency primitives") would suffice.
This also includes verification concepts such as EntryOwner, MetaRegionOwners, and CursorModel. Simply making them hyperlinks without layman-friendly descriptions harms readability.
As a general principle: if a term is specific to Asterinas/OSTD, it needs at least a parenthetical explanation on first use.
There was a problem hiding this comment.
All of these individual concepts are addressed, and I hope we're doing better on this issue as a whole. I'm uncertain as to how much the reader needs to know about page tables. At this level it may be sufficient to just understand that a pt is a complex tree structure, but maybe more context would be good.
There was a problem hiding this comment.
Figure 3 is too dense -- text is unreadable at rendered size: This diagram packs three panels (Challenge, Solution, Conclusion) into a single wide image, each containing labeled boxes, arrows, numbered steps, bullet lists, and fine-print annotations. At typical blog content width, the text shrinks to the point of illegibility.
The core message is simple:
- Challenge: An arbitrary caller can invoke any API function in any order.
- Solution: Each function assumes invariants on entry and proves they hold on exit.
- Conclusion: Any sequence of calls preserves invariants, so the system is UB-free.
Consider: (a) splitting into two or three separate, taller diagrams so each renders at readable size, or (b) stripping secondary details and keeping only the essential flow. Figures 1 and 2 work well because each communicates one idea at readable scale; Figure 3 tries to communicate three.
There was a problem hiding this comment.
I reduced figure three down to just the third panel, which I think captures the point more succinctly.
There was a problem hiding this comment.
Unrelated asset: This SVG logo file is not referenced in the blog post. It may belong in a separate PR.
There was a problem hiding this comment.
Removed. Later I will also go through and make sure we don't have extra images left from previous iterations of this post.
tatetian
left a comment
There was a problem hiding this comment.
(Supplementary comment from the same review — the original submission missed this one due to a parser limitation.)
| > **A caller's soundness depends on the correctness of its callees.** | ||
|
|
||
| So, as we prove that each function exhibits some defined behavior, we are incentivized to make those specifications as tight as possible. The page table module is illustrative. Proving the correctness of [`Entry::replace`](https://asterinas.github.io/api-docs/0.17.1/src/ostd/mm/page_table/node/entry.rs.html#84), that it modifies exactly one entry in a page table node and leaves the rest unchanged, is a prerequisite for proving both the soundness and the correctness of [`page_table::CursorMut::replace_cur_entry`](https://asterinas.github.io/api-docs/0.17.1/src/ostd/mm/page_table/cursor/mod.rs.html#549). The correctness proof of that function is, in turn, a prerequisite for proving the soundness of [`page_table::CursorMut::map`](https://asterinas.github.io/api-docs/0.17.1/ostd/mm/vm_space/struct.CursorMut.html#method.map). Each layer's proof assumptions *motivate* the precision of the correctness specs in the layers below, and validate the | ||
| assumptions of the ones above. |
There was a problem hiding this comment.
Formatting nits:
- Lines 67-68: Hard line-wrap mid-sentence ("...and validate the\nassumptions of the ones above"). The rest of the post uses single long lines per paragraph; clean up for consistency.
- Lines 103, 131: Triple-hyphen
---used as em dash. Use a proper em dash—or consistent spacing to avoid rendering inconsistencies across Markdown parsers.
|
I marked this PR as "Draft". Please resolve or reply all issues. When you are done with the revision, mark it as "Ready for Review". Thanks. |
…t with Verus keywords; update figures
| ```rust-verus | ||
| impl<C: PageTableConfig> EntryOwner<C> { | ||
|
|
||
| pub open spec fn metaregion_sound(self, regions: MetaRegionOwners) -> bool { |
There was a problem hiding this comment.
This code does not match the latest version. Please modify this after we finalize our model in June.
|
|
||
| Advances in AI help us scale even faster. Because proof annotations often follow predictable patterns derived from the system model, AI can be very effective in helping the SMT solver handle proofs that previously would require human guidance. To this end we built **KVerus**, an AI-assisted tool that automatically generates a growing fraction of our proofs. Crucially, AI assistance accelerates the writing process but does not alter the trustworthiness of the results. Every single proof generated by KVerus is strictly checked and validated by Verus's mathematical solver. AI frees our engineers to focus on the big picture questions: specifications, system models, and proof strategy. | ||
|
|
||
| Another common critique of formal verification is that proofs quickly become outdated as code evolves. Verification projects are usually static, pinned to a particular version of the target software. We began our verification on OSTD v0.15 and are currently tracking v0.16.2. Thanks to our modular invariant structure and KVerus's ability to help repair proofs, updating our verification alongside codebase changes has proven quite manageable. Our ultimate goal is continuous verification: updating proofs in the same pull request as the code they cover. |
|
|
||
| We began a year ago with a proof of concept, verifying selected properties of individual functions but leaving the bulk of the code unverified. After taking lessons from that phase and scaling up our efforts, in just over a year we have expanded to cover the entire virtual memory subsystem of the memory management (`mm`) module, from raw physical frame allocation at the bottom to virtual address space mapping at the top. Recall that horizontal composition is vital for proving soundness: verifying an entire subsystem is much more valuable than disconnected functions. Meanwhile, a parallel project called [CortenMM](https://dl.acm.org/doi/10.1145/3731569.3764836) has verified the complex concurrent correctness of the page table's fine-grained locking, and has been published in SOSP '25. | ||
|
|
||
| As a proxy for cost, historically, formal verification requires about 20 lines of mathematical proof for every 1 line of code (a 1:20 ratio). This immense cost has blocked widespread industrial adoption. **We reduced this ratio to below 1:4.** |
There was a problem hiding this comment.
Do we still need to show the proof-code-ratio, as more and more proofs are generated by AI (which is not very good at generating concise proofs)?
We’d love for you to take a look and share any suggestions you might have about the lessons learned from our recent work on the formal verification of Asterinas OSTD. Your input would be really valuable! Thanks!
Principles and outlines:
Target audience: Kernel and Rust developers. We may briefly recap verification basics, but avoid explaining concepts they already know (e.g., do not trust the caller in the "adversarial guarantees" or link to unsafe in “What is soundness”).
Key idea: To prove no UB, we have to prove defined behaviour.
Concise requirement: The blog is currently too long; aim for roughly 5–6 pages with tighter exposition and reduced repetition.
Compositional verification goals:
-- Vertical composability: Individual functions should be verified in a way that enables sound composition across abstraction layers.
-- Horizontal composability: The system architecture should support composition across modules/components.
Invariant design: Move specific and concrete invariants, avoiding vague descriptions.
Whole picture: Put all things into the whole picture, distinguishing user and kernel memory.