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_h1ships 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_h1adds a differential oracle for comparing two HTTP/1.1 implementations under the same scenario. -
Server-side, HTTP/2 — :mod:
blackbull.fault_injection.h2_serverships 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
rawleft-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 standardSERVER_PREFACE_BYTES+ initial SETTINGS at handshake time. Most catalogue scenarios want this (real H2 clients require it before proceeding); setFalseto 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.