tools: config-lint — MCU-load-vs-reserved-memory gate (0x40000 brick class)
The c8a3 brick was a memory-map fault no behavioural sim can catch: a boot-loaded coprocessor firmware dropped at 0x40000, which is a reserved-memory carve-out on Thunder-Boot boards but plain kernel RAM on ours. config-lint is the static gate for it — parse the rkbin loader .ini for every LOADERn=Hpmcu LOAD_ADDR, parse the target devicetree for reserved-memory ranges, fail if any MCU load lands outside a reservation. Tests encode the brick as a regression against the REAL Thunder-Boot .ini (Hpmcu@0x40000): fails with no rtos@40000 node, passes once reserved; our board's non-TB loader (no boot-loaded MCU) always passes. 6/6 green; CLI verified against the on-disk rkbin .ini files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017wB8KB3MMQztRDXCMCkPrf
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
840085bd18
commit
4a1ec1d84e
@@ -83,7 +83,7 @@ patches/ the vendor-SDK delta (mirrors flare-edge/sdk-patches until it moves
|
|||||||
build/ the hermetic image-build wrapper (kernel → rootfs → image), incremental.
|
build/ the hermetic image-build wrapper (kernel → rootfs → image), incremental.
|
||||||
ci/ CI: patches-still-apply, host tests, coverage, benchmarks.
|
ci/ CI: patches-still-apply, host tests, coverage, benchmarks.
|
||||||
docs/ architecture + ADRs (decisions/).
|
docs/ architecture + ADRs (decisions/).
|
||||||
tools/ dev tooling.
|
tools/ dev tooling. config-lint: static target-config gates (MCU-load-vs-reserved-memory — the 0x40000 brick class).
|
||||||
```
|
```
|
||||||
|
|
||||||
## Principles
|
## Principles
|
||||||
|
|||||||
@@ -106,6 +106,16 @@ node." warden-sdk owns these config-lint checks (idblock loader `.ini` vs DT
|
|||||||
reservations, partition table vs image sizes, vermagic vs kernel) as CI gates, so a
|
reservations, partition table vs image sizes, vermagic vs kernel) as CI gates, so a
|
||||||
mistake is caught before a flash rather than on the bench.
|
mistake is caught before a flash rather than on the bench.
|
||||||
|
|
||||||
|
**Built:** `tools/config-lint` implements the first and most important of these —
|
||||||
|
the MCU-load-vs-`reserved-memory` gate. It parses the rkbin loader `.ini` for
|
||||||
|
every `LOADERn=Hpmcu` firmware and its `[LOADERn_PARAM] LOAD_ADDR`, parses the
|
||||||
|
target devicetree (`.dts`, or `dtc -I dtb` output in CI) for `reserved-memory`
|
||||||
|
ranges, and fails if any MCU load lands outside a reservation. Its test suite
|
||||||
|
encodes the c8a3 brick itself: the real Thunder-Boot `.ini` (Hpmcu @ `0x40000`)
|
||||||
|
fails against a DT with no `rtos@40000` node and passes once the reservation is
|
||||||
|
added. **Next** target-config checks: partition-table-vs-image-size and
|
||||||
|
vermagic-vs-kernel.
|
||||||
|
|
||||||
## 6. Kernel forward-port (separate, bounded phase)
|
## 6. Kernel forward-port (separate, bounded phase)
|
||||||
|
|
||||||
Move to **plan44's OpenWrt RV1106 fork — Linux 6.6** (152 RV1106 patches + our
|
Move to **plan44's OpenWrt RV1106 fork — Linux 6.6** (152 RV1106 patches + our
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "warden-config-lint"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Static target-config checks for WardenOS: catch memory-map faults (the 0x40000 MCU-load brick class) and other flash-time config mistakes before a flash, not on the bench."
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "config-lint"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "warden_config_lint"
|
||||||
|
path = "src/lib.rs"
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# config-lint — static target-config gates
|
||||||
|
|
||||||
|
Catches flash-time config faults the behavioural sim cannot: mistakes in the
|
||||||
|
*memory map*, not the logic. The first check is the one that would have caught the
|
||||||
|
**c8a3 brick** — a boot-loaded coprocessor firmware dropped at `0x40000`, which is
|
||||||
|
a `reserved-memory` carve-out on Thunder-Boot boards but plain kernel RAM on ours,
|
||||||
|
so the MCU and the kernel fought over the same DRAM and the board hung before eth0.
|
||||||
|
|
||||||
|
## The check
|
||||||
|
|
||||||
|
Every address the idblock loader drops MCU firmware to must sit inside a
|
||||||
|
`reserved-memory` node in the target devicetree.
|
||||||
|
|
||||||
|
- **MCU loads** come from the rkbin loader `.ini`: each `LOADERn=Hpmcu` (any
|
||||||
|
hpmcu/mcu/amp entry) in `[LOADER_OPTION]`, with its `LOAD_ADDR` from
|
||||||
|
`[LOADERn_PARAM]`.
|
||||||
|
- **Reserved ranges** come from the devicetree: every `reg = <addr size>` inside a
|
||||||
|
`reserved-memory { … }` node.
|
||||||
|
|
||||||
|
A load outside all reservations is a failure (non-zero exit).
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
cargo run -p warden-config-lint -- --ini <loader.ini> --dt <devicetree.dts>
|
||||||
|
|
||||||
|
In CI, feed the *flattened* devicetree so includes and overlays are resolved:
|
||||||
|
|
||||||
|
dtc -I dtb -O dts build/.../rv1106g-warden.dtb > /tmp/warden.dts
|
||||||
|
config-lint --ini .../RKBOOT/RV1106MINIALL*.ini --dt /tmp/warden.dts
|
||||||
|
|
||||||
|
Exit `0` = every MCU load is reserved (or there are none); `1` = a collision was
|
||||||
|
found; `2` = usage/IO error.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
cargo test -p warden-config-lint
|
||||||
|
|
||||||
|
The suite encodes the brick as a regression: the real Thunder-Boot `.ini`
|
||||||
|
(Hpmcu @ `0x40000`) *fails* against a DT with no `rtos@40000` node and *passes*
|
||||||
|
once the reservation is added — and our board's non-TB loader (no boot-loaded MCU)
|
||||||
|
always passes. See `../../docs/architecture.md` §5 and, for the hardware hazard,
|
||||||
|
the `boot-loaded-mcu-0x40000-hazard` note.
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
//! config-lint — static target-config checks the behavioural sim cannot cover.
|
||||||
|
//!
|
||||||
|
//! The c8a3 brick was a memory-map fault, not a logic bug: the boot-loaded MCU's
|
||||||
|
//! load address (`0x40000`) is a `reserved-memory` carve-out on Thunder-Boot
|
||||||
|
//! boards but plain kernel RAM on ours, so the coprocessor firmware and the kernel
|
||||||
|
//! fought over the same DRAM and the board hung before eth0. No sim catches that —
|
||||||
|
//! it needs a static check against the target devicetree: **every address the
|
||||||
|
//! idblock loader drops MCU firmware to must sit inside a `reserved-memory` node.**
|
||||||
|
//!
|
||||||
|
//! This module parses the two authoritative artifacts (the rkbin loader `.ini`
|
||||||
|
//! and the built/target devicetree) and reports any MCU load that would collide.
|
||||||
|
//! Wire the CLI into CI so the mistake is caught before a flash, not on a bench.
|
||||||
|
|
||||||
|
/// One MCU/coprocessor firmware load declared by the loader `.ini`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct McuLoad {
|
||||||
|
pub loader: String, // e.g. "LOADER2"
|
||||||
|
pub name: String, // e.g. "Hpmcu"
|
||||||
|
pub load_addr: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `reserved-memory` range `[start, start+size)`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Range {
|
||||||
|
pub start: u64,
|
||||||
|
pub size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Range {
|
||||||
|
pub fn contains(&self, addr: u64) -> bool {
|
||||||
|
addr >= self.start && addr < self.start.saturating_add(self.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Finding {
|
||||||
|
pub load: McuLoad,
|
||||||
|
pub msg: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_addr(s: &str) -> Option<u64> {
|
||||||
|
let s = s.trim();
|
||||||
|
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
||||||
|
u64::from_str_radix(hex, 16).ok()
|
||||||
|
} else {
|
||||||
|
s.parse::<u64>().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does a loader entry name a coprocessor/MCU firmware (whose LOAD_ADDR matters)?
|
||||||
|
fn is_mcu_loader(name: &str) -> bool {
|
||||||
|
let n = name.to_ascii_lowercase();
|
||||||
|
n.contains("hpmcu") || n.contains("mcu") || n.contains("amp")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract MCU firmware loads from an rkbin loader `.ini`: for each `LOADERn=<name>`
|
||||||
|
/// in `[LOADER_OPTION]` that names an MCU loader, read `LOAD_ADDR` from the matching
|
||||||
|
/// `[LOADERn_PARAM]` section.
|
||||||
|
pub fn parse_ini_mcu_loads(ini: &str) -> Vec<McuLoad> {
|
||||||
|
let mut section = String::new();
|
||||||
|
// loader index (e.g. "LOADER2") -> firmware name (e.g. "Hpmcu")
|
||||||
|
let mut loaders: Vec<(String, String)> = Vec::new();
|
||||||
|
// section name -> LOAD_ADDR
|
||||||
|
let mut load_addrs: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
for raw in ini.lines() {
|
||||||
|
let line = raw.split(['#', ';']).next().unwrap_or("").trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(sec) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
|
||||||
|
section = sec.trim().to_string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some((k, v)) = line.split_once('=') else { continue };
|
||||||
|
let (k, v) = (k.trim(), v.trim());
|
||||||
|
if section == "LOADER_OPTION" && k.to_ascii_uppercase().starts_with("LOADER") {
|
||||||
|
loaders.push((k.to_ascii_uppercase(), v.to_string()));
|
||||||
|
} else if k.eq_ignore_ascii_case("LOAD_ADDR") {
|
||||||
|
if let Some(a) = parse_addr(v) {
|
||||||
|
load_addrs.insert(section.to_ascii_uppercase(), a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (loader, name) in loaders {
|
||||||
|
if !is_mcu_loader(&name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// LOADER2 -> [LOADER2_PARAM]
|
||||||
|
if let Some(&addr) = load_addrs.get(&format!("{loader}_PARAM")) {
|
||||||
|
out.push(McuLoad { loader, name, load_addr: addr });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract `reserved-memory` child ranges from devicetree source (a `.dts`/`.dtsi`,
|
||||||
|
/// or the flattened output of `dtc -I dtb -O dts`). Brace-tracked so only `reg`s
|
||||||
|
/// inside a `reserved-memory { ... }` node are taken. Handles `#size-cells = <1>`
|
||||||
|
/// (RV1106): each `reg = <addr size>` is one range.
|
||||||
|
pub fn parse_reserved_ranges(dt: &str) -> Vec<Range> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let bytes = dt.as_bytes();
|
||||||
|
let mut i = 0;
|
||||||
|
while let Some(pos) = dt[i..].find("reserved-memory") {
|
||||||
|
let mut j = i + pos;
|
||||||
|
// find the opening brace of this node
|
||||||
|
while j < bytes.len() && bytes[j] != b'{' {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if j >= bytes.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut depth = 0i32;
|
||||||
|
let start = j;
|
||||||
|
while j < bytes.len() {
|
||||||
|
match bytes[j] {
|
||||||
|
b'{' => depth += 1,
|
||||||
|
b'}' => {
|
||||||
|
depth -= 1;
|
||||||
|
if depth == 0 {
|
||||||
|
j += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
// scan reg = < a b > inside [start, j)
|
||||||
|
let block = &dt[start..j.min(dt.len())];
|
||||||
|
for reg in block.split("reg").skip(1) {
|
||||||
|
let Some(lt) = reg.find('<') else { continue };
|
||||||
|
let Some(gt) = reg[lt..].find('>') else { continue };
|
||||||
|
let nums: Vec<&str> = reg[lt + 1..lt + gt].split_whitespace().collect();
|
||||||
|
if nums.len() >= 2 {
|
||||||
|
if let (Some(a), Some(s)) = (parse_addr(nums[0]), parse_addr(nums[1])) {
|
||||||
|
out.push(Range { start: a, size: s });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The gate: every MCU load must land inside a reserved-memory range.
|
||||||
|
pub fn check(loads: &[McuLoad], reserved: &[Range]) -> Vec<Finding> {
|
||||||
|
loads
|
||||||
|
.iter()
|
||||||
|
.filter(|l| !reserved.iter().any(|r| r.contains(l.load_addr)))
|
||||||
|
.map(|l| Finding {
|
||||||
|
load: l.clone(),
|
||||||
|
msg: format!(
|
||||||
|
"{}={} loads MCU firmware at {:#x} but no reserved-memory node covers it \
|
||||||
|
— kernel/MCU DRAM collision (the 0x40000 brick class)",
|
||||||
|
l.loader, l.name, l.load_addr
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// The exact idblock .ini that bricked c8a3: Hpmcu at 0x40000.
|
||||||
|
const TB_INI: &str = r#"
|
||||||
|
[LOADER_OPTION]
|
||||||
|
NUM=3
|
||||||
|
LOADER1=FlashData
|
||||||
|
LOADER2=Hpmcu
|
||||||
|
LOADER3=FlashBoot
|
||||||
|
FlashData=bin/rv11/rv1106_ddr.bin
|
||||||
|
Hpmcu=bin/rv11/rv1106_hpmcu_tb.bin
|
||||||
|
FlashBoot=bin/rv11/rv1106_spl.bin
|
||||||
|
[LOADER2_PARAM]
|
||||||
|
LOAD_ADDR=0x40000
|
||||||
|
FLAG=0x10007
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// Our board's actual loader: no Hpmcu at all.
|
||||||
|
const NONTB_INI: &str = "[LOADER_OPTION]\nNUM=2\nLOADER1=FlashData\nLOADER2=FlashBoot\n";
|
||||||
|
|
||||||
|
const DT_WITH_RTOS: &str = r#"
|
||||||
|
reserved-memory {
|
||||||
|
#address-cells = <1>;
|
||||||
|
#size-cells = <1>;
|
||||||
|
ranges;
|
||||||
|
rtos@40000 { reg = <0x40000 0x3c000>; no-map; };
|
||||||
|
ramoops@0f000000 { compatible = "ramoops"; reg = <0x0f000000 0x00100000>; };
|
||||||
|
};
|
||||||
|
"#;
|
||||||
|
const DT_NO_RTOS: &str = r#"
|
||||||
|
reserved-memory {
|
||||||
|
ranges;
|
||||||
|
ramoops@0f000000 { compatible = "ramoops"; reg = <0x0f000000 0x00100000>; };
|
||||||
|
};
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_hpmcu_load_addr() {
|
||||||
|
let loads = parse_ini_mcu_loads(TB_INI);
|
||||||
|
assert_eq!(loads.len(), 1);
|
||||||
|
assert_eq!(loads[0].name, "Hpmcu");
|
||||||
|
assert_eq!(loads[0].load_addr, 0x40000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nontb_ini_has_no_mcu_loads() {
|
||||||
|
assert!(parse_ini_mcu_loads(NONTB_INI).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_reserved_ranges() {
|
||||||
|
let r = parse_reserved_ranges(DT_WITH_RTOS);
|
||||||
|
assert_eq!(r.len(), 2);
|
||||||
|
assert!(r.iter().any(|x| x.start == 0x40000 && x.size == 0x3c000));
|
||||||
|
assert!(r.iter().any(|x| x.start == 0x0f00_0000));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The c8a3 brick, prevented: Hpmcu@0x40000 with NO reserving node -> a finding.
|
||||||
|
#[test]
|
||||||
|
fn flags_the_c8a3_brick() {
|
||||||
|
let loads = parse_ini_mcu_loads(TB_INI);
|
||||||
|
let reserved = parse_reserved_ranges(DT_NO_RTOS);
|
||||||
|
let f = check(&loads, &reserved);
|
||||||
|
assert_eq!(f.len(), 1);
|
||||||
|
assert_eq!(f[0].load.load_addr, 0x40000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fix: add the rtos@40000 reservation -> the same config passes.
|
||||||
|
#[test]
|
||||||
|
fn passes_when_rtos_reserved() {
|
||||||
|
let loads = parse_ini_mcu_loads(TB_INI);
|
||||||
|
let reserved = parse_reserved_ranges(DT_WITH_RTOS);
|
||||||
|
assert!(check(&loads, &reserved).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Our board (no boot-loaded MCU) always passes, reserved or not.
|
||||||
|
#[test]
|
||||||
|
fn nontb_board_always_passes() {
|
||||||
|
let loads = parse_ini_mcu_loads(NONTB_INI);
|
||||||
|
assert!(check(&loads, &parse_reserved_ranges(DT_NO_RTOS)).is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//! config-lint CLI — the MCU-load-vs-reserved-memory gate as a CI check.
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//! config-lint --ini <loader.ini> --dt <devicetree.dts>
|
||||||
|
//!
|
||||||
|
//! The `--dt` argument accepts a `.dts`/`.dtsi` or the flattened output of
|
||||||
|
//! `dtc -I dtb -O dts built.dtb` (preferred in CI — it resolves includes and
|
||||||
|
//! overlays, so it reflects what the board will actually boot). Exits non-zero
|
||||||
|
//! and prints each offending MCU load if any lands outside a `reserved-memory`
|
||||||
|
//! node — the 0x40000 brick class.
|
||||||
|
|
||||||
|
use std::process::ExitCode;
|
||||||
|
use warden_config_lint::{check, parse_ini_mcu_loads, parse_reserved_ranges};
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
let mut ini_path = None;
|
||||||
|
let mut dt_path = None;
|
||||||
|
let mut args = std::env::args().skip(1);
|
||||||
|
while let Some(a) = args.next() {
|
||||||
|
match a.as_str() {
|
||||||
|
"--ini" => ini_path = args.next(),
|
||||||
|
"--dt" => dt_path = args.next(),
|
||||||
|
"-h" | "--help" => {
|
||||||
|
eprintln!("usage: config-lint --ini <loader.ini> --dt <devicetree.dts>");
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
eprintln!("config-lint: unknown argument {other:?}");
|
||||||
|
return ExitCode::from(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (Some(ini_path), Some(dt_path)) = (ini_path, dt_path) else {
|
||||||
|
eprintln!("usage: config-lint --ini <loader.ini> --dt <devicetree.dts>");
|
||||||
|
return ExitCode::from(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
let ini = match std::fs::read_to_string(&ini_path) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("config-lint: cannot read {ini_path}: {e}");
|
||||||
|
return ExitCode::from(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let dt = match std::fs::read_to_string(&dt_path) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("config-lint: cannot read {dt_path}: {e}");
|
||||||
|
return ExitCode::from(2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let loads = parse_ini_mcu_loads(&ini);
|
||||||
|
let reserved = parse_reserved_ranges(&dt);
|
||||||
|
let findings = check(&loads, &reserved);
|
||||||
|
|
||||||
|
if loads.is_empty() {
|
||||||
|
println!("config-lint: no boot-loaded MCU firmware in {ini_path} — nothing to reserve.");
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"config-lint: {} MCU load(s) in {ini_path}, {} reserved-memory range(s) in {dt_path}.",
|
||||||
|
loads.len(),
|
||||||
|
reserved.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if findings.is_empty() {
|
||||||
|
println!("config-lint: OK — every MCU load is inside a reserved-memory node.");
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
} else {
|
||||||
|
for f in &findings {
|
||||||
|
eprintln!("config-lint: FAIL — {}", f.msg);
|
||||||
|
}
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user