Skip to content

Fix/files list panics - #86

Open
ogarcia wants to merge 15 commits into
rustic-rs:mainfrom
ogarcia:fix/files-list-panics
Open

Fix/files list panics#86
ogarcia wants to merge 15 commits into
rustic-rs:mainfrom
ogarcia:fix/files-list-panics

Conversation

@ogarcia

@ogarcia ogarcia commented Aug 14, 2026

Copy link
Copy Markdown

Based on #85, this change resolves a series of errors that the server may throw due to fairly simple operations, such as making a GET request to /{repo}/{tpe}/ without the authorization header.

ogarcia added 15 commits August 14, 2026 07:02
Refresh Cargo.lock to the latest semver-compatible releases and raise the
pinned minimums for chrono, uuid, rustls and once_cell.

The htpasswd snapshot is updated because toml 0.8.23 no longer emits a
blank line between serialized tables.
http-range, inquire, pin-project, dircmp and once_cell are not referenced
anywhere in the crate.
…jors

- rand 0.8 -> 0.10: `distributions` is now `distr`, `thread_rng()` is
  `rng()` and the convenience methods moved from `Rng` to `RngExt`
- strum 0.26 -> 0.28
- toml 0.8 -> 1.1
- base64 0.22 -> 0.23, rstest 0.23 -> 0.26, serial_test 3 -> 4
abscissa_core and abscissa_tokio 0.8 -> 0.9, no source changes required.
- axum 0.7 -> 0.8, axum-extra 0.9 -> 0.12, axum-macros 0.4 -> 0.5,
  axum-auth 0.7 -> 0.8, axum-server 0.7 -> 0.8, axum-range 0.4 -> 1
- path captures use the new `{param}` syntax instead of `:param`
- `FromRequestParts` no longer needs `#[async_trait]`

axum-server 0.8 also drops the unmaintained rustls-pemfile dependency.
`bad_style` is a lint group, so it needs an explicit lower priority than
the individual lints in the table; without it clippy refuses to run on
current toolchains.

Also resolve the warnings this uncovers: drop an unused import, remove a
needless `return`, use `io::Error::other`, iterate over map values and
inline format arguments.
htpasswd-verify has not been released since 2020 and pulls in rust-crypto,
rustc-serialize, time 0.1 and gcc, which accounted for all three RUSTSEC
vulnerabilities reported by `cargo audit`. htauth covers the same job with
maintained RustCrypto primitives and constant-time comparisons.

Behaviour changes:

- verification accepts bcrypt, SHA-256/SHA-512 crypt and APR1-MD5; the
  broken `{SHA}` and `crypt(3)` formats are no longer accepted
- an unsupported or malformed hash is logged and rejected instead of
  panicking, which the old bcrypt path could do
- `Credential::new` is fallible now, since hashing can fail
- htauth generates its own salt, so the rand dependency is gone

`cargo audit` now reports no vulnerabilities.
The updated dependencies require it: htauth needs 1.88 and abscissa 0.9,
axum 0.8, clap 4.6 and uuid 1.24 all need 1.85. Verified with a 1.88.0
toolchain.

Note that `cargo test` needs a newer toolchain than that, because
serial_test 4 declares 1.93.1; building the crate itself does not.
The rsa crate is no longer in the dependency tree, so the ignore has no
effect other than hiding a future regression.
cargo-deny checks the license of the crate under inspection too, so
`check licenses` rejected rustic_server itself for being AGPL-3.0-or-later.
No dependency license was involved.
Compiles clean on rustc 1.97.1 with no edition compatibility warnings left
in our own code. Two changes were needed:

- `check_auth_and_acl` and `check_name` returned `impl IntoResponse` while
  taking a reference argument. In edition 2024 the opaque type captures
  every lifetime in scope, so both now take a named type parameter and
  declare `use<T>` to capture nothing else. Neither actually borrows: they
  return `StatusCode` and `()`.
- Three sites triggered the tail expression drop order lint from inside a
  `tracing::debug!` expansion. The temporaries only own memory, so the
  order was harmless, but binding them first removes the warning and drops
  a duplicated `repo()`/`join()` call in each case.

The edition also implies resolver 3 and rustfmt style edition 2024.
Mechanical reordering of use statements: the 2024 style edition sorts
uppercase-first and splits nested groups differently. No code changes.
`print_request_response` ran `body.collect()` on both the request and the
response, so every upload and download was held in memory in full before
being forwarded. Worse, `web.rs` installed the middleware at INFO, which is
the default level, while the middleware itself only emits `debug!` records:
by default the server buffered every body and then logged nothing.

Bodies here are pack, index and key files, so the buffer was also scanned
by `str::from_utf8` on every request just to discover it is not text.

Measured with restic pushing 6 cycles of 120 MB through one server process,
resident memory after each cycle:

    before   101 -> 184 -> 203 -> 190 -> 152 -> 196 MB, 222 MB when idle
    after     11 ->  13 ->  14 ->  13 ->  14 ->  13 MB,  14 MB when idle

The middleware now logs method, URI, status and headers and passes the body
through untouched, and it is only installed at DEBUG or TRACE. Uploads also
stream to disk end to end now, which halved the backup wall time in the
same test.

Also drop a `Box::leak` of the ACL file contents. `RepoAcl` owns its data,
so the `'static` borrow was never needed; it leaked the file on every
startup.
…ng response

`list_files` copied the request's `Authorization` header into its own
response. That served no purpose and sent the client's Basic credentials
back out, where any proxy or cache on the path could record them.

Reading the header also assumed it was always present. It is not: with
`--no-auth` the extractor accepts a request that carries no credentials, so
`headers.get(AUTHORIZATION).unwrap()` panicked. Because the release profile
sets `panic = "abort"`, that killed the whole server rather than failing the
one request, making a single unauthenticated GET enough to stop the service:

    GET /{repo}/{tpe}/ without an Authorization header
    -> thread panicked at src/handlers/files_list.rs:139
    -> Aborted (core dumped)
`list_files` unwrapped both the file name and the metadata of every entry it
walked, so either one failing killed the process, again because the release
profile sets `panic = "abort"`.

Both fail in practice:

- `entry.metadata()` returns `NotFound` when the file is removed between the
  directory walk and the size lookup. The listing is serialized lazily, so
  the window stays open for the whole response. A `forget --prune` running
  while any client lists files is enough, with no malice involved. Verified
  by deleting 20000 files under 40 concurrent listings, which aborted the
  server before this change and does not now.
- `file_name().to_str()` returns `None` for a name that is not valid UTF-8.
  Such a name cannot be written through the API, but it can reach the
  directory by other means, and it made every listing of that type fail.

Both cases now drop the entry from the listing, the vanished one at debug
level and the undecodable one at warn level. Neither can be a valid
repository object anyway: they are named after a hex digest.
@ogarcia ogarcia mentioned this pull request Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant