Skip to content

Commit 9555efe

Browse files
committed
fix!: assure that special device names on Windows aren't allowed.
Otherwise it's possible to read or write to devices when interacting with references of the 'right' name. This behaviour can be controlled with the new `prohibit_windows_device_names` flag, which is adjustable on the `Store` instance as field, and which now has to be passed during instantiation as part of the new `store::init::Options` struct.
1 parent f1f0ba5 commit 9555efe

File tree

12 files changed

+144
-67
lines changed

12 files changed

+144
-67
lines changed

gix-ref/src/lib.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,25 @@ pub mod peel;
6262
///
6363
#[allow(clippy::empty_docs)]
6464
pub mod store {
65+
///
66+
#[allow(clippy::empty_docs)]
67+
pub mod init {
68+
69+
/// Options for use during [initialization](crate::Store::at).
70+
#[derive(Debug, Copy, Clone, Default)]
71+
pub struct Options {
72+
/// How to write the ref-log.
73+
pub write_reflog: super::WriteReflog,
74+
/// The kind of hash to expect in
75+
pub object_hash: gix_hash::Kind,
76+
/// The equivalent of `core.precomposeUnicode`.
77+
pub precompose_unicode: bool,
78+
/// If `true`, we will avoid reading from or writing to references that contains Windows device names
79+
/// to avoid side effects. This only needs to be `true` on Windows, but can be `true` on other platforms
80+
/// if they need to remain compatible with Windows.
81+
pub prohibit_windows_device_names: bool,
82+
}
83+
}
6584
/// The way a file store handles the reflog
6685
#[derive(Default, Debug, PartialOrd, PartialEq, Ord, Eq, Hash, Clone, Copy)]
6786
pub enum WriteReflog {
@@ -93,9 +112,8 @@ pub mod store {
93112
///
94113
#[path = "general/handle/mod.rs"]
95114
mod handle;
96-
pub use handle::find;
97-
98115
use crate::file;
116+
pub use handle::find;
99117
}
100118

101119
/// The git reference store.

gix-ref/src/store/file/find.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,19 @@ impl file::Store {
251251

252252
/// Read the file contents with a verified full reference path and return it in the given vector if possible.
253253
pub(crate) fn ref_contents(&self, name: &FullNameRef) -> io::Result<Option<Vec<u8>>> {
254-
let ref_path = self.reference_path(name);
254+
let (base, relative_path) = self.reference_path_with_base(name);
255+
if self.prohibit_windows_device_names
256+
&& relative_path
257+
.components()
258+
.filter_map(|c| gix_path::try_os_str_into_bstr(c.as_os_str().into()).ok())
259+
.any(|c| gix_validate::path::component_is_windows_device(c.as_ref()))
260+
{
261+
return Err(std::io::Error::new(
262+
std::io::ErrorKind::Other,
263+
format!("Illegal use of reserved Windows device name in \"{}\"", name.as_bstr()),
264+
));
265+
}
266+
let ref_path = base.join(relative_path);
255267

256268
match std::fs::File::open(&ref_path) {
257269
Ok(mut file) => {

gix-ref/src/store/file/loose/mod.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,26 @@ mod init {
3535
impl file::Store {
3636
/// Create a new instance at the given `git_dir`, which commonly is a standard git repository with a
3737
/// `refs/` subdirectory.
38-
/// The `object_hash` defines which kind of hash we should recognize.
38+
/// Use [`Options`](crate::store::init::Options) to adjust settings.
3939
///
40-
/// Note that if `precompose_unicode` is set, the `git_dir` is also expected to use precomposed unicode,
41-
/// or else some operations that strip prefixes will fail.
40+
/// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options,
41+
/// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail.
4242
pub fn at(
4343
git_dir: PathBuf,
44-
write_reflog: file::WriteReflog,
45-
object_hash: gix_hash::Kind,
46-
precompose_unicode: bool,
44+
crate::store::init::Options {
45+
write_reflog,
46+
object_hash,
47+
precompose_unicode,
48+
prohibit_windows_device_names,
49+
}: crate::store::init::Options,
4750
) -> Self {
4851
file::Store {
4952
git_dir,
5053
packed_buffer_mmap_threshold: packed_refs_mmap_threshold(),
5154
common_dir: None,
5255
write_reflog,
5356
namespace: None,
57+
prohibit_windows_device_names,
5458
packed: gix_fs::SharedFileSnapshotMut::new().into(),
5559
object_hash,
5660
precompose_unicode,
@@ -60,21 +64,25 @@ mod init {
6064
/// Like [`at()`][file::Store::at()], but for _linked_ work-trees which use `git_dir` as private ref store and `common_dir` for
6165
/// shared references.
6266
///
63-
/// Note that if `precompose_unicode` is set, the `git_dir` and `common_dir` are also expected to use precomposed unicode,
64-
/// or else some operations that strip prefixes will fail.
67+
/// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set, the `git_dir` and
68+
/// `common_dir` are also expected to use precomposed unicode, or else some operations that strip prefixes will fail.
6569
pub fn for_linked_worktree(
6670
git_dir: PathBuf,
6771
common_dir: PathBuf,
68-
write_reflog: file::WriteReflog,
69-
object_hash: gix_hash::Kind,
70-
precompose_unicode: bool,
72+
crate::store::init::Options {
73+
write_reflog,
74+
object_hash,
75+
precompose_unicode,
76+
prohibit_windows_device_names,
77+
}: crate::store::init::Options,
7178
) -> Self {
7279
file::Store {
7380
git_dir,
7481
packed_buffer_mmap_threshold: packed_refs_mmap_threshold(),
7582
common_dir: Some(common_dir),
7683
write_reflog,
7784
namespace: None,
85+
prohibit_windows_device_names,
7886
packed: gix_fs::SharedFileSnapshotMut::new().into(),
7987
object_hash,
8088
precompose_unicode,

gix-ref/src/store/file/loose/reflog/create_or_update/tests.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ fn hex_to_id(hex: &str) -> gix_hash::ObjectId {
1414

1515
fn empty_store(writemode: WriteReflog) -> Result<(TempDir, file::Store)> {
1616
let dir = TempDir::new()?;
17-
let store = file::Store::at(dir.path().into(), writemode, gix_hash::Kind::Sha1, false);
17+
let store = file::Store::at(
18+
dir.path().into(),
19+
crate::store::init::Options {
20+
write_reflog: writemode,
21+
..Default::default()
22+
},
23+
);
1824
Ok((dir, store))
1925
}
2026

gix-ref/src/store/file/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ pub struct Store {
2727
pub write_reflog: WriteReflog,
2828
/// The namespace to use for edits and reads
2929
pub namespace: Option<Namespace>,
30+
/// This is only useful on Windows, which may have 'virtual' devices on each level of a path so that
31+
/// reading or writing `refs/heads/CON` for example would read from the console, or write to it.
32+
pub prohibit_windows_device_names: bool,
3033
/// If set, we will convert decomposed unicode like `a\u308` into precomposed unicode like `ä` when reading
3134
/// ref names from disk.
3235
/// Note that this is an internal operation that isn't observable on the outside, but it's needed for lookups

gix-ref/src/store/general/init.rs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use std::path::PathBuf;
22

3-
use crate::store::WriteReflog;
4-
53
mod error {
64
/// The error returned by [`crate::Store::at()`].
75
#[derive(Debug, thiserror::Error)]
@@ -19,23 +17,16 @@ use crate::file;
1917
#[allow(dead_code)]
2018
impl crate::Store {
2119
/// Create a new store at the given location, typically the `.git/` directory.
20+
/// Use [`opts`](crate::store::init::Options) to adjust settings.
2221
///
23-
/// `object_hash` defines the kind of hash to assume when dealing with refs.
24-
/// `precompose_unicode` is used to set to the value of [`crate::file::Store::precompose_unicode].
25-
///
26-
/// Note that if `precompose_unicode` is set, the `git_dir` is also expected to use precomposed unicode,
27-
/// or else some operations that strip prefixes will fail.
28-
pub fn at(
29-
git_dir: PathBuf,
30-
reflog_mode: WriteReflog,
31-
object_hash: gix_hash::Kind,
32-
precompose_unicode: bool,
33-
) -> Result<Self, Error> {
22+
/// Note that if [`precompose_unicode`](crate::store::init::Options::precompose_unicode) is set in the options,
23+
/// the `git_dir` is also expected to use precomposed unicode, or else some operations that strip prefixes will fail.
24+
pub fn at(git_dir: PathBuf, opts: crate::store::init::Options) -> Result<Self, Error> {
3425
// for now, just try to read the directory - later we will do that naturally as we have to figure out if it's a ref-table or not.
3526
std::fs::read_dir(&git_dir)?;
3627
Ok(crate::Store {
3728
inner: crate::store::State::Loose {
38-
store: file::Store::at(git_dir, reflog_mode, object_hash, precompose_unicode),
29+
store: file::Store::at(git_dir, opts),
3930
},
4031
})
4132
}

gix-ref/tests/file/mod.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,13 @@ pub fn store_with_packed_refs() -> crate::Result<Store> {
1515

1616
pub fn store_at(name: &str) -> crate::Result<Store> {
1717
let path = gix_testtools::scripted_fixture_read_only_standalone(name)?;
18-
Ok(Store::at(
19-
path.join(".git"),
20-
gix_ref::store::WriteReflog::Normal,
21-
gix_hash::Kind::Sha1,
22-
false,
23-
))
18+
Ok(Store::at(path.join(".git"), Default::default()))
2419
}
2520

2621
fn store_writable(name: &str) -> crate::Result<(gix_testtools::tempfile::TempDir, Store)> {
2722
let dir = gix_testtools::scripted_fixture_writable_standalone(name)?;
2823
let git_dir = dir.path().join(".git");
29-
Ok((
30-
dir,
31-
Store::at(
32-
git_dir,
33-
gix_ref::store::WriteReflog::Normal,
34-
gix_hash::Kind::Sha1,
35-
false,
36-
),
37-
))
24+
Ok((dir, Store::at(git_dir, Default::default())))
3825
}
3926

4027
struct EmptyCommit;

gix-ref/tests/file/store/mod.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ fn precompose_unicode_journey() -> crate::Result {
2121

2222
let store_decomposed = gix_ref::file::Store::at(
2323
root,
24-
WriteReflog::Always,
25-
gix_hash::Kind::Sha1,
26-
false, /* precompose_unicode */
24+
gix_ref::store::init::Options {
25+
write_reflog: WriteReflog::Always,
26+
precompose_unicode: false,
27+
..Default::default()
28+
},
2729
);
2830
assert!(!store_decomposed.precompose_unicode);
2931

@@ -46,9 +48,11 @@ fn precompose_unicode_journey() -> crate::Result {
4648

4749
let store_precomposed = gix_ref::file::Store::at(
4850
tmp.path().join(precomposed_a), // it's important that root paths are also precomposed then.
49-
WriteReflog::Always,
50-
gix_hash::Kind::Sha1,
51-
true, /* precompose_unicode */
51+
gix_ref::store::init::Options {
52+
write_reflog: WriteReflog::Always,
53+
precompose_unicode: true,
54+
..Default::default()
55+
},
5256
);
5357

5458
let precomposed_ref = format!("refs/heads/{precomposed_a}");

gix-ref/tests/file/store/reflog.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
fn store() -> crate::Result<crate::file::Store> {
22
Ok(crate::file::Store::at(
33
gix_testtools::scripted_fixture_read_only_standalone("make_repo_for_reflog.sh")?.join(".git"),
4-
gix_ref::store::WriteReflog::Disable,
5-
gix_hash::Kind::Sha1,
6-
false,
4+
gix_ref::store::init::Options {
5+
write_reflog: gix_ref::store::WriteReflog::Disable,
6+
..Default::default()
7+
},
78
))
89
}
910

gix-ref/tests/file/transaction/mod.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,7 @@ pub(crate) mod prepare_and_commit {
2222

2323
pub(crate) fn empty_store() -> crate::Result<(gix_testtools::tempfile::TempDir, file::Store)> {
2424
let dir = gix_testtools::tempfile::TempDir::new().unwrap();
25-
let store = file::Store::at(
26-
dir.path().into(),
27-
gix_ref::store::WriteReflog::Normal,
28-
gix_hash::Kind::Sha1,
29-
false,
30-
);
25+
let store = file::Store::at(dir.path().into(), Default::default());
3126
Ok((dir, store))
3227
}
3328

gix-ref/tests/file/transaction/prepare_and_commit/create_or_update/mod.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use gix_ref::{
1010
transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},
1111
Target,
1212
};
13+
use std::error::Error;
1314

1415
use crate::{
1516
file::{
@@ -430,6 +431,63 @@ fn symbolic_reference_writes_reflog_if_previous_value_is_set() -> crate::Result
430431
Ok(())
431432
}
432433

434+
#[test]
435+
fn windows_device_name_is_illegal_with_enabled_windows_protections() -> crate::Result {
436+
let (_keep, mut store) = empty_store()?;
437+
store.prohibit_windows_device_names = true;
438+
let log_ignored = LogChange {
439+
mode: RefLog::AndReference,
440+
force_create_reflog: false,
441+
message: "ignored".into(),
442+
};
443+
444+
let new = Target::Peeled(hex_to_id("28ce6a8b26aa170e1de65536fe8abe1832bd3242"));
445+
for invalid_name in ["refs/heads/CON", "refs/CON/still-invalid"] {
446+
let err = store
447+
.transaction()
448+
.prepare(
449+
Some(RefEdit {
450+
change: Change::Update {
451+
log: log_ignored.clone(),
452+
new: new.clone(),
453+
expected: PreviousValue::Any,
454+
},
455+
name: invalid_name.try_into()?,
456+
deref: false,
457+
}),
458+
Fail::Immediately,
459+
Fail::Immediately,
460+
)
461+
.unwrap_err();
462+
assert_eq!(
463+
err.source().expect("inner").to_string(),
464+
format!("Illegal use of reserved Windows device name in \"{invalid_name}\""),
465+
"it's notable that the check also kicks in when the previous value doesn't matter - we expect a 'read' to happen anyway \
466+
- it can't be optimized away as the previous value is stored in the transaction result right now."
467+
);
468+
}
469+
470+
#[cfg(not(windows))]
471+
{
472+
store.prohibit_windows_device_names = false;
473+
let _prepared_transaction = store.transaction().prepare(
474+
Some(RefEdit {
475+
change: Change::Update {
476+
log: log_ignored.clone(),
477+
new,
478+
expected: PreviousValue::Any,
479+
},
480+
name: "refs/heads/CON".try_into()?,
481+
deref: false,
482+
}),
483+
Fail::Immediately,
484+
Fail::Immediately,
485+
)?;
486+
}
487+
488+
Ok(())
489+
}
490+
433491
#[test]
434492
fn symbolic_head_missing_referent_then_update_referent() -> crate::Result {
435493
for reflog_writemode in &[WriteReflog::Normal, WriteReflog::Disable, WriteReflog::Always] {

gix-ref/tests/file/worktree.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ fn main_store(
2929
let (dir, tmp) = dir(packed, writable)?;
3030
let git_dir = dir.join("repo").join(".git");
3131
Ok((
32-
gix_ref::file::Store::at(git_dir.clone(), Default::default(), Default::default(), false),
32+
gix_ref::file::Store::at(git_dir.clone(), Default::default()),
3333
gix_odb::at(git_dir.join("objects"))?,
3434
tmp,
3535
))
@@ -50,13 +50,7 @@ fn worktree_store(
5050
.into_repository_and_work_tree_directories();
5151
let common_dir = git_dir.join("../..");
5252
Ok((
53-
gix_ref::file::Store::for_linked_worktree(
54-
git_dir,
55-
common_dir.clone(),
56-
Default::default(),
57-
Default::default(),
58-
false,
59-
),
53+
gix_ref::file::Store::for_linked_worktree(git_dir, common_dir.clone(), Default::default()),
6054
gix_odb::at(common_dir.join("objects"))?,
6155
tmp,
6256
))

0 commit comments

Comments
 (0)