diff options
| -rw-r--r-- | .gitignore | 17 | ||||
| -rw-r--r-- | LICENSE | 15 | ||||
| -rw-r--r-- | Makefile | 42 | ||||
| -rw-r--r-- | README | 45 | ||||
| -rw-r--r-- | config.mk | 12 | ||||
| -rw-r--r-- | git-protection.1 | 65 | ||||
| -rw-r--r-- | git-protection.c | 98 |
7 files changed, 294 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8906e61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*\#* +*~ +*.o +*.a +*.t +*.lo +*.to +*.su +*.so +*.so.* +*.dll +*.dylib +*.gch +*.gcov +*.gcno +*.gcda +/git-protection @@ -0,0 +1,15 @@ +ISC License + +© 2026 Mattias Andrée <m@maandree.se> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0b22ed7 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +.POSIX: + +CONFIGFILE = config.mk +include $(CONFIGFILE) + +OBJ =\ + git-protection.o + +HDR = + +all: git-protection +$(OBJ): $(HDR) + +.c.o: + $(CC) -c -o $@ $< $(CFLAGS) $(CPPFLAGS) + +git-protection: $(OBJ) + $(CC) -o $@ $(OBJ) $(LDFLAGS) + $(BUILD_SUDO) sh -c 'chown -- 0:0 $@ && chmod -- 4755 $@' + +install: git-protection + mkdir -p -- "$(DESTDIR)$(PREFIX)/bin" + mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/" + cp -- git-protection "$(DESTDIR)$(PREFIX)/bin/" + cp -- git-protection.1 "$(DESTDIR)$(MANPREFIX)/man1/" + +post-install: + chown -- '0:0' "$(DESTDIR)$(PREFIX)/bin/git-protection" + chmod -- 4755 "$(DESTDIR)$(PREFIX)/bin/git-protection" + +uninstall: + -rm -f -- "$(DESTDIR)$(PREFIX)/bin/git-protection" + -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/git-protection.1" + +clean: + -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch *.gcov *.gcno *.gcda + -rm -f -- git-protection + +.SUFFIXES: +.SUFFIXES: .o .c + +.PHONY: all install post-install uninstall clean @@ -0,0 +1,45 @@ +NAME + git-protection - Spawn a new program with .git mounted as read-only + +SYNOPSIS + git-protection utility [argument] ... + +DESCRIPTION + The git-protection utility runs a specified utility, but makes + the .git directory a read-only mountpoint the specified utility. + +OPTIONS + No options are supported. + +OPERANDS + The following operands are supported: + + utility + The name of the utility to be invoked. + + argument + A string to pass as an argument for the invoked utility. + +EXIT STATUS + If utility is invoked, the exit status of git-protection is the exit + status of utility; otherwise, the git-protection utility exits with + one of the following values: + + 125 An error occurred in the git-protection utility. + + 126 The utility specified by utility was found but could + not be invoked. + + 127 The utility specified by utility could not be found. + +RATIONALE + The git-protection utility can be used as a wrapper around agentic + aritifical intelligence tools to stop them from making destructive + changes to your git repository, only allowing them to write to the + workspace without involving git but read git data. Blocking the + tool from unstaging changes or otherwise remove information that + has been stored in git, actions that these tools otherwise like + to perform. + +SEE ALSO + None. diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..b58b379 --- /dev/null +++ b/config.mk @@ -0,0 +1,12 @@ +PREFIX = /usr +MANPREFIX = $(PREFIX)/share/man + +GIT_PATH = /usr/bin/git + +CC = c99 + +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE -D'GIT_PATH="$(GIT_PATH)"' +CFLAGS = +LDFLAGS = -lsimple + +BUILD_SUDO = @: diff --git a/git-protection.1 b/git-protection.1 new file mode 100644 index 0000000..db13233 --- /dev/null +++ b/git-protection.1 @@ -0,0 +1,65 @@ +.TH GIT-PROTECTION 1 GIT-PROTECTION +.SH NAME +git-protection \- Spawn a new program with .git mounted as read-only + +.SH SYNOPSIS +.B git-protection +.I utility +.RI [ argument ]\ ... + +.SH DESCRIPTION +The +.B git-protection +utility runs a specified utility, but makes the +.I .git +directory a read-only mountpoint the specified utility. + +.SH OPTIONS +No options are supported. + +.SH OPERANDS +The following operands are supported: +.TP +.I utility +The name of the utility to be invoked. +.TP +.I argument +A string to pass as an argument for the invoked utility. + +.SH EXIT STATUS +If +.I utility +is invoked, the exit status of +.B git-protection +is the exit status of utility; otherwise, the +.I git-protection +utility exits with one of the following values: +.TP +125 +An error occurred in the +.I git-protection +utility. +.TP +126 +The utility specified by +.I utility +was found but could not be invoked. +.TP +127 +The utility specified by +.I utility +could not be found. + +.SH RATIONALE +The +.I git-protection +utility can be used as a wrapper around agentic +aritifical intelligence tools to stop them from making destructive +changes to your git repository, only allowing them to write to the +workspace without involving git but read git data. Blocking the +tool from unstaging changes or otherwise remove information that +has been stored in git, actions that these tools otherwise like +to perform. + +.SH SEE ALSO +None. diff --git a/git-protection.c b/git-protection.c new file mode 100644 index 0000000..6084c74 --- /dev/null +++ b/git-protection.c @@ -0,0 +1,98 @@ +/* See LICENSE file for copyright and license details. */ +#include <sys/mount.h> +#include <sched.h> +#include <libsimple.h> +#include <libsimple-arg.h> + +NUSAGE(125, "utility [argument] ..."); + + +int +main(int argc, char *argv[]) +{ + int fds[2], status; + pid_t pid; + char *dir = NULL; + size_t dirsize = 0; + size_t dirlen = 0; + ssize_t r; + + libsimple_default_failure_exit = 125; + + ARGBEGIN { + default: + usage(); + } ARGEND; + + if (!argc) + usage(); + + if (unshare(CLONE_NEWNS)) + eprintf("unshare CLONE_NEWNS:"); + if (mount("none", "/", NULL, MS_REC | MS_SLAVE, NULL)) + eprintf("mount none / NULL MS_REC|MS_SLAVE NULL:"); + + if (pipe(fds)) + eprintf("pipe:"); + pid = fork(); + if (pid < 0) + eprintf("fork:"); + if (pid == 0) { + if (setegid(getgid())) + eprintf("setegid <real group>:"); + if (seteuid(getuid())) + eprintf("seteuid <real user>:"); + + close(fds[0]); + if (fds[1] != STDOUT_FILENO) { + if (dup2(fds[1], STDOUT_FILENO) != STDOUT_FILENO) + eprintf("dup2 <pipe> STDOUT_FILENO:"); + close(fds[1]); + } + execlp(GIT_PATH, "git", "rev-parse", "--show-toplevel", NULL); + eprintf("execlp %s:", GIT_PATH); + } + close(fds[1]); + for (;;) { + if (dirlen == dirsize) + dir = erealloc(dir, dirsize += 512u); + r = read(fds[0], &dir[dirlen], dirsize - dirlen); + if (r <= 0) { + if (!r) + break; + if (errno == EINTR) + continue; + eprintf("read <pipe>:"); + } + dirlen += (size_t)r; + if (!dirlen || dir[--dirlen] != '\n') + eprintf("received invalid output from `git rev-parse --show-toplevel`"); + } + close(fds[0]); + if (waitpid(pid, &status, 0) != pid) + eprintf("waitpid <subprocess>:"); + if (status) { + if (WIFSIGNALED(status)) + eprintf("subprocess git was killed by signal %i\n", WTERMSIG(status)); + exit(libsimple_default_failure_exit); + } + if (dirsize - dirlen < sizeof("/.git")) + dir = erealloc(dir, dirlen + sizeof("/.git")); + stpcpy(&dir[dirlen], "/.git"); + + if (mount(dir, dir, NULL, MS_BIND, NULL)) + eprintf("mount %s %s NULL MS_BIND NULL:", dir, dir); + if (mount("none", dir, NULL, MS_BIND | MS_REMOUNT | MS_RDONLY, NULL)) + eprintf("mount none %s NULL MS_BIND|MS_REMOUNT|MS_RDONLY NULL:", dir); + + free(dir); + + if (setegid(getgid())) + eprintf("setegid <real group>:"); + if (seteuid(getuid())) + eprintf("seteuid <real user>:"); + + execvp(argv[0], argv); + enprintf(errno == ENOENT ? 127 : 126, "execvp %s:", argv[0]); + return 0; +} |
