Skip to content

Commit ab8a0fe

Browse files
committed
fix(deps): replace serde_yaml with safe-Rust serde-saphyr
serde_yaml 0.9 is deprecated and depends on unsafe-libyaml, an archived c2rust translation of libyaml flagged unmaintained by RUSTSEC-2024-0449. Swap in serde-saphyr 2.0 (#![forbid(unsafe_code)]) behind a small crate::yaml module: - hand-written configs (attack profiles, ambush timelines, amuck specs) use serde-saphyr's default resource budget; - machine-written reports (diff loader, a2ml bundles, attack results) get a node/event budget scaled to input length. The default 250k-node cap rejected YAML reports from scans over ~15k files; an alias-free document cannot have more nodes than bytes, so alias expansion stays bounded. Raise rust-version from 1.85.0 to 1.89: every serde-saphyr release that compiles on 1.85 is pre-1.0 and unmaintained (1.1.0 uses let-chains and is_multiple_of despite declaring no MSRV), noyalib needs 1.86+, and the committed lockfile already required 1.88 (encoding_rs, sha2, proptest). Default, http/signing/sarif and test builds verified on 1.89.0 --locked. YAML output is value-equivalent but not byte-identical ('…' -> "…", multi-line strings as | blocks); nothing compares the bytes, and reports written by serde_yaml still load (new fixture test). KYAML input parses (new test). cargo audit: RUSTSEC-2024-0449 gone, no new advisories. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFaqv6iBXrt3W5Akd1Fq5k
1 parent 8c2f198 commit ab8a0fe

18 files changed

Lines changed: 205 additions & 63 deletions

File tree

‎.github/CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ enhancement suggestion:
6767

6868
### Prerequisites
6969

70-
- Rust 1.85.0 or later (MSRV)
70+
- Rust 1.89 or later (MSRV)
7171

7272
- Cargo
7373

‎.machine_readable/svc/k9/panic-attacker-metadata.k9.ncl‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ K9!
5454
# Build configuration
5555
build = {
5656
edition = "2021",
57-
msrv = "1.85.0",
57+
msrv = "1.89",
5858
},
5959

6060
# Language policy

‎0-AI-MANIFEST.a2ml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
(build-system "cargo")
8383
(package-manager "cargo")
8484
(language "rust")
85-
(msrv "1.85.0")
85+
(msrv "1.89")
8686
(edition "2021"))
8787

8888
(lifecycle

‎CHANGELOG.adoc‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22

33
=== [Unreleased]
44

5+
==== Fixed — YAML parser replaced with safe Rust (RUSTSEC-2024-0449)
6+
7+
* *`+serde_yaml+` → `+serde-saphyr+` 2.0.* `+serde_yaml+` 0.9 is
8+
deprecated and pulled in `+unsafe-libyaml+`, an archived c2rust
9+
translation of libyaml flagged unmaintained by RUSTSEC-2024-0449.
10+
`+serde-saphyr+` is `+#![forbid(unsafe_code)]+`. All YAML now goes
11+
through `+src/yaml.rs+`: hand-written configs (attack profiles,
12+
timelines, amuck specs) keep the parser's default resource budget;
13+
machine-written reports get a node/event budget scaled to input
14+
length, so large-scan reports still load while alias expansion stays
15+
bounded.
16+
* *MSRV raised from 1.85.0 to 1.89.* No maintained safe-Rust serde
17+
YAML crate builds on 1.85 (serde-saphyr needs 1.89, noyalib 1.86+),
18+
and the committed lockfile already required 1.88.
19+
* *YAML output is equivalent but not byte-identical.* Quoting switches
20+
from `+'…'+` to `+"…"+` and multi-line strings use `+|+` blocks.
21+
Reports written by earlier versions still load. A bare `+~+`/`+null+`
22+
in a string field is now rejected (YAML 1.2 null); quote it.
23+
524
==== Fixed — assail detector precision (false-positive reduction, 2026-06-24)
625

726
Three `+assail+` analyzer fixes, all conservative (no new false

‎Cargo.lock‎

Lines changed: 56 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
name = "panic-attack"
44
version = "2.5.5"
55
edition = "2021"
6-
rust-version = "1.85.0"
6+
rust-version = "1.89"
77
authors = ["Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>"]
88
license-file = "LICENSE"
99
description = "Universal static analysis, stress testing, and logic-based bug signature detection for 49 languages"
@@ -18,7 +18,8 @@ anyhow = "1.0"
1818
log = "0.4"
1919
serde = { version = "1.0", features = ["derive"] }
2020
serde_json = "1.0"
21-
serde_yaml = "0.9"
21+
# Pure safe-Rust YAML (replaces serde_yaml + unsafe-libyaml, RUSTSEC-2024-0449).
22+
serde-saphyr = "2.0"
2223
regex = "1.10"
2324
chrono = "0.4"
2425
filetime = "0.2"
@@ -41,7 +42,7 @@ signing = ["ed25519-dalek"]
4142
http = ["ureq"]
4243
sarif = []
4344
# Native GUI viewer (eframe/egui). Opt-in because eframe raises MSRV above the
44-
# 1.85.0 baseline; default builds remain pure-CLI and MSRV-clean.
45+
# 1.89 baseline; default builds remain pure-CLI and MSRV-clean.
4546
gui = ["eframe"]
4647

4748
[dev-dependencies]

‎src/a2ml/mod.rs‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,8 @@ fn load_payload_for_kind(kind: ReportBundleKind, input: &Path) -> Result<ReportB
401401
fn load_json_or_yaml<T: DeserializeOwned>(path: &Path) -> Result<T> {
402402
let raw = fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
403403
match path.extension().and_then(|e| e.to_str()) {
404-
Some("yaml") | Some("yml") => {
405-
serde_yaml::from_str::<T>(&raw).with_context(|| format!("parsing {}", path.display()))
406-
}
404+
Some("yaml") | Some("yml") => crate::yaml::from_report_str::<T>(&raw)
405+
.with_context(|| format!("parsing {}", path.display())),
407406
_ => serde_json::from_str::<T>(&raw).with_context(|| format!("parsing {}", path.display())),
408407
}
409408
}
@@ -421,10 +420,10 @@ fn load_attack_results(path: &Path) -> Result<Vec<AttackResult>> {
421420
if let Ok(wrapper) = serde_json::from_str::<AttackWrapper>(&raw) {
422421
return Ok(wrapper.attack_results);
423422
}
424-
if let Ok(results) = serde_yaml::from_str::<Vec<AttackResult>>(&raw) {
423+
if let Ok(results) = crate::yaml::from_report_str::<Vec<AttackResult>>(&raw) {
425424
return Ok(results);
426425
}
427-
if let Ok(wrapper) = serde_yaml::from_str::<AttackWrapper>(&raw) {
426+
if let Ok(wrapper) = crate::yaml::from_report_str::<AttackWrapper>(&raw) {
428427
return Ok(wrapper.attack_results);
429428
}
430429
Err(anyhow!(

‎src/ambush/timeline.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ pub fn load_timeline_with_default(
7272
let spec: TimelineSpec = if path.extension().and_then(|s| s.to_str()) == Some("yaml")
7373
|| path.extension().and_then(|s| s.to_str()) == Some("yml")
7474
{
75-
serde_yaml::from_str(&content)
75+
crate::yaml::from_str(&content)
7676
.with_context(|| format!("parsing yaml timeline {}", path.display()))?
7777
} else {
7878
serde_json::from_str(&content)

‎src/amuck/mod.rs‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -320,19 +320,19 @@ fn load_spec(path: &Path) -> Result<MutationSpecFile> {
320320
.with_context(|| format!("reading spec {}", path.display()))?;
321321
buf
322322
};
323-
let spec =
324-
match path.extension().and_then(|ext| ext.to_str()) {
325-
Some("json") => serde_json::from_str(&content)
326-
.with_context(|| format!("parsing {}", path.display()))?,
327-
Some("yaml") | Some("yml") => serde_yaml::from_str(&content)
328-
.with_context(|| format!("parsing {}", path.display()))?,
329-
_ => {
330-
return Err(anyhow!(
331-
"spec {} must be json/yaml/yml",
332-
path.to_string_lossy()
333-
));
334-
}
335-
};
323+
let spec = match path.extension().and_then(|ext| ext.to_str()) {
324+
Some("json") => {
325+
serde_json::from_str(&content).with_context(|| format!("parsing {}", path.display()))?
326+
}
327+
Some("yaml") | Some("yml") => crate::yaml::from_str(&content)
328+
.with_context(|| format!("parsing {}", path.display()))?,
329+
_ => {
330+
return Err(anyhow!(
331+
"spec {} must be json/yaml/yml",
332+
path.to_string_lossy()
333+
));
334+
}
335+
};
336336
Ok(spec)
337337
}
338338

‎src/attack/profile.rs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use crate::types::{AttackAxis, ProbeMode};
77
use anyhow::{anyhow, Context, Result};
88
use serde::Deserialize;
99
use serde_json;
10-
use serde_yaml;
1110
use std::collections::HashMap;
1211
use std::fs::File;
1312
use std::io::Read;
@@ -43,7 +42,7 @@ impl AttackProfile {
4342
match path.extension().and_then(|ext| ext.to_str()) {
4443
Some("json") => serde_json::from_str(&content)
4544
.with_context(|| format!("parsing json attack profile {}", path.display())),
46-
Some("yaml") | Some("yml") => serde_yaml::from_str(&content)
45+
Some("yaml") | Some("yml") => crate::yaml::from_str(&content)
4746
.with_context(|| format!("parsing yaml attack profile {}", path.display())),
4847
_ => Err(anyhow!(
4948
"unsupported attack profile extension for {}",

0 commit comments

Comments
 (0)