Skip to content

Commit 543e06b

Browse files
authored
Merge pull request #2 from cgwalters/add-kani-proofs
Add kani proofs
2 parents b30fa1d + 6eb61ab commit 543e06b

7 files changed

Lines changed: 197 additions & 31 deletions

File tree

.devcontainer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"image": "ghcr.io/bootc-dev/devenv-debian:latest"
3+
}

.github/workflows/build.yml

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
1-
name: Rust
1+
name: CI
2+
23
on:
34
push:
45
branches: [main]
56
pull_request:
67
branches: [main]
8+
79
permissions:
810
contents: read
911

10-
# don't waste job slots on superseded code
12+
# Don't waste job slots on superseded code
1113
concurrency:
1214
group: ${{ github.workflow }}-${{ github.ref }}
1315
cancel-in-progress: true
1416

15-
env:
16-
CARGO_TERM_COLOR: always
17-
1817
jobs:
19-
tests-stable:
20-
name: Tests, stable toolchain
18+
ci:
19+
name: CI
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Build and run CI in devcontainer
26+
uses: devcontainers/ci@v0.3
27+
with:
28+
push: never
29+
runCmd: just ci
30+
31+
kani:
32+
name: Kani formal verification
2133
runs-on: ubuntu-latest
2234
steps:
23-
- name: Check out repository
35+
- name: Checkout
2436
uses: actions/checkout@v4
25-
- name: Install toolchain
26-
uses: dtolnay/rust-toolchain@v1
37+
38+
- name: Run Kani proofs in devcontainer
39+
uses: devcontainers/ci@v0.3
2740
with:
28-
toolchain: stable
29-
- name: Cache build artifacts
30-
uses: Swatinem/rust-cache@v2
31-
- name: cargo build
32-
run: cargo build --all-targets
33-
- name: cargo test
34-
run: cargo test --all-targets
41+
push: never
42+
runCmd: just kani

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ authors = ["Colin Walters <walters@verbum.org>"]
77
license = "MIT OR Apache-2.0"
88
repository = "https://github.com/cgwalters/spec-json-rpc-fdpass"
99

10+
[lints.rust]
11+
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] }
12+
1013
[dependencies]
1114
serde = { version = "1.0", features = ["derive"] }
1215
serde_json = "1.0"
@@ -20,3 +23,4 @@ jsonrpsee = { version = "0.24", features = ["server", "client-core", "async-clie
2023
[dev-dependencies]
2124
tempfile = "3.0"
2225
async-trait = "0.1"
26+
kani-verifier = "0.67"

Justfile

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Format, lint, and type-check
2+
check:
3+
cargo fmt --check
4+
cargo clippy --all-targets
5+
cargo check --all-targets
6+
7+
# Auto-format code
8+
fmt:
9+
cargo fmt
10+
11+
# Run unit tests (uses nextest if available)
12+
unit:
13+
@if cargo nextest --version >/dev/null 2>&1; then \
14+
cargo nextest run; \
15+
else \
16+
cargo test; \
17+
fi
18+
19+
# Run all tests
20+
test-all: unit
21+
22+
# Build release binaries
23+
build:
24+
cargo build --release
25+
26+
# Build debug binaries
27+
build-debug:
28+
cargo build
29+
30+
# Generate docs
31+
doc:
32+
cargo doc --no-deps
33+
34+
# Open docs in browser
35+
doc-open:
36+
cargo doc --no-deps --open
37+
38+
# Clean build artifacts
39+
clean:
40+
cargo clean
41+
42+
# Full CI check (format, lint, test)
43+
ci: check unit
44+
45+
# Run Kani formal verification proofs
46+
# NOTE: Kani requires rustup (won't work with distro-packaged Rust)
47+
# Install: cargo install --locked kani-verifier && cargo kani setup
48+
# Alternatively, Kani proofs run automatically in CI via GitHub Actions
49+
kani:
50+
cargo kani
51+
52+
# Run a specific Kani proof by name
53+
kani-proof name:
54+
cargo kani --harness {{name}}
55+
56+
# List available Kani proofs
57+
kani-list:
58+
cargo kani --list

src/message.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub fn get_fd_count(value: &serde_json::Value) -> usize {
2020

2121
/// Helper to skip serializing fds field when it's None or 0
2222
fn skip_if_zero_or_none(fds: &Option<usize>) -> bool {
23-
fds.map_or(true, |n| n == 0)
23+
fds.is_none_or(|n| n == 0)
2424
}
2525

2626
/// A JSON-RPC 2.0 request message.
@@ -256,3 +256,66 @@ pub fn file_descriptor_error() -> JsonRpcError<'static> {
256256
None::<serde_json::Value>,
257257
)
258258
}
259+
260+
#[cfg(kani)]
261+
mod verification {
262+
use super::*;
263+
264+
// =========================================================================
265+
// Proofs for skip_if_zero_or_none
266+
// =========================================================================
267+
268+
/// Verify that skip_if_zero_or_none returns true for None
269+
#[kani::proof]
270+
fn check_skip_none() {
271+
let result = skip_if_zero_or_none(&None);
272+
kani::assert(result, "None should be skipped");
273+
}
274+
275+
/// Verify that skip_if_zero_or_none returns true for Some(0)
276+
#[kani::proof]
277+
fn check_skip_zero() {
278+
let result = skip_if_zero_or_none(&Some(0));
279+
kani::assert(result, "Some(0) should be skipped");
280+
}
281+
282+
/// Verify that skip_if_zero_or_none returns false for any non-zero value
283+
#[kani::proof]
284+
fn check_skip_nonzero() {
285+
let n: usize = kani::any();
286+
kani::assume(n > 0);
287+
let result = skip_if_zero_or_none(&Some(n));
288+
kani::assert(!result, "Some(n > 0) should not be skipped");
289+
}
290+
291+
// =========================================================================
292+
// Proofs for JsonRpcMessage::get_fds
293+
// =========================================================================
294+
295+
/// Verify get_fds returns 0 when fds field is None
296+
#[kani::proof]
297+
fn check_get_fds_none() {
298+
let msg = JsonRpcMessage::Notification(JsonRpcNotification {
299+
jsonrpc: String::new(),
300+
method: String::new(),
301+
params: None,
302+
fds: None,
303+
});
304+
let result = msg.get_fds();
305+
kani::assert(result == 0, "None fds should return 0");
306+
}
307+
308+
/// Verify get_fds returns the value when fds field is Some(n)
309+
#[kani::proof]
310+
fn check_get_fds_some() {
311+
let n: usize = kani::any();
312+
let msg = JsonRpcMessage::Notification(JsonRpcNotification {
313+
jsonrpc: String::new(),
314+
method: String::new(),
315+
params: None,
316+
fds: Some(n),
317+
});
318+
let result = msg.get_fds();
319+
kani::assert(result == n, "get_fds should return the fds value");
320+
}
321+
}

src/transport.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use crate::error::{Error, Result};
22
use crate::message::{get_fd_count, JsonRpcMessage, JsonRpcNotification, MessageWithFds};
33
use rustix::fd::AsFd;
4-
use serde::Serialize;
54
use rustix::net::{
65
RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, SendAncillaryBuffer,
76
SendAncillaryMessage, SendFlags,
87
};
8+
use serde::Serialize;
99
use std::collections::VecDeque;
1010
use std::io::{self, IoSlice, IoSliceMut};
1111
use std::num::NonZeroUsize;
@@ -148,7 +148,9 @@ impl Sender {
148148
let remaining_fds = &fds[fds_sent..];
149149

150150
// Determine how many FDs to send in this batch (up to current_max_fds)
151-
let fds_batch = remaining_fds.get(..current_max_fds).unwrap_or(remaining_fds);
151+
let fds_batch = remaining_fds
152+
.get(..current_max_fds)
153+
.unwrap_or(remaining_fds);
152154

153155
let result = self
154156
.stream

tests/integration_tests.rs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,9 @@ async fn test_multiple_messages_with_fds_sequential() -> Result<()> {
195195

196196
// Create multiple test files with different content
197197
let mut temp_files = Vec::new();
198-
let test_contents = vec!["Content 1", "Content 2", "Content 3"];
198+
let test_contents = ["Content 1", "Content 2", "Content 3"];
199199

200-
for (_i, content) in test_contents.iter().enumerate() {
200+
for content in test_contents.iter() {
201201
let mut temp_file = NamedTempFile::new().unwrap();
202202
write!(temp_file, "{}", content).unwrap();
203203
temp_file.flush().unwrap();
@@ -1932,7 +1932,10 @@ async fn test_fd_batching_one_per_message() -> Result<()> {
19321932
assert_eq!(contents.trim(), expected, "FD {} has wrong content", i);
19331933
}
19341934

1935-
Ok((Some(Value::String("All FDs verified".to_string())), Vec::new()))
1935+
Ok((
1936+
Some(Value::String("All FDs verified".to_string())),
1937+
Vec::new(),
1938+
))
19361939
});
19371940

19381941
if let Ok((stream, _)) = listener.accept().await {
@@ -2105,13 +2108,18 @@ async fn test_fd_batching_interleaved_with_no_fd_messages() -> Result<()> {
21052108
tf.flush().unwrap();
21062109
temp_files.push(tf);
21072110
}
2108-
let fds: Vec<OwnedFd> = temp_files.into_iter().map(|tf| tf.into_file().into()).collect();
2111+
let fds: Vec<OwnedFd> = temp_files
2112+
.into_iter()
2113+
.map(|tf| tf.into_file().into())
2114+
.collect();
21092115
let request = JsonRpcRequest::new(
21102116
"with_fds".to_string(),
21112117
Some(serde_json::json!({ "expected_fds": 3 })),
21122118
Value::Number(1.into()),
21132119
);
2114-
sender.send(MessageWithFds::new(JsonRpcMessage::Request(request), fds)).await?;
2120+
sender
2121+
.send(MessageWithFds::new(JsonRpcMessage::Request(request), fds))
2122+
.await?;
21152123
}
21162124

21172125
// Message 2: No FDs
@@ -2121,7 +2129,12 @@ async fn test_fd_batching_interleaved_with_no_fd_messages() -> Result<()> {
21212129
Some(serde_json::json!({ "check": "first" })),
21222130
Value::Number(2.into()),
21232131
);
2124-
sender.send(MessageWithFds::new(JsonRpcMessage::Request(request), vec![])).await?;
2132+
sender
2133+
.send(MessageWithFds::new(
2134+
JsonRpcMessage::Request(request),
2135+
vec![],
2136+
))
2137+
.await?;
21252138
}
21262139

21272140
// Message 3: 2 FDs
@@ -2133,13 +2146,18 @@ async fn test_fd_batching_interleaved_with_no_fd_messages() -> Result<()> {
21332146
tf.flush().unwrap();
21342147
temp_files.push(tf);
21352148
}
2136-
let fds: Vec<OwnedFd> = temp_files.into_iter().map(|tf| tf.into_file().into()).collect();
2149+
let fds: Vec<OwnedFd> = temp_files
2150+
.into_iter()
2151+
.map(|tf| tf.into_file().into())
2152+
.collect();
21372153
let request = JsonRpcRequest::new(
21382154
"with_fds".to_string(),
21392155
Some(serde_json::json!({ "expected_fds": 2 })),
21402156
Value::Number(3.into()),
21412157
);
2142-
sender.send(MessageWithFds::new(JsonRpcMessage::Request(request), fds)).await?;
2158+
sender
2159+
.send(MessageWithFds::new(JsonRpcMessage::Request(request), fds))
2160+
.await?;
21432161
}
21442162

21452163
// Message 4: No FDs
@@ -2149,7 +2167,12 @@ async fn test_fd_batching_interleaved_with_no_fd_messages() -> Result<()> {
21492167
Some(serde_json::json!({ "check": "second" })),
21502168
Value::Number(4.into()),
21512169
);
2152-
sender.send(MessageWithFds::new(JsonRpcMessage::Request(request), vec![])).await?;
2170+
sender
2171+
.send(MessageWithFds::new(
2172+
JsonRpcMessage::Request(request),
2173+
vec![],
2174+
))
2175+
.await?;
21532176
}
21542177

21552178
// Message 5: 4 FDs
@@ -2161,13 +2184,18 @@ async fn test_fd_batching_interleaved_with_no_fd_messages() -> Result<()> {
21612184
tf.flush().unwrap();
21622185
temp_files.push(tf);
21632186
}
2164-
let fds: Vec<OwnedFd> = temp_files.into_iter().map(|tf| tf.into_file().into()).collect();
2187+
let fds: Vec<OwnedFd> = temp_files
2188+
.into_iter()
2189+
.map(|tf| tf.into_file().into())
2190+
.collect();
21652191
let request = JsonRpcRequest::new(
21662192
"with_fds".to_string(),
21672193
Some(serde_json::json!({ "expected_fds": 4 })),
21682194
Value::Number(5.into()),
21692195
);
2170-
sender.send(MessageWithFds::new(JsonRpcMessage::Request(request), fds)).await?;
2196+
sender
2197+
.send(MessageWithFds::new(JsonRpcMessage::Request(request), fds))
2198+
.await?;
21712199
}
21722200

21732201
server_handle.abort();

0 commit comments

Comments
 (0)