Chio/Docs
LOGIN · JOIN

Adversarial testing

Write security tests that fail when a defense breaks, by varying one hostile input beside a valid neighbor.

Research chio@5f63e6e

On this page

A Chio security test fails when its defense is broken. It holds a valid neighboring operation fixed, changes one attacker-controlled input, and reads the protected resource through an oracle that never asks the defense what happened.

The method assumes the test reaches the boundary it names. A malformed message exercises a parser, and a connector can return Deny after it has already written. Conformance cases, mutation testing, fuzzing, temporal counterexamples, and process-failure matrices each answer a different question, and each result counts only for the production boundary it exercised.

Effect oracleChoose a test case.
harnesssystem under testdecodewritesreturns Allowreads the bytesrvalid neighborgdefensefprotected resourceooraclechanged as expectedharnesssystem under testdecodewritesAllowreads the bytesrvalid neighborgdefensefprotectedresourceooraclechanged as expected

The permitted operation changes the resource

The same identity, capability, and policy perform the intended work. The oracle reads the resource and sees exactly the expected change, so the fixture can reach the boundary. Engineering standard.

One security test: the request, the defense under test, the protected resource, and an oracle that reads the resource without asking the defense. Concurrency, restart, and the receipt the kernel signs are left out.

A refusal counts only beside a working neighbor and an independent oracle

A negative test passes by accident when its setup is broken: a bad signature, an unavailable service, or a tool that never starts all produce a refusal. Build the permitted operation first, keep its identity, capability, and policy, and change only the input that represents the attack. The test then asks whether the control tells two usable operations apart. Name the stage the attack targets before writing the assertion: request decoding, signer identity, scope, captured dispatch, executor effect, durable return, or delivery.

Attacker variationIndependent oracleValid neighbor
Broaden one customer price sheet's reader setJoined label equals the exact intersectionThe destination for the pricing team and the pricing lead stays allowed
Change tool or server under one capabilityConnection dispatch count stays flatThe admitted workspace server's read_file still runs
Change the output after a committed callReceipt and output content binding refusesThe unmodified response verifies
Interrupt after commit, before acknowledgementOriginal operation and response survive restartExact original retry, no second effect
Withdraw the writer after its handoffNew writer operation refuses; message remainsAn authorized reviewer reads the handed-off work

The oracle reads the resource from the test side. For a file it reads the bytes after the call, for a provider a controlled receiver records accepted requests, and for a response contribution it queries the adapter's durable control state. A refused operation leaves the resource unchanged, an allowed one changes it exactly as expected, and an unknown outcome keeps its uncertainty and accounting.

What to keep valid and what to vary, per boundary
  • Cumulative quota. Keep identity, capability, policy, and tool fixture; vary competing reservations and capture order.
  • Egress clearance. Keep the authenticated caller and a valid invocation; vary the destination label or a retained source restriction.
  • Approval binding. Keep eligible independent voters and valid signatures; vary a committed operation field after approval.
  • Broker attempt. Keep the provider fixture and original request; vary reuse after capture or loss of completion.
  • Response rollback. Keep the authorized plan and applied contribution; vary adapter failure, a stale fence, or an overlapping owner.

A wrong signer is a good signature test and a poor fixture for a quota race, because signature validation refuses the request before it reaches accounting.

Read more in Engineering standard, where a connector that writes before it checks returns a genuine Deny and the byte oracle catches it, and in Threat coverage, where the tool and server pivot test reads dispatch counters in two deliberately permissive connections.

A deliberately broken defense must make the test fail

Mutation testing checks the test itself. Weaken the decision the test claims to cover, and the protected-state observation must fail. The security adversarial inventory carries these mutations:

  • Reverse the label reader-subset relation. A broadened destination receives restricted data.
  • Allow a missing clearance. An undeclared destination reaches egress.
  • Ignore a security-store error. Unavailable authority becomes a successful decision.
  • Move a tripwire check after dispatch. A canary causes an effect before the refusal.
  • Substitute ingest time for event time. A late event enters a temporal match.
  • Omit an approval-plan field. A changed response is accepted under the old authorization.
  • Report lifted while a contribution remains. Cleanup evidence contradicts the runtime.

The inventory also mutates declassification replay and root-only rollback. A caught mutant ties the assertion to the behavior it changed. A surviving mutant points at a weak test, an equivalent mutation, or a wrong statement of the property, and each needs a different repair.

Read more in Mutation testing.

The reader-intersection counterexample refuses both broader audiences

Your pricing team owns both customer price sheets. Harbor Goods' sheet may be read by the pricing team, your pricing lead, and Harbor Goods' contact; Ostrander's by the pricing team, the pricing lead, and Ostrander's contact. A quote analysis that read both must keep the intersection, the pricing team and the pricing lead, because sending it to either customer's contact would disclose the other customer's terms. Both hostile destinations are well-formed known labels, so the program reaches the lattice and egress decision instead of a parser.

security-research/examples/reader_intersection.rsrust
use chio_flow::{authorize_egress, EgressDenial};
use chio_security_types::{InformationLabel, PrincipalId};
use std::collections::{BTreeMap, BTreeSet};

fn readers(names: &[&str]) -> InformationLabel {
    let owner = PrincipalId::new("larkfield-pricing").unwrap();
    let readers = names
        .iter()
        .map(|name| PrincipalId::new(*name).unwrap())
        .collect();
    InformationLabel::try_known(BTreeMap::from([(owner, readers)]), BTreeSet::new()).unwrap()
}

fn main() {
    let harbor_goods = readers(&["larkfield-pricing", "mara-okafor", "priya-raman"]);
    let ostrander = readers(&["larkfield-pricing", "mara-okafor", "tomas-ibarra"]);
    let joined = harbor_goods.join_restrictions(&ostrander).unwrap();
    let expected = readers(&["larkfield-pricing", "mara-okafor"]);

    assert_eq!(joined, expected);
    assert_eq!(authorize_egress(&joined, Some(&expected)), Ok(()));
    assert_eq!(
        authorize_egress(&joined, Some(&harbor_goods)),
        Err(EgressDenial::FlowViolation)
    );
    assert_eq!(
        authorize_egress(&joined, Some(&ostrander)),
        Err(EgressDenial::FlowViolation)
    );
    println!("Shared-owner readers intersect; neither customer's audience may receive the result");
}
Terminalbash
chio new security-research --example security-research
cd security-research
cargo run --locked --quiet --example reader_intersection
Output
Shared-owner readers intersect; neither customer's audience may receive the result

You should see the one line the program prints after every assertion holds: the join, the exact comparison with the expected label, one allowed destination, and two destinations refused with EgressDenial::FlowViolation. The project carries the unchanged production label and egress modules with a source manifest, and the delegation guide builds its host from the same project.

A mutation that unions the readers or reverses the destination-subset comparison breaks these assertions. Keep the owner and identifier constructors valid while you mutate that comparison, so a caught failure names the lattice decision. The full security-research project adds independent-owner and compartment cases. The program is a portable policy assertion: it opens no receiver and says nothing about operating-system isolation.

Races, crashes, and detectors are tested at the transition they contest

Place two workers at the same decision with explicit barriers or retained transition hooks; a sleep that reproduces a race on one machine misses it on a faster one. Run both orders of each pair: revocation before and after capture, either response expiring first, and the original completion before and after a retry. Then read the durable history and the independent effect count, recording when each worker acquired ownership, when the original capture committed, and which worker's mutation was accepted.

Terminalbash
chio security selftest restart caller-delivery deception temporal --evidence ./evidence
Output
suite            result
restart          pass
caller-delivery  pass
deception        pass
temporal         pass   10 inventories, 40 tests
4 suites passed; evidence in ./evidence

The four suites ship with the release and run against the installed binaries. The restart suite covers physical capture, outcome persistence, release ownership, terminal projection, and competing workers; the caller-delivery suite covers the pinned executor, original delivery, durable report, and recovery binding. Each suite checks an exact named inventory, and ./evidence keeps every log with a SHA-256 manifest.

A crash test keeps its physical cutpoint and the state that survived it: stopping after an in-memory return, inside a database transaction, and after a provider accepted a request are three different tests. The swarm download crashes the host after a mailbox send and reads the mailbox independently; mailbox attribution runs changed-sender conflicts and expired-cursor recovery against the original message binding. Detector tests need positive matches, clear cases, expired artifacts, wrong tenants, altered signatures, late events, duplicates, and capacity failures; response tests need partial application, overlap, stale fences, out-of-order expiry, process death, and failed rollback.

Read more in Recover an operation, which separates what the test observes in capture-before-effect and effect-before-report cases from what a production host can conclude.

A caught defect and an unusable run are classified apart and kept

An assertion failure from the intended mutant supports the coverage claim. A compiler error, a missing fixture, a timeout before the boundary, or an empty filtered run is its own classification: it may reveal a problem, and it carries no evidence about the targeted behavior. A detector that loses its state reports a health failure, which an operator reads differently from a healthy detector with no match.

Fuzzing explores malformed encodings, size limits, parser disagreement, and unexpected transition sequences. Bound bytes before decoding and collections after it, and treat duplicate fields and unsafe numeric representations as attacks on the canonical commitment between signed JSON and typed state. Minimize a failing input while keeping its path to the enforcing function, and record the stage it reached: decoding, canonicalization, authority validation, state transition, or dispatch. Keep the original input when reduction changes timing; a parser counterexample and an effect-boundary counterexample support different claims.

  1. Retain the run. The unmodified baseline, the exact mutation, the command, toolchain, source identity, logs, the failure classification, and the protected-resource observation, with skipped and ignored cases visible.
  2. Repair and rerun. Run the retained counterexample against the fix while the valid neighbor keeps passing, so the repair restores the distinction instead of disabling the feature.
  3. Add it to the inventory. Put the case in the named inventory its gate checks, so a refactor that stops running it fails that gate.

Each result qualifies the boundary it reached. Portable component tests, kernel confinement runs, and durable recovery cases are separate evidence, and threat coverage connects each to the claim it supports.

Read more in Fuzzing and Differential tests.

Next: find the gate that runs your test