RISC-V: WorldGuard Support in OpenSBI
Overview
This post is about the WorldGuard support I recently implemented in OpenSBI for the RISE (RISC-V Software Ecosystem) RP016 project.
This work follows RISC-V: Hardware Isolation Framework in OpenSBI, which introduced the hardware isolation framework, and extends it with a real mechanism: WorldGuard.
Deliverable
A concrete WorldGuard mechanism is introduced on top of the hardware isolation framework with support for:
- boot-time parsing of WorldGuard checker policy from Devicetree
- boot-time programming of platform checker MMIO state for
sifive,wgchecker2 - per-domain parsing of WorldGuard execution metadata
(
worldguard,wid/worldguard,widlist) - runtime reprogramming of WorldGuard hart state on domain exit / enter
- unit-test coverage for both the generic hwiso switch path and the WorldGuard-specific checker / CSR state
Status
Implementation is complete and validated on QEMU virt
(-M virt,aia=aplic,wg=on).
The QEMU that supports WorldGuard can be found at riscv-wg-dts.
Background and Motivation
The previous work introduced a mechanism-agnostic hardware isolation framework in OpenSBI, but the framework alone does not isolate anything. It only provides the place where mechanism-specific code can:
- initialize global isolation hardware,
- parse per-domain policy,
- clear mechanism state when leaving a domain, and
- apply mechanism state when entering a domain.
OpenSBI already uses PMP to constrain CPU-side memory access for each domain. That is useful, but it remains CPU-local and privilege-centric. WorldGuard addresses a different problem: system-level access control based on worlds, so harts and peripherals can be constrained using a shared hardware policy model.
That means OpenSBI needs to handle two separate but related tasks:
- configure platform-wide WorldGuard checkers according to the system resource policy, and
- reprogram the current hart’s WorldGuard execution state when domain ownership changes.
This work adds both pieces on top of the hardware isolation framework.
Design Goals
- Reuse the hardware isolation abstraction.
- Parse and program all platform WorldGuard checker devices at boot.
- Parse per-domain WorldGuard execution metadata from Devicetree.
- Reprogram hart-local WorldGuard state during domain transitions.
- Keep generic hwiso tests and WorldGuard-specific tests separated.
Non-goals
- Support checker models other than
sifive,wgchecker2 - Dynamic reallocation of checker resources after boot
High-Level Architecture
Design Overview
The implementation keeps the hardware isolation framework intact.
The generic hardware isolation core is still responsible for:
- maintaining the list of registered mechanisms
- calling
init()during boot - calling
domain_init()for each OpenSBI domain - calling
domain_exit()anddomain_enter()during domain switch - calling
domain_cleanup()on teardown or error handling
WorldGuard is implemented as one registered mechanism:
static const struct sbi_hwiso_ops worldguard_ops = {
.name = "sifive,wgchecker2",
.init = worldguard_init,
.domain_init = worldguard_domain_init,
.domain_exit = worldguard_domain_exit,
.domain_enter = worldguard_domain_enter,
.domain_cleanup = worldguard_domain_cleanup,
};
At a high level, the flow is:
worldguard_init()checks whether the platform has anysifive,wgchecker2nodes and whether any CPU advertises runtime WG metadata.- If checker nodes exist,
wgchecker2_init()parses Devicetree policy and programs checker MMIO state. - If CPU runtime metadata exists,
worldguard_domain_init()parsesworldguard,widandworldguard,widlistfor each non-root domain. - During a domain switch,
worldguard_domain_exit()quiesces the old execution state andworldguard_domain_enter()applies the new one.
This keeps the mechanism-specific policy in the WorldGuard module while the generic framework only manages sequencing and storage.
Split Between Checker Policy and Runtime Hart State
The implementation deliberately splits WorldGuard support into two independent paths.
Checker programming is platform-wide and happens at boot. Its job is to
translate resource policy in Devicetree into the MMIO programming model
of wgchecker2.
Runtime hart-state programming is per-hart and happens during domain
switch. Its job is to update the current execution world’s CSR state so
the hart runs with the correct MLWID, MWIDDELEG, and SLWID
configuration.
The important point is that checker-only configurations do not force runtime switching on. Runtime CSR programming is enabled only when the Devicetree contains CPU-side WorldGuard execution metadata.
WorldGuard Runtime Context
The platform-level runtime state is cached as:
struct wg_cpu_defaults {
u32 trusted_wid;
u32 nworlds;
u32 valid_wid_mask;
};
struct worldguard_platform_ctx {
u32 hart_count;
bool runtime_enabled;
struct wg_cpu_defaults *hart_defaults;
};
Per-domain state is intentionally small:
struct worldguard_domain_ctx {
bool has_wid;
u32 wid;
u32 widlist_mask;
};
This is enough because all checker state is global and boot-time programmed, while the only per-domain runtime data needed on switch is the selected WID and delegated WID mask.
Boot-Time WorldGuard Checker Programming
The checker implementation lives in platform/generic/wgchecker2.c.
Its responsibility is to discover all active checker nodes, validate the
policy, and convert that policy into the checker’s TOR slot model.
Each checker is modeled with:
struct wgchecker2_checker {
char name[32];
u64 mmio_base;
u64 mmio_size;
u32 slot_count;
u32 subordinate_count;
bool full_checker_rule;
u64 full_checker_perm;
u32 range_count;
struct wgchecker2_range *ranges;
};
Boot-time parsing works as follows:
- discover all nodes compatible with
sifive,wgchecker2 - ignore nodes without
sifive,subordinates, because those do not participate in this software path - parse
regandsifive,slot-count - walk each protected subordinate resource
- parse the resource’s
worldguard_cfgchild - parse
permsstrictly as 64-bit<hi lo>pairs - use
worldguard_cfg/regwhen present, otherwise fall back to the resource node’s ownreg - support a full-checker rule when a checker protects exactly one subordinate with one permission value and no explicit range list
Before programming hardware, the implementation validates:
- minimum alignment
- zero-sized or wrapped ranges
- overlapping ranges
- impossible permission / range shapes
- slot exhaustion
Adjacent ranges with identical permissions are sorted and compacted, which keeps the final TOR programming minimal.
The checker programming logic is also careful about the update order. For DRAM checker programming, the reset-time trusted-WID bypass slot is kept alive until the new rule set has been fully written. Otherwise OpenSBI could lose access to its own RAM while reprogramming the checker.
Runtime WorldGuard Reprogramming on Domain Switch
Runtime programming lives in platform/generic/worldguard.c.
The implementation adds support for the WorldGuard-related CSRs and hart extension flags:
CSR_MLWIDCSR_MWIDDELEGCSR_SLWIDSBI_HART_EXT_SMWGSBI_HART_EXT_SSWG
The hart extension checks matter because not every platform exposing checker policy is guaranteed to expose all runtime execution-state CSRs.
CPU Defaults
CPU-side default WorldGuard execution state is parsed from
/cpus/cpu@X/worldguard nodes compatible with riscv,wgcpu.
Each hart may define:
mwid: the fallback machine WIDmwidlist: the valid / delegable WID set for that hart
If no CPU metadata exists, runtime WorldGuard switching remains
disabled. If the node exists, the parser builds a per-hart default state
and derives a valid_wid_mask.
Per-domain WorldGuard Metadata
When runtime is enabled, each non-root domain is expected to carry a WG
node under its hw-isolation container. The current implementation
parses:
- mandatory
worldguard,wid - optional
worldguard,widlist
Then it validates the result against every possible hart in that domain. In other words, a domain cannot request a WID or delegated WID set that is not supported by one of its possible harts.
Domain Exit
worldguard_domain_exit() quiesces the old domain state by:
- restoring
MLWIDto the current hart’s fallbackmwid - clearing
MWIDDELEG
This gives OpenSBI a predictable machine-world state before the next domain is entered.
Domain Enter
worldguard_domain_enter() computes the destination runtime state using
the current hart’s valid WID mask and the destination domain context.
The logic is:
- Select
MLWIDfrom the domain’sworldguard,widif it is valid on this hart; otherwise fall back to the hart defaultmwid. - Compute
MWIDDELEGfrom the intersection of the domain’sworldguard,widlistand the hart’s valid WID mask. - Select
SLWIDusing:- the domain
widitself if it is delegated, - otherwise the lowest delegated WID,
- otherwise the selected
MLWID.
- the domain
The actual CSR writes are gated by hart extensions:
- if
SMWGis missing, runtime programming is skipped - if
SMWGexists butSSWGis missing, onlyMLWIDis written - if both exist,
MWIDDELEGandSLWIDare also programmed when delegation is active
Devicetree Binding Model
The design uses three layers of Devicetree information:
- platform checker nodes
- CPU default WorldGuard execution state
- per-domain WorldGuard metadata
Platform Checker Nodes
System resources and checker devices are described in the normal system topology. A simplified example looks like:
wgchecker@6000000 {
compatible = "sifive,wgchecker2";
reg = <0x0 0x6000000 0x0 0x1000>;
sifive,slot-count = <16>;
sifive,subordinates = <&memory0>;
};
memory@80000000 {
reg = <0x0 0x80000000 0x0 0x40000000>;
worldguard_cfg {
reg = <0x0 0x80000000 0x0 0x40000000
0x0 0xc0000000 0x0 0x01000000
0x0 0xc1000000 0x0 0x3f000000>;
perms = <0x0 0xcf 0x0 0xcc 0x0 0xcf>;
};
};
This lets OpenSBI build a checker rule set directly from protected resources instead of hard-coding platform policy in C.
CPU Default WorldGuard Execution State
CPU default runtime state is described under each CPU:
cpu@0 {
...
worldguard {
compatible = "riscv,wgcpu";
mwid = <3>;
mwidlist = <0 1 3>;
};
};
These values act as the per-hart fallback state used on domain exit and as the validity mask for runtime delegation decisions.
Per-domain WorldGuard Metadata
Per-domain metadata lives under the hw-isolation container introduced
by the hardware isolation framework work:
domain@0 {
compatible = "opensbi,domain,instance";
...
hw-isolation {
worldguard {
compatible = "sifive,wgchecker2";
worldguard,wid = <0>;
worldguard,widlist = <0 1 3>;
};
};
};
For the current implementation, the compatible string is reused as
sifive,wgchecker2 so the generic hwiso framework can match the
mechanism cleanly.
Test Suite and Validation
The final patch series includes both generic hwiso testing and WorldGuard-specific testing on QEMU virt.
Generic Hardware Isolation Runtime Test
lib/sbi/sbi_hwiso_test.c now exercises a domain-switch sequence using
the generic framework:
- boot-time test callback
- failure-mode test callback
- domain exit / enter sequencing
- state verification after direct hwiso hook calls
- state verification after full domain-context entry
Mechanism-specific WorldGuard Test
platform/generic/virt/qemu_virt_wgchecker_test.c adds QEMU-specific
assertions for:
- three checker instances being programmed as expected
- per-domain runtime state for
domain@0anddomain@1 - quiesced state after domain exit
- a failure-mode access test that triggers a store access fault at a denied address
The failure-mode test is especially useful because it confirms the result is not just “registers look right”, but that the checker configuration actually blocks an illegal access.
Representative boot logs look like:
[HWISO] init sifive,wgchecker2
[WG] checker wgchecker@6002000 base=0x6002000 slots=1 rules=0 full-checker
[WG] checker wgchecker@6001000 base=0x6001000 slots=16 rules=0 full-checker
[WG] checker wgchecker@6000000 base=0x6000000 slots=16 rules=3
[HWISO] ops: sifive,wgchecker2, init domain: domain@1
[HWISO] ops: sifive,wgchecker2, init domain: domain@0
And representative runtime switch logs look like:
[HWISO] ops: sifive,wgchecker2, domain exit src=domain@0 dst=domain@1
[WG] domain_exit src=domain@0 dst=domain@1 mlwid=3 mwiddeleg=0x0
[HWISO] ops: sifive,wgchecker2, domain enter dst=domain@1 src=domain@0
[WG] domain_enter dst=domain@1 mlwid=1 mwiddeleg=0xa slwid=1
This shows the two key pieces of the implementation:
- checker state was accepted and programmed during boot
- hart-local WG execution state was reprogrammed during domain switch
Build Steps and Test Instructions
Build via Buildroot Project
The complete test environment is prepared in the RP016-M5 Buildroot branch used for this work.
Get Buildroot source code:
$ git clone https://gitlab.com/riseproject/riscv-optee/buildroot.git -b rp016_m5
Configure Buildroot:
$ cd buildroot
$ make qemu_riscv64_virt_optee_defconfig
Build:
$ make -j$(nproc)
If your host has CMake newer than 3.30, build with:
$ make -j$(nproc) CMAKE_POLICY_VERSION_MINIMUM=3.5
Running OpenSBI Hardware Isolation Unit Tests
Start QEMU:
$ ./output/images/start-qemu-dto.sh
This script compiles and applies the test overlay
qemu-virt-hwiso-overlay.dts to the dumped QEMU base Devicetree before
restarting QEMU.
At boot, SBIUNIT runs automatically and covers:
- generic hwiso boot checks
- a WorldGuard failure-mode access test
- domain-switch state verification
Closing Notes
This is where the hardware isolation framework starts doing real mechanism work.
The important result is not only that a wgchecker2 parser now exists,
but that OpenSBI has a complete WorldGuard path from Devicetree policy
to actual hardware state:
- checker MMIO state is programmed at boot
- per-domain WID metadata is stored in the OpenSBI domain model
- hart-local WorldGuard execution state is updated when domains switch
- unit tests verify both expected state and expected failure behavior
That makes the WorldGuard support useful as both a concrete isolation mechanism and a reference model for how future hardware isolation engines can plug into the same OpenSBI lifecycle.