Protocol 1.0 threat model and resource policy
Status: security model for the portable protocol 1.0 candidate. This document explains the threat assumptions and maps them to normative requirements, implementation owners, and executable evidence. The requirements themselves live in specification.md, wire-abi.md, and virtqueue.md.
Security goals
The portable device treats every guest-controlled byte, count, object ID, descriptor shape, timeout, command ordering, and reset timing as hostile. Its goals are to:
- preserve memory safety without trusting wire values or guest mappings;
- keep guest-visible objects isolated by device, reset epoch, kind, and context;
- bound the CPU work and allocation attributable to one command;
- bound live object counts and provider-retained bulk storage;
- publish no uninitialized or ownership-ambiguous response;
- recover deterministically when provider ownership is known; and
- stop admitting work and require backend discard when ownership cannot be proven.
Availability of the host process or physical accelerator against a malicious host backend is not a portable-layer guarantee. The device still preserves guest-visible ownership truth when a trusted backend reports a hang, device loss, or indeterminate result.
Trust boundaries
untrusted guest
|
| descriptors, bytes, counts, IDs, timing
v
transport adapter -- validates/maps guest memory and queue topology
|
| address-free readable/writable ports and owned chain tokens
v
command engine -- validates frames, ownership, quotas, and resource policy
|
| semantic values and provider-owned handles only
v
accelerator backend -- trusted implementation, failure-prone provider/device
|
v
trusted host process and platform integration
The guest is adversarial. The transport adapter, command engine, backend implementation, and host process are trusted code, but mappings, allocations, provider calls, and hardware may fail. Safe Rust prevents memory unsafety in the portable crates; it does not make a dishonest backend conformant.
The transport adapter owns guest-address validation, access permissions, pinning, and base Virtio ring synchronization. The command engine never receives an address. The backend receives neither wire structures nor guest memory and must not retain borrowed byte ports after a call returns.
In-scope threats and bounds
| Attacker-controlled dimension or failure | Bound and enforcement owner | Failure behavior | Executable evidence |
|---|---|---|---|
| Descriptor topology, direction, loops, and segmentation | The transport validates a flattened chain in one pass. WireConfig::max_chain_descriptors is 2 through HARD_MAX_CHAIN_DESCRIPTORS (256); the reference split queue also bounds traversal by its configured table. | Invalid topology completes without backend invocation; unusable chains write no bytes. | chain_layout_rejects_direction_and_length_errors, vq_004_vq_005_vq_006_malformed_topology_is_classified_boundedly, and unusable_frames_write_nothing_and_never_become_dispatchable |
| Request and response byte totals | Configuration limits are capped by HARD_MAX_REQUEST_BYTES and HARD_MAX_RESPONSE_BYTES (16 MiB each). Checked addition enforces exact frame lengths and response capacity before mutation. | Oversized or inconsistent frames return RESOURCE_LIMIT or INVALID_ARGUMENT; short success capacity writes nothing. | edge_vectors_cover_limits_reserved_bits_and_exact_lengths, short_success_capacity_writes_nothing, and short_responses_and_backend_rejection_never_publish_state |
| Variable binding arrays | DeviceLimits::max_bindings_per_submission is nonzero and at most HARD_MAX_BINDINGS (4096). Decode performs one fallible bounded reservation and sorts in place. Event metadata retains at most the same bounded ID count. | Limit excess returns RESOURCE_LIMIT; allocation failure returns OUT_OF_MEMORY; no backend call occurs. | duplicate_submission_slots_are_rejected_before_dispatch, bindings_are_nonempty_bounded_and_unique, and submission_validation_rejects_before_backend_admission |
| Object IDs and cross-context references | IDs are nonzero, namespace-, kind-, slot-, and generation-tagged. Slots retire before generation wrap. Every combined operation checks one context before provider invocation. | Zero or malformed IDs are invalid; stale, wrong-kind, cross-context, and reset-invalidated IDs are rejected without handle access. | stale_ids_never_resolve_after_slot_reuse, exhausted_generations_retire_the_slot_before_an_id_can_revive, and wrong_kind_cross_context_and_cross_device_ids_fail_before_provider_use |
| Live contexts, buffers, programs, execution queues, and events | Nonzero DeviceLimits cap contexts and each per-context child class. Aggregate table capacities use checked multiplication. Event/reference maxima are checked before state exists. | Exhaustion returns RESOURCE_LIMIT before provider invocation or table growth. Fallible table reservation maps to OUT_OF_MEMORY. | limits_are_enforced_before_growth, invalid_limits_are_rejected_before_tables_exist, and quota_exhaustion_never_invokes_provider_callbacks |
| Logical buffer size and transfer ranges | max_buffer_bytes caps each logical allocation. Nonzero ranges use checked end arithmetic and must fit the logical buffer. Usage bits gate read, write, and program access. | Oversize returns RESOURCE_LIMIT; arithmetic/range failure returns INVALID_ARGUMENT or OUT_OF_BOUNDS; permission mismatch returns PERMISSION_DENIED. | transfer_range_overflow_is_a_protocol_error, semantic_validation_prevents_backend_calls, and mutable_state_allows_every_program_access_mode |
| Aggregate provider buffer backing | A mandatory device-private ResourcePolicy::max_buffer_backing_bytes limits the sum of actual BufferInfo::allocation_bytes, not logical guest bytes. The state prechecks the logical lower bound before allocation and reconciles the actual charge before publishing an ID. | An over-budget allocation is released before its ID is exposed. Rejected or indeterminate cleanup makes the backend discard-required and retains or quarantines the exact charge. | aggregate_policy_uses_actual_backing_and_charges_until_release_commits, aggregate_retained_byte_policy_cleans_up_before_exposing_an_id, rejected_cleanup_of_an_unpublished_allocation_requires_backend_discard, and indeterminate_cleanup_of_an_unpublished_allocation_quarantines_actual_backing |
| Artifact input and resident program storage | max_artifact_bytes bounds the borrowed input payload. ResourcePolicy::max_program_resident_bytes bounds the aggregate declared resident charge before provider invocation. A conforming backend retains no more storage than ArtifactRef::resident_bytes. | Excess returns RESOURCE_LIMIT before loading. Invalid or unsupported artifacts create no program resource. | byte_limits_and_resident_charges_have_distinct_semantics, program_policy_rejects_before_provider_invocation, and malformed_reference_artifacts_do_not_create_resident_programs |
| TOSA structure, semantics, and graph cardinality | virtio-accel-tosa::Limits caps input bytes, verifier depth/tables/apparent bytes, graph objects, edges, names, ranks, and constants before returning a safe view. Model::validate_for checks stable-op semantics and Model::analyze_for derives bounded dense IDs, dependency order, liveness, constant state, runtime obligations, and specialization inputs without copying graph payloads. Runtime values use exact bounded encodings; specialization keys and caches have explicit word and entry limits. | Malformed tables/unions, draft schema entries, invalid references, unsupported target combinations, illegal constants/geometry, missing or malformed dynamic compile-time inputs, allocation failure, and limit excess return an error before provider compilation. Advisory REQUIRE failures are reported as unpredictable rather than misclassified as mandatory errors. | virtio-accel-tosa unit/integration tests plus the tosa_parse fuzz target seeded from an upstream stable graph and exercising semantic analysis and runtime validation for two targets |
| Command-queue occupancy and guest request tracking | Virtio QueueSize bounds descriptor/ring ownership. GuestConfig::max_inflight is nonzero and no greater than queue capacity. The split reference model preallocates ring state at configuration. | Prepublication pressure returns caller ownership as retryable backpressure; it is not fabricated as a protocol response. | publication_backpressure_returns_the_chain, prepublication_backpressure_returns_chain_and_operation, and queue_pressure_returns_caller_ownership_before_draining |
| Submission and in-flight references | Per-context events and per-submission bindings bound retained invocation metadata. Queue, program, and buffer reference increments are checked before admission and released exactly once with the event. | Referenced objects return BUSY; rejected admission rolls back references; indeterminate admission retains an event ownership token. | complete_lifecycle_tracks_children_references_and_release_rollback, rejected_and_indeterminate_submissions_preserve_the_admission_boundary, and finite_timeout_rejection_retains_all_referenced_resources |
| Cancellation, completion, release, and reset races | One mutable command processor serializes semantic transitions. Provider events own native completion races. Reset makes one bounded child-before-parent pass and never waits for progress. | Rejected release restores the same ID; indeterminate release invalidates it and requires backend discard. A pending uncancellable event prevents backend reuse. | accepted_events_retain_resources_and_resolve_cancel_completion_races, rejected_reset_release_is_quarantined_and_reset_is_idempotent, and reset_reclaims_completed_and_cancelled_events_exactly_once |
| Response truncation and information disclosure | Command-specific capacity is preflighted before mutation. Payload bytes are initialized before the header commits their length; used length includes only initialized bytes. | Unusable capacity writes nothing. Post-mutation write failure sets recovery instead of claiming rejection. | payload_guard_commits_the_preflighted_length, direct_payload_region_does_not_touch_excess_capacity, and response_failure_after_creation_requires_reset |
| Backend device loss or ownership uncertainty | The acceptance and release result types distinguish rejected from indeterminate outcomes. Quarantined counts and retained-byte charges survive until the complete backend instance is discarded. | New semantic work stops. Reset is sticky, makes no later provider calls, and reports represented plus quarantined resources. | event_faults_and_unreportable_admission_require_recovery, indeterminate_event_release_quarantines_the_complete_object_graph, and device_loss_crosses_backend_engine_and_guest_recovery_boundaries |
All named tests are located in the corresponding crate’s src unit tests,
crates/virtio-accel-device/tests/command_processor.rs,
crates/virtio-accel-device/tests/state_model.rs,
or tests/portable_end_to_end.rs. Coverage-guided malformed
input checks live under fuzz/: protocol decode is compared with the clean-room codec,
descriptor segmentation is driven through the split-queue model, and bounded stateful sequences
check resource accounting after every action. The TOSA target mutates a stable graph, traverses
all safe views, decodes stable attributes, and runs target-semantic validation. Another target drives the reference guest client
against a non-conforming device. That direction is reference-implementation robustness rather than
a portable-layer security boundary, because a malicious backend or host process is excluded below;
it exists so the driver-side obligations in this protocol — opaque unknown statuses, recovery on
unknown event states, bounded in-flight tracking, and epoch-scoped handle staleness — hold against
inputs no example test enumerates. The deterministic state-model suite runs a bounded
seed set in normal CI, asserts stale-ID retirement, context isolation, reference release, quota, and
retained-byte invariants after every action, and prints minimized replayable schedules for failures.
An ignored deep test provides manual or nightly-style exploration with VIRTIO_ACCEL_STATE_MODEL_SEED.
Resource-accounting rules
DeviceLimits are backend capabilities and per-object/count bounds visible to the driver.
WireConfig bounds command framing and descriptor presentation. ResourcePolicy is host policy:
it is supplied when constructing a CommandProcessor, is never guest-controlled, and is not a
promise that an allocation will succeed.
The policy has two nonzero aggregate limits:
max_buffer_backing_bytes: actual provider allocation bytes across all live buffer handles; andmax_program_resident_bytes: declared resident charges across all live program handles.
RetainedBytes uses u128 totals so the sum of a u32 object count and u64 per-object charges is
representable. A charge begins before the corresponding object ID is returned. It remains while a
handle is live or in the Releasing state and ends only when provider release succeeds. Rejected
release restores the handle without dropping its charge. Indeterminate release transfers the
charge to quarantine and requires the entire backend instance to be discarded.
Buffer padding is provider knowledge, so the command engine first checks the logical byte lower
bound, then reconciles the returned BufferInfo::allocation_bytes. If the actual charge crosses
the aggregate budget, the engine attempts release before exposing the new ID. Failure to prove that
cleanup succeeded cannot be represented as an ordinary RESOURCE_LIMIT; it is device loss.
Program resident storage is declared before provider invocation. ArtifactRef::resident_bytes is
the maximum storage the provider may retain for the returned program, including compiled code and
provider metadata attributable to that program. A backend that cannot honor the charge rejects the
load. Temporary provider memory used only during the synchronous call is governed by the backend
and host allocator, not counted as retained program storage. The portable layer cannot observe that
internal allocation, so a backend or isolated provider worker must enforce its own transient compile
memory and work budget when hostile artifacts can expand beyond their bounded input.
Object-table capacity and per-event binding vectors are bounded by count limits rather than byte estimates because their Rust layout is implementation-specific. Quantitative retained metadata, allocation, and copy-path budgets are tracked in performance.md; changing their representation does not change the security bounds above.
Progress and denial of service
Every portable parsing, lookup, queue, polling, cancellation, and reset operation is bounded by a validated count and contains no retry loop waiting for guest or provider progress. Repeated hostile commands can consume an unbounded amount of CPU over unbounded time; rate limiting belongs to the transport or host integration because the portable layer has no clock, scheduler, tenant identity, or worker policy. Rate limiting must not change ownership or fabricate successful responses. Malformed guest values and configured resource exhaustion are ordinary protocol errors: they do not panic, create an unbounded allocation, or expose uninitialized response storage.
Provider lifecycle and explicit transfer calls are synchronous and may block according to the
Accelerator contract. The portable layer cannot safely preempt arbitrary provider code or reclaim
native handles from a hung call. Integrations that require availability against provider hangs must
use an appropriate process or worker with CPU and memory limits, a watchdog, or a hardware-reset
boundary and discard the complete backend instance after loss. Turning provider calls into futures
would not itself enforce bounded polling or safe cancellation.
Timeouts are relative admission constraints, not host watchdogs. A zero timeout is infinite. After admission, an uncertain timeout retains an event rather than permitting a duplicate submission.
Exclusions
Protocol 1.0 does not claim protection against:
- a malicious backend or compromised host process;
- physical attacks, accelerator side channels, power analysis, or denial of service by hardware;
- confidentiality between mutually untrusted workloads inside one provider context unless the provider supplies that isolation;
- guest-memory pinning limits, IOMMU policy, or cache-coherency bugs in a future platform adapter;
- artifact-language safety inside an opaque vendor compiler or executable; or
- platform external-memory lifetime and synchronization, which remain an unadvertised feature.
These exclusions do not permit a portable adapter to weaken object ownership, response atomicity, or resource accounting. A future feature that crosses one of these boundaries needs its own threat model, negotiation, conformance evidence, and release audit. The non-normative external-memory handoff design enumerates those gates; it does not narrow this Protocol 1.0 exclusion or authorize advertising the reserved feature.
Review obligations
A change that adds a guest-controlled count, retained allocation, new descriptor topology, new queue, new asynchronous ownership state, or new platform handle must update this document and map the dimension to one authoritative limit. It must also add executable evidence and refresh the normative requirement ledger. Silent defaults, duplicate conflicting caps, and best-effort cleanup across an indeterminate ownership boundary are not conformant.