flowproof 0.4.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flowproof-0.4.0 → flowproof-0.4.1}/Cargo.lock +7 -7
- {flowproof-0.4.0 → flowproof-0.4.1}/Cargo.toml +1 -1
- {flowproof-0.4.0 → flowproof-0.4.1}/PKG-INFO +1 -1
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/agent_runner.rs +2 -1
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/egress.rs +10 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/egress_linux.rs +178 -88
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/src/lib.rs +1 -1
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/src/recorder.rs +12 -2
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/src/spec.rs +559 -2
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/Cargo.toml +2 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/src/agent_flow.rs +168 -10
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/src/lib.rs +265 -5
- flowproof-0.4.1/crates/flowproof-cli/tests/agent_flow_e2e.rs +493 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/egress_e2e.rs +53 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/llm_author_e2e.rs +1 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/notepad_author_e2e.rs +1 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/web_e2e.rs +19 -4
- flowproof-0.4.1/crates/flowproof-trace/schema/trace-v1.schema.json +902 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/src/format.rs +101 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/flowproof/__init__.py +2 -2
- {flowproof-0.4.0 → flowproof-0.4.1}/pyproject.toml +1 -1
- flowproof-0.4.0/crates/flowproof-cli/tests/agent_flow_e2e.rs +0 -228
- flowproof-0.4.0/crates/flowproof-trace/schema/trace-v1.schema.json +0 -472
- {flowproof-0.4.0 → flowproof-0.4.1}/README.md +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/Cargo.toml +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/agent_proxy.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/lib.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/mcp_core.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/mcp_http.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/mcp_stdio.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/sap_com.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/vision.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-adapters/src/web.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/Cargo.toml +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/src/agent_steps.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/src/author.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/src/clarify.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/src/heal.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/src/llm.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-agent/src/rules.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/src/capture.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/src/main.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/api_pipeline.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/calc_e2e.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/clock_e2e.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/examples_resolve.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/grid_cell_e2e.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/mcp_stdio_e2e.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/notepad_e2e.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/sap_e2e.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/sap_pipeline.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/sap_sim_e2e.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/scoped_container_e2e.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/skip_unless_env.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/suite_env_from.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/suite_flow_isolation.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/suite_missing_trace.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/support/sap_simulator.py +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-cli/tests/vision_pipeline.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/Cargo.toml +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/app.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/backend.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/gdi.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/lib.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/mock.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/oob.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/recording.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/redact.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/visual.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-driver/src/window.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-python/Cargo.toml +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-python/src/lib.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-replay/Cargo.toml +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-replay/src/lib.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-replay/src/report.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-replay/tests/replay_calc.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/Cargo.toml +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/src/cassette.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/src/cassette_diff.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/src/egress.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/src/lib.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/src/secret.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/src/substitution.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/src/toolcalls.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/tests/fixtures/sample.trace.jsonl +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/crates/flowproof-trace/tests/schema_conformance.rs +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/flowproof/cli.py +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/flowproof/flow.py +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/flowproof/mcp_server.py +0 -0
- {flowproof-0.4.0 → flowproof-0.4.1}/flowproof/py.typed +0 -0
|
@@ -760,7 +760,7 @@ dependencies = [
|
|
|
760
760
|
|
|
761
761
|
[[package]]
|
|
762
762
|
name = "flowproof-adapters"
|
|
763
|
-
version = "0.4.
|
|
763
|
+
version = "0.4.1"
|
|
764
764
|
dependencies = [
|
|
765
765
|
"anyhow",
|
|
766
766
|
"flowproof-driver",
|
|
@@ -779,7 +779,7 @@ dependencies = [
|
|
|
779
779
|
|
|
780
780
|
[[package]]
|
|
781
781
|
name = "flowproof-agent"
|
|
782
|
-
version = "0.4.
|
|
782
|
+
version = "0.4.1"
|
|
783
783
|
dependencies = [
|
|
784
784
|
"chrono",
|
|
785
785
|
"flowproof-driver",
|
|
@@ -795,7 +795,7 @@ dependencies = [
|
|
|
795
795
|
|
|
796
796
|
[[package]]
|
|
797
797
|
name = "flowproof-cli"
|
|
798
|
-
version = "0.4.
|
|
798
|
+
version = "0.4.1"
|
|
799
799
|
dependencies = [
|
|
800
800
|
"ab_glyph",
|
|
801
801
|
"chrono",
|
|
@@ -816,7 +816,7 @@ dependencies = [
|
|
|
816
816
|
|
|
817
817
|
[[package]]
|
|
818
818
|
name = "flowproof-driver"
|
|
819
|
-
version = "0.4.
|
|
819
|
+
version = "0.4.1"
|
|
820
820
|
dependencies = [
|
|
821
821
|
"image",
|
|
822
822
|
"postgres",
|
|
@@ -830,7 +830,7 @@ dependencies = [
|
|
|
830
830
|
|
|
831
831
|
[[package]]
|
|
832
832
|
name = "flowproof-python"
|
|
833
|
-
version = "0.4.
|
|
833
|
+
version = "0.4.1"
|
|
834
834
|
dependencies = [
|
|
835
835
|
"flowproof-agent",
|
|
836
836
|
"flowproof-cli",
|
|
@@ -842,7 +842,7 @@ dependencies = [
|
|
|
842
842
|
|
|
843
843
|
[[package]]
|
|
844
844
|
name = "flowproof-replay"
|
|
845
|
-
version = "0.4.
|
|
845
|
+
version = "0.4.1"
|
|
846
846
|
dependencies = [
|
|
847
847
|
"chrono",
|
|
848
848
|
"flowproof-agent",
|
|
@@ -856,7 +856,7 @@ dependencies = [
|
|
|
856
856
|
|
|
857
857
|
[[package]]
|
|
858
858
|
name = "flowproof-trace"
|
|
859
|
-
version = "0.4.
|
|
859
|
+
version = "0.4.1"
|
|
860
860
|
dependencies = [
|
|
861
861
|
"jsonschema",
|
|
862
862
|
"regex",
|
|
@@ -3,7 +3,7 @@ resolver = "2"
|
|
|
3
3
|
members = ["crates/flowproof-driver", "crates/flowproof-trace", "crates/flowproof-replay", "crates/flowproof-agent", "crates/flowproof-adapters", "crates/flowproof-cli", "crates/flowproof-python"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "0.4.
|
|
6
|
+
version = "0.4.1"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
repository = "https://github.com/automators-com/flowproof"
|
|
@@ -391,7 +391,8 @@ pub fn run_against_contained(
|
|
|
391
391
|
command: command.trim().to_string(),
|
|
392
392
|
source,
|
|
393
393
|
})?;
|
|
394
|
-
// Start the supervisor:
|
|
394
|
+
// Start the supervisor: collect the notify fd the handoff thread acquired
|
|
395
|
+
// out of the child while `spawn` ran, and service it for the run.
|
|
395
396
|
let supervisor = prep
|
|
396
397
|
.into_supervisor(spawned)
|
|
397
398
|
.map_err(|source| RunError::Spawn {
|
|
@@ -46,6 +46,16 @@ impl Containment {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/// The tier for a flow that does not ENGAGE egress: it declares no
|
|
50
|
+
/// `allow_egress` and asserts no egress, so no seccomp filter is installed
|
|
51
|
+
/// and there is nothing to contain. Containment is opt-in; an unengaged
|
|
52
|
+
/// flow claims no tier.
|
|
53
|
+
pub fn not_engaged() -> Self {
|
|
54
|
+
Containment::NotContained(
|
|
55
|
+
"flow does not engage egress (no allow_egress or assert_no_egress)".to_string(),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
/// The tier for a `url:` flow: a service flowproof did not start cannot
|
|
50
60
|
/// be contained.
|
|
51
61
|
pub fn url_flow() -> Self {
|
|
@@ -15,9 +15,22 @@
|
|
|
15
15
|
//! closing the two paths that reach the network under the notifier.
|
|
16
16
|
//!
|
|
17
17
|
//! The filter is installed with `SECCOMP_FILTER_FLAG_NEW_LISTENER`, whose
|
|
18
|
-
//! return value is a notify fd. That fd lives in the child, so it is
|
|
19
|
-
//!
|
|
20
|
-
//!
|
|
18
|
+
//! return value is a notify fd. That fd lives in the child, so it is handed to
|
|
19
|
+
//! the parent - but NOT with `SCM_RIGHTS`/`sendmsg`, since `sendmsg` is one of
|
|
20
|
+
//! the syscalls the filter traps, so using it here would suspend the child on
|
|
21
|
+
//! the very notifier the parent has not started servicing yet (a deadlock).
|
|
22
|
+
//! Instead the child WRITES its `(pid, notify-fd-number)` over a pre-created
|
|
23
|
+
//! `socketpair` (`getpid`/`write` are not trapped), a PARENT HANDOFF THREAD
|
|
24
|
+
//! acquires the actual fd with `pidfd_open`+`pidfd_getfd` (the same infra the
|
|
25
|
+
//! supervisor uses to act on the child's sockets) and WRITES back a one-byte
|
|
26
|
+
//! ack so the child may close its copy and proceed to exec. A supervisor
|
|
27
|
+
//! thread then services the notify fd for the whole run.
|
|
28
|
+
//!
|
|
29
|
+
//! The handoff runs on its own thread, started BEFORE `Command::spawn`,
|
|
30
|
+
//! because `spawn` blocks until the child execs and the child cannot exec
|
|
31
|
+
//! until it gets the ack: the read+ack must happen concurrently with `spawn`,
|
|
32
|
+
//! not after it. The child sends its own pid because `child.id()` is not yet
|
|
33
|
+
//! available while `spawn` is still blocked.
|
|
21
34
|
//!
|
|
22
35
|
//! # The TOCTOU-safe pattern
|
|
23
36
|
//!
|
|
@@ -233,24 +246,32 @@ pub fn probe_containment() -> Containment {
|
|
|
233
246
|
Containment::Enforced
|
|
234
247
|
}
|
|
235
248
|
|
|
236
|
-
/// A prepared filter
|
|
237
|
-
/// `Command`'s `pre_exec`. Created BEFORE spawn; turned into a live
|
|
238
|
-
///
|
|
249
|
+
/// A prepared filter plus the in-flight parent-side handoff, wired into a
|
|
250
|
+
/// `Command`'s `pre_exec`. Created BEFORE spawn; turned into a live supervisor
|
|
251
|
+
/// AFTER spawn.
|
|
252
|
+
///
|
|
253
|
+
/// The handoff runs on its OWN thread, started here in [`install`] rather than
|
|
254
|
+
/// after spawn, and this is load-bearing: `Command::spawn` BLOCKS until the
|
|
255
|
+
/// child execs, and the child cannot exec until the parent acks its notify fd.
|
|
256
|
+
/// If the parent tried to do the read+ack after `spawn` returned, it would
|
|
257
|
+
/// deadlock - the parent waiting in `spawn` for an exec that waits on an ack
|
|
258
|
+
/// the parent has not sent. So the ack must come from a concurrent thread.
|
|
239
259
|
pub struct EgressPrep {
|
|
240
|
-
|
|
260
|
+
handoff: std::thread::JoinHandle<io::Result<OwnedFd>>,
|
|
241
261
|
allow: AllowSet,
|
|
242
262
|
}
|
|
243
263
|
|
|
244
264
|
/// Install the egress filter into `cmd` via `pre_exec` and return the
|
|
245
265
|
/// parent-side handle. The filter and the child socket are moved into the
|
|
246
|
-
/// closure;
|
|
247
|
-
/// fd
|
|
266
|
+
/// `pre_exec` closure; a handoff thread owns the parent's socket end and
|
|
267
|
+
/// acquires the notify fd out of the child while `spawn` runs.
|
|
248
268
|
pub fn install(cmd: &mut Command, allow: &AllowSet) -> io::Result<EgressPrep> {
|
|
249
269
|
let filter = build_filter();
|
|
250
270
|
|
|
251
|
-
// A stream socketpair carries the notify
|
|
252
|
-
//
|
|
253
|
-
// (pre_exec runs first), and the execed program must
|
|
271
|
+
// A stream socketpair carries the child's (pid, notify-fd-number) to the
|
|
272
|
+
// parent and the parent's ack back. Both ends are close-on-exec: the child
|
|
273
|
+
// sends before exec (pre_exec runs first), and the execed program must
|
|
274
|
+
// inherit neither.
|
|
254
275
|
let mut fds = [0 as RawFd; 2];
|
|
255
276
|
let rc = unsafe {
|
|
256
277
|
libc::socketpair(
|
|
@@ -273,18 +294,28 @@ pub fn install(cmd: &mut Command, allow: &AllowSet) -> io::Result<EgressPrep> {
|
|
|
273
294
|
unsafe {
|
|
274
295
|
cmd.pre_exec(move || child_install(&filter, child_sock));
|
|
275
296
|
}
|
|
297
|
+
|
|
298
|
+
// The handoff thread: it blocks reading the child's message, so it must run
|
|
299
|
+
// CONCURRENTLY with the caller's `cmd.spawn()` (see `EgressPrep`). It owns
|
|
300
|
+
// `parent_sock` and yields the acquired notify fd.
|
|
301
|
+
let handoff = std::thread::spawn(move || recv_notify_fd(parent_sock));
|
|
302
|
+
|
|
276
303
|
Ok(EgressPrep {
|
|
277
|
-
|
|
304
|
+
handoff,
|
|
278
305
|
allow: allow.clone(),
|
|
279
306
|
})
|
|
280
307
|
}
|
|
281
308
|
|
|
282
309
|
impl EgressPrep {
|
|
283
|
-
/// After spawn:
|
|
284
|
-
/// supervisor thread that services it for the run.
|
|
285
|
-
/// the child was launched, the zero for every
|
|
310
|
+
/// After spawn: collect the notify fd the handoff thread acquired from the
|
|
311
|
+
/// child, and start the supervisor thread that services it for the run.
|
|
312
|
+
/// `spawn` is the instant the child was launched, the zero for every
|
|
313
|
+
/// event's monotonic `at_ms`.
|
|
286
314
|
pub fn into_supervisor(self, spawn: Instant) -> io::Result<Supervisor> {
|
|
287
|
-
let notify_fd =
|
|
315
|
+
let notify_fd = self
|
|
316
|
+
.handoff
|
|
317
|
+
.join()
|
|
318
|
+
.map_err(|_| io::Error::other("egress handoff thread panicked"))??;
|
|
288
319
|
Ok(Supervisor::start(notify_fd, self.allow, spawn))
|
|
289
320
|
}
|
|
290
321
|
}
|
|
@@ -302,7 +333,10 @@ fn child_install(filter: &[libc::sock_filter], child_sock: RawFd) -> io::Result<
|
|
|
302
333
|
filter: filter.as_ptr() as *mut libc::sock_filter,
|
|
303
334
|
};
|
|
304
335
|
// 2. Install the filter with NEW_LISTENER; the return value is the notify
|
|
305
|
-
// fd, which the parent will service.
|
|
336
|
+
// fd, which the parent will service. The filter is active IMMEDIATELY,
|
|
337
|
+
// so from here on the trapped syscalls (connect/send*/listen) would
|
|
338
|
+
// suspend us on the notifier - hence the handoff below uses only
|
|
339
|
+
// `write`/`read`, which the filter does NOT trap.
|
|
306
340
|
let notify_fd = unsafe {
|
|
307
341
|
libc::syscall(
|
|
308
342
|
libc::SYS_seccomp,
|
|
@@ -314,91 +348,147 @@ fn child_install(filter: &[libc::sock_filter], child_sock: RawFd) -> io::Result<
|
|
|
314
348
|
if notify_fd < 0 {
|
|
315
349
|
return Err(io::Error::last_os_error());
|
|
316
350
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
//
|
|
351
|
+
let notify_fd = notify_fd as RawFd;
|
|
352
|
+
// 3. Hand the notify fd to the parent WITHOUT a trapped syscall: write our
|
|
353
|
+
// PID and the notify fd's NUMBER, then block on the parent's one-byte
|
|
354
|
+
// ack. The pid lets the handoff thread `pidfd_open`+`pidfd_getfd` the fd
|
|
355
|
+
// (it cannot use `child.id()` - `spawn` has not returned yet, since it
|
|
356
|
+
// blocks until we exec). We must NOT close `notify_fd` until the parent
|
|
357
|
+
// has acquired its own reference to the same open file description;
|
|
358
|
+
// closing early would tear down the listener. `getpid`/`write`/`read`
|
|
359
|
+
// are not in the filter, so none traps and the child never blocks on the
|
|
360
|
+
// un-serviced notifier.
|
|
361
|
+
let pid = unsafe { libc::getpid() };
|
|
362
|
+
let handoff = write_handoff(child_sock, pid, notify_fd).and_then(|()| wait_ack(child_sock));
|
|
363
|
+
// 4. Whether the handoff succeeded or failed, drop both fds before exec so
|
|
364
|
+
// the child's real program (python) inherits NEITHER the notify fd nor
|
|
365
|
+
// the socketpair. The socketpair is already CLOEXEC, but the notify fd
|
|
366
|
+
// is not, so this explicit close is what keeps it out of the exec.
|
|
320
367
|
unsafe {
|
|
321
|
-
libc::close(notify_fd
|
|
368
|
+
libc::close(notify_fd);
|
|
322
369
|
libc::close(child_sock);
|
|
323
370
|
}
|
|
324
|
-
|
|
371
|
+
handoff
|
|
325
372
|
}
|
|
326
373
|
|
|
327
|
-
///
|
|
328
|
-
///
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
374
|
+
/// Write the child's `(pid, notify-fd-number)` as two native-endian `i32`s to
|
|
375
|
+
/// the socketpair. Used in place of an `SCM_RIGHTS`/`sendmsg` fd passage,
|
|
376
|
+
/// because `sendmsg` is trapped by the filter that is already installed;
|
|
377
|
+
/// `write` is not.
|
|
378
|
+
fn write_handoff(sock: RawFd, pid: libc::pid_t, fd: RawFd) -> io::Result<()> {
|
|
379
|
+
// RawFd and pid_t are both i32 on Linux: a fixed 8-byte message the parent
|
|
380
|
+
// reads back with `i32::from_ne_bytes`.
|
|
381
|
+
let mut msg = [0u8; 8];
|
|
382
|
+
msg[0..4].copy_from_slice(&pid.to_ne_bytes());
|
|
383
|
+
msg[4..8].copy_from_slice(&fd.to_ne_bytes());
|
|
384
|
+
write_all(sock, &msg)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// Block until the parent writes its one-byte ack, meaning it has acquired the
|
|
388
|
+
/// notify fd and the child may close its copy and proceed to exec. `read` is
|
|
389
|
+
/// not trapped by the filter.
|
|
390
|
+
fn wait_ack(sock: RawFd) -> io::Result<()> {
|
|
391
|
+
let mut ack = [0u8; 1];
|
|
392
|
+
read_exact(sock, &mut ack)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Parent side of the handoff, run on its own thread (see [`install`]): read
|
|
396
|
+
/// the child's `(pid, notify-fd-number)`, acquire the actual fd out of the
|
|
397
|
+
/// child with `pidfd_open`+`pidfd_getfd` (the same mechanism [`dup_child_fd`]
|
|
398
|
+
/// uses for the child's sockets), then write the one-byte ack so the child can
|
|
399
|
+
/// close its copy and exec. Owns `sock` and drops it on return.
|
|
400
|
+
fn recv_notify_fd(sock: OwnedFd) -> io::Result<OwnedFd> {
|
|
401
|
+
let raw = sock.as_raw_fd();
|
|
402
|
+
// Bound the wait for the child's message. On the happy path the child
|
|
403
|
+
// writes within milliseconds; a bound means that if `cmd.spawn` FAILS (the
|
|
404
|
+
// child never runs) this thread errors out instead of blocking forever on a
|
|
405
|
+
// socketpair whose peer never speaks.
|
|
406
|
+
if !poll_readable(raw, HANDOFF_TIMEOUT_MS)? {
|
|
407
|
+
return Err(io::Error::new(
|
|
408
|
+
io::ErrorKind::TimedOut,
|
|
409
|
+
"egress handoff: child never sent its notify fd",
|
|
410
|
+
));
|
|
411
|
+
}
|
|
412
|
+
let mut msg = [0u8; 8];
|
|
413
|
+
read_exact(raw, &mut msg)?;
|
|
414
|
+
let child_pid = i32::from_ne_bytes([msg[0], msg[1], msg[2], msg[3]]) as u32;
|
|
415
|
+
let fd_number = i32::from_ne_bytes([msg[4], msg[5], msg[6], msg[7]]) as RawFd;
|
|
416
|
+
// Acquire OUR OWN reference to the child's notify file description. Once we
|
|
417
|
+
// hold it, the child closing its copy does not tear down the listener.
|
|
418
|
+
// pidfd_getfd sets CLOEXEC on the returned fd, so flowproof's own future
|
|
419
|
+
// execs do not leak it.
|
|
420
|
+
let notify_fd = dup_child_fd(child_pid, fd_number)?;
|
|
421
|
+
// Ack: the child is blocked on this one byte before it closes and execs.
|
|
422
|
+
write_all(raw, &[1u8])?;
|
|
423
|
+
Ok(notify_fd)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// How long the parent handoff thread waits for the child's message before
|
|
427
|
+
/// giving up. Generous: the child writes within milliseconds on success, so
|
|
428
|
+
/// this only fires when the child never ran (a failed `cmd.spawn`).
|
|
429
|
+
const HANDOFF_TIMEOUT_MS: libc::c_int = 30_000;
|
|
430
|
+
|
|
431
|
+
/// `poll` a fd for readability with a millisecond timeout. `Ok(true)` means
|
|
432
|
+
/// readable (or an error condition the following read will surface),
|
|
433
|
+
/// `Ok(false)` means the timeout elapsed with nothing to read.
|
|
434
|
+
fn poll_readable(sock: RawFd, timeout_ms: libc::c_int) -> io::Result<bool> {
|
|
435
|
+
loop {
|
|
436
|
+
let mut pfd = libc::pollfd {
|
|
437
|
+
fd: sock,
|
|
438
|
+
events: libc::POLLIN,
|
|
439
|
+
revents: 0,
|
|
440
|
+
};
|
|
441
|
+
let rc = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
|
|
442
|
+
if rc < 0 {
|
|
443
|
+
let e = io::Error::last_os_error();
|
|
444
|
+
if e.raw_os_error() == Some(libc::EINTR) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
return Err(e);
|
|
354
448
|
}
|
|
449
|
+
return Ok(rc > 0);
|
|
355
450
|
}
|
|
356
|
-
Ok(())
|
|
357
451
|
}
|
|
358
452
|
|
|
359
|
-
///
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
};
|
|
366
|
-
let mut cmsg_buf = [0u8; unsafe_cmsg_space()];
|
|
367
|
-
let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
|
|
368
|
-
msg.msg_iov = &mut iov;
|
|
369
|
-
msg.msg_iovlen = 1;
|
|
370
|
-
msg.msg_control = cmsg_buf.as_mut_ptr() as *mut libc::c_void;
|
|
371
|
-
msg.msg_controllen = cmsg_buf.len() as _;
|
|
372
|
-
unsafe {
|
|
373
|
-
let n = libc::recvmsg(sock, &mut msg, 0);
|
|
453
|
+
/// `write` the whole buffer, retrying short writes and EINTR. Async-signal-safe
|
|
454
|
+
/// (used in the child): no allocation, so failures carry a bare errno rather
|
|
455
|
+
/// than a formatted message.
|
|
456
|
+
fn write_all(sock: RawFd, mut buf: &[u8]) -> io::Result<()> {
|
|
457
|
+
while !buf.is_empty() {
|
|
458
|
+
let n = unsafe { libc::write(sock, buf.as_ptr() as *const libc::c_void, buf.len()) };
|
|
374
459
|
if n < 0 {
|
|
375
|
-
|
|
460
|
+
let e = io::Error::last_os_error();
|
|
461
|
+
if e.raw_os_error() == Some(libc::EINTR) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
return Err(e);
|
|
376
465
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|| (*cmsg).cmsg_level != libc::SOL_SOCKET
|
|
380
|
-
|| (*cmsg).cmsg_type != libc::SCM_RIGHTS
|
|
381
|
-
{
|
|
382
|
-
return Err(io::Error::other(
|
|
383
|
-
"no notify fd in the child's SCM_RIGHTS message",
|
|
384
|
-
));
|
|
466
|
+
if n == 0 {
|
|
467
|
+
return Err(io::Error::from_raw_os_error(libc::EPIPE));
|
|
385
468
|
}
|
|
386
|
-
|
|
387
|
-
std::ptr::copy_nonoverlapping(
|
|
388
|
-
libc::CMSG_DATA(cmsg),
|
|
389
|
-
&mut fd as *mut RawFd as *mut u8,
|
|
390
|
-
std::mem::size_of::<RawFd>(),
|
|
391
|
-
);
|
|
392
|
-
Ok(OwnedFd::from_raw_fd(fd))
|
|
469
|
+
buf = &buf[n as usize..];
|
|
393
470
|
}
|
|
471
|
+
Ok(())
|
|
394
472
|
}
|
|
395
473
|
|
|
396
|
-
///
|
|
397
|
-
///
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
474
|
+
/// `read` exactly `buf.len()` bytes, retrying short reads and EINTR. A closed
|
|
475
|
+
/// peer (0 bytes) before the buffer is filled is an error. Async-signal-safe.
|
|
476
|
+
fn read_exact(sock: RawFd, mut buf: &mut [u8]) -> io::Result<()> {
|
|
477
|
+
while !buf.is_empty() {
|
|
478
|
+
let n = unsafe { libc::read(sock, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
|
|
479
|
+
if n < 0 {
|
|
480
|
+
let e = io::Error::last_os_error();
|
|
481
|
+
if e.raw_os_error() == Some(libc::EINTR) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
return Err(e);
|
|
485
|
+
}
|
|
486
|
+
if n == 0 {
|
|
487
|
+
return Err(io::Error::from_raw_os_error(libc::ECONNRESET));
|
|
488
|
+
}
|
|
489
|
+
buf = &mut buf[n as usize..];
|
|
490
|
+
}
|
|
491
|
+
Ok(())
|
|
402
492
|
}
|
|
403
493
|
|
|
404
494
|
/// The live supervisor: a thread servicing the notify fd, plus the shared log
|
|
@@ -19,7 +19,7 @@ pub use llm::{HttpModelClient, ModelClient};
|
|
|
19
19
|
pub use recorder::{
|
|
20
20
|
record, record_incremental, record_with_author, Author, RecordError, RecordSummary,
|
|
21
21
|
};
|
|
22
|
-
pub use spec::{FlowSpec, McpServerSpec, SpecStep, SuiteManifest};
|
|
22
|
+
pub use spec::{check_control_ids, FlowSpec, McpServerSpec, SessionRef, SpecStep, SuiteManifest};
|
|
23
23
|
|
|
24
24
|
use std::env;
|
|
25
25
|
|
|
@@ -1519,7 +1519,10 @@ pub fn record_with_reuse<D: AppDriver, C: ModelClient>(
|
|
|
1519
1519
|
) -> Result<RecordSummary, RecordError> {
|
|
1520
1520
|
let mut reuse = old_steps.map(ReuseCursor::new);
|
|
1521
1521
|
let target = launch_target(spec)?;
|
|
1522
|
-
|
|
1522
|
+
// The session is an inline mapping by now: a `session: <name>` ref is
|
|
1523
|
+
// dereferenced to its identity's inline setup at flow load, so record
|
|
1524
|
+
// never sees an unresolved name.
|
|
1525
|
+
if let Some(setup) = spec.session.as_ref().and_then(|s| s.inline()) {
|
|
1523
1526
|
let (cookies, local_storage) = setup.resolved()?;
|
|
1524
1527
|
driver.stage_session(flowproof_driver::WebSession {
|
|
1525
1528
|
cookies,
|
|
@@ -2092,7 +2095,10 @@ pub fn record_with_reuse<D: AppDriver, C: ModelClient>(
|
|
|
2092
2095
|
.filter_map(|rule| serde_json::to_value(rule).ok())
|
|
2093
2096
|
.collect(),
|
|
2094
2097
|
// The RAW session setup — cookie values keep their `${VAR}` refs.
|
|
2095
|
-
session:
|
|
2098
|
+
// A `session: <name>` ref has been dereferenced to its identity's
|
|
2099
|
+
// inline setup at flow load, so the header carries the setup itself,
|
|
2100
|
+
// never a pointer to the suite. The trace stays self-contained.
|
|
2101
|
+
session: spec.session.as_ref().and_then(|s| s.inline()).cloned(),
|
|
2096
2102
|
// Mock rules travel with the trace: what was mocked at record MUST
|
|
2097
2103
|
// be mocked at replay, or the two executions test different things.
|
|
2098
2104
|
mock: spec.mock.clone(),
|
|
@@ -2167,6 +2173,10 @@ pub fn record_with_reuse<D: AppDriver, C: ModelClient>(
|
|
|
2167
2173
|
dpi_scale: None,
|
|
2168
2174
|
locale: None,
|
|
2169
2175
|
},
|
|
2176
|
+
// The control this flow validates, copied so the evidence is
|
|
2177
|
+
// self-describing. Additive/optional: absent for flows with no
|
|
2178
|
+
// `control:` block, which serialize byte-identical to before.
|
|
2179
|
+
control: spec.control.clone(),
|
|
2170
2180
|
};
|
|
2171
2181
|
|
|
2172
2182
|
let io_err = |source: std::io::Error| RecordError::Io {
|