fix(storage): address CodeRabbit round — key backslash rejection, lock-poll error path, config doc

- validate_key: reject backslashes outright — keys are forward-slash
  delimited, and on Windows PathBuf::join treats '\' as a separator,
  letting a key smuggle path components past the segment checks.
- LockedConn::acquire: when the pg_try_advisory_lock poll itself errors,
  close the pinned connection deliberately with an accurate rationale
  instead of falling through to Drop's dropped-holder warning. Closing
  (not repooling) is intentional: a lost response means the session may
  hold the lock, and repooling would strand it.
- config.rs: storage_backend doc no longer claims ipfs is auto-selected.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Kevin CodexandClaude Fable 5 committed 2026-07-23 15:12:41 +08:00
1 parent f17fee06b4
commit d30af0d20b
4 files changed
+27 -6

No files matched your search

+3 -2
View File
@@ -137,8 +137,9 @@ pub struct Config {
/// Object-storage backend: `s3` (any S3-compatible service), `fs` (local
/// directory), or `ipfs` (Kubo MFS). Empty = auto-detect: `s3` when a bucket
/// is set, else `fs` when a storage dir is set, else `ipfs` when an IPFS API
/// is set, else local-only.
/// is set, else `fs` when a storage dir is set, else local-only. `ipfs` is
/// never auto-selected (`GITLAWB_IPFS_API` alone keeps its pinning-only
/// meaning) — set this explicitly to `ipfs` to opt in.
#[arg(long, env = "GITLAWB_STORAGE_BACKEND", default_value = "")]
pub storage_backend: String,
+16 -2
View File
@@ -477,11 +477,25 @@ impl LockedConn {
};
for attempt in 0..LOCK_ACQUIRE_TIMEOUT_SECS {
let conn = this.conn.as_mut().expect("connection present until unlock");
let row: (bool,) = sqlx::query_as("SELECT pg_try_advisory_lock($1)")
let row: (bool,) = match sqlx::query_as("SELECT pg_try_advisory_lock($1)")
.bind(lock_key)
.fetch_one(&mut **conn)
.await
.context("trying advisory lock")?;
{
Ok(row) => row,
Err(e) => {
// The poll itself failed, so the lock's server-side state
// is unknown: if the query executed but the response was
// lost, this session HOLDS the lock, and repooling the
// connection would strand it. Close the connection
// deliberately (freeing any lock it may hold) instead of
// going through Drop's generic dropped-holder warning.
if let Some(conn) = this.conn.take() {
drop(conn.detach());
}
return Err(e).context("trying advisory lock");
}
};
if row.0 {
return Ok(this);
}
+3
View File
@@ -200,6 +200,9 @@ mod tests {
let store = FsBlobStore::new(dir.path()).unwrap();
assert!(store.get("../escape").await.is_err());
assert!(store.put("a/../../etc/passwd", Bytes::new()).await.is_err());
// Backslashes are separators on Windows — must be rejected as keys.
assert!(store.get("a\\..\\escape").await.is_err());
assert!(store.put("repos\\v1\\x", Bytes::new()).await.is_err());
}
#[tokio::test]
+5 -2
View File
@@ -151,8 +151,11 @@ pub(crate) fn validate_key(key: &str) -> Result<()> {
if key.split('/').any(|seg| seg == ".." || seg == ".") {
anyhow::bail!("blob key contains traversal segment: {key}");
}
if key.starts_with('/') || key.contains('\0') {
anyhow::bail!("blob key is absolute or contains null byte: {key}");
// Backslash is rejected outright: keys are forward-slash-delimited, and on
// Windows `PathBuf::join` treats `\` as a separator, which would let a key
// smuggle path components past the segment checks above.
if key.starts_with('/') || key.contains('\\') || key.contains('\0') {
anyhow::bail!("blob key is absolute, contains '\\', or contains null byte: {key}");
}
Ok(())
}