History

Assure recursive shared indices (possibly malicious) don't crash us

The way this is resolved is similar to what git does by means of comparing the expected and actual hashes of the file at hand. Note that we do not actually protect against recursion but assume that nobody can manufacture a file hash that in fact is recursive. We know that this can actually happen but chose to not consider it worth protecting against just yet, as seemingly neither does git.

Sebastian Thiel committed 3 years ago

Commit ede718c1 parent d6ba3667 files · +136 −74Browse files

Unified · Split

7 files
  1. git-index/src/decode/mod.rs +18 −0
  2. git-index/src/extension/decode.rs +8 −4
  3. git-index/src/extension/link.rs +72 −0
  4. git-index/src/extension/mod.rs +2 −1
  5. git-index/src/file/init.rs +2 −65
  6. git-index/tests/fixtures/make_index/V2_split_index_recursive.sh +19 −0
  7. git-index/tests/index/file/read.rs +15 −4
git-index/src/decode/mod.rs+18−0
@@ -22,6 +22,11 @@ mod error { · 21 unmodified lines
2222 Extension(#[from] extension::decode::Error),
2323 #[error("Index trailer should have been {expected} bytes long, but was {actual}")]
2424 UnexpectedTrailerLength { expected: usize, actual: usize },
25+ #[error("Shared index checksum was {actual_checksum} but should have been {expected_checksum}")]
26+ ChecksumMismatch {
27+ actual_checksum: git_hash::ObjectId,
28+ expected_checksum: git_hash::ObjectId,
29+ },
2530 }
2631 }
2732 pub use error::Error;
@@ -41,6 +46,10 @@ pub struct Options { · 13 unmodified lines
4146 /// The minimum size in bytes to load extensions in their own thread, assuming there is enough `num_threads` available.
4247 /// If set to 0, for example, extensions will always be read in their own thread if enough threads are available.
4348 pub min_extension_block_in_bytes_for_threading: usize,
49+ /// Set the expected hash of this index if we are read as part of a `link` extension.
50+ ///
51+ /// We will abort reading this file if it doesn't match.
52+ pub expected_checksum: Option<git_hash::ObjectId>,
4453 }
4554
4655 impl State {
@@ -53,6 +62,7 @@ impl State { · 6 unmodified lines
5362 Options {
5463 thread_limit,
5564 min_extension_block_in_bytes_for_threading,
65+ expected_checksum,
5666 }: Options,
5767 ) -> Result<(Self, git_hash::ObjectId), Error> {
5868 let (version, num_entries, post_header_data) = header::decode(data, object_hash)?;
@@ -205,6 +215,14 @@ impl State { · 146 unmodified lines
205215 }
206216
207217 let checksum = git_hash::ObjectId::from(data);
218+ if let Some(expected_checksum) = expected_checksum {
219+ if checksum != expected_checksum {
220+ return Err(Error::ChecksumMismatch {
221+ actual_checksum: checksum,
222+ expected_checksum,
223+ });
224+ }
225+ }
208226 let EntriesOutcome {
209227 entries,
210228 path_backing,
git-index/src/extension/decode.rs+8−4
@@ -2,7 +2,7 @@ use std::convert::TryInto; · 1 unmodified line
22
33 use crate::{extension, extension::Signature, util::from_be_u32};
44
5−pub fn header(data: &[u8]) -> (Signature, u32, &[u8]) {
5+pub(crate) fn header(data: &[u8]) -> (Signature, u32, &[u8]) {
66 let (signature, data) = data.split_at(4);
77 let (size, data) = data.split_at(4);
88 (signature.try_into().unwrap(), from_be_u32(size), data)
@@ -11,8 +11,9 @@ pub fn header(data: &[u8]) -> (Signature, u32, &[u8]) { · 2 unmodified lines
1111 mod error {
1212 use crate::extension;
1313
14− /// The error returned by [extension::decode::all()][extension::decode::all()].
14+ /// The error returned when decoding extensions.
1515 #[derive(Debug, thiserror::Error)]
16+ #[allow(missing_docs)]
1617 pub enum Error {
1718 #[error(
1819 "Encountered mandatory extension '{}' which isn't implemented yet",
@@ -25,7 +26,10 @@ mod error { · 6 unmodified lines
2526 }
2627 pub use error::Error;
2728
28−pub fn all(maybe_beginning_of_extensions: &[u8], object_hash: git_hash::Kind) -> Result<(Outcome, &[u8]), Error> {
29+pub(crate) fn all(
30+ maybe_beginning_of_extensions: &[u8],
31+ object_hash: git_hash::Kind,
32+) -> Result<(Outcome, &[u8]), Error> {
2933 let mut ext_iter = match extension::Iter::new_without_checksum(maybe_beginning_of_extensions, object_hash) {
3034 Some(iter) => iter,
3135 None => return Ok((Outcome::default(), maybe_beginning_of_extensions)),
@@ -66,7 +70,7 @@ pub fn all(maybe_beginning_of_extensions: &[u8], object_hash: git_hash::Kind) -> · 34 unmodified lines
6670 }
6771
6872 #[derive(Default)]
69−pub struct Outcome {
73+pub(crate) struct Outcome {
7074 pub tree: Option<extension::Tree>,
7175 pub link: Option<extension::Link>,
7276 pub resolve_undo: Option<extension::resolve_undo::Paths>,
git-index/src/extension/link.rs+72−0
@@ -60,3 +60,75 @@ pub(crate) fn decode(data: &[u8], object_hash: git_hash::Kind) -> Result<Link, d · 59 unmodified lines
6060 bitmaps: Some(Bitmaps { delete, replace }),
6161 })
6262 }
63+
64+impl Link {
65+ pub(crate) fn dissolve_into(
66+ self,
67+ split_index: &mut crate::File,
68+ object_hash: git_hash::Kind,
69+ options: crate::decode::Options,
70+ ) -> Result<(), crate::file::init::Error> {
71+ let shared_index_path = split_index
72+ .path
73+ .parent()
74+ .expect("split index file in .git folder")
75+ .join(format!("sharedindex.{}", self.shared_index_checksum));
76+ let mut shared_index = crate::File::at(
77+ &shared_index_path,
78+ object_hash,
79+ crate::decode::Options {
80+ expected_checksum: self.shared_index_checksum.into(),
81+ ..options
82+ },
83+ )?;
84+
85+ if let Some(bitmaps) = &self.bitmaps {
86+ let mut counter = 0;
87+ let shared_entries = shared_index.entries_mut();
88+ let split_entries = split_index.entries();
89+
90+ bitmaps.replace.for_each_set_bit(|index| {
91+ match (shared_entries.get_mut(index), split_entries.get(counter)) {
92+ (Some(shared_entry), Some(split_entry)) => {
93+ shared_entry.stat = split_entry.stat;
94+ shared_entry.id = split_entry.id;
95+ shared_entry.flags = split_entry.flags;
96+ shared_entry.mode = split_entry.mode;
97+ }
98+ _ => unreachable!(),
99+ }
100+
101+ counter += 1;
102+ Some(())
103+ });
104+
105+ if split_entries.len() > counter {
106+ split_entries[counter..].iter().for_each(|split_entry| {
107+ let mut e = split_entry.clone();
108+ let start = shared_index.path_backing.len();
109+ e.path = start..start + split_entry.path.len();
110+ shared_index.entries.push(e);
111+
112+ shared_index
113+ .path_backing
114+ .extend_from_slice(&split_index.path_backing[split_entry.path.clone()]);
115+ });
116+ }
117+
118+ let mut removed_count = 0;
119+ bitmaps.delete.for_each_set_bit(|index| {
120+ shared_index.entries.remove(index - removed_count);
121+ removed_count += 1;
122+ Some(())
123+ });
124+
125+ let mut shared_entries = std::mem::take(&mut shared_index.entries);
126+ shared_entries.sort_by(|a, b| a.cmp(b, &shared_index.state));
127+
128+ std::mem::swap(&mut split_index.entries, &mut shared_entries);
129+ std::mem::swap(&mut split_index.path_backing, &mut shared_index.path_backing);
130+ }
131+
132+ Ok(())
133+ }
134+}
git-index/src/extension/mod.rs+2−1
@@ -73,7 +73,8 @@ mod iter; · 72 unmodified lines
7373
7474 pub(crate) mod fs_monitor;
7575
76−pub(crate) mod decode;
76+///
77+pub mod decode;
7778
7879 ///
7980 pub mod tree;
git-index/src/file/init.rs+2−65
@@ -40,8 +40,8 @@ impl File { · 39 unmodified lines
4040 path,
4141 checksum: Some(checksum),
4242 };
43− if let Some(link) = file.link.take() {
44− dissolve_link_extension(&mut file, link, object_hash, options)?;
43+ if let Some(mut link) = file.link.take() {
44+ link.dissolve_into(&mut file, object_hash, options)?;
4545 }
4646
4747 Ok(file)
@@ -58,66 +58,3 @@ impl File { · 10 unmodified lines
5858 }
5959 }
6060 }
61−
62−fn dissolve_link_extension(
63− split_index: &mut File,
64− link: extension::Link,
65− object_hash: git_hash::Kind,
66− options: decode::Options,
67−) -> Result<(), Error> {
68− let shared_index_path = split_index
69− .path
70− .parent()
71− .expect("split index file in .git folder")
72− .join(format!("sharedindex.{}", link.shared_index_checksum));
73− let mut shared_index = File::at(&shared_index_path, object_hash, options)?;
74−
75− if let Some(bitmaps) = &link.bitmaps {
76− let mut counter = 0;
77− let mut shared_entries = shared_index.entries_mut();
78− let split_entries = split_index.entries();
79−
80− bitmaps.replace.for_each_set_bit(|index| {
81− match (shared_entries.get_mut(index), split_entries.get(counter)) {
82− (Some(shared_entry), Some(split_entry)) => {
83− shared_entry.stat = split_entry.stat;
84− shared_entry.id = split_entry.id;
85− shared_entry.flags = split_entry.flags;
86− shared_entry.mode = split_entry.mode;
87− }
88− _ => unreachable!(),
89− }
90−
91− counter += 1;
92− Some(())
93− });
94−
95− if split_entries.len() > counter {
96− split_entries[counter..].iter().for_each(|split_entry| {
97− let mut e = split_entry.clone();
98− let start = shared_index.path_backing.len();
99− e.path = start..start + split_entry.path.len();
100− shared_index.entries.push(e);
101−
102− shared_index
103− .path_backing
104− .extend_from_slice(&split_index.path_backing[split_entry.path.clone()]);
105− });
106− }
107−
108− let mut removed_count = 0;
109− bitmaps.delete.for_each_set_bit(|index| {
110− shared_index.entries.remove(index - removed_count);
111− removed_count += 1;
112− Some(())
113− });
114−
115− let mut shared_entries = std::mem::take(&mut shared_index.entries);
116− shared_entries.sort_by(|a, b| a.cmp(b, &shared_index.state));
117−
118− std::mem::swap(&mut split_index.entries, &mut shared_entries);
119− std::mem::swap(&mut split_index.path_backing, &mut shared_index.path_backing);
120− }
121−
122− Ok(())
123−}
git-index/tests/fixtures/make_index/V2_split_index_recursive.sh+19−0
@@ -0,0 +1,19 @@
1+#!/bin/bash
2+set -eu -o pipefail
3+
4+export GIT_INDEX_VERSION=2
5+git init -q
6+git config index.threads 1
7+
8+touch a
9+git add a
10+git commit -m "empty"
11+
12+git update-index --split-index
13+
14+shared_index=".git/sharedindex.*"
15+test -f $shared_index || \
16+ { echo "shared index must be present in 'split' repository" && exit 42; }
17+
18+cp .git/index $shared_index
19+
git-index/tests/index/file/read.rs+15−4
@@ -23,14 +23,16 @@ pub(crate) fn loose_file(name: &str) -> git_index::File { · 22 unmodified lines
2323 let file = git_index::File::at(path, git_hash::Kind::Sha1, Default::default()).unwrap();
2424 verify(file)
2525 }
26−pub(crate) fn file(name: &str) -> git_index::File {
26+pub(crate) fn try_file(name: &str) -> Result<git_index::File, git_index::file::init::Error> {
2727 let file = git_index::File::at(
2828 crate::fixture_index_path(name),
2929 git_hash::Kind::Sha1,
3030 Default::default(),
31− )
32− .unwrap();
33− verify(file)
31+ )?;
32+ Ok(verify(file))
33+}
34+pub(crate) fn file(name: &str) -> git_index::File {
35+ try_file(name).unwrap()
3436 }
3537 fn file_opt(name: &str, opts: git_index::decode::Options) -> git_index::File {
3638 let file = git_index::File::at(crate::fixture_index_path(name), git_hash::Kind::Sha1, opts).unwrap();
@@ -290,6 +292,15 @@ fn v2_split_index() { · 253 unmodified lines
290292 assert_eq!(file.version(), Version::V2);
291293 }
292294
295+#[test]
296+fn v2_split_index_recursion_is_handled_gracefully() {
297+ let err = try_file("v2_split_index_recursive").expect_err("recursion fails gracefully");
298+ assert!(matches!(
299+ err,
300+ git_index::file::init::Error::Decode(git_index::decode::Error::ChecksumMismatch { .. })
301+ ));
302+}
303+
293304 #[test]
294305 fn split_index_and_regular_index_of_same_content_are_indeed_the_same() {
295306 let base =

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t