[Eclipse Ankaios] Unbounded allocation in control-interface FIFO reader allows a workload to abort the Ankaios agent
> [!warning] AI-Generated Vulnerability Report > This report was produced using AI-assisted security analysis, adversarially verified but not human-confirmed. This finding was challenged against the codebase and survived; we filter out low-confidence results rather than forwarding raw model output. Technical details, exploitability, and severity may still be wrong. Please validate independently before acting. <details open> <summary><strong>Basic information</strong></summary> **Project name:** Eclipse Ankaios **Project id:** automotive.ankaios **Repository:** https://github.com/eclipse-ankaios/ankaios </details> <details> <summary><strong>What are the affected versions?</strong></summary> All released versions that expose the control interface to workloads — `v0.1.0` through `v1.0.0` inclusive — contain the unbounded `vec![0; size]`. The `input_pipe.rs` form is present from `v0.6.0-rc1` onward; identical code lived in `reopen_file.rs` before that. </details> <details open> <summary><strong>Summary</strong></summary> The Ankaios agent reads length-delimited protobuf messages from a FIFO that is bind-mounted into workload containers. The length prefix is decoded as a `u64` varint and passed directly to `vec![0; size]` with no upper bound. A workload that writes a 9-byte varint encoding `isize::MAX` causes `handle_alloc_error()` to abort the entire `ank-agent` process, taking down orchestration for every workload on the node. </details> <details> <summary><strong>Severity</strong></summary> **Medium** * **Trust boundary crossed:** workload container → host orchestrator process. The control interface exists precisely so that workloads can be given *limited* authority; the very first bytes a workload sends are parsed before any authorisation check runs. * **Precondition:** the workload must have a non-empty `controlInterfaceAccess.allowRules` so that the agent creates and mounts the FIFO (`WorkloadSpec::needs_control_interface`, `ankaios_api/src/ank_base/workload.rs:89-91`). This is a documented, recommended pattern (e.g. granting a workload read access to its own state, or log access to one peer). It is **not** the default for every workload, which keeps this out of High. * **Impact:** full-process abort of `ank-agent` from 9 attacker-controlled bytes. All co-located workloads lose lifecycle management, log streaming and state propagation. If the agent is supervised (`systemd` `Restart=always`) the workload can re-trigger on every restart for a persistent DoS. * **Not High** because a fresh quickstart install with no `controlInterfaceAccess` configured is unaffected; the operator must have granted *some* access first. </details> <details> <summary><strong>Weakness / CWE</strong></summary> * CWE-789: Memory Allocation with Excessive Size Value * CWE-1284: Improper Validation of Specified Quantity in Input * CWE-400: Uncontrolled Resource Consumption </details> <details> <summary><strong>Location</strong></summary> `agent/src/control_interface/input_pipe.rs:59-68` ```rust async fn try_read_protobuf_data(file: &mut BufReader<Receiver>) -> Result<Vec<u8>, Error> { let varint_data = Self::try_read_varint_data(file).await?; let mut varint_data = Box::new(&varint_data[..]); let size = prost::encoding::decode_varint(&mut varint_data)? as usize; let mut buf = vec![0; size]; // ← unbounded file.read_exact(&mut buf[..]).await?; Ok(buf) } ``` `try_read_varint_data` (`input_pipe.rs:70-82`) reads up to `MAX_VARINT_SIZE = 19` bytes from the FIFO; `decode_varint` accepts any value up to `u64::MAX`. </details> <details> <summary><strong>Origin</strong></summary> The pattern has existed since the initial commit (`1ee8e336`, `agent/src/control_interface/reopen_file.rs:89-93`), and was carried over verbatim when the file was renamed to `input_pipe.rs` in `fd5e5b55` / `a8ce4010` (PR \#487, *"Switch the Control Interface to tokio net pipes"*). </details> <details> <summary><strong>Methodology</strong></summary> The control interface FIFO (`/run/ankaios/control_interface/output` from the workload's perspective) was identified as the primary workload→agent trust boundary. `ControlInterfaceTask::run` (`agent/src/control_interface/control_interface_task.rs`) calls `InputPipe::read_protobuf_data` on bytes written by the workload; the authoriser only runs *after* decoding succeeds, so framing is fully attacker-controlled. The varint→`vec!` path was inspected for a length bound and none was found. </details> <details> <summary><strong>Validation</strong></summary> Two reproductions were written and executed at the audited commit. ### A. In-tree unit test — proves `size` flows unmodified into `vec![0; size]` A test was appended to `agent/src/control_interface/input_pipe.rs`: ```rust #[tokio::test(flavor = "multi_thread")] async fn security_audit_unbounded_alloc_vmsize() { let tmpdir = tempfile::tempdir().unwrap(); let fifo = tmpdir.path().join("fifo"); mkfifo(&fifo, Mode::S_IRWXU).unwrap(); let mut reading_side = super::InputPipe::open(&fifo); let attacker_len: u64 = 4 * 1024 * 1024 * 1024; // 4 GiB let mut varint = Vec::new(); prost::encoding::encode_varint(attacker_len, &mut varint); eprintln!("[audit] attacker writes {} bytes (varint for {} GiB): {:02x?}", varint.len(), attacker_len / (1024*1024*1024), varint); let vsz_before = vm_kb("VmSize"); let read_handle = tokio::spawn(async move { let _ = reading_side.read_protobuf_data().await; }); let mut writing_side = super::OpenOptions::new().open_sender(&fifo).unwrap(); writing_side.write_all(&varint).await.unwrap(); tokio::time::sleep(Duration::from_millis(300)).await; let vsz_after = vm_kb("VmSize"); /* … assert delta ≈ 4 GiB … */ } ``` Run: ``` $ PROTOC=/tmp/protoc/bin/protoc cargo test -p ank-agent --target x86_64-unknown-linux-gnu \ security_audit_unbounded_alloc_vmsize -- --nocapture --test-threads=1 running 1 test test control_interface::input_pipe::tests::security_audit_unbounded_alloc_vmsize ... [audit] attacker writes 5 bytes (varint for 4 GiB): [80, 80, 80, 80, 10] [audit] agent VmSize before: 1173948 kB [audit] agent VmSize after: 5368256 kB [audit] VmSize delta: 4.00 GiB from a 5-byte FIFO write [audit] CONFIRMED: vec![0; size] sized directly from untrusted varint. ok ``` ### B. Standalone — proves whole-process abort `reports/repro/agent_alloc_dos/src/main.rs` copies `try_read_protobuf_data` / `try_read_varint_data` verbatim from `input_pipe.rs` and reads from a FIFO. `reports/repro/agent_alloc_dos/run.sh` writes the 9-byte varint for `isize::MAX` and waits: ``` $ bash reports/repro/agent_alloc_dos/run.sh == building victim (verbatim copy of InputPipe::try_read_protobuf_data) == == launching victim (stands in for ank-agent control-interface task) == [victim] opening FIFO /tmp/ankaios_ci_output.5yEoqC [victim] reading length-delimited protobuf (agent code path) == attacker: workload writes 9 bytes to FIFO (varint for isize::MAX) == == waiting for victim to exit == memory allocation of 9223372036854775807 bytes failed note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace reports/repro/agent_alloc_dos/run.sh: line 40: 2222520 Aborted (core dumped) "$BIN" "$FIFO" == result == victim exit status: 134 victim was killed by signal 6 (SIGABRT=6, SIGSEGV=11) CONFIRMED: 9 attacker-controlled bytes on the control-interface FIFO abort the agent process via handle_alloc_error(). ``` `handle_alloc_error` calls `abort()`, which is process-wide; in the real agent the per-workload `ControlInterfaceTask` is just a `tokio::spawn`ed task inside the single `ank-agent` process, so the abort takes the whole agent with it. </details> <details> <summary><strong>Detail</strong></summary> * The FIFO pair is created per-workload under `<run_folder>/<workload_instance>/control_interface/{input,output}` (`agent/src/control_interface.rs`) and bind-mounted into the container at `/run/ankaios/control_interface/` by the runtime connector. The workload writes raw bytes to `output`; nothing on the agent side filters those bytes before `InputPipe::read_protobuf_data` is invoked. * `try_read_varint_data` reads up to 19 bytes — enough to encode any `u64`. `prost::encoding::decode_varint` will happily return values up to `u64::MAX`; the cast `as usize` is lossless on 64-bit targets. * For `size > isize::MAX`, `Layout::array::<u8>` fails and `RawVec` calls `capacity_overflow()`, which **panics** — that only kills the spawned task. The attacker therefore picks a size `≤ isize::MAX` that the allocator will refuse: * On Linux with default heuristic overcommit (`vm.overcommit_memory=0`), `mmap` of obviously-too-large anonymous regions is rejected; the 9.2 EiB request from `isize::MAX` is rejected on every realistic host. `Global::allocate_zeroed` returns `Err`, `RawVec` calls `handle_alloc_error`, the process is **aborted**. * On hosts with `RLIMIT_AS` set or strict overcommit (`vm.overcommit_memory=2`), much smaller values trigger the same abort. * If the platform's overcommit *does* accept the mapping, `read_exact` then faults pages in 1:1 with bytes the workload streams, eventually triggering the kernel OOM-killer against `ank-agent`. This is a slower variant of the same DoS. </details> <details> <summary><strong>Prior Art</strong></summary> * `git log --all --grep` for `varint`, `allocation`, `decode_varint`, `DoS`, `denial` returned no relevant commits. * GitHub issue \#316 (*"Handle not read control interface messages in agent"*) is about the **opposite** direction (agent flooding the workload's `input` pipe) and does not touch this code path. * No GitHub Security Advisories are published for the repository. </details> <details> <summary><strong>Steps to reproduce</strong></summary> From inside a workload container that has any `controlInterfaceAccess` allow rule: **Proof of concept:** [poc.sh](/uploads/7e1b41fd4e48577b85a41e6ce750dc7c/poc.sh) That single 9-byte write aborts `ank-agent` on the host. </details> <details> <summary><strong>Do you know any mitigations of the issue?</strong></summary> Bound the decoded length before allocating. The agent already defines a 2 MiB log-message ceiling in `common/src/message_size.rs`; the same scale is appropriate here, and well above any legitimate `ToAnkaios` message. **Suggested fix:** [fix.rs](/uploads/bb812039fe9aa1f30bd27405320064cd/fix.rs) The error path is already handled by `ControlInterfaceTask::run`, which treats `Err` from `read_protobuf_data` as a malformed frame and continues. <!-- l1-helper-dup: 11f42999505b10f962653af954a06eb3590d9ece902a841fb00b7d34a04d2ee5 --> </details>
issue

Copyright © Eclipse Foundation AISBL. All rights reserved.     Privacy Policy | Terms of Use | Copyright Agent