Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f0e1ba0
Add VDOM fuzzing harness
ealmloff May 19, 2026
bbb1778
use the test case builder
ealmloff May 19, 2026
69a490c
move over more tests
ealmloff May 19, 2026
d265fbe
fuzzing passes
ealmloff May 20, 2026
7c8d1ab
cache templates
ealmloff May 20, 2026
7e58391
simplify oracle
ealmloff May 20, 2026
45d36a1
normalize dynamic nodes instead of matching many times
ealmloff May 20, 2026
1e530e0
ignore fuzz logs
ealmloff May 20, 2026
13da663
simplify op model
ealmloff May 20, 2026
09fcce5
add ci workflow
ealmloff May 20, 2026
e43d87a
capture fuzz panics
ealmloff May 20, 2026
277163c
100% fuzzing code coverage for core diffing
ealmloff May 20, 2026
c0148dc
trim strategies from fuzzer
ealmloff May 20, 2026
9f7df13
fix component drop order
ealmloff May 20, 2026
785a936
fix component drop order
ealmloff May 20, 2026
c8f97d7
minimize the diff a bit
ealmloff May 21, 2026
7d4e474
restore oracle as a dev dep
ealmloff May 21, 2026
d654677
fuzzing passes
ealmloff May 21, 2026
c8e4aca
remove pending_* states from fuzzer
ealmloff May 21, 2026
6943bde
cover more event surface
ealmloff May 21, 2026
54935c5
ignore recursive event calls for now
ealmloff May 21, 2026
b8c2fff
clarify debug assert
ealmloff May 21, 2026
474c32f
simplify attribute merging
ealmloff May 21, 2026
3ef3d18
simplify node diff
ealmloff May 21, 2026
7260909
remove dynamic_node_is_rendered_in_dom
ealmloff May 21, 2026
84dead8
move oracle
ealmloff May 21, 2026
0030137
wip new attribute diff
ealmloff May 21, 2026
0260035
split out attribute diffing
ealmloff May 21, 2026
38ebaa3
clean up diff
ealmloff May 21, 2026
7461a01
add a note about the new debug assert
ealmloff May 21, 2026
af38b30
clean up crash files
ealmloff May 21, 2026
a5813ad
ignore crash files
ealmloff May 21, 2026
1d8ca2c
documentation for the new attribute diffing behavior
ealmloff May 21, 2026
cc99931
remove OptimizedStrategy
ealmloff May 22, 2026
1f86e9a
simplify suspense
ealmloff May 22, 2026
149d03d
more trims
ealmloff May 22, 2026
76dbf6b
fix raw attribute sorting
ealmloff May 22, 2026
198a9aa
sort with runtime names
ealmloff May 22, 2026
2c2e5ee
remove Sequence and thread locals
ealmloff May 22, 2026
fc9bf05
move listener tracking logic out of oracle
ealmloff May 22, 2026
4cfb2e1
revert scope_should_render
ealmloff May 22, 2026
1c20008
rely on sorted spread attributes
ealmloff May 22, 2026
2de66a7
remove sorted range
ealmloff May 22, 2026
0b2a133
remove dead case
ealmloff May 22, 2026
9e632e5
move panic_message into fuzz
ealmloff May 22, 2026
9d6f5e9
remove VNodeEdit
ealmloff May 22, 2026
19b6ff3
move over the rest of the core tests
ealmloff May 22, 2026
ecb8d97
switch suspense to use the oracle
ealmloff May 22, 2026
cc14d36
add more comments to attributes
ealmloff May 22, 2026
5cfdb2b
fix fuzzer
ealmloff May 22, 2026
154ebdb
symbolic ignores
ealmloff May 22, 2026
ef83a9d
fuzz CI: probe llvm-symbolizer location
ealmloff May 22, 2026
1b52cc4
fuzz CI: gitignore .claude session state
ealmloff May 22, 2026
0c51c26
move options up
ealmloff May 22, 2026
cdab9aa
exclude fuzz from tests
ealmloff May 22, 2026
1ab8ab4
fix clippy
ealmloff May 22, 2026
8a72fdd
more clippy fixes
ealmloff May 22, 2026
a8e9bd9
disable leak detection for coverage
ealmloff May 22, 2026
b85185b
fix coverage ci
ealmloff May 22, 2026
4a42f80
force more collisions in fuzzing
ealmloff May 22, 2026
f15f52b
fix pr comment workflow
ealmloff May 22, 2026
796d613
fix fmt
ealmloff May 22, 2026
e94d71e
fix code cov comment
ealmloff May 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions .github/workflows/vdom-fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
name: VDOM Fuzz

on:
push:
branches:
- main
paths:
- ".github/workflows/vdom-fuzz.yml"
- "Cargo.lock"
- "Cargo.toml"
- "codecov.yml"
- "packages/fuzz/**"
- "packages/dioxus-renderer-oracle/**"
- "packages/core/**"
- "packages/core-types/**"
- "packages/dioxus/**"
- "packages/ssr/**"

pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
paths:
- ".github/workflows/vdom-fuzz.yml"
- "Cargo.lock"
- "Cargo.toml"
- "codecov.yml"
- "packages/fuzz/**"
- "packages/dioxus-renderer-oracle/**"
- "packages/core/**"
- "packages/core-types/**"
- "packages/dioxus/**"
- "packages/ssr/**"

workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
FUZZ_DIR: packages/fuzz/fuzz
FUZZ_TARGET: vdom_ops
RUST_BACKTRACE: 1
rust_nightly: nightly-2025-10-05

jobs:
test-and-coverage:
if: github.event.pull_request.draft == false
name: "Fuzz | Test and coverage"
runs-on: warp-ubuntu-latest-x64-4x
timeout-minutes: 45
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v5

- name: Install Rust ${{ env.rust_nightly }}
uses: dtolnay/rust-toolchain@nightly
with:
toolchain: ${{ env.rust_nightly }}
components: llvm-tools-preview

- uses: taiki-e/install-action@cargo-fuzz

- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
cache-workspace-crates: "true"
cache-provider: "warpbuild"

- name: Test fuzz support crate
run: cargo test -p fuzz --lib --examples

- name: Smoke test fuzz target
run: |
mkdir -p "$RUNNER_TEMP/fuzz-corpus" "$RUNNER_TEMP/fuzz-artifacts"
cargo +${{ env.rust_nightly }} fuzz run --fuzz-dir "$FUZZ_DIR" "$FUZZ_TARGET" "$RUNNER_TEMP/fuzz-corpus" -- \
-runs=256 \
-artifact_prefix="$RUNNER_TEMP/fuzz-artifacts/"

- name: Generate fuzz coverage
id: coverage
run: |
cargo +${{ env.rust_nightly }} fuzz coverage --fuzz-dir "$FUZZ_DIR" "$FUZZ_TARGET" "$RUNNER_TEMP/fuzz-corpus" -- -runs=0

target_triple="$(rustc +${{ env.rust_nightly }} -vV | sed -n 's/^host: //p')"
llvm_cov="$(rustc +${{ env.rust_nightly }} --print sysroot)/lib/rustlib/$target_triple/bin/llvm-cov"
coverage_binary="$FUZZ_DIR/target/$target_triple/coverage/$target_triple/release/$FUZZ_TARGET"
coverage_profile="$FUZZ_DIR/coverage/$FUZZ_TARGET/coverage.profdata"
coverage_lcov="$RUNNER_TEMP/fuzz.lcov"
coverage_report="$RUNNER_TEMP/fuzz-coverage.txt"
coverage_comment="$RUNNER_TEMP/fuzz-coverage.md"

test -x "$coverage_binary"
test -s "$coverage_profile"

"$llvm_cov" report \
--instr-profile="$coverage_profile" \
"$coverage_binary" \
--sources packages/fuzz/src \
| tee "$coverage_report"

"$llvm_cov" export \
--format=lcov \
--instr-profile="$coverage_profile" \
"$coverage_binary" \
--sources packages/fuzz/src \
> "$coverage_lcov"

test -s "$coverage_lcov"
test -s "$coverage_report"

COVERAGE_REPORT="$coverage_report" \
COVERAGE_COMMENT="$coverage_comment" \
python3 - <<'PY'
import os
import sys
from pathlib import Path

report_path = Path(os.environ["COVERAGE_REPORT"])
comment_path = Path(os.environ["COVERAGE_COMMENT"])
output_path = Path(os.environ["GITHUB_OUTPUT"])

total = next(
(line for line in report_path.read_text(encoding="utf-8").splitlines() if line.startswith("TOTAL")),
None,
)
if total is None:
print("llvm-cov report did not include a TOTAL row", file=sys.stderr)
sys.exit(1)

fields = total.split()
if len(fields) < 10:
print(f"Unexpected llvm-cov TOTAL row: {total}", file=sys.stderr)
sys.exit(1)

comment = f"""## Dioxus VDOM fuzz coverage

Coverage generated from `cargo fuzz coverage` for `packages/fuzz/src` after the `{os.environ["FUZZ_TARGET"]}` smoke corpus run.

| Metric | Coverage |
| --- | ---: |
| Regions | {fields[3]} |
| Functions | {fields[6]} |
| Lines | {fields[9]} |
"""

comment_path.write_text(comment, encoding="utf-8")

with output_path.open("a", encoding="utf-8") as output:
output.write("comment<<EOF\n")
output.write(comment)
output.write("\nEOF\n")
PY

- name: Comment fuzz coverage on PR
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3
with:
pr-number: ${{ github.event.pull_request.number }}
message: ${{ steps.coverage.outputs.comment }}
comment-tag: fuzz-coverage

- name: Upload fuzz coverage to Codecov
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
files: ${{ runner.temp }}/fuzz.lcov
flags: fuzz
name: fuzz
token: ${{ secrets.CODECOV_TOKEN }}

- name: Upload fuzz coverage artifact
if: always()
uses: actions/upload-artifact@v6
with:
name: fuzz-coverage
path: |
${{ runner.temp }}/fuzz.lcov
${{ runner.temp }}/fuzz-coverage.txt
${{ runner.temp }}/fuzz-coverage.md
if-no-files-found: ignore
retention-days: 7

- name: Upload fuzz failure artifacts
if: failure()
uses: actions/upload-artifact@v6
with:
name: fuzz-artifacts
path: ${{ runner.temp }}/fuzz-artifacts
if-no-files-found: ignore
retention-days: 7
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ tmp/
*.wat

# External macos drives have extra ._ files
._*
._*

# Fuzzing logs
fuzz-*.log
53 changes: 53 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ resolver = "2"
members = [
"packages/dioxus",
"packages/core",
"packages/dioxus-renderer-oracle",
"packages/fuzz",
"packages/fuzz/fuzz",
"packages/core-types",
"packages/cli",
"packages/cli-config",
Expand Down Expand Up @@ -121,6 +124,7 @@ version = "0.8.0-alpha.0"
[workspace.dependencies]
dioxus = { path = "packages/dioxus", version = "0.8.0-alpha.0" }
dioxus-core = { path = "packages/core", version = "0.8.0-alpha.0" }
dioxus-renderer-oracle = { path = "packages/dioxus-renderer-oracle", version = "0.8.0-alpha.0" }
dioxus-core-types = { path = "packages/core-types", version = "0.8.0-alpha.0" }
dioxus-core-macro = { path = "packages/core-macro", version = "0.8.0-alpha.0" }
dioxus-config-macro = { path = "packages/config-macro", version = "0.8.0-alpha.0" }
Expand Down Expand Up @@ -400,6 +404,14 @@ incremental = true
[profile.dev.package.walrus]
opt-level = 3

# Keep debug assertions for fuzzing, but compile the fuzz harness and reusable
# fuzzer crate with release-style optimizations.
[profile.dev.package.dioxus-fuzz]
opt-level = 3

[profile.dev.package.dioxus-vdom-fuzz]
opt-level = 3

# ensure we have adversarial setup for tls
[profile.dev.package.cross-tls-crate]
opt-level = 2
Expand Down
1 change: 1 addition & 0 deletions packages/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ xxhash-rust = { workspace = true, features = ["const_xxh64"] }

[dev-dependencies]
dioxus = { workspace = true }
dioxus-renderer-oracle = { workspace = true }
dioxus-ssr = { workspace = true }
dioxus-html = { workspace = true, features = ["serialize"] }
tokio = { workspace = true, features = ["full"] }
Expand Down
37 changes: 33 additions & 4 deletions packages/core/src/arena.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::innerlude::ScopeOrder;
use crate::innerlude::{NoOpMutations, ScopeOrder};
use crate::{ScopeId, virtual_dom::VirtualDom};

/// An Element's unique identifier.
Expand Down Expand Up @@ -72,10 +72,10 @@ impl VirtualDom {
elements.try_remove(el.0).is_some()
}

// Drop a scope without dropping its children
//
// Note: This will not remove any ids from the arena
// Drop a scope whose rendered nodes have already been removed.
pub(crate) fn drop_scope(&mut self, id: ScopeId) {
self.drop_orphaned_child_scopes(id);

let height = {
let scope = self.scopes.remove(id.0);
let context = scope.state();
Expand All @@ -87,6 +87,35 @@ impl VirtualDom {
// If this scope was a suspense boundary, remove it from the resolved scopes
self.resolved_scopes.retain(|s| s != &id);
}

pub(crate) fn drop_orphaned_child_scopes(&mut self, parent: ScopeId) {
// Parent rendered output can be removed before every child scope has
// been dropped. Clean those children without emitting more DOM edits.
let children = self
.scopes
.iter()
.filter_map(|(idx, _)| {
let scope = ScopeId(idx);
let parent_id = self
.runtime
.try_get_state(scope)
.and_then(|scope| scope.parent_id());
(parent_id == Some(parent)).then_some(scope)
})
.collect::<Vec<_>>();

for child in children {
if !self.scopes.contains(child.0) {
continue;
}

if self.scopes[child.0].last_rendered_node.is_some() {
self.remove_component_node(None::<&mut NoOpMutations>, true, child, None);
} else {
self.drop_scope(child);
}
}
}
}

impl ElementPath {
Expand Down
Loading
Loading