Repository navigation
fix: comprehensive audit — 66 security/correctness/performance findings resolved - #25
Merged
Merged
Conversation
Audit findings H2 and L1: - MethodFilter::from_method mapped unknown methods to an empty bitmask, which subset-matches every registered filter, so extension methods (PURGE, PROPFIND, ...) executed the first registered handler with 200. Unknown methods now get a dedicated EXTENSION bit and fall through to 405 Method Not Allowed. ANY keeps catch-all semantics. - HEAD requests on GET-only routes returned 405. MethodRouter::call now serves HEAD with the GET handler when no dedicated HEAD handler is registered (RFC 9110 section 9.3.2); explicit .head() takes precedence. Adds regression tests in both crates. Co-Authored-By: Claude <noreply@anthropic.com>
…M14) H1 — no request body size limit anywhere: Bytes, String, Json and Form buffered the entire request body with no cap, letting one unauthenticated request force an attacker-chosen heap allocation. New arvik_core::body_limit module provides DefaultBodyLimit extension, DEFAULT_BODY_LIMIT (2 MiB), BodyLimitError/BodyTooLarge, and a resolve_limit helper. Body::to_bytes_limited rejects oversized Content-Length early and caps mid-stream otherwise. All four buffering extractors now reject over-limit bodies with 413 Payload Too Large (new JsonRejection/FormRejection::PayloadTooLarge variants). DefaultBodyLimitLayer raises/disables the limit per route; re-exported from the facade alongside DEFAULT_BODY_LIMIT. M14 — RequestBodyLimitLayer buffered every body fully into memory: replaced the collect-then-check implementation with a LimitedBody frame filter that passes chunks through untouched and errors with BodyTooLarge once the limit is crossed. Readers map that marker to 413, so streaming uploads survive and enforcement happens during the read. Co-Authored-By: Claude <noreply@anthropic.com>
Router::nest consumed only other.routes, silently discarding the sub-router's .layer()/.route_layer() middleware — auth, CORS or rate-limiting attached inside a nested router never ran, a silent security misconfiguration. merge() had the same hole for layers. - nest/merge now fold sub-router layers onto each flattened route's method-router stack (handler <- method layers <- route_layer <- layer), preserving the documented ordering. - Documented flattening caveat: sub-router layers run on matched routes only; unmatched prefix paths fall through to the outer fallback. - nest now panics with guidance when the sub-router has its own .fallback() instead of silently dropping it (flattening cannot scope a fallback under a prefix). - New MethodRouter::extend_layers pub(crate) accessor; regression tests cover nest (layer + route_layer ordering), no leakage to parent routes/unmatched paths, fallback rejection, and merge preservation. Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings H4, H5, L7. H4 — one transient accept error killed the whole server: all three accept loops propagated errors with `?`, so EMFILE/ENFILE (fd exhaustion under load) or ECONNABORTED permanently stopped listening, and an unsupported tcp_keepalive_retries setting killed the server on the first connection. Errors are now classified in handle_accept_result: transient failures are logged and skipped, resource exhaustion backs off 100ms before continuing, only genuinely broken listeners (EBADF/ EINVAL/EACCES) remain fatal. apply_stream_options failures drop the single connection instead of the server; accept workers pace persistent failures instead of spinning. H5 — TLS handshakes had no timeout, so slow-loris clients pinned a task and socket forever. rustls and native-tls accepts are wrapped in tokio::time::timeout driven by ServerConfig::handshake_timeout (default 10s; None disables). L7 — no user-configurable HTTP/1 hardening surface: added http1_header_read_timeout and http1_max_buf_size knobs wired into the auto builder. Co-Authored-By: Claude <noreply@anthropic.com>
H6 — spoofable keys: extract_ip trusted client-controlled X-Forwarded-For/X-Real-IP before the socket address with no trust config, so any client minted fresh buckets per request (and each new key grew the shared map). Clients are now keyed by socket address by default; proxy headers are honored only via RateLimitLayer::trust_proxies, applying the rightmost-untrusted-hop rule, requiring well-formed IPs (garbage falls back to the peer), and bounding custom-header key length at 128 bytes. M8 — window silently ignored: Bucket::new hardcoded refill_rate = capacity/second, so new(100, 60s) admitted 6000/min. refill_rate is now capacity/window; the constructor panics on zero windows. M9 — one global Mutex<HashMap>: every request serialized behind a single lock and buckets were never evicted. State is now sharded into 32 independently locked maps (hot path allocates nothing for existing keys) with an amortized sweep evicting buckets idle >5 minutes. Tests: spoofed-header rejection, untrusted-peer rejection, rightmost-untrusted-hop resolution, malformed-XFF fallback, key bounding, refill-rate math, window-driven Retry-After, idle eviction, concurrent multi-shard throughput. Co-Authored-By: Claude <noreply@anthropic.com>
…H7, M13) H7 — MapRequestBodyLayer/MapResponseBodyLayer replaced the body but reused the original parts untouched, so any size-changing transform (including the documented append-footer example) left a stale Content-Length: hyper framed responses by the old length producing truncated bodies/connection errors, and request-side length checks saw a value that no longer described the body. Both layers now refresh Content-Length to the transformed size whenever the header is present. M13 — DecompressionLayer removed Content-Encoding but kept the compressed-size Content-Length (~N times smaller than the real body), defeating downstream size checks. The decompressed length is now set. Regression tests cover both transform directions (docs' own append-footer example) and gzip request decompression. Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings M2, M15, M16, L22. M2 — router percent-decoding handed handlers traversal-ready strings: %2F decoded to / and %2E%2E to .. inside captured params after matching, so encoded dot-segment paths reached filesystem consumers. percent_decode now keeps structure-changing escapes encoded (%2F, %5C) and control characters escaped (%00, %0A, %0D); literal URL separators and ordinary decodes are unaffected. M15 — ServeDir/ServeFile built Content-Length/ETag/Last-Modified from a metadata() stat taken before a separate File::open: a deploy swapping the file in between produced mismatched headers, truncated bodies, or poisoned caches. The file is now opened first and metadata is read off the open descriptor, so validators always describe the streamed bytes. M16 — If-Range was ignored entirely; resumable clients could stitch bytes from two file versions into corrupt files. Ranges are now gated by strong-comparison If-Range validation (ETag exact match or exact Last-Modified date); stale validators downgrade to a full 200. fs_etag switched from weak (W/) to strong (nginx-style mtime+len) so If-Range can function at all. L22 — symlinks were followed without containment, allowing reads outside the root when a link could be planted inside it. ServeDir now canonicalizes both sides and rejects requests resolving outside the root by default; opt out with .follow_symlinks(true) for trusted trees. Tests: decode preservation, weak-etag If-Range refusal, matching vs stale If-Range (206 vs 200), symlink escape blocked by default and allowed on opt-in. Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings M5, M6, M20, M21, L12, L23. M5 — graceful shutdown never signalled connections: hyper kept serving NEW requests on established keep-alive connections for the entire drain window and then hard-aborted in-flight work. Each connection task now holds a watch::Receiver; on shutdown the accept loops signal it and the connection calls hyper's graceful_shutdown() — idle connections close immediately, in-flight responses finish. The drain timeout remains as a last-resort abort. Integration test proves an idle keep-alive connection now closes in ~0s instead of holding a 30s drain. M6/M20 — TLS and config hot-reload watched file inodes: temp-file+ rename deploys and certbot symlink rotation silently orphaned the watches forever. Both now watch parent directories (deduplicated), filter to create/modify/remove events, resolve by path at reload time, and forward watcher errors arriving during the debounce window instead of swallowing them. M21 — OtelConfig::install() published the global tracer provider before trying to install its subscriber; on conflict with ArvikLogger::init it returned Err leaving the provider's exporter task running forever. Subscriber installation now happens first; on failure the provider is shut down and the global reference reset before returning. Added ArvikLogger::init_with_otel() composing log + OTel layers into ONE subscriber so both features can be used together. L12 — catch_panic called JoinError::into_panic() without is_panic(), re-panicking on task cancellation during shutdown; cancellations are now handled as normal termination. L23 — unsupported Sec-WebSocket-Version returned 400 JSON; RFC 6455 requires 426 Upgrade Required with Sec-WebSocket-Version: 13. Co-Authored-By: Claude <noreply@anthropic.com>
M10/L14 — Bearer tokens, Basic passwords, and CSRF double-submit tokens were compared with short-circuiting string equality, leaking matched prefix length byte-by-byte through response timing. All three now use a constant-time XOR-accumulate comparison (secret_eq). M11 — very_permissive() mirrored any origin together with Access-Control-Allow-Credentials: true, making every site a trusted credentialed origin — CORS fully disabled while looking merely convenient. very_permissive() no longer enables credentials, and the credentials + wildcard/mirrored-origins combination now panics at construction with actionable guidance instead of silently shipping. allow_credentials(true) also no longer auto-switches wildcard origins to mirroring (that switch was the vulnerability). L15 — cacheable preflight responses carried only Vary: Origin even when methods/headers were reflected per request; a CDN could serve one page's preflight to another and have browsers block legitimate calls. Preflights now emit Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers. Tests: secret_eq semantics, bearer strategy behavior, construction-time panic for both dangerous combos, explicit origins + credentials accepted, preflight Vary completeness. Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings M3, M7, M12, M22, M24, L13, L19, L26, L31. M3 — router dispatch rebuilt middleware stacks per request: every routed request deep-cloned the whole MethodRouter (clone_box per handler), allocated a base service, and re-folded every route layer. MethodRouter handlers now live behind an Arc (clone = refcount bump; builders use copy-on-write), and into_service() pre-folds route_layers into ready-made services so dispatch clones one stack. BoxCloneService now requires Send+Sync, matching axum's service bounds. M7 — TCP_NODELAY defaults to enabled; Nagle x delayed-ACK stalled small responses by tens of ms (.tcp_nodelay(false) to opt out). M12 — response compression streamed through async_compression encoders via StreamReader/ReaderStream instead of buffering the entire body: TTFB stays live for large responses and peak memory drops from two whole-body copies to one chunk. HEAD skips compression; Content-Length is removed for streamed output. M22 — OTEL stdout exporter uses batch processing instead of the simple processor that serialized + wrote JSON inline on request workers. M24 — /metrics gathers and encodes inside spawn_blocking instead of stalling tokio workers during scrapes. L13 — catch_panic wraps the handler future in catch_unwind directly, removing a per-request task spawn. L19 — security headers precompute HeaderValues at layer construction; no config clone or HSTS format! per request. L26 — Path extraction borrows params (&str) through the deserializer; no per-key/per-value String clones or per-field copies. L31 — logging/tracing middlewares skip span-field string building and propagator extraction when INFO spans are filtered out anyway. Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings M1, M17, M18, M19, L4, L5, L6, L10; M4 partial; L9 verified N/A. M1/L4 — ResponseBuilder::json no longer .expect()s on serialization failure (returns 500 like Json<T>), and body() surfaces deferred builder errors — e.g. a user-controlled Redirect target containing control characters — as a plain 500 instead of panicking the connection task. L5 — invalid header names/values in tuple responses and AppendHeaders are now logged loudly instead of silently dropped (a vanished CSP or Set-Cookie previously produced no signal at all). L6 — From<String>/From<&'static str> for Error keep messages internal; with_message() remains the explicit opt-in for client-visible text, matching the crate's leak guarantee. M17 — SSE Event::id truncates at the first CR/LF in addition to stripping NUL, closing the framing-injection vector that silently dropped event payloads. M18 — typos in ARVIK_* env vars inside known config sections now fail the build naming the variable instead of silently using file/default values; foreign variables sharing the prefix are still ignored. M19 — HTTP/2 window sizes above the h2 protocol maximum ((1<<31)-1) are rejected at validation time instead of panicking inside h2 on the first HTTP/2 connection. L10 — RustlsConfig::with_alpn_http2_only() restricts the ALPN offer to h2 for http2_only deployments that previously invited http/1.1 negotiation they could not honor. L9 — verified not applicable on native-tls 0.2.18: accept_alpn is infallible there. M4 (partial) — connections rejected at max_connections now receive a best-effort 503 Service Unavailable + Connection: close before the socket is dropped, instead of a bare TCP reset load balancers read as a dead node. Co-Authored-By: Claude <noreply@anthropic.com>
Audit findings H8, M23, L2-L5, L8, L11(doc), L16-L18, L20, L21,
L24, L25, L27-L30, L32-L34.
M23 — logs and traces record uri.path() only; query strings routinely
carry reset tokens and API keys.
H8 — TlsSection has a manual Debug that renders pkcs12_password as
***REDACTED***; any {:?} of the config is now safe.
L2 — path params take their names from matchit's matched keys instead
of positional pattern parsing that misaligned mid-segment params
(/images/img{id}.png/{tag} exposed wrong names). Regression test added;
the param_names plumbing is gone entirely.
L3 — percent_decode returns Cow (no alloc on the no-% fast path);
nest_service prefix comparison no longer formats a String per request.
L8 — on_connected/on_disconnected hooks spawn fire-and-forget so a
blocking hook cannot stall the accept loop.
L11 — CatchPanic's doc scope now states plainly that body-streaming
panics occur outside its wrapper (handler panics remain covered).
L16/L21 — new arvik_core::accept module parses real RFC 9110 q-values:
explicit ;q=0 refusals beat the * wildcard, multi-line header values
combine, priority order preserved. Compression middleware and static
precompressed selection both use it — brotli is no longer served to
clients that refused it with br;q=0.
L17 — DefaultMakeSpan::include_headers(true) actually records request
headers into the span field http.request_headers.
L18 — trace middleware instruments the handler future instead of
holding span.enter() across await (thread-local stack corruption).
L20 — unknown Range units and multi-range requests serve a full 200 as
RFC 9110 requires, not 416.
L24 — Form validates Content-Type via mime parse (…urlencodedfoo no
longer passes).
L25 — MissingPathParams reports the server-side wiring mistake as 500,
matching sibling rejections.
L27 — env parsing uses vars_os, skipping non-UTF-8 variables instead of
panicking on unrelated process state.
L28 — client-supplied x-request-id must be printable ASCII <= 128 bytes
or it is replaced with a fresh UUID.
L29 — failed health checks log the underlying error and return a
generic "check failed" in the public /health/ready body.
L30 — readiness probes cache results for a configurable TTL (default
1s) so kubelet/LB bursts cost one dependency round-trip.
L32 — proc macros expand FoundCrate::Itself to `crate` per
proc-macro-crate docs.
L33 — the test cookie jar honors Max-Age<=0/past Expires deletions and
merges with explicit Cookie headers instead of clobbering them, so
logout assertions behave like real browsers.
L34 — workspace is clippy-clean: items-after-test-module fixed and the
test module moved to end of file.
Co-Authored-By: Claude <noreply@anthropic.com>
Local run of all six ci.yml commands surfaced issues invisible under default-feature builds: - accept_native_tls never created or sent its connection-shutdown watch channel: graceful shutdown (M5 fix) silently did not apply to the native-tls path, and the loop failed to compile once referenced. - clippy --all-features flagged an identity conversion in ArvikLogger init (try_init already returns the LoggerError box type). - cargo doc emitted 75 broken intra-doc links and 9 function/module ambiguity warnings across facade docs, middleware tables, router service docs, ws split/message, static, and body docs. All resolved: unresolvable references demoted to plain code spans, ambiguous names disambiguated with () URLs. Verified end to end with RUSTFLAGS=-D warnings: check (default + all features), clippy --all-features -D warnings, test --all-features (171 passed / 0 failed), fmt --check, doc --all-features --no-deps (0 warnings). Co-Authored-By: Claude <noreply@anthropic.com>
Mirrors the .github structure from our other projects: - ISSUE_TEMPLATE/bug_report.md — minimal arvik server reproduction skeleton, environment table with feature flags - ISSUE_TEMPLATE/feature_request.md — use-case-first format with affected-crate list matching the workspace - PULL_REQUEST_TEMPLATE.md — change-type checklist aligned with the actual CI gates (fmt, clippy --all-features -D warnings, test --all-features, doc) - FUNDING.yml — sponsor button configuration - release-notes/0.9.5.md — per-version release notes for the performance sprint, following the established format Co-Authored-By: Claude <noreply@anthropic.com>
CI's stable toolchain (rustc/clippy 1.98) enables clippy::result_large_err by default and flags the nine arvik-ws socket functions that return tungstenite::Error by value (~200-byte enum). Boxing the error would break the public API contract and allocate on every send/recv call; the large-variant cases are rare runtime failures. Adds a documented crate-level allow instead. Verified on rustc 1.98.0 (same as CI): cargo clippy --workspace --all-features -- -D warnings passes with zero errors. Co-Authored-By: Claude <noreply@anthropic.com>
3 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Comprehensive Security, Correctness & Performance Audit — 66 Findings Resolved
Overview
This branch resolves all 66 findings from a full line-by-line audit of every
.rsfile across the 14 workspace crates (~23.5k lines). The audit coveredcorrectness bugs, security vulnerabilities, protocol-compliance issues,
reliability gaps, and performance losses, cross-verified against vendored
dependency sources (hyper 1.9, h2, multer 3.1, cookie 0.18, matchit 0.8.6,
tokio 1.51). Two findings were reproduced at runtime before fixing.
Scope: 14 commits · 64 files changed · +4,081 / −721 lines · ~90 new tests
Full finding-by-finding tracker (IDs
H1–H8,M1–M24,L1–L34) lives inAUDIT_FINDINGS.md(kept out of the repo per project preference).Security fixes (14 findings)
Critical behavior corrections
H2): an empty method bitmasksubset-matched every registered filter, so a
PURGErequest on aDELETE-only route ran the DELETE handler with 200 — runtime-confirmed.
Extension methods now get a dedicated
MethodFilter::EXTENSIONbit and fallthrough to
405;ANYkeeps catch-all semantics. (runtime-confirmed)H1):Json/Form/Bytes/Stringbuffered arbitrary attacker-chosen payloads into one heap allocation. A new
arvik_core::body_limitmodule enforces a 2 MiB default with413rejections;
DefaultBodyLimitLayerraises/disables it per route.nest()silently dropped sub-router middleware (H3): auth/CORS/ratelimiting attached inside a nested router never ran. Layers now fold onto
flattened routes; a sub-router
.fallback()panics loudly instead ofvanishing.
H6+M8+M9): keys camefrom spoofable
X-Forwarded-For(fresh bucket per header value), the windowparameter was discarded (
100/minactually allowed6000/min), and oneglobal mutex serialized all requests with unbounded key growth. Rewritten:
socket-address keys by default,
trust_proxies()rightmost-untrusted-hopresolution, real
capacity/windowrefill, 32 lock shards, idle-bucketeviction.
H5): trickled ClientHello bytes pinnedtasks/sockets forever. Now bounded by
ServerConfig::handshake_timeout()(default 10 s) on both TLS backends.
very_permissive()mirrored any origin with credentials (M11),making every site a trusted credentialed origin. Credentials are dropped
from that preset and the combination now panics at construction with
guidance.
Path safety & static files
%2F,%5C,%00, CR/LF stay encoded post-match (M2).deploys mid-request could truncate responses or poison caches. File is
opened first; metadata is read off the open descriptor (
M15)..follow_symlinks(true)opts in (L22).Hardening
(
M10,L14).From<String> for Errorno longer reflects interpolated strings intoclient responses;
.with_message()is the explicit opt-in (L6).M23); client-suppliedrequest ids validated or regenerated (
L28); health-check failure detailslogged instead of returned in the public probe body (
L29); keystorepassword redacted from config
Debugoutput (H8).Reliability & correctness (25 findings)
H4): one transient error (EMFILE,ECONNABORTED) used to kill the whole server permanently. Errors are nowclassified — transient skipped, resource exhaustion backs off 100 ms, only
broken listeners remain fatal. TCP-option failures drop one connection, not
the server.
M5): connections were never signalled — hyperserved new requests during the entire drain window, then hard-aborted.
Each connection task now holds a shutdown receiver and calls hyper's
graceful_shutdown(): idle keep-alive closes instantly (integration testmeasures ~0 s vs the old 30 s), in-flight responses finish, the timeout is a
last resort. Over-capacity rejects answer
503 Connection: closeinstead ofa bare RST (
M4partial; default cap stays opt-in by design).inodes, so any temp-file+rename or certbot symlink rotation silenced them
forever — both watch parent directories now (
M6,M20). OTEL provider nolonger leaks its exporter task when subscriber install conflicts; new
ArvikLogger::init_with_otel()composes logging+tracing into one subscriber(
M21). Lifecycle hooks spawn fire-and-forget so blocking hooks can't stallthe accept loop (
L8).Content-Lengthafter body transforms anddecompression caused truncated/corrupt framing (
H7,M13); SSE event idssanitize CR/LF, closing payload-drop injection (
M17);If-Rangeis nowenforced with strong comparison so resumable downloads can't stitch two file
versions (
M16, fs ETags switched weak→strong); unknown Range units andmulti-range requests serve full 200 per RFC 9110 (
L20); WS versionrejection returns RFC 6455
426+ version hint (L23); HEAD falls back toGET handlers (
L1, runtime-confirmed).ResponseBuilder::jsonand deferred builder errors(e.g. user-controlled redirect targets) return 500s instead of crashing
connection tasks (
M1,L4); catch_panic no longer re-panics on taskcancellation (
L12) and drops its per-request task spawn (L13); h2 windowoverflow is caught at validation instead of panicking on first HTTP/2
traffic (
M19); non-UTF-8 env vars no longer abort config loading (L27);invalid headers log loudly instead of vanishing silently (
L5).ARVIK_*variables inside known sections failthe build naming the variable (
M18); proc macros resolveFoundCrate::Itselfas
crate(L32); test cookie jar honors expiry deletions and mergesexplicit Cookie headers so logout assertions behave like real browsers
(
L33); form content-type validated via media-type parse (L24).Performance (12 findings)
Ranked by measured impact:
M3+L3): handler lists live behind anArc(router clone = refcount bump instead of deep-cloning every boxed handler)
and route layers pre-fold into ready-made services at
into_service()—dispatch no longer rebuilds N wrappers and clones every handler per request.
M12): bodies stream through the encoders viaStreamReader/ReaderStreaminstead of full-buffering — live TTFB forlarge responses, peak memory drops from two whole-body copies to one chunk.
Streaming limit enforcement in
RequestBodyLimitLayerlikewise stopsbuffering (
M14).M9): 32 independent locks replace the singleglobal mutex; the hot path allocates nothing for existing keys.
M7): removes Nagle × delayed-ACK stalls onsmall responses (opt out with
.tcp_nodelay(false))./metricsencodes inspawn_blocking(M24);OTEL stdout uses batch export instead of sync per-span writes (
M22);span-field strings skip building when spans are filtered out (
L31).L19), Pathextraction borrows
&strend-to-end with zero String clones (L26),percent-decode returns
Cow(L3), readiness probes cache results behinda configurable TTL (
L30).New shared
arvik_core::acceptnegotiator implements real RFC 9110 q-valuesemantics (explicit
;q=0beats*, multi-line values combine) for bothcompression selection and precompressed assets (
L16,L21) — brotli is nolonger served to clients that refused it.
Intentional behavior changes (flagging for reviewers)
413; overridable405 Method Not Allowednest()middlewareW/"…")Verification
Every fix landed with regression tests where feasible (~90 added). Final state,
run locally with CI's exact flags:
The CI sweep also fixed three issues invisible under default-feature builds:
the native-tls accept loop was missing its graceful-shutdown channel entirely,
one identity conversion clippy flagged under
--all-features, and ~84 broken/ambiguous rustdoc links across facade docs.
Community files
Also adds
.github/FUNDING.yml,ISSUE_TEMPLATE/{bug_report,feature_request}.md(adapted reproduction skeletons + crate lists),
PULL_REQUEST_TEMPLATE.mdaligned to these exact CI gates, and
release-notes/0.9.5.md.