···245245 }
246246}
247247248248+// `worktree_stream` builds a `gix_filter::Pipeline`
249249+// out of whatever config it has loaded,
250250+// so a filter command defined in any of those sources
251251+// runs against the tree being archived.
252252+// The repository's own config is then the only source gix reads,
253253+// and mr knot wrote that file when it created the repo under the scan path.
254254+// We pin the trust because gix otherwise works it out from who owns the git dir,
255255+// and it reduces the trust when someone else owns that dir,
256256+// at which point it applies a 16MiB object allocation limit
257257+// and treats the repository's own config sections as untrusted.
258258+// Under isolation no safe.directory entry can restore `Full` trust,
259259+// since gix honors that key from only system/global config.
260260+fn isolated_open_options() -> gix::open::Options {
261261+ gix::open::Options::isolated().with(gix::sec::Trust::Full)
262262+}
263263+248264pub(crate) fn init_bare_with_format(
249265 path: &Path,
250266 format: ObjectFormat,
···257273 object_hash,
258274 ..Default::default()
259275 },
260260- gix::open::Options::default(),
276276+ isolated_open_options(),
261277 )
262278 .map(Into::into)
263279 .map_err(|error| error.to_string())
···270286}
271287272288fn init_bare_idempotent(path: PathBuf) -> Result<Repo, GitError> {
273273- if let Ok(git) = gix::open(&path) {
289289+ if let Ok(git) = gix::open_opts(&path, isolated_open_options()) {
274290 return Ok(assembled(git, path));
275291 }
276292 let parent = path.parent().ok_or_else(|| GitError::Create {
···283299 })?;
284300 let staging = staging_path(parent);
285301 let _ = std::fs::remove_dir_all(&staging);
286286- gix::init_bare(&staging).map_err(|error| GitError::Create {
302302+ init_bare_with_format(&staging, ObjectFormat::SHA1).map_err(|message| GitError::Create {
287303 path: staging.clone(),
288288- message: error.to_string(),
304304+ message,
289305 })?;
290306 match std::fs::rename(&staging, &path) {
291307 Ok(()) => Repo::open(path),
···442458impl Repo {
443459 pub fn open(path: impl Into<PathBuf>) -> Result<Repo, GitError> {
444460 let path = path.into();
445445- let git = gix::open(&path).map_err(|error| GitError::Open {
446446- path: path.clone(),
447447- message: error.to_string(),
448448- })?;
461461+ let git =
462462+ gix::open_opts(&path, isolated_open_options()).map_err(|error| GitError::Open {
463463+ path: path.clone(),
464464+ message: error.to_string(),
465465+ })?;
449466 Ok(assembled(git, path))
450467 }
451468···10841101 let layout = Layout::new(dir.path());
10851102 let did = RepoDid::new("did:plc:squid").unwrap();
10861103 (dir, layout, did)
11041104+ }
11051105+11061106+ #[test]
11071107+ fn every_repo_opens_against_its_own_config_and_nothing_ambient() {
11081108+ let permissions = isolated_open_options().permissions;
11091109+ let config = permissions.config;
11101110+ assert!(
11111111+ !config.system
11121112+ && !config.git
11131113+ && !config.user
11141114+ && !config.env
11151115+ && !config.includes
11161116+ && !config.git_binary,
11171117+ "a filter driver in ambient config would execute when worktree_stream archives a \
11181118+ pushed tree, so we read only the repository's own config: {config:?}"
11191119+ );
11201120+ assert!(
11211121+ !permissions.attributes.system
11221122+ && !permissions.attributes.git
11231123+ && !permissions.attributes.git_binary,
11241124+ "only the archived tree's own .gitattributes may set filter= on a path: {:?}",
11251125+ permissions.attributes
11261126+ );
11271127+ let denied = |permission| matches!(permission, gix::sec::Permission::Deny);
11281128+ let env = permissions.env;
11291129+ assert!(
11301130+ denied(env.xdg_config_home)
11311131+ && denied(env.home)
11321132+ && denied(env.git_prefix)
11331133+ && denied(env.ssh_prefix)
11341134+ && denied(env.identity)
11351135+ && denied(env.objects)
11361136+ && denied(env.http_transport),
11371137+ "gix resolves GIT_CONFIG_KEY_n, HOME and XDG_CONFIG_HOME into config that can define a \
11381138+ filter driver: {env:?}"
11391139+ );
11401140+ assert!(
11411141+ permissions.is_isolated(),
11421142+ "every permission must match the set gix itself calls isolated, including any field a \
11431143+ gix upgrade adds that the three checks above don't name: {permissions:?}"
11441144+ );
11451145+ }
11461146+11471147+ #[cfg(unix)]
11481148+ #[test]
11491149+ fn the_knot_keeps_full_trust_on_a_git_dir_it_no_longer_owns() {
11501150+ const NOBODY: u32 = 65534;
11511151+ let (_dir, layout, did) = repo();
11521152+ layout.create(&did).unwrap();
11531153+ let path = layout.repo_path(&did).unwrap();
11541154+ let unreachable_uid = |kind| {
11551155+ matches!(
11561156+ kind,
11571157+ std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::InvalidInput
11581158+ )
11591159+ };
11601160+ match std::os::unix::fs::chown(&path, Some(NOBODY), None) {
11611161+ Err(error) if unreachable_uid(error.kind()) => {
11621162+ eprintln!(
11631163+ "skipping the foreign-owner trust check: chown to {NOBODY} needs root and a \
11641164+ uid mapping reaching that far"
11651165+ );
11661166+ return;
11671167+ }
11681168+ outcome => outcome.unwrap(),
11691169+ }
11701170+ assert_eq!(
11711171+ layout.open(&did).unwrap().git().git_dir_trust(),
11721172+ gix::sec::Trust::Full,
11731173+ "reduced trust applies a 16MiB limit to every object allocation"
11741174+ );
11751175+ assert_eq!(
11761176+ gix::open_opts(&path, gix::open::Options::isolated())
11771177+ .unwrap()
11781178+ .git_dir_trust(),
11791179+ gix::sec::Trust::Reduced,
11801180+ "gix raised the trust of a git dir owned by another user with no safe.directory entry \
11811181+ in reach"
11821182+ );
10871183 }
1088118410891185 #[test]
···44use knot_types::RepoDid;
5566pub use knot_fixtures::{
77- available as git_available, commit as commit_file, must as git_ok, run as git,
77+ available as git_available, commit as commit_file, contains, must as git_ok, run as git,
88};
991010pub fn seeded() -> (tempfile::TempDir, tempfile::TempDir, Layout, RepoDid) {
···1212use knot_pack::{RepoLookup, RepoResolver, RepoTarget};
1313use knot_types::{ObjectFormat, RepoDid};
14141515-pub use knot_fixtures::{commit, must, run as git};
1515+pub use knot_fixtures::{commit, contains, must, run as git};
16161717pub fn pkt(payload: &[u8]) -> Vec<u8> {
1818 let mut out = format!("{:04x}", payload.len() + 4).into_bytes();
···1360136013611361 let tar = std::fs::read(&out_tar).unwrap();
13621362 assert!(
13631363- tar.windows(b"README.md".len()).any(|w| w == b"README.md"),
13631363+ knot_fixtures::contains(&tar, b"README.md"),
13641364 "archived tar must contain the README.md entry"
13651365 );
13661366}