From: Stefan Sperling Subject: do not allow versioned files in meta-data directories To: gameoftrees@openbsd.org Date: Sat, 9 May 2026 23:28:35 +0200 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 < /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 < $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