"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
do not allow versioned files in meta-data directories
To:
gameoftrees@openbsd.org
Date:
Sat, 9 May 2026 23:28:35 +0200

Download raw body.

Thread
Reject attempts to put versioned files into .git, .got, or .cvg directories.
 
Problem spotted by Runxi Yu and reported to me privately on IRC.

This has a slight security impact if users are not careful.
There is no way to directly execute arbitrary code by changing meta-data
in .got or .cvg. But if a user can be tricked into checking out a bad .got
directory then a malicious got.conf file could be installed which sets
the default "origin" remote to a malicious server. A man-in-the-middle
attack becomes possible if the user fetches from the malicious remote
server and accepts the wrong SSH host key without verification.

And I think we need to reject versioned files in .git as well, mostly to
avoid exposing Git users to bad settings used by Git clients started in a
got work tree for some reason.

ok?
 
M  got/got.1                    |   1+  0-
M  lib/fileindex.c              |  40+  1-
M  lib/got_lib_fileindex.h      |   2+  0-
M  lib/worktree.c               |   8+  0-
M  regress/cmdline/add.sh       |  91+  0-
M  regress/cmdline/checkout.sh  |  50+  0-

6 files changed, 192 insertions(+), 1 deletion(-)

commit - d8352339234706d4918c7b2c799bea32615bce6b
commit + bfb0c422d5b75974fffed99d1b7af8e98c8fd1eb
blob - 28b36f3cb34c0596473ec1cfe4251cca2302d9ba
blob + 64c195b4209cc4928aeeafbcbb22ab518ccab5b9
--- got/got.1
+++ got/got.1
@@ -670,6 +670,7 @@ Show the status of each affected file, using the follo
 .Bl -column YXZ description
 .It A Ta new file was added
 .It E Ta file already exists in work tree's meta-data
+.It ? Ta file with a path outside of the work tree was skipped
 .El
 .Pp
 If the
blob - dbef298e12a03f1a40e8adf8a5cffe916aca0681
blob + 61e4d2755fca1c170546162bd85ca7c301a13c1e
--- lib/fileindex.c
+++ lib/fileindex.c
@@ -39,7 +39,16 @@
 #include "got_lib_hash.h"
 #include "got_lib_fileindex.h"
 #include "got_lib_worktree.h"
+#include "got_lib_delta.h"
+#include "got_lib_object.h"
+#include "got_lib_pack.h"
+#include "got_lib_object_cache.h"
+#include "got_lib_repository.h"
 
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
 /* got_fileindex_entry flags */
 #define GOT_FILEIDX_F_PATH_LEN		0x00000fff
 #define GOT_FILEIDX_F_STAGE		0x0000f000
@@ -158,11 +167,42 @@ got_fileindex_entry_mark_skipped(struct got_fileindex_
 }
 
 const struct got_error *
+got_fileindex_entry_relpath_allowed(const char *relpath, size_t relpath_len)
+{
+	const char *forbidden[] = {
+		GOT_GIT_DIR,
+		GOT_WORKTREE_GOT_DIR,
+		GOT_WORKTREE_CVG_DIR
+	};
+	size_t i;
+
+	for (i = 0; i < nitems(forbidden); i++) {
+		if (got_path_cmp(relpath, forbidden[i],
+		    relpath_len, strlen(forbidden[i])) == 0 ||
+		    got_path_is_child(relpath, forbidden[i],
+		    strlen(forbidden[i]))) {
+			return got_error_fmt(GOT_ERR_BAD_PATH,
+			    "path '%s' cannot be added to the file-index",
+			    relpath);
+		}
+	}
+
+	return NULL;
+}
+
+const struct got_error *
 got_fileindex_entry_alloc(struct got_fileindex_entry **ie,
     const char *relpath)
 {
+	const struct got_error *err;
 	size_t len;
 
+	len = strlen(relpath);
+
+	err = got_fileindex_entry_relpath_allowed(relpath, len);
+	if (err)
+		return err;
+
 	*ie = calloc(1, sizeof(**ie));
 	if (*ie == NULL)
 		return got_error_from_errno("calloc");
@@ -175,7 +215,6 @@ got_fileindex_entry_alloc(struct got_fileindex_entry *
 		return err;
 	}
 
-	len = strlen(relpath);
 	if (len > GOT_FILEIDX_F_PATH_LEN)
 		len = GOT_FILEIDX_F_PATH_LEN;
 	(*ie)->flags |= len;
blob - ffe9361ee22cfae0355ac626b5e8b456f4408e7d
blob + a1cb25ef6b6fbcb873a249f0c7c073e56105accb
--- lib/got_lib_fileindex.h
+++ lib/got_lib_fileindex.h
@@ -124,6 +124,8 @@ const struct got_error *got_fileindex_entry_update(str
 void got_fileindex_entry_mark_skipped(struct got_fileindex_entry *);
 const struct got_error *got_fileindex_entry_alloc(struct got_fileindex_entry **,
     const char *);
+const struct got_error *got_fileindex_entry_relpath_allowed(const char *,
+    size_t );
 void got_fileindex_entry_free(struct got_fileindex_entry *);
 
 struct got_fileindex *got_fileindex_alloc(enum got_hash_algorithm);
blob - eb978efbf47067ad8d8d95b3a1e5873bc0ad486a
blob + 84beca72bcf731e8bbe07af4536b44fde4c8f1c0
--- lib/worktree.c
+++ lib/worktree.c
@@ -1947,6 +1947,14 @@ update_blob(struct got_worktree *worktree,
 	int fd1 = -1, fd2 = -1;
 	int update_timestamps = 0;
 
+	err = got_fileindex_entry_relpath_allowed(path, strlen(path));
+	if (err) {
+		if (err->code != GOT_ERR_BAD_PATH)
+			return err;
+		return (*progress_cb)(progress_arg,
+		    GOT_STATUS_UNVERSIONED, path);
+	}
+
 	if (asprintf(&ondisk_path, "%s/%s", worktree->root_path, path) == -1)
 		return got_error_from_errno("asprintf");
 
blob - ebba1caf39b7ef4c28dcefcc7bb43a6ec5597c76
blob + f6ffb7eca936a84b2f2bc78f0b4de1823df43800
--- regress/cmdline/add.sh
+++ regress/cmdline/add.sh
@@ -485,6 +485,95 @@ test_add_symlink() {
 	test_done "$testroot" "$ret"
 }
 
+test_add_file_in_dotgit() {
+	local testroot=`test_init add_file_in_dotgit`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	mkdir -p $testroot/wt/.git
+	echo "new file" > $testroot/wt/.git/foo
+
+	(cd $testroot/wt && got add .git/foo \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "got add succeeded unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	echo -n > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	cat > $testroot/stderr.expected <<EOF
+got: path '.git/foo' cannot be added to the file-index: bad path
+EOF
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_add_file_in_dotgot() {
+	local testroot=`test_init add_file_in_dotgot`
+
+	got checkout $testroot/repo $testroot/wt > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "new file" > $testroot/wt/.got/foo
+
+	(cd $testroot/wt && got add .got/foo \
+		> $testroot/stdout 2> $testroot/stderr)
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "got add succeeded unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	echo -n > $testroot/stdout.expected
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	cat > $testroot/stderr.expected <<EOF
+got: path '.got/foo' cannot be added to the file-index: bad path
+EOF
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_add_basic
 run_test test_double_add
@@ -495,3 +584,5 @@ run_test test_add_force_delete_commit
 run_test test_add_directory
 run_test test_add_clashes_with_submodule
 run_test test_add_symlink
+run_test test_add_file_in_dotgit
+run_test test_add_file_in_dotgot
blob - 8ccd872d89efa4e676629a263300d659c3dd6ceb
blob + 1ac42106f911a923120f43f0bf02eb7fd88cd57f
--- regress/cmdline/checkout.sh
+++ regress/cmdline/checkout.sh
@@ -1075,6 +1075,55 @@ test_checkout_commit_keywords() {
 	test_done "$testroot" "$ret"
 }
 
+test_checkout_tree_with_dot_got() {
+	local testroot=`test_init checkout_tree_with_dot_got`
+
+	mkdir -p $testroot/repo/.got
+	echo 'foo' > $testroot/repo/.got/foo
+	git -C $testroot/repo add .got/foo
+	git_commit $testroot/repo -m "adding .got/foo"
+
+	local commit_id=`git_show_head $testroot/repo`
+
+	echo "?  $testroot/wt/.got/foo" > $testroot/stdout.expected
+	echo "A  $testroot/wt/alpha" >> $testroot/stdout.expected
+	echo "A  $testroot/wt/beta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt/epsilon/zeta" >> $testroot/stdout.expected
+	echo "A  $testroot/wt/gamma/delta" >> $testroot/stdout.expected
+	echo "Checked out refs/heads/master: $commit_id" \
+		>> $testroot/stdout.expected
+	echo "Now shut up and hack" >> $testroot/stdout.expected
+
+	got checkout $testroot/repo $testroot/wt > $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	echo "alpha" > $testroot/content.expected
+	echo "beta" >> $testroot/content.expected
+	echo "zeta" >> $testroot/content.expected
+	echo "delta" >> $testroot/content.expected
+	cat $testroot/wt/alpha $testroot/wt/beta $testroot/wt/epsilon/zeta \
+	    $testroot/wt/gamma/delta > $testroot/content
+
+	cmp -s $testroot/content.expected $testroot/content
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/content.expected $testroot/content
+	fi
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_checkout_basic
 run_test test_checkout_dir_exists
@@ -1094,3 +1143,4 @@ run_test test_checkout_quiet
 run_test test_checkout_umask
 run_test test_checkout_ulimit_n
 run_test test_checkout_commit_keywords
+run_test test_checkout_tree_with_dot_got