Skip to content

Latest commit

 

History

History
641 lines (507 loc) · 44.4 KB

File metadata and controls

641 lines (507 loc) · 44.4 KB

Object storage reliability audit

Audit date: 2026-10-02. Baseline: b584ce7 on master, initially clean.

The priority is trustworthy storage behavior across supported runtimes and providers. Expanding the API should follow correctness, bounded resource use, and repeatable compatibility tests.

This began as a source review and local test audit. Subsequent MinIO verification and authorized cloud-provider attempts are recorded separately below; neither establishes a security certification or complete provider compatibility. Findings distinguish implemented repairs from open work.

First repair batch

Implementation is delegated to GPT-6 Luna agents, with parent review and integration.

  • Async-std downloads: replace full-body buffering and suppressed read errors with bounded, lazy chunks and error propagation; stream writer downloads incrementally.
  • Signature V4 queries: encode each key and value before sorting pairs, preserving duplicate-key ordering and existing signing vectors.
  • CI: check formatting without modifying sources, remove duplicate execution, and include nonignored sync tests in the normal test target.

The patches were reviewed and validated locally as recorded below. These repairs do not close the remaining findings. Supporting corrections retain the result of with_path_style() in two existing sync tests and remove redundant formatting borrows flagged by current Clippy.

Findings and repair status

P1: Resolved dependency graph contains known vulnerabilities and unmaintained backends

cargo audit against RustSec checkout 6de4455103aced2cba86e3b86e5c090b22827cf1 reports 30 vulnerability/package matches across 29 advisory IDs, plus 25 warnings, in the existing ignored local Cargo.lock. This is a lockfile-wide result, including optional and development dependencies; it is not a count of reachable exploits or a fresh-resolution result for consumers.

Examples include bytes reserve overflow, time stack exhaustion, and quick-xml namespace allocation and duplicate-attribute complexity. Maintenance warnings cover async-std and Surf.

Feature-specific cargo tree checks confirm that async-std-rustls-tls selects rustls 0.18.1 via Surf/http-client/async-tls, and with-async-std-hyper selects Hyper 0.13.10 via Surf/http-client. Those older major-version constraints will not all be repaired by refreshing the lockfile.

The preserved baseline and fresh-resolution follow-ups are recorded below. The quick-xml constraint is now upgraded to 0.41; ten advisory IDs remain in the fresh graph's legacy async-std transport branches. The next dependency repair requires a maintenance plan for those backends while preserving supported runtime/TLS configurations. No clean security bill is claimed.

Fresh dependency resolution, 2026-10-02

An isolated source snapshot at 6c97cb9 resolved 414 packages without changing the working checkout's ignored lockfile or copying credentials. Both graphs were audited against freshly fetched RustSec database db663534ae858abb3fbad408a041ce04209c377f (1,279 advisories): the fresh graph has 12 vulnerability matches across 12 IDs, versus 30 matches across 29 IDs in the preserved baseline. Thus 18 matches disappear through fresh compatible resolution; no production dependency upgrade was applied here.

Normal-dependency inverse trees across 11 feature configurations locate the remaining affected packages:

  • quick-xml 0.38.4: all configurations; the two recorded advisories require >=0.41.0, outside the current 0.38 constraint.
  • h2 0.2.7, hyper 0.13.10, and tokio 0.2.25: async-std's Hyper configuration.
  • rustls 0.18.1, ring 0.16.20, and webpki 0.21.4: async-std's rustls configuration.

This establishes inclusion in production dependency graphs, not exploitability of each vulnerable code path. Fresh-graph warnings are separate: one notice, 11 unmaintained matches across 10 packages, and five unsoundness matches across four packages. Surf/http-client's existing backend constraints prevent a simple compatible version bump from removing the old HTTP/TLS stacks. A no-TLS HTTP/1 feature-tree probe removed the Hyper branch, but that alternative has not been compiled or compatibility-tested. No backend replacement is claimed.

P1: HTTP 200 can incorrectly report a failed storage operation as successful — repaired in source

At the audit baseline, Bucket::complete_multipart_upload returned response data without inspecting its XML root. The high-level streaming upload reduced that response to status and byte count. Bucket::copy_object likewise returned only the HTTP status. An embedded error could therefore become apparent success.

AWS explicitly documents this behavior for multipart completion and copy operations.

Repair: copy and multipart completion now validate the XML document structure and operation-specific root for 2xx responses. Embedded <Error> responses retain the raw body in HttpFailWithBody, including service codes and request identifiers present in that body. Invalid/truncated XML and unexpected roots return errors. Whitespace heartbeats, namespace variations, and optional fields are supported. Non-2xx handling remains unchanged, and validation does not add automatic retries. Local tests cover all three runtimes with fail-on-err both enabled and disabled. Successful copy and multipart operations also pass against local MinIO as recorded below; transport interruption and hosted-provider coverage remain outstanding. No general response-body rule was added to unrelated operations.

P1: Credential debug output exposes secrets — repaired and integrated in source; release pending

At the audit baseline, aws-creds/src/credentials.rs derived Debug for Credentials and StsResponseCredentials, including secret keys and session tokens. Bucket also derives Debug over its credential storage. This is a disclosure path if consumers log these types; this audit did not inspect or find an actual secret leak.

Repair: local aws-creds now manually redacts key and token values in Debug, including nested STS credential output, while retaining field presence and expiration. Synthetic sentinel tests cover normal/pretty formatting and unchanged serialization. Credential resolution is unchanged. S3 now uses the local crate through a path-plus-version dependency, with a Bucket Debug regression confirming integration. Publishing the coordinated releases remains outstanding; source integration is not a fix delivered to registry consumers.

P1: Multipart failures can leave incomplete uploads behind — repaired in source

At the baseline, high-level streaming uploads could return from reader, part, ETag parsing, or completion errors without aborting the multipart upload. An abort error could also replace the original failure, and non-2xx completion could return apparent success with fail-on-err disabled.

After successful initiation, returned failures now take one logical best-effort abort path and preserve the original error. The later retry-policy repair excludes abort and completion from automatic replay. Async part futures are dropped before cleanup; sync streaming uses a private raw part sender to avoid duplicating the public chunk helper's abort. Its small-file fallback retains its deliberate abort and existing status-return behavior. Public signatures and standalone multipart helpers are unchanged. Completion requires a 2xx status and the existing success-XML check.

A bounded local fixture covers reader errors, failed parts, HTTP completion errors, embedded Error XML in HTTP 200, abort failure preserving the reader error, and sync fallback without a second abort. It checks request sequences, accounts for configured retries without changing global state, and remains available until the client returns to catch extra requests. All three runtimes passed with fail-on-err both enabled and disabled. Bypassing cleanup made the test fail on the missing abort; restoring it passed.

This does not guarantee remote cleanup after cancellation or lost responses. Dropping the caller's future cannot run asynchronous cleanup, and requests already sent can still be processed. AWS documents that in-flight parts may require repeated aborts. The tests do not simulate concurrent in-flight part cancellation or every transport/ETag failure. Full local and provider follow-up is recorded below.

P1: Large uploads silently lose custom headers — repaired and provider-verified

At the baseline, the small-object branch applied builder custom_headers, while the multipart branch forwarded none of them. Metadata, cache policy, storage class, encryption settings, and write conditions could silently change at 8 MiB.

The repair privately routes per-call headers by multipart operation. Object properties and unknown provider-specific headers go to initiation. SSE-C headers are carried through initiation, parts, and completion; expected-owner and requester-pays controls also reach abort. If-Match and If-None-Match go to completion. The explicit content-type argument remains authoritative, matching the existing small-PUT behavior; completion retains its XML content type.

Per-call body framing and whole-object checksum headers that cannot safely be reused for streamed parts now return UnsupportedMultipartHeader before initiation. The error contains only the header name. Small PUTs and bucket-global extra-header behavior are unchanged. Global extras remain caller-controlled; the per-call routing policy does not validate or reinterpret them.

The policy follows AWS's multipart initiation, part upload, and completion contracts. It does not add streaming checksum computation or change public Command variants or method signatures. Local and provider verification is recorded below.

P2: Multipart pagination drops the upload-ID cursor — repaired in source

At the audit baseline, ListMultipartUploadsResult modeled NextKeyMarker but not NextUploadIdMarker. The command and page API likewise omitted the upload-ID marker. Advancing by key alone could skip remaining uploads for the same key when a page boundary fell within that key.

The AWS pagination contract requires both markers for general-purpose buckets. Directory buckets differ.

The command, response model, and page API now carry both markers. Aggregate listing follows the returned pair, clears an omitted upload-ID marker, and rejects missing key markers and repeated/cyclic cursor pairs on truncated responses. Same-key pages with advancing upload IDs remain valid. This changes public method/struct/enum shapes in the planned 0.38 minor release; migration details are in RELEASING.md.

Bounded local HTTP tests pass on Tokio, async-std, and sync. They check encoded upload IDs, same-key uploads spanning pages, key-only continuation, and missing, repeated, and cyclic cursors. Removing upload-ID query forwarding reproduced the regression; restoring it passed. The full make ci gate passed in 136.21 seconds, including rebuilding changed artifacts. The exact ignored MinIO test also passed on all three native-TLS runtimes: two pending uploads sharing one key were returned exactly once across two one-item pages. All tracked uploads were aborted, and independent object/upload listings were empty and untruncated after each run. This establishes MinIO page-API behavior; aggregate edge cases have local fixture coverage, and cloud-provider multipart pagination was not tested in this follow-up.

P2: Upload concurrency follows machine memory, not a caller budget — upper bound repaired

The original calculate_max_concurrent_chunks documented a maximum of 10 but clamped to 100. Each chunk is 8 MiB, so one upload could queue roughly 800 MiB of payload before request-body copies and transport overhead. Multiple uploads independently use the same machine-wide memory estimate.

The implementation now restores the documented maximum of 10 and preserves the minimum of 2 and unknown-memory fallback of 3. Arithmetic stays in u64 until after clamping. Boundary tests fail with the old upper bound and pass on Tokio and async-std with the fix. This is a per-upload chunk-count heuristic, not a process-wide memory budget or RSS guarantee.

This host reports no available-memory estimate, so a proposed large scheduling fixture exercised fallback 3 even with the old upper bound. It was removed rather than retained as misleading, costly cap evidence. Peak RSS, throughput, and multi-upload budgeting remain unmeasured; add an explicit configuration surface only if measurements justify it.

The full make ci gate passed in 112.72 seconds. Exact existing MinIO large-stream tests passed on the final Tokio-native and async-std-native binaries, with independent empty and untruncated object/upload listings afterward. These are compatibility checks of the existing scheduler, not measurements of the upper fanout bound on this host.

P2: Retry policy lacks error and operation classification — repaired in source

At the baseline, the backends used the generic retry! macro to retry every error up to a global count, without classifying replay safety or service errors. The public macro remains unchanged; backend requests now use a private policy that allows reads and multipart PUTs with a stable upload ID, part number, and immutable body. Ordinary PUTs, initiation, completion, copy, delete, abort, and configuration mutations are not automatically replayed after ambiguous failures.

With fail-on-err, HTTP 408, 429, 500, 502, 503, and 504 are retry candidates for eligible operations. Without it, raw HTTP responses remain successful transport results and do not trigger status-based retries. Surf now retains service status and body in HttpFailWithBody. Backend transport-error granularity differs: Surf and Reqwest send failures can include permanent connection/TLS errors; sync retries a narrow set of I/O failures. No error-message parsing is used.

Body consumption stays outside replay. Failed error-body reads retain their original backend error and are explicitly nonretryable. Internal retry logging does not print request URLs, credentials, or service bodies. The global retry count and quadratic delay remain; jitter and caller-specific retry budgets are still separate work. Local wire tests cover service and transport failures, mutation non-replay, identical part replay, final-error preservation, and the sync status path.

All six native-TLS runtime/fail-on-err combinations passed 12 retry tests and the existing multipart failure fixture. The full make ci gate passed in 118.76 seconds; the subsequently strengthened, independent six-status table passed on all three runtimes. The frozen patch then passed 68 exact MinIO tests across 11 configurations and 100 cloud tests across eight configurations and five providers. All 40 cloud prefixes and every MinIO post-test object/upload listing were independently verified empty and untruncated. Superego's final review reported no concerns.

P2: Credential refresh can block async execution and change identity — repaired in source

Previously, refresh ran synchronous provider I/O under an async write lock and reloaded the default provider chain. Refresh now retains its originating STS token-file, container URI, or IMDS mechanism. Token-file refresh rereads the captured lexical path, allowing token rotation. Direct OIDC tokens are not retained; expired credentials without a refresh source return NoRefreshSource. Serialization retains its five public fields and deliberately omits the source. See RELEASING.md for constructor and exhaustive-error-match migration notes.

Async Bucket refresh uses a shared gate, rechecks expiry after acquiring it, runs provider work on a blocking worker, and replaces credentials only on success under a short write lock. Deterministic tests cover source preservation, failure atomicity, token-file rotation, and concurrent callers with responsive credential reads on Tokio and async-std. Cancelling the waiter can release the gate while its blocking worker continues; durable single-flight under cancellation and proactive expiry margins remain open. Live STS/IMDS rotation has not been verified, and source preservation does not pin a remote IAM principal against changes made at the provider.

P2: Low-level UploadPart command differs from the working Bucket path

The public Command::UploadPart variant carries an upload ID, part number, and content, but its URL arm does not forward the multipart query and its payload hash uses the empty-body hash. Current Bucket multipart helpers instead use Command::PutObject { multipart: Some(..) }, which has separate URL and payload handling. Provider tests of the Bucket helpers do not verify this low-level variant. Keep it outside the replay-safe allowlist until its wire behavior has focused signing and request tests; do not infer safety from its variant name.

P2: Signing reads credential fields independently

The request trait reads the access key, secret key, and session/security token through separate Bucket lock acquisitions. Replacing the shared credentials between these reads can produce a signature assembled from different credential generations. This exists independently of the refresh-source repair; serializing refreshes does not make a multi-read signing sequence atomic. A separate repair should use one credential snapshot per signing attempt and cover concurrent rotation in both ordinary and presigned requests. No such repair is claimed here.

P2: Workspace tests do not prove local credentials/region integration — credentials integrated in source

At the baseline, s3/Cargo.toml depended on registry versions of aws-creds and aws-region, so workspace tests could exercise support crates independently while S3 linked registry copies. S3 now uses local aws-creds through a path-plus-version dependency; aws-region still comes from the registry.

The credential release order and package-verification boundary are documented in RELEASING.md. Registry verification awaits the credential release. Local region integration remains separate outstanding work.

P2: Timeout behavior differs by backend and API — Tokio and async-std repaired in source

At the baseline, set_request_timeout only changed the public field while the cached Tokio client retained its configuration. with_request_timeout rebuilt that client and reset proxy/TLS settings. The Surf request path did not consult the bucket timeout, and documentation referred to an older Hyper backend and inconsistent defaults.

Tokio now applies the bucket's timeout to each HTTP request, including the response body. Both response and status paths read the current field, and the timeout builder reuses the configured client. The default 60-second timeout is now enforced; None removes that total deadline. This is a per-request policy, not a deadline for an entire multi-request operation or its retries.

Bounded local tests cover delayed headers, stalled public streams/writers, exact partial writer output without replay, clearing a builder-configured timeout, and preservation of proxy/TLS options. Disabling request timeout application reproduced both header and status-path regressions. The final full make ci gate passed in 110.55 seconds. All 25 targeted MinIO tests passed across Tokio native TLS, blocking, no-TLS, and rustls configurations. All 35 cloud tests passed across Tokio native TLS, rustls, and blocking against AWS, Wasabi, GCS, R2, and DigitalOcean. Independent listings confirmed empty, untruncated test prefixes after those runs.

Async-std now uses a reusable private Surf client with its transport timeout disabled and applies an absolute deadline per attempt, from send through body reads. A reader wrapper preserves the deadline in the public Surf response, buffered downloads, writer copies, and lazy streams. None removes the library deadline. Tests cover delayed headers, late body consumption, exact partial writer output, lazy streams, error-body reads, timeout changes, and connection reuse across native TLS, rustls, and Hyper, with fail-on-err on and off. Bypassing the body wrapper made the public-response regression test fail. These deadlines do not bound an arbitrary external writer's own blocked write. Sync behavior remains transport-specific.

P2: R2 rejects generated body headers on bodyless requests — repaired and provider-verified

Live R2 tests reproduced HTTP 403 on range GET in Tokio, async-std, and sync, after successful PUT, ordinary GET, and existence checks. The shared header builder generated and signed Content-Length: 0; R2's error body showed that header's value missing from its canonical request. Omitting generated body headers for ranges allowed both range reads to pass, then exposed the same problem on DELETE.

The repair omits generated body headers for GET, HEAD, and DELETE commands, which all have empty request bodies in the current command model. Caller headers, byte ranges, URL construction, and the signing algorithm are unchanged. PUT and XML POST body headers and the existing CopyObject special case are preserved. This follows the recommendation for bodyless requests in RFC 9110 section 8.6. Cross-runtime regressions cover ranges, DELETE, signed-header membership, and body-bearing operations. The post-repair provider matrix passed as recorded below.

Further coverage needed

  • Signing: repeated header values and whitespace, dot-segment object keys, reserved characters in upload/version identifiers, and presigned requests using temporary credentials. Query ordering is the only signing repair in this batch.
  • XML: fuzz representative list, error, tagging, lifecycle, and multipart responses; distinguish malformed/truncated responses from empty results.
  • Features: document supported combinations, test blocking wrappers and tags/fail-on-err independently, and establish a tested minimum Rust version. Mutually exclusive runtime features make a blanket --all-features check unsuitable for the main crate.
  • Sync documentation: the first batch exposed 29 doctest compile failures and temporarily limited sync CI to --lib. Resolved in the test-suite follow-up below, which restores doctests and example compilation.
  • Providers: deterministic local HTTP fault tests on each change; scheduled disposable MinIO tests; explicitly authorized AWS/R2/GCS/Wasabi smoke tests before compatibility claims.
  • Performance: record throughput, allocations, peak memory, cancellation latency, and connection reuse for concurrent small/large operations. This audit does not establish benchmark results.
  • Release discipline: check dependencies against a current advisory database, publish a compatibility matrix and changelog, and verify examples against the actual released artifacts.

Verification record: first repair batch

Baseline on Rust 1.98.1, macOS aarch64:

  • cargo test --workspace --lib: local aws-creds 4 passed / 1 ignored; local aws-region 4 passed; rust-s3 59 passed / 25 ignored.
  • Real-service ignored tests were not run.
  • The initial cargo audit database fetch failed. A separate shallow checkout of the public RustSec database was obtained; cargo audit --db /tmp/rust-s3-advisory-db-20261002 --no-fetch --json completed and reported the findings above. The checkout hash is recorded to identify the exact advisory snapshot.

After repairs:

  • cargo fmt --all -- --check and git diff --check: passed.
  • Root make clippy: passed all nine S3 runtime/TLS configurations and both supporting crates. Two pairs of redundant formatting borrows (async and sync) were removed to satisfy Rust 1.98 Clippy.
  • Root make test: passed. The normal CI target runs these same formatting, lint, and test stages.
Configuration Library tests passed Doctests passed Ignored library tests
Tokio default/native TLS 60 43 25
Tokio without TLS 59 41 22
Tokio rustls 60 42 20
Async-std Hyper 60 41 22
Async-std native TLS 60 41 22
Async-std rustls 60 41 22
Sync native TLS 54 Not run 22
Sync rustls 54 Not run 22
Sync without TLS 54 Not run 22
Local aws-region 4 1 0
Local aws-creds 4 3 1

The new streaming tests cover bounded chunks, lazy reads, error propagation, empty bodies, declared-length limits, and an actual localhost HTTP download to a writer, including writer failure. Signing regression tests cover encoded ordering of reserved/Unicode keys and duplicate-key values, preserve existing AWS signing vectors, and verify decoded query pairs survive a round trip. The streaming and signing regressions failed before their fixes.

No real-service ignored tests, performance benchmarks, or deployed/published artifact tests were run. Advisory reachability was not established. Blocking wrappers, fail-on-err across every backend, and sync doctests remain outside the passing matrix above.

Test-suite follow-up

The second batch makes make and make ci credential-free defaults, preserves all nine runtime/TLS test configurations, and compiles their examples. Provider tests require explicit opt-in. See TESTING.md for commands and coverage.

Sync and blocking documentation compile failures are repaired. Four representative doctest configurations replace repeated TLS-only documentation builds: Tokio default (43 passed), Tokio blocking (76), async-std blocking with tags (75), and sync with tags (36). This closes the sync doctest gap above. Provider examples remain no_run: compilation does not establish runtime correctness of blocking wrappers or provider compatibility.

The localhost 404 fixture now has request/server deadlines and stops modifying global retry state. The credential-profile environment test runs in a bounded child process with synthetic credentials, avoiding process-wide environment mutation. Each fixture passed 20 repeated runs.

Local verification on Rust 1.98.1, macOS aarch64:

  • make ci passed formatting, all nine S3 Clippy/test/example configurations, four doctest configurations, and both support crates (90.30 seconds, including rebuilding changed artifacts).
  • Warm make test passed in 38.25 seconds; a subsequent make -j8 test passed in 23.90 seconds. Make serializes the shared build phases; -j8 is not evidence of parallel test acceleration.
  • The earlier, narrower suite at 9212e43 took 19.28 seconds in an uncontended warm run. The broader suite is locally slower in these measurements; no speedup is claimed. Timings are individual observations, not a controlled benchmark.
  • Workflow validation with actionlint and git diff --check passed. Existing unused GCS test-helper warnings remain in the Tokio rustls test build.

GitHub CI now separates three runtime jobs and one support job, adds compiler/dependency-scoped caches, timeouts, and cancellation of superseded runs. Hosted execution and its speed remain unverified. Real-provider tests were not run. Remaining work includes runtime blocking-wrapper tests, backend fault coverage, provider fixtures, and measured hosted CI latency; dependency and production-behavior findings above remain open.

Correctness follow-up verification

The next batch repairs embedded HTTP 200 errors and local credential debug formatting as described above. Implementation was delegated to GPT-6 Luna, then reviewed and integrated by the parent.

  • The local HTTP regression failed with the original copy/completion call-site behavior: copy reported success for HTTP 200 <Error>. Restoring the validator made the regression pass. The fixture also checks valid copy/completion responses, multipart error bodies with whitespace heartbeats, and malformed XML returned over complete HTTP framing.
  • Credential sentinel tests failed with the original derived formatter and pass with redaction. Tests use only synthetic values; serialization and provider resolution are unchanged.
  • make ci passed on Rust 1.98.1, macOS aarch64, in 56.24 seconds including changed-artifact compilation: all nine S3 Clippy/test/example configurations, two focused fail-on-err runs, all four doctest configurations, and both supporting crates.
  • S3 library counts are 63 passed for default Tokio, 62 for Tokio without TLS, 63 for Tokio rustls and each async-std configuration, and 57 for each sync configuration. Both additional fail-on-err runs execute three matching tests. Doctest counts remain 43/76/75/36. Local aws-creds now has eight passing library tests and one ignored test; its three doctests pass.
  • A subsequent warm make test passed in 16.19 seconds. This is one local observation with more coverage than earlier runs, not a controlled speed comparison or a hosted CI measurement.
  • Formatting and diff checks passed; independent review found no blocking issues. No real-provider tests, transport-interruption tests, publication, or hosted workflow execution was performed. Header-only request identifiers are not added to the existing body error type; identifiers in the service XML remain available.

MinIO provider verification

On 2026-10-02, the installed MinIO RELEASE.2025-10-15T17-29-55Z (9e49d5e7a648f00e26f2246f4dc28e6b07f8c84a, darwin/arm64) was started on loopback using temporary data, certificates, and generated credentials. Tests used its dedicated rust-s3 bucket and an empty temporary working directory. No existing provider credentials or storage were used.

The existing five default-Tokio MinIO tests passed first. Luna then strengthened test assertions: nonuniform 20 MB data and exact writer/stream contents, stream item errors and offsets, sorted tag readback, UUID-scoped copy/content checks, and actual delete status checks in the blocking helper. Production code was unchanged in this batch.

Configuration MinIO tests passed
Tokio native TLS / no TLS / rustls 5 / 5 / 5
async-std Hyper / native TLS / rustls 5 / 5 / 5
Sync native TLS / rustls / no TLS 5 / 5 / 5
Tokio native TLS with blocking 6
async-std native TLS with blocking 6

All 57 strengthened test executions passed, with tags enabled in every configuration. Each invocation was restricted to minio --ignored --test-threads=1. Aggregate test execution was 26.81 seconds; sequential build and test time was 65.82 seconds on this machine. These are suite timings, not storage throughput benchmarks.

The normal make ci gate also passed after the test changes (46.89 seconds, including changed-artifact compilation), as did formatting and diff checks.

Post-test listings confirmed zero objects and zero incomplete multipart uploads, with neither listing truncated. The test bucket was deleted successfully; the owned server stopped, temporary data and credentials were removed, and port 9000 was released. TESTING.md describes the isolated procedure.

This establishes local MinIO success-path coverage, including real blocking API execution. It does not establish TLS handshake coverage (the endpoint used HTTP), cloud-provider compatibility, or cleanup after failure/cancellation. Cloud credentials were not loaded for that MinIO run. The later cloud attempts are recorded below. The production findings above remain open.

Cloud-provider verification attempt

The user subsequently authorized loading the existing .envrc credentials. Values were used privately without changing the file. Authenticated HEAD, object/multipart listing, and versioning checks succeeded against the existing test buckets on AWS, Wasabi, GCS, R2, and DigitalOcean. Versioning was disabled. These curl checks establish endpoint access, not rust-s3 compatibility.

Luna added RUST_S3_TEST_PREFIX isolation to the object fixtures and restored or added DigitalOcean CRUD/multipart, Wasabi multipart, and R2 blocking wrappers. AWS tag readback now asserts exact tags. Selected tests used unique prefixes and empty temporary working directories. Bucket configuration/ACL tests were excluded; only the selected run's objects and uploads were eligible for cleanup.

The initial cloud library attempts stalled:

  • AWS default Tokio CRUD exceeded a 180-second process deadline. Sync native TLS failed its first PUT with a connection timeout after 66 seconds. Tokio rustls also exceeded 180 seconds; temporary stage logging located it at the first PUT.
  • Wasabi default Tokio CRUD was interrupted after approximately 100 seconds. The remaining provider/runtime matrix was not executed.
  • A temporary unsigned reqwest GET to the AWS bucket endpoint also timed out after five seconds, reproducing the problem without signing or credentials.
  • Signed curl PUT/DELETE probes to the same AWS virtual host succeeded, including the fixture's 3 KB payload and encoded +test.file suffix.
  • Little Snitch is running on the host. A per-executable network restriction is a hypothesis awaiting user confirmation; no firewall settings were changed and no library transport defect is claimed from these observations.

Independent post-attempt listings confirmed zero objects and zero incomplete uploads under every attempted test prefix. The curl probe objects were deleted with HTTP 204. Temporary diagnostic Rust code was removed. At that point, GCS Tokio rustls exclusions remained a coverage gap, not a passing configuration.

Two local make ci attempts after prefix changes failed in the XML-response localhost fixture: first with async-std rustls, then with Tokio rustls. The affected test passed 20 focused repetitions and the exact full async-std rustls suite passed 10 repetitions (63 passed, 25 ignored each), but those successes did not close the recurring full-gate failure.

The follow-up reproduced its cause on macOS: sockets accepted from a nonblocking listener retained nonblocking mode, so a read before the client sent its first byte immediately returned WouldBlock. Both affected localhost fixtures now explicitly restore blocking mode on accepted streams while retaining their read/write and server deadlines. A channel-coordinated delayed-client regression failed when that setup was removed and passed when restored. The Tokio and async-std rustls suites then passed with 64 library tests each. The complete make ci gate passed in 59.12 seconds, including all nine S3 configurations, the additional error-feature checks, four doctest shapes, and both support crates. This is one local timing observation, not a throughput benchmark.

A subsequent unsigned HTTPS probe returned HTTP 403 from AWS as expected, so the cloud matrix could resume. No firewall configuration was changed; the cause of the earlier host-connectivity stall remains unconfirmed.

The default-Tokio rerun then passed all selected tests on AWS (5), Wasabi (2), GCS (3), and DigitalOcean (2). R2 CRUD failed at the first range GET with HTTP 403 SignatureDoesNotMatch; R2's canonical request showed an empty Content-Length value where the shared header builder signed 0. Native-TLS async-std and sync runs reproduced the same range failure. Each failed run's single object was independently removed and both listings were verified empty. R2 streaming tests and the remaining cloud runtime/TLS matrix were still pending at that point.

The prefix-enabled MinIO fixtures passed another 57 executions across the same 11 configurations (28.76 aggregate test seconds). The first launch attempt failed because the harness's server process had exited; the successful rerun used a persistent foreground process and verified health before tests. Every successful run had empty, untruncated object and multipart listings. The owned bucket was deleted (204, followed by 404), the server stopped, and its data and generated credentials removed. These results precede the R2 range-header repair.

Completed provider matrix after bodyless-header repair

All 100 selected cloud test executions passed against the configured AWS, Wasabi, GCS, R2, and DigitalOcean test buckets. The matrix comprises 15 tests per configuration for Tokio, async-std, and sync with native TLS and rustls (90), plus one blocking CRUD/range/list-pagination test per provider for each async runtime (10). GCS Tokio-rustls tests are now enabled and passed. There were no skipped or uncompiled selections. Independent signed listings verified zero objects and incomplete uploads under all 40 provider/configuration prefixes.

The first blocking runs passed on four providers but exposed a fixture ordering assumption on R2. An independent signed curl probe returned file2,file3 on page one and file on page two, with valid truncation and continuation fields. The three probe objects were removed and both listings verified empty. The fixture now verifies exact, duplicate-free keys across both pages while retaining strict page sizes, status, truncation, and token checks. It does not alter library ordering or claim that R2 provides lexicographic ordering. All ten cloud blocking tests passed after this fixture correction.

The production header repair also passed all 57 MinIO executions across 11 configurations (26.95 aggregate test seconds). Both blocking configurations were rerun after the listing assertion change: another 12 passes. Each prefix was verified empty; the disposable bucket was deleted (204, followed by 404), the owned server stopped, and its generated credentials and data removed.

make ci passed after the production repair, covering all nine runtime/TLS configurations, error-feature checks, four doctest shapes, and both support crates. The subsequent change affected only the ignored blocking fixture; both blocking configurations were rebuilt and exercised on MinIO and all five cloud providers. Final formatting and diff checks passed.

These results establish the selected success paths and real TLS connections to cloud providers. They do not establish failure/cancellation cleanup, exhaustive S3 API compatibility, throughput benchmarks, dependency remediation, hosted CI, or release of the local credential fix. Those roadmap items remain open.

Multipart cleanup verification follow-up

The cleanup repair passed the full make ci gate in 205.94 seconds with changed artifacts rebuilt, and a cached rerun in 99.93 seconds with no compilation steps. These are local elapsed-time observations; concurrent cloud testing and host load affect them. All nine runtime/TLS configurations, four doctest shapes, support crates, and both focused non-default fail-on-err targets passed. The new cleanup checks are retained in those focused CI targets.

The frozen source also passed all 57 MinIO executions across 11 configurations (30.22 aggregate test seconds), followed by the same 100 cloud executions across AWS, Wasabi, GCS, R2, and DigitalOcean described above. All 40 cloud prefixes were independently verified empty. MinIO listings were empty and untruncated; the bucket was deleted (204, then 404), and the owned server, data, certificates, and generated credentials were removed. This provider rerun establishes success-path compatibility after the cleanup change; failure cleanup evidence comes from the bounded local fault fixtures, not injected cloud failures.

Multipart header verification follow-up

Tokio and async-std passed synthetic wire tests below, at, and above the multipart threshold. The tests verify object properties, SSE-C, conditions, owner/payer controls including abort, generated body headers, bucket-global/per-call precedence, part numbers independent of arrival order, and rejection before any request. Disabling routing reproduced the missing per-call metadata at initiation; restoring it passed. Public sync behavior remains unchanged.

The complete make ci gate passed with no warnings in 290.80 seconds, including rebuilding changed artifacts. The frozen source then passed all 57 MinIO tests across 11 configurations (31.68 aggregate test seconds) and all 100 cloud tests across the five providers and eight runtime/TLS/blocking configurations. Async small and multipart upload fixtures now verify persisted metadata, cache control, and content type through HEAD, alongside exact payload bytes. Sync fixtures retain their existing checks.

All 40 cloud prefixes were independently verified empty. MinIO listings were empty and untruncated, its bucket deletion returned 204 followed by 404, and the owned server, synthetic credentials, data, and certificates were removed. These results verify live object-property preservation; real-provider SSE-C and conditional-write scenarios remain outside the live matrix and have only synthetic local wire coverage.

XML dependency and credential integration follow-up

The source now prepares aws-creds 0.40.0 and rust-s3 0.38.0, both using quick-xml 0.41. S3 uses a path-plus-version dependency on the local credential crate, so its redacted credential formatting is now exercised through Bucket. A synthetic sentinel regression checks both ordinary and pretty Debug output. The XML adapter preserves the previous text-normalization behavior and uses the new decoder-aware attribute API. The full make ci gate passed without warnings.

An isolated fresh resolution audited against the same RustSec database db663534ae858abb3fbad408a041ce04209c377f reports 10 vulnerability matches across 10 IDs, down from 12: both quick-xml advisories are removed. The remaining matches are in h2, Hyper, Tokio 0.2, ring, rustls 0.18, and webpki, associated with the legacy async-std transport branches described above. Separate warnings remain: one notice, 11 unmaintained matches, and five unsoundness matches. This is not a clean security assessment or a claim that every listed code path is exploitable.

aws-creds package creation and verification passed. S3 package listing passed, but registry-based packaging cannot resolve the new credential version until it is published. Neither crate has been published. RELEASING.md records the required release order and public quick-xml error-type compatibility impact behind the minor version bumps.

The frozen dependency patch passed 57 MinIO tests across 11 configurations (28.12 aggregate test seconds) and all 100 selected cloud tests across AWS, Wasabi, GCS, R2, and DigitalOcean. All 40 cloud prefixes were independently verified empty, as were MinIO object and upload listings. The isolated MinIO server is retained temporarily for the next pagination regression.

Credential refresh and async-std deadline verification

The combined frozen changes passed make ci in 145.97 seconds, including formatting, linting, runtime/TLS tests, examples, and representative doctests. Credential unit tests passed with HTTP support (13) and without it (11). The obsolete ignored default-provider fallback test was removed; its replacement is deterministic and requires no credentials. The gated refresh test passed on Tokio and async-std, and the public body deadline test rejects a deadline reset at the first body read. aws-creds package creation and verification passed; Cargo reported the existing yanked spin 0.9.8 lockfile entry. No publication has occurred.

Fresh binaries passed all 68 MinIO tests across 11 configurations and all 100 cloud tests across eight configurations against AWS, Wasabi, GCS, R2, and DigitalOcean. All 40 cloud prefixes and all MinIO post-test object/upload listings were independently checked empty and untruncated. The temporary MinIO bucket was deleted (204, then 404), its owned process stopped, and generated credentials, certificates, and service data removed. Logs and results remain available locally. This verifies existing provider operations, not live STS or IMDS rotation, injected cloud failures, or cancellation safety.

Order of work

  1. Land the reviewed streaming, signing query, and CI repairs with regression evidence.
  2. Remediate the remaining dependency advisories, release and integrate credential redaction, and address the remaining lifecycle and API findings in separate changes. Source fixes for false-success responses and local credential formatting are recorded above.
  3. Resolve cursor correctness, retry semantics, refresh identity, and timeout parity with a shared local fault-test suite.
  4. Establish provider compatibility and performance baselines before expanding API coverage.