Skip to content

miri: added support for MIRI#6

Open
dcalavrezo-qorix wants to merge 3 commits intoscore_mainfrom
dcalavrezo_miri
Open

miri: added support for MIRI#6
dcalavrezo-qorix wants to merge 3 commits intoscore_mainfrom
dcalavrezo_miri

Conversation

@dcalavrezo-qorix
Copy link
Copy Markdown

@dcalavrezo-qorix dcalavrezo-qorix commented Mar 30, 2026

Summary

This PR adds first-class Miri support to rules_rust through two new public rules:

  • miri_test
  • miri_binary

The implementation uses the direct miri driver, not cargo miri.

That means Bazel remains in control of the crate graph and action inputs, while Miri is only used for the final interpreted
execution step.

What This PR Adds

1. New public rules

The following rules are now exported from @rules_rust//rust:defs.bzl:

  • miri_test
  • miri_binary

These are currently wrapper rules around already-declared Rust targets.

Example:

load("@rules_rust//rust:defs.bzl", "rust_test", "miri_test")

rust_test(
    name = "tests",
    crate = "my_lib",
)

miri_test(
    name = "tests_miri",
    crate = ":tests",
)

This first version intentionally wraps existing Rust targets instead of re-exposing the full rust_test / rust_binary attribute
surface.

2. New Miri toolchain type

A new toolchain type is introduced:

  • @rules_rust//rust:miri_toolchain_type

This is separate from the normal Rust toolchain because Miri requires a different runtime contract:

  • bin/miri
  • a prebuilt Miri sysroot
  • the runtime files needed to execute the Miri driver inside Bazel runfiles/sandbox

3. Miri-specific rebuild mode for target-side crates

A new internal build setting / transition enables a Miri-specific compilation mode for target-side Rust crates.

When enabled:

  • target-side crates are rebuilt with the Miri sysroot
  • target-side crates are compiled with:
    • -Zalways-encode-mir

Host-side units are not rebuilt in Miri mode:

  • build scripts remain native
  • proc-macro crates remain native
  • other exec-configuration helpers remain native

This split is required for real projects to work correctly under Miri.

4. Direct miri launchers

miri_test and miri_binary generate Bazel launchers that invoke the direct miri driver with a rustc-shaped command line:

  • --sysroot=...
  • --crate-name=...
  • --crate-type=...
  • --edition=...
  • --target=...
  • --extern=...
  • -Ldependency=...

For miri_test, the generated launcher also forwards libtest-style filtering and runs with:

  • --test
  • --test-threads=1

Why This Is Structured as a Separate Toolchain

Miri is not just another flag on the normal Rust toolchain.

The normal Rust toolchain defines:

  • rustc
  • normal std/sysroot
  • normal compile-time/runtime inputs

The Miri toolchain defines:

  • miri
  • a Miri-specific sysroot
  • Miri runtime files

Keeping these as separate toolchain types makes the model explicit and keeps normal Rust builds unchanged for users who do not use
Miri.

How Consuming Repos Use It

Consuming repos need two things:

  1. a normal Rust toolchain
  2. a registered Miri toolchain

In our S-CORE ecosyste, the Miri toolchain is provided by score_toolchains_rust.

1. Register the Rust and Miri toolchains

Example .bazelrc configuration:

build:per-x86_64-linux --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_unknown_linux_gnu
build:per-x86_64-linux --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_unknown_linux_gnu_miri

The first toolchain is the normal Ferrocene Rust toolchain.

The second toolchain provides:

  • miri
  • the prebuilt Miri sysroot
  • runtime files required by the Miri launcher

2. Wrap existing Rust targets

Tests

load("@rules_rust//rust:defs.bzl", "rust_test", "miri_test")

rust_test(
name = "tests",
crate = "my_lib",
)

miri_test(
name = "tests_miri",
crate = ":tests",
tags = ["manual"],
)

Binaries

load("@rules_rust//rust:defs.bzl", "rust_binary", "miri_binary")

rust_binary(
name = "app",
srcs = ["src/main.rs"],
)

miri_binary(
name = "app_miri",
crate = ":app",
tags = ["manual"],
)

3. Run them with Bazel

bazel test --config=per-x86_64-linux //path/to:tests_miri
bazel run --config=per-x86_64-linux //path/to:app_miri

Rule Parameters

miri_test

Current useful parameters:

  • crate
    • existing Rust target to execute under Miri
    • typically wrap a rust_test(...)
  • miri_flags
    • flags passed directly to the miri driver
    • defaults to:
      • -Zmiri-disable-isolation
  • miri_args
    • arguments forwarded after --
    • for tests, this is useful for libtest filtering
  • env
    • extra runtime environment variables
  • env_inherit
    • runtime environment variables inherited from the outer environment
  • platform
    • optional platform override for the wrapped target before Miri-mode rebuild

Example with a test filter:

miri_test(
name = "tests_miri_json_backend",
crate = ":tests",
miri_args = ["json_backend::"],
tags = ["manual"],
)

miri_binary

Current useful parameters are the same as miri_test, except it wraps a rust_binary-like target and executes it under Miri.

Current Limitations

This PR is intentionally scoped as a first version.

1. Not full rust_test / rust_binary parity

These rules do not yet expose the entire attribute surface of rust_test() or rust_binary().

They currently wrap existing Rust targets instead.

2. Pure-Rust graphs only

The current direct-driver implementation rejects targets with native linker inputs.

That means this version supports:

  • pure-Rust test targets
  • pure-Rust binaries

but not targets that pull in native link artifacts such as:

  • cc_library
  • other non-Rust linker inputs

3. Requires a registered Miri toolchain

If miri_test or miri_binary is used without registering a Miri toolchain, analysis fails with a clear error.

Validation

This implementation was validated end-to-end against a real consumer repo (persistency) using a Ferrocene-backed Miri toolchain
from score_toolchains_rust.

Validated flow:

  • wrap an existing rust_test(...) target with miri_test(...)
  • run it with Bazel
  • execute the full rust_kvs test harness under Miri

Result:

  • all non-ignored tests passed under Miri
  • the sharded Miri suite also passed

  ### 1. New public rules

  The following rules are now exported from `@rules_rust//rust:defs.bzl`:

  - `miri_test`
  - `miri_binary`

  These are currently **wrapper rules** around already-declared Rust targets.

  Example:

  ```bzl
  load("@rules_rust//rust:defs.bzl", "rust_test", "miri_test")

  rust_test(
      name = "tests",
      crate = "my_lib",
  )

  miri_test(
      name = "tests_miri",
      crate = ":tests",
  )

  This first version intentionally wraps existing Rust targets instead of re-exposing the full rust_test / rust_binary attribute
  surface.

  ### 2. New Miri toolchain type

  A new toolchain type is introduced:

  - @rules_rust//rust:miri_toolchain_type

  This is separate from the normal Rust toolchain because Miri requires a different runtime contract:

  - bin/miri
  - a prebuilt Miri sysroot
  - the runtime files needed to execute the Miri driver inside Bazel runfiles/sandbox

  ### 3. Miri-specific rebuild mode for target-side crates

  A new internal build setting / transition enables a Miri-specific compilation mode for target-side Rust crates.

  When enabled:

  - target-side crates are rebuilt with the Miri sysroot
  - target-side crates are compiled with:
      - -Zalways-encode-mir

  Host-side units are not rebuilt in Miri mode:

  - build scripts remain native
  - proc-macro crates remain native
  - other exec-configuration helpers remain native

  This split is required for real projects to work correctly under Miri.

  ### 4. Direct miri launchers

  miri_test and miri_binary generate Bazel launchers that invoke the direct miri driver with a rustc-shaped command line:

  - --sysroot=...
  - --crate-name=...
  - --crate-type=...
  - --edition=...
  - --target=...
  - --extern=...
  - -Ldependency=...

  For miri_test, the generated launcher also forwards libtest-style filtering and runs with:

  - --test
  - --test-threads=1

  ## Why This Is Structured as a Separate Toolchain

  Miri is not just another flag on the normal Rust toolchain.

  The normal Rust toolchain defines:

  - rustc
  - normal std/sysroot
  - normal compile-time/runtime inputs

  The Miri toolchain defines:

  - miri
  - a Miri-specific sysroot
  - Miri runtime files

  Keeping these as separate toolchain types makes the model explicit and keeps normal Rust builds unchanged for users who do not use
  Miri.

  ## How Consuming Repos Use It

  Consuming repos need two things:

  1. a normal Rust toolchain
  2. a registered Miri toolchain

  In our S-CORE ecosyste, the Miri toolchain is provided by score_toolchains_rust.

  ### 1. Register the Rust and Miri toolchains

  Example .bazelrc configuration:

  build:per-x86_64-linux --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_unknown_linux_gnu
  build:per-x86_64-linux --extra_toolchains=@score_toolchains_rust//toolchains/ferrocene:ferrocene_x86_64_unknown_linux_gnu_miri

  The first toolchain is the normal Ferrocene Rust toolchain.

  The second toolchain provides:

  - miri
  - the prebuilt Miri sysroot
  - runtime files required by the Miri launcher

  ### 2. Wrap existing Rust targets

  #### Tests

  load("@rules_rust//rust:defs.bzl", "rust_test", "miri_test")

  rust_test(
      name = "tests",
      crate = "my_lib",
  )

  miri_test(
      name = "tests_miri",
      crate = ":tests",
      tags = ["manual"],
  )

  #### Binaries

  load("@rules_rust//rust:defs.bzl", "rust_binary", "miri_binary")

  rust_binary(
      name = "app",
      srcs = ["src/main.rs"],
  )

  miri_binary(
      name = "app_miri",
      crate = ":app",
      tags = ["manual"],
  )

  ### 3. Run them with Bazel

  bazel test --config=per-x86_64-linux //path/to:tests_miri
  bazel run  --config=per-x86_64-linux //path/to:app_miri

  ## Rule Parameters

  ### miri_test

  Current useful parameters:

  - crate
      - existing Rust target to execute under Miri
      - typically wrap a rust_test(...)
  - miri_flags
      - flags passed directly to the miri driver
      - defaults to:
          - -Zmiri-disable-isolation
  - miri_args
      - arguments forwarded after --
      - for tests, this is useful for libtest filtering
  - env
      - extra runtime environment variables
  - env_inherit
      - runtime environment variables inherited from the outer environment
  - platform
      - optional platform override for the wrapped target before Miri-mode rebuild

  Example with a test filter:

  miri_test(
      name = "tests_miri_json_backend",
      crate = ":tests",
      miri_args = ["json_backend::"],
      tags = ["manual"],
  )

  ### miri_binary

  Current useful parameters are the same as miri_test, except it wraps a rust_binary-like target and executes it under Miri.

  ## Current Limitations

  This PR is intentionally scoped as a first version.

  ### 1. Not full rust_test / rust_binary parity

  These rules do not yet expose the entire attribute surface of rust_test() or rust_binary().

  They currently wrap existing Rust targets instead.

  ### 2. Pure-Rust graphs only

  The current direct-driver implementation rejects targets with native linker inputs.

  That means this version supports:

  - pure-Rust test targets
  - pure-Rust binaries

  but not targets that pull in native link artifacts such as:

  - cc_library
  - other non-Rust linker inputs

  ### 3. Requires a registered Miri toolchain

  If miri_test or miri_binary is used without registering a Miri toolchain, analysis fails with a clear error.

  ## Validation

  This implementation was validated end-to-end against a real consumer repo (persistency) using a Ferrocene-backed Miri toolchain
  from score_toolchains_rust.

  Validated flow:

  - wrap an existing rust_test(...) target with miri_test(...)
  - run it with Bazel
  - execute the full rust_kvs test harness under Miri

  Result:

  - all non-ignored tests passed under Miri
  - the sharded Miri suite also passed

Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
Added comments into the code for better understanding

Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
  - stage native linker inputs and C++ runtime libs in Miri runfiles
  - allow miri_test and miri_binary to inspect the C++ toolchain
  - reuse rustc native library selection helpers for PIC/runtime handling
  - export RULES_RUST_MIRI from the launcher for runtime-specific workarounds
  - fix Bazel test runfiles initialization for the Miri launcher

Signed-off-by: Dan Calavrezo <195309321+dcalavrezo-qorix@users.noreply.github.com>
@MaximilianSoerenPollak
Copy link
Copy Markdown

@pawelrutkaq @darkwisebear

I added you two as reviewers here as you also contributed to this repo.
I can not review this as I do not have any idea what the goal of htis repo is, nor what the goal of the PR is etc.

If you think it's okay I can click approve if needed, but beyond that I can't do much here.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class Miri support to rules_rust by introducing wrapper rules that re-analyze an existing Rust target graph in a Miri-specific mode and execute it via the direct miri driver, backed by a dedicated Miri toolchain type.

Changes:

  • Introduces miri_test / miri_binary wrapper rules that generate runfiles-based launchers invoking the direct miri driver.
  • Adds a miri_toolchain_type and rust_miri_toolchain rule to model Miri’s driver + sysroot + runtime closure separately from the normal Rust toolchain.
  • Adds a Miri build setting + transition and updates Rust compilation to rebuild target-side crates against the Miri sysroot and include -Zalways-encode-mir.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
rust/toolchain.bzl Adds rust_miri_toolchain rule producing ToolchainInfo for Miri driver + sysroot inputs.
rust/private/rustc.bzl Plumbs Miri mode into compile inputs/args: sysroot selection, -Zalways-encode-mir, toolchain lookup helpers.
rust/private/rust.bzl Makes all Rust rules optionally aware of the Miri toolchain and threads the Miri build setting through common attrs.
rust/private/miri.bzl Implements miri_test/miri_binary rules and runfiles-based launcher script generation.
rust/private/miri_config.bzl Adds the Miri transition and rlocationpath helper for runfiles resolution.
rust/private/BUILD.bazel Declares the miri_enabled build setting (bool_flag).
rust/defs.bzl Exports miri_test and miri_binary as public rules.
rust/BUILD.bazel Adds miri_toolchain_type.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +963 to +965
transitive_inputs = [ctx.attr.sysroot_files[DefaultInfo].files]
if ctx.attr.runtime_files:
transitive_inputs.append(ctx.attr.runtime_files[DefaultInfo].files)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sysroot_files/runtime_files are declared as attr.label(allow_files = True), but the implementation assumes these are rule targets by indexing [DefaultInfo].files. If a user passes a single file label, this will fail at analysis time. Consider using ctx.files.sysroot_files / ctx.files.runtime_files (and wrapping in a depset) so both file and filegroup-like targets are supported.

Suggested change
transitive_inputs = [ctx.attr.sysroot_files[DefaultInfo].files]
if ctx.attr.runtime_files:
transitive_inputs.append(ctx.attr.runtime_files[DefaultInfo].files)
transitive_inputs = [depset(ctx.files.sysroot_files)]
if ctx.files.runtime_files:
transitive_inputs.append(depset(ctx.files.runtime_files))

Copilot uses AI. Check for mistakes.
Comment on lines +286 to +289
return [
DefaultInfo(executable = script, runfiles = runfiles),
RunEnvironmentInfo(
environment = dict(ctx.attr.env),
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rust_miri_toolchain exposes an env dict, but miri_test/miri_binary currently ignore it and only use ctx.attr.env. If the Miri driver requires runtime env (e.g. loader paths), this will silently break. Merge miri_toolchain.env (and possibly the Rust toolchain env if needed) into RunEnvironmentInfo.environment, with a clear precedence rule.

Suggested change
return [
DefaultInfo(executable = script, runfiles = runfiles),
RunEnvironmentInfo(
environment = dict(ctx.attr.env),
# Merge environment variables from the Rust toolchain, the Miri toolchain,
# and the rule's own `env` attribute. Precedence (later overrides earlier):
# 1. Rust toolchain env
# 2. Miri toolchain env
# 3. User-specified ctx.attr.env
merged_env = {}
if hasattr(toolchain, "env"):
merged_env.update(toolchain.env)
if hasattr(miri_toolchain, "env"):
merged_env.update(miri_toolchain.env)
merged_env.update(ctx.attr.env)
return [
DefaultInfo(executable = script, runfiles = runfiles),
RunEnvironmentInfo(
environment = merged_env,

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +91
def _native_runfiles(ctx, crate, dep_info):
linker_inputs = dep_info.transitive_noncrates.to_list()
if not linker_inputs:
return depset()

cc_toolchain, feature_configuration = find_cc_toolchain(ctx)
if cc_toolchain:
use_pic = miri_should_use_pic(cc_toolchain, feature_configuration, crate.type, ctx.var["COMPILATION_MODE"])
runtime_libs = cc_toolchain.dynamic_runtime_lib(feature_configuration = feature_configuration) if crate.type in ["dylib", "cdylib"] else cc_toolchain.static_runtime_lib(feature_configuration = feature_configuration)
else:
use_pic = False
runtime_libs = depset()

# Even when Miri interprets the Rust crate directly, mixed Rust/native
# targets still need their native inputs present in runfiles so any
# rustc-level link metadata and runtime discovery has a chance to resolve.
return depset(
direct = miri_collect_libs_from_linker_inputs(linker_inputs, use_pic) + [
additional_input
for linker_input in linker_inputs
for additional_input in linker_input.additional_inputs
],
transitive = [runtime_libs],
)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says the direct-driver implementation rejects targets with native linker inputs, but _native_runfiles currently stages dep_info.transitive_noncrates instead of failing. Either update the description or enforce the limitation by fail()ing when transitive_noncrates is non-empty (and keep the staging logic only once native/mixed-mode support is actually implemented).

Copilot uses AI. Check for mistakes.
Comment on lines +348 to +362
# V1 keeps the public surface small: users wrap an existing rust_test target
# instead of re-declaring srcs/deps on a parallel Miri-only rule.
miri_test = rule(
implementation = _miri_test_impl,
executable = True,
fragments = ["cpp"],
test = True,
attrs = _MIRI_COMMON_ATTRS,
cfg = miri_transition,
toolchains = [
str(Label("//rust:toolchain_type")),
str(Label("//rust:miri_toolchain_type")),
config_common.toolchain_type("@bazel_tools//tools/cpp:toolchain_type", mandatory = False),
],
doc = dedent("""\
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New public rules + transition (miri_test, miri_binary, miri_transition) introduce non-trivial behavior (toolchain resolution, runfiles staging, command-line reconstruction) but this PR doesn't add repository tests exercising them. Add at least one integration test that wraps a simple rust_test and verifies it runs under the generated launcher (and one negative test that fails analysis when no Miri toolchain is registered).

Copilot uses AI. Check for mistakes.
@mikehaller
Copy link
Copy Markdown

We've been using cargo miri for Eclipse Kuksa and for future migration to a Bazel/S-Core setup, this PR seems to be helpful for Kuksa as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants