Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
493 changes: 440 additions & 53 deletions .fusa-reqs.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rcp"
version = "5.0.0"
version = "5.0.1"
edition = "2021"
rust-version = "1.75"
license = "MPL-2.0"
Expand Down
2 changes: 1 addition & 1 deletion INCIDENT-RESPONSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ This plan covers security incidents affecting the rust-RCP library or any deploy
### 3. Analysis and Fix

1. Root cause analysis — trace to specific `REQ-*` requirement.
2. Write a failing test reproducing the vulnerability (`// fusa:test REQ-XXX`).
2. Write a failing test reproducing the vulnerability (`//fusa:test REQ-XXX`).
3. Implement the fix; add the requirement annotation.
4. Update `.fusa-problems.json` with problem record.
5. Re-run `rsfusa check` to verify coverage.
Expand Down
2 changes: 1 addition & 1 deletion SAFETY_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ See `HARA.md` and `.fusa-hara.json` for the full HARA. Ten hazards (H-001 to H-0

### 4.2 Requirements Tracing

All safety requirements are annotated with `// fusa:req REQ-XXX` in source files. Test cases are annotated with `// fusa:test REQ-XXX`. The `rsfusa` tool validates traceability in CI.
All safety requirements are annotated with `//fusa:req REQ-XXX` in source files. Test cases are annotated with `//fusa:test REQ-XXX`. The `rsfusa` tool validates traceability in CI.

### 4.3 Verification Strategy

Expand Down
16 changes: 8 additions & 8 deletions fuzz/fuzz_targets/fuzz_avtpdu_acf_decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@ use libfuzzer_sys::fuzz_target;
// only failure mode under test is a panic inside the crate's own decode
// logic, never an assertion in this harness.
fuzz_target!(|data: &[u8]| {
// fusa:req REQ-NTSCF-005
// fusa:req REQ-NTSCF-006
//fusa:req REQ-NTSCF-005
//fusa:req REQ-NTSCF-006
let _ = rcp::avtp::decode_ntscf_header(data);

// fusa:req REQ-TSCF-005
// fusa:req REQ-TSCF-006
//fusa:req REQ-TSCF-005
//fusa:req REQ-TSCF-006
let _ = rcp::avtp::decode_tscf_header(data);

// fusa:req REQ-HVSEL-005
//fusa:req REQ-HVSEL-005
// select_header_variant is exercised under both TimeSyncCapability
// outcomes, since the rule branches on it before decoding the body.
let _ = rcp::avtp::select_header_variant(data, rcp::avtp::TimeSyncCapability::Capable);
let _ = rcp::avtp::select_header_variant(data, rcp::avtp::TimeSyncCapability::Incapable);

// fusa:req REQ-BMI-004
//fusa:req REQ-BMI-004
let _ = rcp::acf::decode_byte_message_info(data);

// fusa:req REQ-ABB-005
//fusa:req REQ-ABB-005
let _ = rcp::acf::decode_acf_abb(data);

// fusa:req REQ-GBB-005
//fusa:req REQ-GBB-005
let _ = rcp::acf::decode_acf_gbb(data);

// Belt-and-suspenders: parse_stream_id/StreamId::from_u64 take a plain
Expand Down
2 changes: 1 addition & 1 deletion fuzz/fuzz_targets/fuzz_config_parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
// fusa:req REQ-CFG-005
//fusa:req REQ-CFG-005
if let Ok(s) = std::str::from_utf8(data) {
let _ = rcp::config::from_json(s);
let _ = rcp::config::from_yaml(s);
Expand Down
2 changes: 1 addition & 1 deletion fuzz/fuzz_targets/fuzz_e2e_unwrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ use libfuzzer_sys::fuzz_target;
// an arbitrary caller-supplied byte slice, matching this harness's own
// `data: &[u8]` shape.
fuzz_target!(|data: &[u8]| {
// fusa:req REQ-CRC-002
//fusa:req REQ-CRC-002
let _ = rcp::e2e::crc32_tc18(data);
});
4 changes: 2 additions & 2 deletions fuzz/fuzz_targets/fuzz_wire_decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use libfuzzer_sys::fuzz_target;
// into both ACF decoders. Each call is `let _ = ...;`: the only failure
// mode under test is a panic inside the crate's own decode logic.
fuzz_target!(|data: &[u8]| {
// fusa:req REQ-WIRE-008
// fusa:req REQ-WIRE-009
//fusa:req REQ-WIRE-008
//fusa:req REQ-WIRE-009
if let Ok((_hdr, payload)) = rcp::avtp::decode_ntscf_frame(data) {
let _ = rcp::acf::decode_acf_abb(payload);
let _ = rcp::acf::decode_acf_gbb(payload);
Expand Down
46 changes: 37 additions & 9 deletions scripts/fusa-gap-check.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/usr/bin/env bash
# FuSa gap check: verify every requirement has source annotation and test annotation.
# Tag format is //fusa:req / //fusa:test with NO space after the slashes — this is
# exactly what rust-FuSa (rsfusa) trace.rs::annotation_kind matches; a space makes
# the annotation invisible to the real tool.
# Exits 1 if any gap is found. Run from repo root.

set -euo pipefail
Expand All @@ -22,24 +25,38 @@ reqs_data = json.loads(reqs_file.read_text())
# .fusa-reqs.json schema 1.0: {"schemaVersion": "1.0", "requirements": [...]}.
# Older files were a bare array; accept both for robustness.
reqs_list = reqs_data["requirements"] if isinstance(reqs_data, dict) else reqs_data
declared = {r["id"] for r in reqs_list}

# A requirement carrying "status": "not-implemented" is a deliberate, honest
# record that TC18 mandates something this crate does NOT do. It is part of
# the requirements corpus — so that the corpus is a complete map of TC18's
# normative surface rather than only the parts that happen to be built — but
# by definition it has no implementation and no test to trace to, so it is
# exempt from the annotation requirement below and reported separately.
NOT_IMPL = "not-implemented"
unimpl = {r["id"]: r for r in reqs_list if r.get("status") == NOT_IMPL}
declared = {r["id"] for r in reqs_list} - set(unimpl)

src_text = "\n".join(p.read_text() for p in src_dir.rglob("*.rs"))
in_src = set(re.findall(r"//\s*fusa:req\s+(REQ-[\w-]+)", src_text))
in_test = set(re.findall(r"//\s*fusa:test\s+(REQ-[\w-]+)", src_text))
in_src = set(re.findall(r"//fusa:req\s+(REQ-[\w-]+)", src_text))
in_test = set(re.findall(r"//fusa:test\s+(REQ-[\w-]+)", src_text))

gaps = []
for req_id in sorted(declared):
missing = []
if req_id not in in_src:
missing.append("source annotation (// fusa:req)")
missing.append("source annotation (//fusa:req)")
if req_id not in in_test:
missing.append("test annotation (// fusa:test)")
missing.append("test annotation (//fusa:test)")
if missing:
gaps.append((req_id, missing))

undeclared_src = in_src - declared
undeclared_test = in_test - declared
undeclared_src = in_src - declared - set(unimpl)
undeclared_test = in_test - declared - set(unimpl)

# An entry marked not-implemented must not also be traced to code/tests —
# that would mean the marker is stale and the corpus is lying in the other
# direction. Treat it as a gap so it gets fixed.
stale_unimpl = sorted((in_src | in_test) & set(unimpl))

if gaps:
print(f"\nFuSa GAP REPORT — {len(gaps)} requirement(s) with missing coverage:\n")
Expand All @@ -56,12 +73,23 @@ if undeclared_test:
for r in sorted(undeclared_test):
print(f" {r}")

if stale_unimpl:
print(f"\nERROR: {len(stale_unimpl)} requirement(s) marked "
f'"{NOT_IMPL}" but traced to code and/or tests:')
for r in stale_unimpl:
print(f" {r}")

total = len(declared)
covered = len(declared - {g[0] for g in gaps})
pct = 100 * covered // total if total else 0
print(f"\nCoverage: {covered}/{total} ({pct}%) requirements fully traced")
print(f"\nCoverage: {covered}/{total} ({pct}%) implemented requirements fully traced")
if unimpl:
print(f"Declared but NOT implemented (TC18 clauses this crate does not "
f"satisfy): {len(unimpl)}")
for req_id in sorted(unimpl):
print(f" {req_id}: {unimpl[req_id].get('title', '')}")

if gaps:
if gaps or stale_unimpl:
sys.exit(1)
print("OK — no FuSa gaps detected")
EOF
Loading
Loading