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
22 Extension(#[from] extension::decode::Error),22 Extension(#[from] extension::decode::Error),
23 #[error("Index trailer should have been {expected} bytes long, but was {actual}")]23 #[error("Index trailer should have been {expected} bytes long, but was {actual}")]
24 UnexpectedTrailerLength { expected: usize, actual: usize },24 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 },
25 }30 }
26}31}
27pub use error::Error;32pub use error::Error;
@@ -41,6 +46,10 @@ pub struct Options { · 13 unmodified lines
41 /// The minimum size in bytes to load extensions in their own thread, assuming there is enough `num_threads` available.46 /// The minimum size in bytes to load extensions in their own thread, assuming there is enough `num_threads` available.
42 /// If set to 0, for example, extensions will always be read in their own thread if enough threads are available.47 /// If set to 0, for example, extensions will always be read in their own thread if enough threads are available.
43 pub min_extension_block_in_bytes_for_threading: usize,48 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>,
44}53}
45 54
46impl State {55impl State {
@@ -53,6 +62,7 @@ impl State { · 6 unmodified lines
53 Options {62 Options {
54 thread_limit,63 thread_limit,
55 min_extension_block_in_bytes_for_threading,64 min_extension_block_in_bytes_for_threading,
65 expected_checksum,
56 }: Options,66 }: Options,
57 ) -> Result<(Self, git_hash::ObjectId), Error> {67 ) -> Result<(Self, git_hash::ObjectId), Error> {
58 let (version, num_entries, post_header_data) = header::decode(data, object_hash)?;68 let (version, num_entries, post_header_data) = header::decode(data, object_hash)?;
@@ -205,6 +215,14 @@ impl State { · 146 unmodified lines
205 }215 }
206 216
207 let checksum = git_hash::ObjectId::from(data);217 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 }
208 let EntriesOutcome {226 let EntriesOutcome {
209 entries,227 entries,
210 path_backing,228 path_backing,
git-index/src/extension/decode.rs+8−4
@@ -2,7 +2,7 @@ use std::convert::TryInto; · 1 unmodified line
2 2
3use crate::{extension, extension::Signature, util::from_be_u32};3use crate::{extension, extension::Signature, util::from_be_u32};
4 4
5pub fn header(data: &[u8]) -> (Signature, u32, &[u8]) {5pub(crate) fn header(data: &[u8]) -> (Signature, u32, &[u8]) {
6 let (signature, data) = data.split_at(4);6 let (signature, data) = data.split_at(4);
7 let (size, data) = data.split_at(4);7 let (size, data) = data.split_at(4);
8 (signature.try_into().unwrap(), from_be_u32(size), data)8 (signature.try_into().unwrap(), from_be_u32(size), data)
@@ -11,8 +11,9 @@ pub fn header(data: &[u8]) -> (Signature, u32, &[u8]) { · 2 unmodified lines
11mod error {11mod error {
12 use crate::extension;12 use crate::extension;
13 13
14 /// The error returned by [extension::decode::all()][extension::decode::all()].14 /// The error returned when decoding extensions.
15 #[derive(Debug, thiserror::Error)]15 #[derive(Debug, thiserror::Error)]
16 #[allow(missing_docs)]
16 pub enum Error {17 pub enum Error {
17 #[error(18 #[error(
18 "Encountered mandatory extension '{}' which isn't implemented yet",19 "Encountered mandatory extension '{}' which isn't implemented yet",
@@ -25,7 +26,10 @@ mod error { · 6 unmodified lines
25}26}
26pub use error::Error;27pub use error::Error;
27 28
28pub fn all(maybe_beginning_of_extensions: &[u8], object_hash: git_hash::Kind) -> Result<(Outcome, &[u8]), Error> {29pub(crate) fn all(
30 maybe_beginning_of_extensions: &[u8],
31 object_hash: git_hash::Kind,
32) -> Result<(Outcome, &[u8]), Error> {
29 let mut ext_iter = match extension::Iter::new_without_checksum(maybe_beginning_of_extensions, object_hash) {33 let mut ext_iter = match extension::Iter::new_without_checksum(maybe_beginning_of_extensions, object_hash) {
30 Some(iter) => iter,34 Some(iter) => iter,
31 None => return Ok((Outcome::default(), maybe_beginning_of_extensions)),35 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
66}70}
67 71
68#[derive(Default)]72#[derive(Default)]
69pub struct Outcome {73pub(crate) struct Outcome {
70 pub tree: Option<extension::Tree>,74 pub tree: Option<extension::Tree>,
71 pub link: Option<extension::Link>,75 pub link: Option<extension::Link>,
72 pub resolve_undo: Option<extension::resolve_undo::Paths>,76 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
60 bitmaps: Some(Bitmaps { delete, replace }),60 bitmaps: Some(Bitmaps { delete, replace }),
61 })61 })
62}62}
63
64impl 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
73 73
74pub(crate) mod fs_monitor;74pub(crate) mod fs_monitor;
75 75
76pub(crate) mod decode;76///
77pub mod decode;
77 78
78///79///
79pub mod tree;80pub mod tree;
git-index/src/file/init.rs+2−65
@@ -40,8 +40,8 @@ impl File { · 39 unmodified lines
40 path,40 path,
41 checksum: Some(checksum),41 checksum: Some(checksum),
42 };42 };
43 if let Some(link) = file.link.take() {43 if let Some(mut link) = file.link.take() {
44 dissolve_link_extension(&mut file, link, object_hash, options)?;44 link.dissolve_into(&mut file, object_hash, options)?;
45 }45 }
46 46
47 Ok(file)47 Ok(file)
@@ -58,66 +58,3 @@ impl File { · 10 unmodified lines
58 }58 }
59 }59 }
60}60}
61
62fn 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
2set -eu -o pipefail
3
4export GIT_INDEX_VERSION=2
5git init -q
6git config index.threads 1
7
8touch a
9git add a
10git commit -m "empty"
11
12git update-index --split-index
13
14shared_index=".git/sharedindex.*"
15test -f $shared_index || \
16 { echo "shared index must be present in 'split' repository" && exit 42; }
17
18cp .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
23 let file = git_index::File::at(path, git_hash::Kind::Sha1, Default::default()).unwrap();23 let file = git_index::File::at(path, git_hash::Kind::Sha1, Default::default()).unwrap();
24 verify(file)24 verify(file)
25}25}
26pub(crate) fn file(name: &str) -> git_index::File {26pub(crate) fn try_file(name: &str) -> Result<git_index::File, git_index::file::init::Error> {
27 let file = git_index::File::at(27 let file = git_index::File::at(
28 crate::fixture_index_path(name),28 crate::fixture_index_path(name),
29 git_hash::Kind::Sha1,29 git_hash::Kind::Sha1,
30 Default::default(),30 Default::default(),
31 )31 )?;
32 .unwrap();32 Ok(verify(file))
33 verify(file)33}
34pub(crate) fn file(name: &str) -> git_index::File {
35 try_file(name).unwrap()
34}36}
35fn file_opt(name: &str, opts: git_index::decode::Options) -> git_index::File {37fn file_opt(name: &str, opts: git_index::decode::Options) -> git_index::File {
36 let file = git_index::File::at(crate::fixture_index_path(name), git_hash::Kind::Sha1, opts).unwrap();38 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
290 assert_eq!(file.version(), Version::V2);292 assert_eq!(file.version(), Version::V2);
291}293}
292 294
295#[test]
296fn 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
293#[test]304#[test]
294fn split_index_and_regular_index_of_same_content_are_indeed_the_same() {305fn split_index_and_regular_index_of_same_content_are_indeed_the_same() {
295 let base =306 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