GLM 5.2
DUNIVERSE AUDIT REPORT: qubes-mirage-firewall Vendored Dependencies
Audit Scope
This audit covers the source code of the vendored dependencies that would be placed in the duniverse/ directory during the make depend build step of qubes-mirage-firewall. The duniverse/ directory is not committed to the repository — it is generated at build time by opam-monorepo, which fetches the source code of each dependency at the version resolved by the pinned opam-repository commit (a20cf55ad5f40faa4817b7e80ec43b935dda1d5a).
This audit is the follow-up to the two prior audits:
- Upstream Supply Chain Audit — version-level CVE analysis
- Downstream Source Code Audit — the firewall’s own ~12 source files
Resolved Dependency Versions
The following direct dependencies were resolved from the pinned opam-repository commit:
| Package |
Resolved Version |
Source Commit |
Source Repo |
| mirage-nat |
3.0.2 |
8df31ecd0de2a447fede93311da48f0bb0c664f1 |
mirage/mirage-nat |
| arp |
4.0.0 |
8eec8d7a9f01ad3d992ea6582fdbe87666381520 |
mirage/arp |
| tcpip |
9.0.1 |
ee22b76879cda4f00cd942664fb55904a9d63378 |
mirage/mirage-tcpip |
| dns-client |
10.2.1 |
3990bab3064ac2ed3dffbb9aad5831ea19f8571c |
mirage/ocaml-dns |
| mirage-qubes |
2.0.0 |
ff58fca6f334cad15076cd42ab173c84dd6a6ff3 |
mirage/mirage-qubes |
| vchan |
6.0.2 |
78071e55bad05aabeb9c317c7198e961f7947a48 |
mirage/ocaml-vchan |
Transitive dependencies (not individually audited but noted): cstruct, ethernet, ipaddr, macaddr, lru, mirage-xen, mirage-net-xen, shared-memory-ring, mirage-crypto-rng, mirage-sleep, mirage-flow, lwt, fmt, logs, duration, randomconv, domain-name, mtime, xenstore, xenstore_transport, io-page, ohex, dune.
Files Audited
mirage-nat 3.0.2 (commit 8df31ec)
lib/mirage_nat.ml — Module type definitions for NAT table operations
lib/nat_packet.ml — Packet parsing, serialization, fragmentation
lib/nat_rewrite.ml — Core NAT translation logic (TCP/UDP/ICMP rewriting)
lib/mirage_nat_lru.ml — LRU-backed NAT table implementation
arp 4.0.0 (commit 8eec8d7)
src/arp_handler.ml — ARP cache management, request/reply handling
src/arp_packet.ml — ARP packet encoding/decoding
CHANGES.md — Changelog (confirmed no security fix between 4.0.0 and 4.1.0 entries)
tcpip 9.0.1 (commit ee22b76)
src/ipv4/ipv4_packet.ml — IPv4 packet parsing and marshalling
src/tcp/tcp_packet.ml — TCP packet parsing and marshalling
src/udp/udp_packet.ml — UDP packet parsing and marshalling
mirage-qubes 2.0.0 (commit ff58fca)
lib/dB.ml — QubesDB protocol client (vchan-based database reads/writes)
lib/rExec.ml — Qrexec protocol implementation (remote command execution)
lib/s.ml — Module type signatures
ocaml-vchan 6.0.2 (commit 78071e5)
lib/xenstore.ml — XenStore path management for vchan connections
lib/endpoint.ml — Vchan shared memory ring buffer implementation
FINDINGS
DU-01: ARP Cache Has No Size Limit — OSEC-2026-02 Confirmed (HIGH)
Location: arp/src/arp_handler.ml, handle_reply function and cache type definition
Code:
type 'a t = {
cache : 'a entry M.t; (* M = Map.Make(Ipaddr.V4) *)
...
}
let handle_reply t source mac =
let extcache =
let cache = M.add source (Dynamic (mac, t.epoch + t.timeout)) t.cache in
{ t with cache }
in
...
Analysis: The ARP cache uses Map.Make(Ipaddr.V4), which is an unbounded balanced tree. There is no maximum size constraint. Every ARP reply from a unique IPv4 address adds a new entry to the cache. The handle_reply function adds entries unconditionally — any ARP reply from a new source IP creates a Dynamic entry that persists until its timeout expires.
The OSEC-2026-02 advisory confirms this: an attacker with Layer-2 access can spoof ARP replies for arbitrary IP addresses, causing the cache to grow until the unikernel exhausts its 32MB of memory and crashes.
The fix in arp 4.1.0 (released October 20, 2025) replaces the unbounded Map with an LRU cache that has a fixed upper bound, dropping old entries when full. The pinned opam-repository commit (a20cf55) only includes arp 4.0.0 — the fixed version 4.1.0 is not available at this commit.
Impact: Any Layer-2 adjacent attacker can crash the firewall by flooding spoofed ARP replies, causing an out-of-memory condition. With only 32MB of RAM, the firewall can be crashed with a modest number of entries (each Dynamic entry stores a Macaddr.t and an integer, but the Map tree overhead adds significant per-entry overhead).
Risk Rating: HIGH — Direct denial-of-service vector. The firewall is confirmed vulnerable.
Recommendation: Update the pinned opam-repository commit to one that includes arp 4.1.0, or add an explicit upper version bound pin to arp >= 4.1.0 in config.ml.
DU-02: NAT Table Uses LRU with Fixed Capacity but No TTL Expiry (MEDIUM)
Location: mirage-nat/lib/mirage_nat_lru.ml
Code:
module Port_cache = Lru.F.Make(Ports)(Uniform_weights(Ports))
...
type t = {
defaults : defaults;
mutable tcp: Port_cache.t;
mutable udp: Port_cache.t;
mutable icmp: Id_cache.t;
}
Analysis: The NAT table implementation uses the lru library with fixed-capacity caches for TCP, UDP, and ICMP entries. This is good — it prevents unbounded memory growth, unlike the ARP cache.
However, mirage-nat’s own README states: “Mirage_nat_lru expires the least recently used rules in response to memory pressure. In practice, this means rules will stick around as long as there’s space for them, with no consideration for whether communication between hosts is still occurring.”
There is no time-based expiry. Once a NAT entry is created, it persists until evicted by LRU pressure. This means:
- A remote host that was contacted can send traffic back through the NAT long after the local host considers the connection closed
- An attacker who can observe or predict the LRU eviction pattern could potentially re-use stale NAT mappings
The remove_connections function does allow explicit eviction by source IP (used when firewall rules change), but there’s no periodic cleanup.
Impact: Stale NAT entries could allow traffic from external hosts that should no longer have access. This is a well-documented limitation of mirage-nat, not a new discovery.
Risk Rating: MEDIUM — Long-lived NAT entries could be abused for connection hijacking after session termination.
Recommendation: Implement time-based NAT entry expiry in addition to LRU eviction. This would require changes to mirage-nat itself.
DU-03: assert false in ICMP NAT Rewrite — Potential Crash (MEDIUM)
Location: mirage-nat/lib/nat_rewrite.ml, ICMP module
Code:
let rewrite ~new_ip_header (`ICMP (icmp, payload)) new_id =
match icmp.Icmpv4_packet.subheader with
| Icmpv4_packet.Id_and_seq (_, seq) ->
let new_icmp = {icmp with Icmpv4_packet.subheader = Icmpv4_packet.Id_and_seq (new_id, seq)} in
`IPv4 (new_ip_header, `ICMP (new_icmp, payload))
| _ -> assert false (* We already checked this in [channel] *)
Analysis: The rewrite function for ICMP packets contains an assert false that triggers if the ICMP subheader is not Id_and_seq. The comment claims this was already checked in the channel function, which does filter: channel returns None for non-Id_and_seq headers, and translate_by_transport skips entries where channel returns None.
However, this creates a tight coupling between the channel and rewrite functions. If any future code path calls rewrite without first checking channel, the unikernel will crash with an assertion failure. In the current codebase, the invariant holds, but this is a fragile pattern — the type system does not enforce it.
This is notably similar to the downstream finding D-02 (assert false in the firewall’s own rules.ml for DNS rule matching), suggesting a pattern of using assert false for “impossible” cases that could become possible under edge conditions.
Impact: If an ICMP packet with a non-Id_and_seq subheader reaches the rewrite function through any code path, the entire firewall crashes.
Risk Rating: MEDIUM — Currently protected by upstream filtering, but fragile by design.
Recommendation: Replace assert false with proper error handling that returns Error Untranslated` instead of crashing.
DU-04: NAT Port Allocation Retry Loop Uses Fixed 100 Attempts (LOW)
Location: mirage-nat/lib/nat_rewrite.ml, add function
Code:
let rec try_add_mapping retries =
let* xl_port = Option.to_result ~none:`Overlap (port_gen ()) in
let opt_again = function
| Error `Overlap as e ->
if retries > 0 then try_add_mapping (retries - 1) else e
| Ok () -> Ok ()
in
...
in
try_add_mapping 100
Analysis: When adding a NAT rule, the code tries to find a free port by calling port_gen () up to 100 times. If all 100 attempts produce ports that overlap with existing entries, the operation fails with Error Overlap`. This is reasonable — the port space is 65535 - 1024 = 64511 ports, so with a non-full NAT table, 100 retries should find a free port.
However, this interacts with the downstream finding D-04 (the firewall’s my_nat.ml last_resort_port fallback). The firewall’s pick_port function uses Random.int to generate port candidates, and if mirage-nat’s add returns Overlap after 100 retries, the firewall falls back to last_resort_port, creating a DNS cache poisoning vector.
Impact: Under NAT table pressure, the retry loop may exhaust, causing the downstream firewall to use its unsafe fallback port.
Risk Rating: LOW — The retry count is adequate under normal conditions, but the interaction with the downstream fallback is concerning.
Recommendation: Consider increasing the retry count or using a deterministic port scanning approach instead of random selection.
DU-05: IPv4 Packet Parser Does Not Validate Options Length Against Buffer (LOW)
Location: tcpip/src/ipv4/ipv4_packet.ml, Unmarshal.header_of_cstruct
Code:
let get_header_length buf =
let length_of_hlen_version n = (n land 0x0f) * 4 in
let hlen = get_hlen_version buf |> length_of_hlen_version in
let len = get_len buf in
if len < sizeof_ipv4 then Error (...)
else if len < hlen then Error (...)
else if hlen < sizeof_ipv4 then Error (...)
else if Cstruct.length buf < hlen then Error (...)
else Ok hlen
Analysis: The IPv4 header parser performs multiple validation checks on the header length field. It correctly verifies that:
- The total length is at least the minimum IPv4 header size (20 bytes)
- The total length is at least the stated header length
- The stated header length is at least 20 bytes
- The buffer is at least as large as the stated header length
This is thorough and well-implemented. The parser also validates the IP version field (0x40 for IPv4).
Positive finding — the IPv4 parser has robust input validation.
Risk Rating: LOW — No vulnerability found. Good defensive coding.
DU-06: TCP/UDP Packet Parsers Validate Lengths Correctly (LOW)
Location: tcpip/src/tcp/tcp_packet.ml and tcpip/src/udp/udp_packet.ml
Analysis:
- TCP parser validates that the packet is at least
sizeof_tcp (20 bytes), then checks that the data offset field doesn’t claim a header larger than the buffer. It also rejects data offsets less than 20.
- UDP parser validates minimum header length (8 bytes), checks that the stated total length is at least the header size, and verifies the payload length against the buffer.
Both parsers use Result.bind for error propagation and never call assert or failwith on untrusted input.
Positive finding — both transport layer parsers have proper bounds checking.
Risk Rating: LOW — No vulnerability found.
DU-07: ARP Packet Decoder Validates Length and Header Fields (LOW)
Location: arp/src/arp_packet.ml, decode function
Code:
let decode buf =
let check_len buf = Cstruct.length buf >= size in (* size = 28 *)
let check_hdr buf =
Cstruct.BE.get_uint16 buf 0 = ether_htype &&
Cstruct.BE.get_uint16 buf 2 = ipv4_ethertype &&
Cstruct.get_uint8 buf 4 = ether_size &&
Cstruct.get_uint8 buf 5 = ipv4_size
in
guard (check_len buf) Too_short >>= fun () ->
guard (check_hdr buf) Unusable >>= fun () ->
...
Analysis: The ARP packet decoder validates:
- The buffer is at least 28 bytes (minimum ARP packet size)
- The hardware type is Ethernet (0x0001)
- The protocol type is IPv4 (0x0800)
- The hardware address length is 6 (Ethernet MAC)
- The protocol address length is 4 (IPv4)
The operation field is validated to be either Request (1) or Reply (2); any other value returns Unknown_operation.
Positive finding — the ARP decoder is properly defensive against malformed packets.
Risk Rating: LOW — No vulnerability found.
DU-08: QubesDB Client Trusts Data from dom0 Without Length Validation (MEDIUM)
Location: mirage-qubes/lib/dB.ml, recv and listen functions
Code:
let recv t =
QV.recv t >>!= fun (hdr, data) ->
let ty = get_msg_header_ty hdr in
...
let path = get_msg_header_path hdr in
let path = String.sub path 0 (String.index path '\x00') in
Lwt.return (ty, path, data)
Analysis: The QubesDB client receives messages over a vchan from dom0. The recv function extracts a path from the header by finding the first null byte. If the header contains no null byte, String.index will raise Not_found, crashing the unikernel.
The listen function processes QDB_CMD_WRITE messages by directly adding the received path/value pair to the store without length validation:
| QDB_CMD_WRITE, path, value ->
t.store |> KeyMap.add path value |> update t;
There is no limit on the number of keys, the key length, or the value length. A compromised dom0 (or a bug in the QubesDB daemon) could send arbitrarily large data to exhaust the firewall’s 32MB of memory.
Impact: If dom0 sends malformed QubesDB messages (either maliciously or due to a bug), the firewall could crash. The threat model considers dom0 trusted, but defense-in-depth suggests hardening.
Risk Rating: MEDIUM — Requires dom0 compromise or QubesDB daemon bug to exploit, but the impact is a firewall crash.
Recommendation: Add null-byte validation before String.index, and consider limiting key/value sizes.
DU-09: Qrexec Protocol Parser Does Not Validate Command Length (MEDIUM)
Location: mirage-qubes/lib/rExec.ml, parse_cmdline
Code:
let parse_cmdline cmd =
if cmd.[String.length cmd - 1] <> '\x00' then
failwith "Command not null-terminated"
else (
let cmd = String.sub cmd 0 (String.length cmd - 1) in
match cmd |> split ':' with
| None -> Fmt.failwith "Missing ':' in %S" cmd
| Some (user, cmd) -> Lwt.return (user, cmd)
)
Analysis: The parse_cmdline function accesses cmd.[String.length cmd - 1] without first checking that cmd is non-empty. If an empty command string is received, this will raise an Invalid_argument exception, crashing the unikernel.
The function also uses failwith (which raises Failure) for malformed commands rather than returning an error result. This exception would be caught by the with_flow handler’s exception handler, but it still results in an abnormal termination of the qrexec session.
Additionally, the rExec.ml exec function extracts the command line from the message without validating its length:
let cmdline = String.sub msg sizeof_exec_params (len - sizeof_exec_params) in
If len < sizeof_exec_params, this will raise an Invalid_argument exception.
Impact: A malformed qrexec message from dom0 could crash the firewall or terminate a qrexec session abnormally.
Risk Rating: MEDIUM — Requires dom0 to send malformed data, but the crash impact is severe.
Recommendation: Add length validation before string access, and use Result types instead of exceptions for error handling.
DU-10: Vchan Endpoint Trusts Shared Memory Ring Sizes from Peer (MEDIUM)
Location: ocaml-vchan/lib/endpoint.ml, client function
Code:
let client ~domid ~port () =
...
let v = Io_page.to_cstruct (M.buf_of_mapping mapping) in
Location.of_order (get_lo v)
>>|= fun lo ->
Location.of_order (get_ro v)
>>|= fun ro ->
let nb_left_pages = Location.to_length lo / 4096 in
let nb_right_pages = Location.to_length ro / 4096 in
...
let lgrants = unmarshal sizeof_vchan_interface nb_left_pages in
let rgrants = unmarshal (sizeof_vchan_interface + nb_left_pages*4) nb_right_pages in
Analysis: When connecting as a vchan client, the endpoint reads the ring buffer order (size) from the shared memory page, which is controlled by the server (the peer domain). The order values determine how many grant references are read from the shared page. If the server provides maliciously large order values, the client could attempt to map an excessive number of pages, or if the order values cause Location.to_length to return a very large number, the nb_left_pages / nb_right_pages calculations could lead to excessive memory allocation.
The unmarshal function reads grant references from offsets starting at sizeof_vchan_interface, and the number of grants read is determined by the peer-controlled nb_left_pages/nb_right_pages. There is no upper bound check on these values.
Impact: A malicious vchan peer (typically dom0) could cause excessive memory allocation or out-of-bounds reads from the shared page.
Risk Rating: MEDIUM — Requires a malicious or compromised peer domain. In the Qubes firewall context, the vchan peer is dom0 (trusted), but defense-in-depth warrants validation.
Recommendation: Validate that ring order values are within reasonable bounds (e.g., 0-10, corresponding to 1-1024 pages) before using them.
DU-11: Vchan Write Does Not Validate Ring Buffer Size Against Actual Allocation (LOW)
Location: ocaml-vchan/lib/endpoint.ml, _write_noblock function
Code:
let _write_noblock vch buf =
let len = Cstruct.length buf in
let real_idx = Int32.(logand (wr_prod vch) (of_int (wr_ring_size vch) - 1l) |> to_int) in
let avail_contig = wr_ring_size vch - real_idx in
let avail_contig = if avail_contig > len then len else avail_contig in
Cstruct.blit buf 0 vch.write real_idx avail_contig;
(if avail_contig < len then
Cstruct.blit buf avail_contig vch.write 0 (len - avail_contig));
set_wr_prod vch Int32.(wr_prod vch + of_int len);
send_notify vch Write
Analysis: The write function uses wr_ring_size (derived from the peer-controlled order field) to calculate the ring buffer size and mask the producer index. If the actual allocated buffer (vch.write) is smaller than wr_ring_size claims, the Cstruct.blit could write beyond the buffer boundary.
However, in practice, the buffer is allocated based on the same order field during connection setup, so they should match. The risk arises only if the shared page is modified after connection setup.
Impact: If the shared page’s order field is modified after connection, writes could go out of bounds.
Risk Rating: LOW — Requires modification of shared memory after connection, which is a race condition requiring peer domain access.
DU-12: NAT Packet Reassembly Uses Fragment Cache with Fixed Size (LOW)
Location: mirage-nat/lib/nat_packet.ml, of_ipv4_packet
Code:
let of_ipv4_packet cache ~now packet : Fragments.Cache.t * (t option, error) result =
match Ipv4_packet.Unmarshal.of_cstruct packet with
| Error e -> ...
| Ok (ip_packet, payload) ->
let cache', r = Fragments.process cache now ip_packet payload in
...
Analysis: The NAT packet parser delegates IP fragment reassembly to a Fragments.Cache module. The cache is created by the firewall with a fixed 256KB size per client (as seen in the downstream audit). This prevents unbounded memory growth from fragment floods.
The fragment cache is passed in by the caller and returned with any modifications, following a functional pattern. This is good design — it allows the caller to control the memory budget.
Positive finding — the fragment cache design prevents memory exhaustion.
Risk Rating: LOW — No vulnerability found. Good design.
DU-13: ICMP Error Translation Parses Embedded Packet Headers (LOW)
Location: mirage-nat/lib/nat_rewrite.ml, translate_icmp_error
Code:
let translate_icmp_error table ~outer_ip ~icmp ~icmp_payload ~inner_transport_header =
...
match Icmp_payload.get_encapsulated_packet_channel inner_ip inner_transport_header with
| Error _ as e -> e
| Ok (`ICMP id) -> ...
| Ok (`TCP (src_port, dst_port)) -> ...
| Ok (`UDP (src_port, dst_port)) -> ...
Analysis: The ICMP error translation code parses the inner IPv4 packet embedded in ICMP error messages. The get_encapsulated_packet_channel function checks that the payload is at least 8 bytes before extracting transport-layer port numbers:
let get_encapsulated_packet_channel ip payload =
if Cstruct.length payload < 8 then Error `Untranslated
else ...
This is correct — RFC 792 requires ICMP error messages to include at least the first 8 bytes of the original packet’s IP header and 8 bytes of the original payload. The code handles truncated payloads gracefully by returning Error Untranslated`.
Positive finding — proper bounds checking on embedded packet headers.
Risk Rating: LOW — No vulnerability found.
DU-14: QubesDB full_db_sync Has No Limit on Number of Entries (LOW)
Location: mirage-qubes/lib/dB.ml, full_db_sync
Code:
let full_db_sync t =
send t.vchan QDB_CMD_MULTIREAD >>!= fun () ->
let rec loop () =
recv t.vchan >>= function
| QDB_RESP_MULTIREAD, "", _ -> Lwt.return `Done
| QDB_RESP_MULTIREAD, path, data ->
t.store <- t.store |> KeyMap.add path data;
loop ()
| ty, _, _ -> Fmt.failwith "Unexpected QubesDB message: %s" ...
in
loop ()
Analysis: The initial database sync reads all entries from dom0’s QubesDB without any limit on the number of entries or total data size. Each entry is added to an in-memory Map.Make(String), which is unbounded.
If dom0 sends a very large number of QubesDB entries (either maliciously or due to a bug), the firewall’s 32MB memory could be exhausted during the initial sync, before the firewall is even operational.
The recv function also uses String.index path '\x00' without checking that a null byte exists, which could crash on malformed data (as noted in DU-08).
Impact: Excessive QubesDB entries could cause OOM during startup.
Risk Rating: LOW — Requires dom0 to send excessive data, which requires dom0 compromise.
Summary of Findings
High Risk
| ID |
Finding |
Component |
Impact |
| DU-01 |
ARP cache unbounded — OSEC-2026-02 confirmed |
arp 4.0.0 |
Layer-2 DoS via memory exhaustion |
Medium Risk
| ID |
Finding |
Component |
Impact |
| DU-02 |
NAT table has no TTL expiry |
mirage-nat 3.0.2 |
Stale NAT entries allow post-session traffic |
| DU-03 |
assert false in ICMP NAT rewrite |
mirage-nat 3.0.2 |
Crash on unexpected ICMP subheader |
| DU-08 |
QubesDB client lacks input validation |
mirage-qubes 2.0.0 |
Crash on malformed QubesDB messages |
| DU-09 |
Qrexec parser doesn’t validate command length |
mirage-qubes 2.0.0 |
Crash on empty/malformed command |
| DU-10 |
Vchan trusts ring sizes from peer |
ocaml-vchan 6.0.2 |
Excessive memory allocation from malicious peer |
Low Risk
| ID |
Finding |
Component |
Impact |
| DU-04 |
NAT port retry loop fixed at 100 |
mirage-nat 3.0.2 |
May exhaust under NAT pressure |
| DU-05 |
IPv4 parser validation (positive) |
tcpip 9.0.1 |
No vulnerability — good validation |
| DU-06 |
TCP/UDP parser validation (positive) |
tcpip 9.0.1 |
No vulnerability — good validation |
| DU-07 |
ARP packet decoder validation (positive) |
arp 4.0.0 |
No vulnerability — good validation |
| DU-11 |
Vchan write trusts ring size post-connection |
ocaml-vchan 6.0.2 |
Race condition requiring peer access |
| DU-12 |
Fragment cache design (positive) |
mirage-nat 3.0.2 |
No vulnerability — good design |
| DU-13 |
ICMP error translation (positive) |
mirage-nat 3.0.2 |
No vulnerability — proper bounds checking |
| DU-14 |
QubesDB sync has no entry limit |
mirage-qubes 2.0.0 |
OOM during startup with dom0 compromise |
Positive Security Observations
-
Protocol parsers are defensive: The IPv4, TCP, UDP, and ARP packet parsers in tcpip and arp all perform thorough bounds checking and return proper error results rather than crashing on malformed input.
-
NAT table uses LRU eviction: Unlike the ARP cache, the NAT table in mirage-nat uses fixed-capacity LRU caches, preventing unbounded memory growth.
-
Fragment cache is caller-controlled: The fragment reassembly cache is sized by the caller (the firewall), allowing the memory budget to be controlled.
-
ICMP error translation is careful: The code that parses embedded packet headers in ICMP error messages properly validates minimum lengths and handles truncated data gracefully.
-
Fail-secure by default: The NAT translation logic returns Error Untranslated` for any packet it can’t handle, which causes the firewall to drop the packet rather than forward it incorrectly.
Recommendations
Immediate (P0)
- Update arp to 4.1.0 (DU-01): The pinned opam-repository commit must be updated to include arp 4.1.0, which fixes OSEC-2026-02 by replacing the unbounded
Map with an LRU cache. This is the most critical finding — a Layer-2 attacker can trivially crash the 32MB firewall.
Short-term (P1)
-
Add input validation to QubesDB client (DU-08, DU-14): Validate that path strings contain null terminators before using String.index, and consider limiting the number of QubesDB entries stored.
-
Fix qrexec command parsing (DU-09): Add length checks before accessing string elements in parse_cmdline.
-
Replace assert false in ICMP rewrite (DU-03): Return Error Untranslated` instead of crashing.
-
Add bounds validation to vchan ring sizes (DU-10): Validate that ring order values are within reasonable bounds before using them.
Long-term (P2)
-
Implement TTL-based NAT entry expiry (DU-02): Add time-based expiry to NAT entries in mirage-nat, complementing the existing LRU eviction.
-
Add upper version bounds to all dependencies in config.ml: This prevents unexpected version changes that could introduce new vulnerabilities.
-
Consider auditing remaining transitive dependencies: This audit focused on the 6 most security-critical libraries. The remaining transitive dependencies (cstruct, ethernet, mirage-crypto-rng, lru, lwt, etc.) should be audited in a future pass, particularly cstruct (which handles all byte buffer operations) and mirage-crypto-rng (which provides cryptographic random number generation).
Dependency Version Summary
| Package |
Audited Version |
Latest Available at Pinned Commit |
Fixed Version (if applicable) |
Status |
| mirage-nat |
3.0.2 |
3.0.2 |
N/A |
Latest available |
| arp |
4.0.0 |
4.0.0 |
4.1.0 |
VULNERABLE (OSEC-2026-02) |
| tcpip |
9.0.1 |
9.0.1 |
N/A |
Latest available |
| dns-client |
10.2.1 |
10.2.1 |
N/A |
Latest available |
| mirage-qubes |
2.0.0 |
2.0.0 |
N/A |
Latest available |
| vchan |
6.0.2 |
6.0.2 |
N/A |
Latest available |
The only dependency with a known unpatched vulnerability at the pinned commit is arp 4.0.0 (OSEC-2026-02). The fix (arp 4.1.0) was released October 20, 2025, but is not included in the opam-repository at the pinned commit.
It would be more productive to upgrade the base image and the pinned versions first so that we can discover any new vulnerabilities instead of old ones, so mention my username after the upgrade and I will perform similar audits again.