Skip to content

blackbull.fault_injection

blackbull.fault_injection

BlackBull's deliberate-misbehaviour toolkit.

A single namespace for the two directions of protocol fault injection:

  • Client-side, HTTP/1.1 — :mod:blackbull.fault_injection.scenario_h1 ships a programmable client (driven through :meth:blackbull.client.HTTP1Client.execute_scenario) that emits deliberately bad HTTP/1.1 against a target server: trickled bytes, partial headers, mid-request idle, abrupt RST. :mod:blackbull.fault_injection.oracle_h1 adds a differential oracle for comparing two HTTP/1.1 implementations under the same scenario.

  • Server-side, HTTP/2 — :mod:blackbull.fault_injection.h2_server ships a programmable server that emits deliberately bad HTTP/2 toward a target client: half-closed streams, exhausted windows, illegal SETTINGS, weird frame sequences. A canned-misbehaviour catalogue lives at :mod:blackbull.fault_injection.catalogue.

This module is an opt-in testing instrument. The HTTP/2 server refuses to start when BB_PRODUCTION is set in the environment so a deliberate-misbehaviour code path cannot accidentally fire on a production deployment (see out-of-scope.md).

See docs/guide/fault_injection.md for a tutorial.

Abort dataclass

Hard-close the connection (transport.abort → RST on Linux).

Distinct from the graceful writer.close() / wait_closed() in HTTP1Client.__aexit__. Subsequent steps short-circuit; the executor stops walking the scenario after an Abort.

Category

Bases: str, Enum

Why a differential example was (not) accepted.

Subclassing str makes the values JSON-serialisable directly and keeps assert ctx.category == 'OK'-style sites readable.

CloseGracefully dataclass

Send a GOAWAY then close cleanly.

Subsequent scenario steps short-circuit (this is a terminator just like :class:Abort). error_code is one of the :class:~blackbull.protocol.frame_types.ErrorCodes values; last_stream_id advertises the last stream the server is willing to process — pass 0 to refuse all client streams, or the highest accepted stream ID otherwise.

H2Abort dataclass

Hard-close the connection (transport.abort → RST on Linux).

H2FaultServer

Programmable HTTP/2 server emitting a :class:ScenarioH2.

Async context manager: enter to bind + start accepting, exit to shut down. self.url is set after enter and gives the URL a real h2c client can dial.

Parameters

scenario: The :class:ScenarioH2 the executor walks for each accepted connection. host: Bind host. Defaults to '127.0.0.1' — binding to a non-localhost interface is rejected unless allow_remote is set (the misbehaviour mode is for local tests only). port: Bind port. Defaults to 0 (kernel-assigned random port). allow_remote: Bypass the localhost-only safety check. Off by default. ssl_context: Optional :class:ssl.SSLContext. When provided, the server terminates TLS and self.url is https://...; ALPN must offer h2 so clients negotiating H/2 over TLS (httpx, curl --http2, …) connect cleanly. When None (default), the server speaks plaintext h2c — usable with prior-knowledge clients only.

Attributes

url: http://<host>:<port>/ after start, or https://... when a TLS context is provided. last_result: :class:ScenarioH2Result from the most recently completed connection. None before any client has connected.

wait_for_connection_done(timeout=5.0) async

Block until a client has connected and the scenario finished.

H2FaultServerError

Bases: RuntimeError

Raised when H2FaultServer cannot start.

The most common cause is BB_PRODUCTION being set in the environment: deliberate-misbehaviour machinery refuses to run in a production context regardless of how it was reached.

H2Sleep dataclass

Idle for duration seconds without reading or writing.

ReadResponse dataclass

Read one HTTP/1.1 response from the connection.

timeout bounds the entire status-line + headers + body read. On timeout the executor records the outcome on the :class:ScenarioResult and does not raise — the caller decides whether to treat that as a transport-fail or normal outcome.

Scenario dataclass

A sequence of steps the executor walks against one connection.

from_bytes(raw) classmethod

Decode arbitrary bytes into a scenario.

Total function: every byte string yields a valid scenario, including the empty string (→ empty scenario). Designed so atheris's coverage-guided byte mutations always produce runnable input — the fuzzer never spends cycles on parser errors.

Encoding:

  • The decoder walks raw left-to-right. At each position the next byte selects an opcode via % 4 (every byte value is therefore a legal opcode tag).
  • Each opcode then consumes a small payload from the following bytes. If the payload is short (end of input), decoding stops cleanly and the partial scenario is returned.

Opcode layout::

byte % 4 == 0  → SEND
    next 2 bytes (big-endian uint16) = length;
    next ``length`` bytes = data;
    next 1 byte (% len(_BYTE_INTERVAL_TABLE))
      → byte_interval.
byte % 4 == 1  → SLEEP
    next 1 byte (% len(_SLEEP_TABLE)) → duration.
byte % 4 == 2  → READ
    next 1 byte (% len(_TIMEOUT_TABLE)) → timeout.
byte % 4 == 3  → ABORT
    no payload.  Remaining bytes are discarded — an
    Abort short-circuits execution anyway, so it's the
    natural terminator.

Bounded payload sizes (uint16 length) keep individual scenarios well under 64 KiB, which is what we want for per-iteration fuzz throughput.

from_json(src) classmethod

Parse JSON Lines back to a :class:Scenario.

Skips blank lines so files that end with a trailing newline (the conventional git-friendly shape) parse cleanly.

to_json()

Serialise to JSON Lines: one {"op": ..., ...} per line.

Bytes payloads are base64-encoded so the result round-trips through stdout / git / json.loads without escape ambiguity. Round-tripped by :meth:from_json.

well_formed(raw_request, *, response_timeout=5.0) classmethod

Wrap a complete raw HTTP/1.1 request as a one-shot scenario.

Equivalent to "send these bytes, then read one response". Used by the legacy diff_*.txt corpus loader and by the Hypothesis well_formed_scenario_strategy.

ScenarioH2 dataclass

Sequence of steps a programmable H2 server walks per connection.

Two control knobs sit outside the step list because they apply to the whole connection, not to one step:

  • send_preface: whether the server sends the standard SERVER_PREFACE_BYTES + initial SETTINGS at handshake time. Most catalogue scenarios want this (real H2 clients require it before proceeding); set False to exercise client behaviour against a server that skips the handshake.
  • initial_settings: tuple of (setting_id, value) pairs the server advertises in its initial SETTINGS frame. Used by the "exhausted window" and "illegal SETTINGS" catalogue entries to inject a hostile starting state before the first step runs.

ScenarioH2Result dataclass

Outcome of one :class:ScenarioH2 run.

Mirrors :class:~blackbull.fault_injection.scenario_h1.ScenarioResult's shape so callers can write uniform pytest assertions across protocols.

ScenarioResult dataclass

Outcome of one :meth:HTTP1Client.execute_scenario call.

Exactly one of response / exception / timed_out / aborted is the meaningful field; the others are None / False. The executor never raises, so callers (differential test, fuzz harness) categorise on this object instead of writing try/except boilerplate per scenario.

SendBytes dataclass

Push raw bytes onto the connection.

byte_interval > 0 transmits one byte at a time with that delay between bytes — the primitive slowloris-style stall that lets scenarios express trickled headers or trickled bodies without dropping to a raw asyncio socket.

SendFrame dataclass

Emit one parsed frame onto the connection.

Routed through :class:~blackbull.protocol.frame.FrameFactory so the on-wire serialisation matches the framework's normal output. Use :class:SendRawBytes for frames the factory cannot construct.

SendRawBytes dataclass

Push arbitrary bytes onto the connection.

Escape hatch for malformed frames (illegal type byte, length exceeding SETTINGS_MAX_FRAME_SIZE, etc.) that the typed :class:SendFrame path will not produce.

byte_interval > 0 transmits one byte at a time with that delay — useful for stalled-handshake patterns where the client is expected to enforce a preface-completion timeout.

SideOutcome dataclass

One side's response in a differential pair.

Exactly one of (response, exception) is populated. timed_out is True if the failure was an :class:asyncio.TimeoutError from the per-example wait_for; we record it separately because timeouts are semantically distinct from other transport errors.

Sleep dataclass

Idle for duration seconds without sending or reading.

Useful for post-headers idle, mid-keep-alive idle, and pre-response stall scenarios where the server is expected to time out and close.

StepOp

Bases: str, Enum

Tag for serialising / decoding a step. str mixin so values drop straight into JSON without a custom encoder.

StepOpH2

Bases: str, Enum

Tag used by the JSON serialiser.

WaitForClientFrame dataclass

Block until an inbound frame matches match.

Declarative grammar — see module docstring for the supported keys. Frames the client sends that do not match are still consumed (the executor remains responsive to the wire) but do not advance this step.

On timeout expiry the executor records the miss on :class:ScenarioH2Result and proceeds to the next step.

categorize(ng, bb)

Bucket a differential example into a :class:Category.

Order of the checks matters: both-rejected wins over individual transport failures so we don't flag inputs that nginx also refused.

frame_matches(frame, match)

Return True iff frame satisfies every key in match.

Recognised keys: type, stream_id, flags_set, flags_unset. Unknown keys fail closed — an unrecognised match key is almost certainly a typo in a catalogue entry, and silently matching on a missing key would hide the bug.

make_self_signed_h2_context()

Return a server-side SSLContext that negotiates h2 via ALPN.

The returned context loads an ephemeral RSA self-signed cert from a tempdir and advertises h2 (and http/1.1 as a fallback) in the ALPN protocol list. Use it with :class:H2FaultServer's ssl_context= parameter.

The certificate path is attached to the returned object as bb_ca_cert_path: str so callers can hand it to a client library (e.g. httpx.AsyncClient(verify=ctx.bb_ca_cert_path)) instead of disabling certificate validation.

Requires cryptography — installed via the [fault-injection] extra (pip install 'blackbull[fault-injection]').

normalize_response(resp)

Drop volatile / framing-only headers; preserve status + body.

run_scenario(host, port, scenario) async

Execute scenario against (host, port) and return (outcome, wire_bytes).

Drives the scenario through :meth:HTTP1Client.execute_scenario, which itself never raises; this wrapper only adds an outer asyncio.wait_for so a runaway scenario (e.g. trickled bytes plus a long read timeout) can't blow the per-side budget.

Returns the captured wire bytes (both servers receive identical bytes, so a single capture is enough for failure diagnostics).

scenario_h2_from_json(src)

Parse JSON Lines back to a :class:ScenarioH2.

scenario_h2_to_json(scenario)

Serialise scenario to JSON Lines (one step per line).

Header lines (send_preface flag, initial_settings) sit on the first line under the op HEADER so the file is one line-oriented stream with no out-of-band metadata.

serialize_frame(frame)

Convert a :class:FrameBase instance to wire bytes.

Restricted to the frame types this server emits: SETTINGS, WINDOW_UPDATE, RST_STREAM, GOAWAY, PING, DATA. Anything else must go through :class:SendRawBytes instead.