Skip to content

Commit e324d37

Browse files
fix: reject malformed namespaced hash mappings (#216)
1 parent cf2aa07 commit e324d37

2 files changed

Lines changed: 99 additions & 25 deletions

File tree

src/git/versioned_store/namespaced.rs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1883,30 +1883,42 @@ impl<const N: usize> NamespacedKvStore<N, GitNodeStorage<N>, GitMetadataBackend>
18831883
// Load from both mapping files
18841884
for path in [&ns_mapping_path, &global_mapping_path] {
18851885
if let Ok(data) = self.inner.metadata.read_file_at_commit(commit_id, path) {
1886-
let mapping_str = String::from_utf8(data).unwrap_or_default();
1887-
for line in mapping_str.lines() {
1888-
if let Some((hash_hex, object_hex)) = line.split_once(':') {
1889-
if hash_hex.len() == N * 2 {
1890-
let mut hash_bytes = Vec::new();
1891-
for i in 0..N {
1892-
if let Ok(byte) =
1893-
u8::from_str_radix(&hash_hex[i * 2..i * 2 + 2], 16)
1894-
{
1895-
hash_bytes.push(byte);
1896-
} else {
1897-
break;
1898-
}
1899-
}
1900-
if hash_bytes.len() == N {
1901-
if let Ok(object_id) =
1902-
gix::ObjectId::from_hex(object_hex.as_bytes())
1903-
{
1904-
let hash = ValueDigest::raw_hash(&hash_bytes);
1905-
hash_mappings.insert(hash, object_id);
1906-
}
1907-
}
1908-
}
1886+
let mapping_str = String::from_utf8(data).map_err(|e| {
1887+
GitKvError::GitObjectError(format!("Invalid UTF-8 in {path}: {e}"))
1888+
})?;
1889+
for (line_index, line) in mapping_str.lines().enumerate() {
1890+
if line.trim().is_empty() {
1891+
continue;
19091892
}
1893+
1894+
let line_number = line_index + 1;
1895+
let (hash_hex, object_hex) = line.split_once(':').ok_or_else(|| {
1896+
GitKvError::GitObjectError(format!(
1897+
"Malformed hash mapping in {path} on line {line_number}"
1898+
))
1899+
})?;
1900+
1901+
if hash_hex.len() != N * 2 {
1902+
return Err(GitKvError::GitObjectError(format!(
1903+
"Invalid prolly hash in {path} on line {line_number}: expected {} hex chars, got {}",
1904+
N * 2,
1905+
hash_hex.len()
1906+
)));
1907+
}
1908+
1909+
let hash_bytes = hex::decode(hash_hex).map_err(|e| {
1910+
GitKvError::GitObjectError(format!(
1911+
"Invalid prolly hash in {path} on line {line_number}: {e}"
1912+
))
1913+
})?;
1914+
let object_id =
1915+
gix::ObjectId::from_hex(object_hex.as_bytes()).map_err(|e| {
1916+
GitKvError::GitObjectError(format!(
1917+
"Invalid git object id in {path} on line {line_number}: {e}"
1918+
))
1919+
})?;
1920+
let hash = ValueDigest::raw_hash(&hash_bytes);
1921+
hash_mappings.insert(hash, object_id);
19101922
}
19111923
}
19121924
}

src/git/versioned_store/tests.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,12 +291,13 @@ mod proof_tests {
291291

292292
#[cfg(test)]
293293
mod tests {
294+
use crate::diff::IgnoreConflictsResolver;
294295
use crate::git::types::DiffOperation;
295296
#[cfg(feature = "rocksdb_storage")]
296297
use crate::git::versioned_store::RocksDBVersionedKvStore;
297298
use crate::git::versioned_store::{
298-
FileVersionedKvStore, GitVersionedKvStore, HistoricalAccess, HistoricalCommitAccess,
299-
InMemoryVersionedKvStore, ThreadSafeGitVersionedKvStore,
299+
FileVersionedKvStore, GitNamespacedKvStore, GitVersionedKvStore, HistoricalAccess,
300+
HistoricalCommitAccess, InMemoryVersionedKvStore, ThreadSafeGitVersionedKvStore,
300301
};
301302
use crate::tree::Tree;
302303
use tempfile::TempDir;
@@ -341,6 +342,67 @@ mod tests {
341342
assert!(store.is_ok());
342343
}
343344

345+
#[test]
346+
fn namespaced_merge_rejects_invalid_committed_hash_mapping_entry() {
347+
let temp_dir = TempDir::new().unwrap();
348+
gix::init(temp_dir.path()).unwrap();
349+
let dataset_dir = temp_dir.path().join("dataset");
350+
std::fs::create_dir_all(&dataset_dir).unwrap();
351+
let _cwd = CwdGuard::set(&dataset_dir);
352+
353+
std::process::Command::new("git")
354+
.args(["config", "user.name", "ProllyTree Test"])
355+
.current_dir(temp_dir.path())
356+
.output()
357+
.expect("git config user.name");
358+
std::process::Command::new("git")
359+
.args(["config", "user.email", "prollytree@example.test"])
360+
.current_dir(temp_dir.path())
361+
.output()
362+
.expect("git config user.email");
363+
364+
let mut store = GitNamespacedKvStore::<32>::init(&dataset_dir).unwrap();
365+
store
366+
.namespace("personal")
367+
.insert(b"base".to_vec(), b"base-value".to_vec())
368+
.unwrap();
369+
store.commit("base").unwrap();
370+
371+
store.create_branch("feature").unwrap();
372+
store
373+
.namespace("personal")
374+
.insert(b"feature".to_vec(), b"feature-value".to_vec())
375+
.unwrap();
376+
store.commit("feature data").unwrap();
377+
378+
let mapping_path = dataset_dir.join("prolly_hash_mappings");
379+
let mut mappings = std::fs::read_to_string(&mapping_path).unwrap();
380+
mappings.push_str("not-hex:also-not-a-git-object\n");
381+
std::fs::write(&mapping_path, mappings).unwrap();
382+
383+
let add = std::process::Command::new("git")
384+
.args(["add", "dataset/prolly_hash_mappings"])
385+
.current_dir(temp_dir.path())
386+
.output()
387+
.expect("git add");
388+
assert!(add.status.success(), "git add failed: {add:?}");
389+
let commit = std::process::Command::new("git")
390+
.args(["commit", "-m", "corrupt feature mappings"])
391+
.current_dir(temp_dir.path())
392+
.output()
393+
.expect("git commit");
394+
assert!(commit.status.success(), "git commit failed: {commit:?}");
395+
396+
store.checkout("main").unwrap();
397+
let err = store
398+
.merge("feature", &IgnoreConflictsResolver)
399+
.expect_err("invalid committed mapping entries must fail merge");
400+
assert!(
401+
err.to_string().contains("Invalid prolly hash"),
402+
"unexpected error: {err}"
403+
);
404+
}
405+
344406
#[test]
345407
fn test_basic_kv_operations() {
346408
let temp_dir = TempDir::new().unwrap();

0 commit comments

Comments
 (0)