docs/superpowers/plans/2026-05-30-ubsan-through-ffi.md
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make sanitizers.yml's UBSAN job actually catch UB through the FFI boundary on Linux CI, and emit a parsed findings table to $GITHUB_STEP_SUMMARY so the informational signal is consumable.
Architecture: The C side is already instrumented via ENABLE_UBSAN (raylib's CompilerFlags.cmake:37 adds -fsanitize=undefined). The missing piece is on the Rust link step: RUSTFLAGS="-Clink-arg=-fsanitize=undefined" makes rustc forward the flag to its Linux linker driver (gcc), which then auto-links libubsan and resolves the __ubsan_handle_* symbols. A temporary canary (deliberate INT_MAX + 1) validates the wire-up actually works; the canary is added in Task 1 and reverted in Task 5. A bash report parser (ci/ubsan-report.sh) aggregates per-process ubsan.log* files into a markdown table.
Tech Stack: Bash + GNU awk (gawk default on ubuntu-latest) for the report parser; raylib-sys's existing cc::Build pattern for the canary C shim; Rust 2024 edition (unsafe extern "C" block syntax) for the FFI declaration.
Spec reference: docs/superpowers/specs/2026-05-30-ubsan-through-ffi-design.md.
Files:
raylib-sys/binding/ubsan_canary.c (temporary; deleted in Task 5)raylib-sys/build.rs (add gen_ubsan_canary() + call from main(); both deleted in Task 5)raylib/tests/integration_ubsan_canary.rs (temporary; deleted in Task 5)The canary is a deliberate UB-emitting C function compiled into the build, plus a Rust integration test that calls it. The function performs INT_MAX + 1 with volatile to defeat constant-folding. The test calls it via a manual unsafe extern "C" declaration — no bindgen changes, keeping the surface area minimal for Task 5's revert.
The test must pass today (because UBSAN isn't wired yet — the UB call silently runs); after Tasks 2-3, the test still passes (because halt_on_error=0 is set), but the report parser sees the hit. That's the validation Task 4 performs on CI.
raylib-sys/binding/ubsan_canary.cWrite file contents:
/* UBSAN wire-up canary — TEMPORARY, deleted in Task 5.
*
* Performs a deliberate INT_MAX + 1 signed overflow so the sanitizers
* workflow can prove `-Clink-arg=-fsanitize=undefined` is actually
* linking libubsan. See docs/superpowers/specs/2026-05-30-ubsan-through-ffi-design.md §4.3.
*
* Both operands are `volatile` to defeat constant-folding; otherwise
* gcc with -O2 would compute the overflow at compile time and emit
* no runtime UB check.
*/
#include <limits.h>
void rlrust_ubsan_canary(void) {
volatile int x = INT_MAX;
volatile int y = x + 1;
(void)y;
}
gen_ubsan_canary() to raylib-sys/build.rsAppend this function near the existing gen_utils() / gen_raymath() definitions (around line 393):
fn gen_ubsan_canary() {
cc::Build::new()
.files(vec!["binding/ubsan_canary.c"])
.include("binding")
.warnings(false)
.extra_warnings(false)
.compile("ubsan_canary");
}
Then add the call inside main()'s #[cfg(not(feature = "nobuild"))] block (around line 524). Locate this block:
#[cfg(not(feature = "nobuild"))]
{
gen_utils();
gen_raymath();
}
Replace with:
#[cfg(not(feature = "nobuild"))]
{
gen_utils();
gen_raymath();
gen_ubsan_canary();
}
raylib/tests/integration_ubsan_canary.rsWrite file contents:
//! UBSAN wire-up canary — TEMPORARY, deleted in Task 5.
//!
//! Calls a deliberately-UB C function via FFI to validate the
//! sanitizers workflow's `-Clink-arg=-fsanitize=undefined` wire-up.
//! The test always passes (UBSAN runs with halt_on_error=0); the
//! assertion the workstream cares about is "the Step Summary parser
//! sees a signed-integer-overflow hit for ubsan_canary.c", checked
//! post-run on CI in Task 4.
//!
//! See docs/superpowers/specs/2026-05-30-ubsan-through-ffi-design.md §4.3.
unsafe extern "C" {
fn rlrust_ubsan_canary();
}
#[test]
fn ubsan_canary_triggers_signed_overflow() {
unsafe { rlrust_ubsan_canary() };
}
Run from the repo root:
cargo test -p raylib --no-default-features \
--features software_renderer,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RMODELS,SUPPORT_MODULE_RAUDIO,SUPPORT_IMAGE_GENERATION \
--test integration_ubsan_canary -- --test-threads=1
Expected output (last lines):
running 1 test
test ubsan_canary_triggers_signed_overflow ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
If the test fails to compile or run, fix before proceeding. The canary should be silently UB at this point — no UBSAN runtime is linked yet.
Run:
cargo fmt --check
cargo clippy --all-targets --no-default-features \
--features software_renderer,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RMODELS,SUPPORT_MODULE_RAUDIO,SUPPORT_IMAGE_GENERATION \
-- -D warnings
Expected: both exit 0 with no diagnostics. If clippy flags the canary's unsafe block or the C-side volatile indirection, suppress with #[allow(...)] minimally inline (not crate-wide).
git add raylib-sys/binding/ubsan_canary.c raylib-sys/build.rs raylib/tests/integration_ubsan_canary.rs
git commit -m "$(cat <<'EOF'
test(ubsan): canary scaffolding for UBSAN wire-up validation
Adds a deliberately-UB C shim (INT_MAX + 1 with volatile to defeat
constant-folding) + an integration test that calls it via a manual
unsafe extern "C" declaration (no bindgen changes). Compiled into the
build via a new gen_ubsan_canary() in raylib-sys/build.rs.
The test passes today (UBSAN runtime not yet linked → UB runs silently).
After Tasks 2-3 wire RUSTFLAGS=-Clink-arg=-fsanitize=undefined, the
canary will trigger a signed-integer-overflow diagnostic that Task 4
confirms on CI, then Task 5 reverts the canary entirely.
Per spec §4.3 / §10 commit 3.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
Files:
.github/workflows/sanitizers.yml (UBSAN step's env block + comment cleanup)Adds the load-bearing RUSTFLAGS + UBSAN_OPTIONS env vars to the existing UBSAN step. Drops the stale comment that claimed UBSAN couldn't be wired ("rustc's -Zsanitizer does not support 'undefined'..."). The comment's premise is correct but its conclusion is wrong: UBSAN doesn't go through -Zsanitizer, it goes through -Clink-arg. Replace it with a short accurate note.
Locate the UBSAN step in .github/workflows/sanitizers.yml (currently around line 44). The whole step today is:
- name: UBSAN — software_renderer render tests
# rustc's -Zsanitizer does not support 'undefined' on the current nightly
# (valid values: address, cfi, hwaddress, leak, memory, thread, realtime, …).
# UBSAN coverage comes from the C side: ENABLE_UBSAN passes -fsanitize=undefined
# to the cmake raylib build, which is where all the FFI-boundary C code lives.
# No Rust-side RUSTFLAGS needed; drop them to avoid a fatal rustc arg error.
continue-on-error: true
run: >
cargo +nightly test -p raylib -Z build-std
--target x86_64-unknown-linux-gnu
--no-default-features
--features software_renderer,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RMODELS,SUPPORT_MODULE_RAUDIO,SUPPORT_IMAGE_GENERATION,ENABLE_UBSAN
--test render_shapes --test render_text --test integration_model_animations -- --test-threads=1
Replace it with:
- name: UBSAN — software_renderer render tests
# C side is instrumented via ENABLE_UBSAN (CompilerFlags.cmake:37 adds
# -fsanitize=undefined). The Rust-side link step needs to pull libubsan
# so the __ubsan_handle_* symbols resolve — that's what -Clink-arg does
# below. UBSAN_OPTIONS log_path captures per-process diagnostics for
# the report step that follows.
continue-on-error: true
env:
RUSTFLAGS: "-Clink-arg=-fsanitize=undefined"
UBSAN_OPTIONS: "halt_on_error=0:print_stacktrace=1:log_path=ubsan.log"
run: >
cargo +nightly test -p raylib -Z build-std
--target x86_64-unknown-linux-gnu
--no-default-features
--features software_renderer,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RMODELS,SUPPORT_MODULE_RAUDIO,SUPPORT_IMAGE_GENERATION,ENABLE_UBSAN
--test render_shapes --test render_text --test integration_model_animations --test integration_ubsan_canary -- --test-threads=1
Two real changes:
env block with RUSTFLAGS + UBSAN_OPTIONS.--test integration_ubsan_canary appended to the test target list (this is the only test that exercises the canary).Run from the repo root:
python -c "import yaml; yaml.safe_load(open('.github/workflows/sanitizers.yml'))"
Expected: no output, exit 0. If yaml.safe_load raises, fix the indentation / quoting before proceeding. (If python / pyyaml isn't available, fall back to gh workflow view sanitizers --yaml after pushing the branch, or any YAML validator the engineer has.)
git add .github/workflows/sanitizers.yml
git commit -m "$(cat <<'EOF'
ci(sanitizers): wire UBSAN runtime via -Clink-arg=-fsanitize=undefined
The C side is already instrumented via ENABLE_UBSAN
(CompilerFlags.cmake:37). The missing piece is on the Rust link step:
RUSTFLAGS=-Clink-arg=-fsanitize=undefined forwards the flag to rustc's
default Linux linker driver (gcc/cc), which then auto-links libubsan
and resolves the __ubsan_handle_* symbols emitted into raylib's .o.
UBSAN_OPTIONS sets halt_on_error=0 (collect all hits, not just the
first) + print_stacktrace=1 + log_path=ubsan.log so the report step
in the next commit has a known input path.
The integration_ubsan_canary test target is added to the cargo test
invocation; it's the wire-up validator (deleted in a later revert).
Per spec §4.1 / §6 / §10 commit 1.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
Files:
ci/ubsan-report.sh (executable).github/workflows/sanitizers.yml (append the report step after the UBSAN test step)The report parser turns the raw libubsan output into a digestible markdown table in $GITHUB_STEP_SUMMARY. Parses every ubsan.log* file in the working directory (libubsan appends .pid per-process), groups by error-kind, and emits an aggregated table with raw log preserved in a <details> collapse.
ci/ directory + the parser scriptRun from the repo root:
mkdir -p ci
Then create ci/ubsan-report.sh with these contents:
#!/usr/bin/env bash
# UBSAN findings report generator for sanitizers.yml.
#
# Reads ubsan.log* files produced by libubsan (with UBSAN_OPTIONS log_path=ubsan.log,
# libubsan appends .pid per-process). Parses runtime-error diagnostics into kind/
# count/first-occurrence rows, then writes a markdown table to $GITHUB_STEP_SUMMARY
# followed by the raw log inside a <details> collapse.
#
# Always exits 0 — the surrounding job has continue-on-error: true, but explicit
# success here keeps the script's contract clear.
#
# See docs/superpowers/specs/2026-05-30-ubsan-through-ffi-design.md §4.2.
set -u
SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/stdout}"
# libubsan appends .pid; tests may spawn several. Match both `ubsan.log` and `ubsan.log.*`.
shopt -s nullglob
logs=(ubsan.log ubsan.log.*)
if (( ${#logs[@]} == 0 )); then
echo "## UBSAN: no findings (no log files produced)" >> "$SUMMARY"
exit 0
fi
combined=$(cat "${logs[@]}" 2>/dev/null || true)
# Whitespace-only check.
if [[ -z "${combined//[[:space:]]/}" ]]; then
echo "## UBSAN: no findings (logs present but empty)" >> "$SUMMARY"
exit 0
fi
# Parse runtime-error lines. libubsan's diagnostic format is:
# <file>:<line>:<col>: runtime error: <kind>: <details...>
# gawk's match() with capture-array is GNU-specific; ubuntu-latest has gawk by default.
parsed=$(echo "$combined" | awk '
/runtime error:/ {
if (match($0, /([^ :]+):([0-9]+):([0-9]+): runtime error: ([a-zA-Z0-9_-]+)/, arr)) {
kind = arr[4];
loc = arr[1] ":" arr[2] ":" arr[3];
count[kind]++;
if (!(kind in first)) first[kind] = loc;
}
}
END {
n = 0; total = 0;
for (k in count) { n++; total += count[k]; }
printf "TOTAL\t%d\t%d\n", total, n;
for (k in count) printf "ROW\t%s\t%d\t%s\n", k, count[k], first[k];
}
')
total=$(printf "%s\n" "$parsed" | awk -F'\t' '/^TOTAL/ {print $2}')
kinds=$(printf "%s\n" "$parsed" | awk -F'\t' '/^TOTAL/ {print $3}')
emit_raw_log() {
{
echo ""
echo "<details><summary>Raw log</summary>"
echo ""
echo '```'
echo "$combined"
echo '```'
echo ""
echo "</details>"
} >> "$SUMMARY"
}
if [[ -z "$total" || "$total" == "0" ]]; then
echo "## UBSAN: no findings (logs present, no parseable runtime-error lines)" >> "$SUMMARY"
emit_raw_log
exit 0
fi
{
echo "## UBSAN findings ($total hits across $kinds kinds)"
echo ""
echo "| kind | count | first occurrence |"
echo "|------|-------|------------------|"
printf "%s\n" "$parsed" | awk -F'\t' '/^ROW/ {
printf "| %s | %s | %s |\n", $2, $3, $4;
}'
} >> "$SUMMARY"
emit_raw_log
exit 0
Run from the repo root:
chmod +x ci/ubsan-report.sh
Run from the repo root (still on Windows / your dev machine — bash via WSL or git-bash):
cd /tmp || cd ${TEMP:-.}
mkdir -p ubsan-smoke && cd ubsan-smoke
cat > ubsan.log.1234 <<'EOF'
ubsan_canary.c:11:21: runtime error: signed-integer-overflow: 2147483647 + 1 cannot be represented in type 'int'
#0 0x55a... in rlrust_ubsan_canary
#1 0x55a... in main
EOF
GITHUB_STEP_SUMMARY=summary.md bash <REPO_ROOT>/ci/ubsan-report.sh
cat summary.md
Replace <REPO_ROOT> with the absolute path to this checkout. Expected output of cat summary.md:
## UBSAN findings (1 hits across 1 kinds)
| kind | count | first occurrence |
|------|-------|------------------|
| signed-integer-overflow | 1 | ubsan_canary.c:11:21 |
<details><summary>Raw log</summary>
```
ubsan_canary.c:11:21: runtime error: signed-integer-overflow: ...
#0 0x55a... in rlrust_ubsan_canary
#1 0x55a... in main
```
</details>
Then clean up: rm -rf /tmp/ubsan-smoke (or the temp dir you used). If the table is wrong, fix the awk parsing before proceeding.
sanitizers.ymlLocate the end of the UBSAN test step (the run block ending in --test-threads=1). Append this step immediately after, at the same indentation level:
- name: UBSAN findings report
if: always()
run: bash ci/ubsan-report.sh
if: always() ensures the report runs even if the test step failed. The script always exits 0 so it never breaks the job.
Run:
python -c "import yaml; yaml.safe_load(open('.github/workflows/sanitizers.yml'))"
Expected: no output, exit 0.
git add ci/ubsan-report.sh .github/workflows/sanitizers.yml
git commit -m "$(cat <<'EOF'
ci(sanitizers): add UBSAN findings report parser
ci/ubsan-report.sh: parses libubsan's per-process ubsan.log* files into
a kind/count/first-occurrence markdown table, emits it to
$GITHUB_STEP_SUMMARY with the raw log preserved in a <details> collapse.
Always exits 0; gracefully handles missing/empty logs ("no findings").
sanitizers.yml: appends a new "UBSAN findings report" step gated on
if: always() so the report runs even when the test step fails.
Per spec §4.2 / §7 / §10 commit 2.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
Files: none (this task verifies CI behavior; no local edits)
The validation gate for the workstream. Pushes the three commits to the fork, triggers the sanitizers workflow, and confirms the Step Summary shows the canary's signed-integer-overflow hit. If the report shows no findings (or doesn't show the canary kind), Tasks 1-3 have a bug and must be fixed before proceeding.
Run from the repo root:
git push fork 6.0-rc
If the push is rejected as non-fast-forward, abort — the fork has diverged and needs investigation before continuing.
sanitizers.yml triggers on push to 6.0-rc, so a run should kick off automatically.
gh run list --workflow=sanitizers.yml --branch=6.0-rc --limit=3
Note the run ID of the most recent (top) entry. The status will be in_progress initially.
gh run watch <run-id>
Replace <run-id> with the ID from Step 2. The job will likely conclude success (because continue-on-error: true — even if any step fails, the overall job is green). Wait for the watch command to exit.
gh run view <run-id> --log | grep -A 100 "UBSAN findings\|UBSAN: no findings"
(Alternative: open the run in the browser via gh run view <run-id> --web and scroll to the UBSAN job's Summary tab.)
Expected: the Step Summary contains a section like:
## UBSAN findings (N hits across K kinds)
| kind | count | first occurrence |
|-------------------------|-------|-------------------------------|
| signed-integer-overflow | … | ubsan_canary.c:11:21 |
| … | … | … |
The exact first-occurrence file path may vary slightly depending on where libubsan resolved the symbol from (could be binding/ubsan_canary.c or just ubsan_canary.c). What matters: signed-integer-overflow appears in the table, attributed to a path containing ubsan_canary.
If the table shows ## UBSAN: no findings or omits the canary kind:
ubsan_canary_triggers_signed_overflow. If the test name doesn't appear, the --test integration_ubsan_canary arg didn't make it into the cargo invocation.libubsan.so is on the runner's lib path: search the log for cannot open shared object file: libubsan. If present, gcc on ubuntu-latest 24.04 may need apt-get install libubsan1 — add an install step to sanitizers.yml.This output is the seed for Task 6's baseline note. Copy the full table from the Step Summary into a scratch file (a note app, or next-session-prompt.md works fine since it's untracked). The other findings besides the canary's overflow are the "real" baseline that needs documenting. There is no commit for this task — it's purely a CI-verification gate.
Files:
raylib-sys/binding/ubsan_canary.craylib-sys/build.rs (remove gen_ubsan_canary() definition + the call from main())raylib/tests/integration_ubsan_canary.rs.github/workflows/sanitizers.yml (drop the --test integration_ubsan_canary arg)Now that Task 4 has confirmed the wire-up works, the canary's job is done. Delete it so the final merge state contains no canary files.
git rm raylib-sys/binding/ubsan_canary.c
gen_ubsan_canary() from raylib-sys/build.rsFind the function definition (added in Task 1 step 2) and delete it. Then find the gen_ubsan_canary(); call inside main()'s #[cfg(not(feature = "nobuild"))] block and delete that line. The block should return to exactly:
#[cfg(not(feature = "nobuild"))]
{
gen_utils();
gen_raymath();
}
git rm raylib/tests/integration_ubsan_canary.rs
In .github/workflows/sanitizers.yml, locate the UBSAN test step's run: line. Remove the --test integration_ubsan_canary token. After the edit the trailing portion should read:
--test render_shapes --test render_text --test integration_model_animations -- --test-threads=1
Run from the repo root:
cargo build -p raylib-sys --no-default-features --features software_renderer,SUPPORT_MODULE_RTEXTURES,SUPPORT_MODULE_RSHAPES,SUPPORT_MODULE_RTEXT,SUPPORT_MODULE_RMODELS,SUPPORT_MODULE_RAUDIO,SUPPORT_IMAGE_GENERATION,ENABLE_UBSAN
Expected: clean build, no errors about missing ubsan_canary.c or unresolved rlrust_ubsan_canary.
git grep -n ubsan_canary
git grep -n rlrust_ubsan_canary
Expected: zero matches for both. If anything matches (including comments), delete the reference before committing.
python -c "import yaml; yaml.safe_load(open('.github/workflows/sanitizers.yml'))"
Expected: no output, exit 0.
git add raylib-sys/build.rs .github/workflows/sanitizers.yml
git commit -m "$(cat <<'EOF'
revert: drop UBSAN canary after wire-up validated on CI
The canary (deliberate INT_MAX + 1 in ubsan_canary.c, called from
integration_ubsan_canary.rs) was added in Task 1 of the UBSAN
workstream to validate that -Clink-arg=-fsanitize=undefined actually
links libubsan + the report parser catches the hit. A fork CI run
confirmed both, so the canary is now redundant; deleting it keeps the
final merge state clean.
Files deleted: raylib-sys/binding/ubsan_canary.c,
raylib/tests/integration_ubsan_canary.rs.
build.rs: gen_ubsan_canary() definition and its call removed.
sanitizers.yml: --test integration_ubsan_canary dropped from the
UBSAN cargo test invocation.
Per spec §4.3 / §10 commit 4.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
Files:
docs/superpowers/notes/ws-ubsan-ffi-complete.mdCLAUDE.md (workstream status line)Records what UBSAN found on the post-canary clean run (the data from Task 4 step 5, filtered to exclude the canary's signed-integer-overflow since the canary is gone). Owner reads this baseline note to decide which findings warrant follow-up workstreams.
git push fork 6.0-rc
gh run list --workflow=sanitizers.yml --branch=6.0-rc --limit=1
gh run watch <run-id>
This run gives the true baseline — UBSAN findings without the canary contaminating the table. Pull the Step Summary as in Task 4 step 4.
Create docs/superpowers/notes/ws-ubsan-ffi-complete.md with the following template, filling in the baseline-findings section with the actual table from Step 1:
# UBSAN-through-FFI complete — sanitizers.yml UBSAN job now catches UB through the FFI boundary
**Status:** DONE on branch `6.0-rc` (pushed to `fork`). Fifth pre-WS9 workstream in the owner-locked queue (after pixel-pointers, hashes, mixed-audio, raylib-test salvage). Spec: `docs/superpowers/specs/2026-05-30-ubsan-through-ffi-design.md`. Plan: `docs/superpowers/plans/2026-05-30-ubsan-through-ffi.md`.
## What shipped
`sanitizers.yml`'s UBSAN job is no longer "best-effort" — `-Clink-arg=-fsanitize=undefined` now wires libubsan into the final link step, and a parsed report in `$GITHUB_STEP_SUMMARY` makes the informational signal consumable. Job stays `continue-on-error: true` (D8 informational, never gates the matrix).
- `.github/workflows/sanitizers.yml` — UBSAN step gets `RUSTFLAGS=-Clink-arg=-fsanitize=undefined` + `UBSAN_OPTIONS=halt_on_error=0:print_stacktrace=1:log_path=ubsan.log`, plus a new `if: always()` report step.
- `ci/ubsan-report.sh` (new) — bash + gawk parser. ~70 LOC. Groups libubsan diagnostics by kind, emits markdown table + raw log `<details>` collapse to `$GITHUB_STEP_SUMMARY`. Always exits 0.
## Wire-up validation
A temporary canary (`binding/ubsan_canary.c` doing `INT_MAX + 1` with volatile to defeat constant-folding, called from `integration_ubsan_canary.rs`) was added in Task 1, confirmed by Task 4 to trigger `signed-integer-overflow` in the report, and reverted in Task 5. The fork CI run for the canary push showed the table containing `signed-integer-overflow | 1 | ubsan_canary.c:...` — proof that the link wire-up is real, not just a clean build.
## Baseline findings (post-canary, against `render_shapes` + `render_text` + `integration_model_animations`)
<!-- TODO: replace this block with the actual table from Task 6 Step 1 -->
<!-- Format:
| kind | count | first occurrence | category |
|-------------------------|-------|--------------------------------------------|-------------------|
| ... | ... | ... | intentional / latent / unclear |
-->
(or "No findings — UBSAN ran clean against all three test targets on the post-canary run.")
## Tracked-deferred follow-ups
Carried out of this workstream:
1. **macOS / Windows UBSAN coverage** — clang on macOS uses libclang_rt.ubsan, MSVC has `/fsanitize=undefined` since VS 2022. Both unproven against the rustc + cmake integration; deferred.
2. **Promote UBSAN to required gate** — keep `continue-on-error: true` until baseline runs stabilize. Revisit after a few merges of clean data.
3. **UBSAN suppression file** — owner directive: report everything raw. Add a `UBSAN_OPTIONS=suppressions=...` file only if hit volume becomes unmanageable.
4. **Upstream fixes for any UBSAN findings** — one follow-up workstream per fix (or batched).
## CI inventory (all green on the fork, branch `6.0-rc`)
| Workflow | Jobs | Status |
|---------------|-----------------------------------------------|--------------------------------|
| `check` | fmt, clippy, docs, cargo-deny, msrv | ✅ |
| `test` | unit ×6, no-default ×3, software-render ×3 | ✅ |
| `web` | wasm-build (sys + safe) | ✅ |
| `sanitizers` | asan-ubsan (informational) | ✅ (ASAN clean; UBSAN reports) |
| `book` | mdbook build | ✅ |
**UBSAN ✅. Next: rustdoc rewrite (remaining ~200 stubs) → safe-abstractions for GuiGetIcons/GuiLoadIcons + PR #296 → WS9 showcase → final-release.**
Replace the <!-- TODO --> block with either:
The actual baseline table populated from Step 1's run, with each row tagged intentional / latent / unclear in the category column. (Use your judgment — owner reviews this; speculative categorization is fine, "unclear" is an honest answer.)
Or the "No findings" sentence if the table came back empty.
Step 3: Update CLAUDE.md status line
Open CLAUDE.md and find the "Pre-WS9 queue" line at the bottom of the "raylib 6.0 upgrade" section. It currently reads:
**Pre-WS9 queue ← NEXT** (owner-locked 2026-05-29): `pixel-pointers` ✅ → `hashes` ✅ → `mixed-audio` ✅ → `raylib-test` ✅ → UBSAN ← NEXT → rustdoc rewrite (remaining ~200 stubs) → safe-abstractions for `GuiGetIcons`/`GuiLoadIcons` + PR #296 → **WS9 showcase** → GitHub Pages (finale) → final-release.
Replace with:
**Pre-WS9 queue ← NEXT** (owner-locked 2026-05-29): `pixel-pointers` ✅ → `hashes` ✅ → `mixed-audio` ✅ → `raylib-test` ✅ → UBSAN ✅ → rustdoc rewrite (remaining ~200 stubs) ← NEXT → safe-abstractions for `GuiGetIcons`/`GuiLoadIcons` + PR #296 → **WS9 showcase** → GitHub Pages (finale) → final-release.
Two changes: UBSAN ← NEXT → UBSAN ✅, and rustdoc rewrite (remaining ~200 stubs) → rustdoc rewrite (remaining ~200 stubs) ← NEXT.
git add docs/superpowers/notes/ws-ubsan-ffi-complete.md CLAUDE.md
git commit -m "$(cat <<'EOF'
docs(ws-ubsan): baseline findings note + CLAUDE.md status line
ws-ubsan-ffi-complete.md records the post-canary baseline UBSAN run
against render_shapes + render_text + integration_model_animations,
with each kind tagged intentional/latent/unclear for owner review.
Documents the wire-up validation via the canary + the four
tracked-deferred follow-ups (macOS/Windows scope, gate promotion,
suppression file, upstream fixes).
CLAUDE.md status line: UBSAN ← NEXT → UBSAN ✅ → rustdoc rewrite ← NEXT.
Per spec §1 goal 6 / §10 commit 5.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
EOF
)"
git push fork 6.0-rc
Watch the final sanitizers run to confirm the report still works post-revert:
gh run list --workflow=sanitizers.yml --branch=6.0-rc --limit=1
gh run watch <run-id>
Expected: same baseline findings as Task 6 step 1, no canary entry. If something diverges, investigate before declaring done.
gh run list --branch=6.0-rc --limit=10
Expected: most recent runs of check, test, web, sanitizers, book all show ✅. If any workflow is failing on the post-revert HEAD that wasn't failing before, dig into the breakage before declaring the workstream done.
When all six tasks check out:
CLAUDE.md status line: UBSAN ✅ → rustdoc rewrite ← NEXT.6.0-rc.Hand off to the next workstream: rustdoc rewrite (the remaining ~200 stub-level items WS7 didn't enrich). That's its own brainstorm → spec → plan → execute cycle; do not start it from this plan.