[PATCH koji] added koji-helper setuid program

Enrico Scholz enrico.scholz at informatik.tu-chemnitz.de
Sat Sep 15 19:41:12 UTC 2007


This patch adds a 'koji-helper' setuid program which implements the
following methods:

* koji-helper rmrf <dir>
    removes everything under <dir>, inclusive <dir>.  It does not cross
    filesystem borders

* koji-helper rmtree <dir>
    removes everything under <dir>, but not <dir> itself. It does not cross
    filesystem borders


Methods above are implemented to replace the python 'safe_rmtree()' method
which was never safe, nor will work when 'kojid' is running as non-root.

Signed-off-by: Enrico Scholz <enrico.scholz at informatik.tu-chemnitz.de>
---

 Makefile          |   15 ++-
 builder/kojid     |   53 +++--------
 koji.spec         |    3 -
 src/koji-helper.c |  260 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 286 insertions(+), 45 deletions(-)

diff --git a/Makefile b/Makefile
index cd88d4b..2a239fd 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,8 @@ NAME=koji
 SPECFILE = $(firstword $(wildcard *.spec))
 SUBDIRS = hub builder koji cli docs util www
 
+sbindir = /usr/sbin
+
 ifdef DIST
 DIST_DEFINES := --define "dist $(DIST)"
 endif
@@ -52,11 +54,14 @@ ifndef TAG
 TAG=$(NAME)-$(VERSION)-$(RELEASE)
 endif
 
-_default:
-	@echo "read the makefile"
+all:	src/koji-helper
+
+src/koji-helper:	src/koji-helper.c
+	$(CC) $(CFLAGS) $< -o $@
 
 clean:
 	rm -f *.o *.so *.pyc *~ koji*.bz2 koji*.src.rpm
+	rm -f src/koji-helper
 	rm -rf koji-$(VERSION)
 	for d in $(SUBDIRS); do make -s -C $$d clean; done
 
@@ -100,14 +105,16 @@ force-tag::
 #	@$(MAKE) tag TAG_OPTS="-F $(TAG_OPTS)"
 
 DESTDIR ?= /
-install:
+install:	all
 	@if [ "$(DESTDIR)" = "" ]; then \
 		echo " "; \
 		echo "ERROR: A destdir is required"; \
 		exit 1; \
 	fi
 
-	mkdir -p $(DESTDIR)
+	mkdir -p $(DESTDIR)$(sbindir)
+
+	install -p -m0710 src/koji-helper $(DESTDIR)$(sbindir)
 
 	for d in $(SUBDIRS); do make DESTDIR=`cd $(DESTDIR); pwd` \
 		-C $$d install; [ $$? = 0 ] || exit 1; done
diff --git a/builder/kojid b/builder/kojid
index 6e973fe..5940954 100755
--- a/builder/kojid
+++ b/builder/kojid
@@ -150,35 +150,17 @@ def log_output(path, args, outfile, uploadpath, cwd=None, logerror=0, append=0,
                     outfd.close()
                 return status[1]
 
-def safe_rmtree(path, unmount=False, strict=True):
+def safe_rmtree(path, strict=True, op='rmrf'):
     logger = logging.getLogger("koji.build")
-    #safe remove: with -xdev the find cmd will not cross filesystems
-    #             (though it will cross bind mounts from the same filesystem)
-    if not os.path.exists(path):
-        logger.debug("No such path: %s" % path)
-        return
-    if unmount:
-        umount_all(path)
-    #first rm -f non-directories
-    logger.debug('Scrubbing files in %s' % path)
-    rv = os.system("find '%s' -xdev \\! -type d -print0 |xargs -0 rm -f" % path)
-    msg = 'file removal failed (code %r) for %s' % (rv,path)
-    if rv != 0:
-        logger.warn(msg)
-        if strict:
-            raise koji.GenericError, msg
-        else:
-            return rv
-    #them rmdir directories
-    #with -depth, we start at the bottom and work up
-    logger.debug('Scrubbing directories in %s' % path)
-    rv = os.system("find '%s' -xdev -depth -type d -print0 |xargs -0 rmdir" % path)
-    msg = 'dir removal failed (code %r) for %s' % (rv,path)
-    if rv != 0:
-        logger.warn(msg)
-        if strict:
-            raise koji.GenericError, msg
-    return rv
+    rc     = os.spawnvp(os.P_WAIT, '/usr/sbin/koji-helper', ['/usr/sbin/koji-helper', op, path])
+    if rc!=0:
+	msg = "directory removal failed (code %r) for %s" % (rc,path)
+	logger.warn(msg)
+	if strict:
+		raise koji.GenericError, msg
+	else:
+		return rc
+    return rc
 
 def umount_all(topdir):
     "Unmount every mount under topdir"
@@ -635,7 +617,7 @@ class TaskManager(object):
                 if age > 3600*24:
                     #dir untouched for a day
                     self.logger.info("Removing buildroot: %s" % desc)
-                    if topdir and safe_rmtree(topdir, unmount=True, strict=False) != 0:
+                    if topdir and safe_rmtree(topdir, strict=False) != 0:
                         continue
                     #also remove the config
                     try:
@@ -644,15 +626,7 @@ class TaskManager(object):
                         self.logger.warn("%s: can't remove config: %s" % (desc, e))
                 elif age > 120:
                     if rootdir:
-                        try:
-                            flist = os.listdir(rootdir)
-                        except OSError, e:
-                            self.logger.warn("%s: can't list rootdir: %s" % (desc, e))
-                            continue
-                        if flist:
-                            self.logger.info("%s: clearing rootdir" % desc)
-                        for fn in flist:
-                            safe_rmtree("%s/%s" % (rootdir,fn), unmount=True, strict=False)
+                        safe_rmtree(rootdir, strict=False, op='rmtree')
                 else:
                     self.logger.debug("Recent buildroot: %s: %i seconds" % (desc,age))
         self.logger.debug("Local buildroots: %d" % len(local_br))
@@ -1211,8 +1185,7 @@ class BaseTaskHandler(object):
     def removeWorkdir(self):
         if self.workdir is None:
             return
-        safe_rmtree(self.workdir, unmount=False, strict=True)
-        #os.spawnvp(os.P_WAIT, 'rm', ['rm', '-rf', self.workdir])
+        os.spawnvp(os.P_WAIT, 'rm', ['rm', '-rf', self.workdir])
 
     def wait(self, subtasks=None, all=False, failany=False):
         """Wait on subtasks
diff --git a/koji.spec b/koji.spec
index 13d3bf0..f14bb6e 100644
--- a/koji.spec
+++ b/koji.spec
@@ -16,7 +16,6 @@ Group: Applications/System
 URL: http://hosted.fedoraproject.org/projects/koji
 Source: %{name}-%{PACKAGE_VERSION}.tar.bz2
 BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
-BuildArch: noarch
 Requires: python-krbV >= 1.0.13
 Requires: rpm-python
 Requires: pyOpenSSL
@@ -89,6 +88,7 @@ koji-web is a web UI to the Koji system.
 %setup -q
 
 %build
+make CFLAGS="$CFLAGS" CC="%__cc" all
 
 %install
 rm -rf $RPM_BUILD_ROOT
@@ -125,6 +125,7 @@ rm -rf $RPM_BUILD_ROOT
 
 %files builder
 %defattr(-,root,root)
+%attr(4710,root,kojibuilder) %_sbindir/koji-helper
 %{_sbindir}/kojid
 %{_initrddir}/kojid
 %config(noreplace) %{_sysconfdir}/sysconfig/kojid
diff --git a/src/koji-helper.c b/src/koji-helper.c
new file mode 100644
index 0000000..a3d0921
--- /dev/null
+++ b/src/koji-helper.c
@@ -0,0 +1,260 @@
+/*	--*- c -*--
+ * Copyright (C) 2007 Enrico Scholz <enrico.scholz at informatik.tu-chemnitz.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define _GNU_SOURCE
+
+#ifndef MOCK_ROOT
+#  define MOCK_ROOT	"/var/lib/mock"
+#endif
+
+#include <sys/stat.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <stdbool.h>
+
+static int __attribute__((__nonnull__(1, 2)))
+safe_chdir(char const *path, struct stat const *exp_st) 
+{
+	struct stat		cur_st;
+	
+	if (strchr(path, '/')) {
+		fprintf(stderr, "safe_chdir(): invalid char in path '%s'\n", path);
+		return -1;
+	}
+
+	if (strcmp(path, "..")==0) {
+		fprintf(stderr, "safe_chdir(): parent dir referred\n");
+		return -1;
+	}
+
+	if (chdir(path) < 0) {
+		fprintf(stderr, "chdir(%s): %s\n", path, strerror(errno));
+		return -1;
+	}
+
+	if (stat(".", &cur_st) < 0) {
+		fprintf(stderr, "stat(%s): %s\n", path, strerror(errno));
+		return -2;
+	}
+
+	if (cur_st.st_dev != exp_st->st_dev ||
+	    cur_st.st_ino != exp_st->st_ino) {
+		fprintf(stderr, "RACE: path '%s' changed before chdir()\n", path);
+		return -2;
+	}
+
+	return 0;
+}
+
+static int
+rmrf_cwd(struct stat *cwd_st)
+{
+	DIR		*cwd = opendir(".");
+	int		rc   = -1;
+
+	if (!cwd) {
+		perror("opendir()");
+		return -1;
+	}
+
+	for (;;) {
+		struct dirent	*ent = readdir(cwd);
+		struct stat	st;
+
+		if (!ent)
+			break;
+
+		if (ent->d_name[0] == '.' &&
+		    (ent->d_name[1] == '\0'||
+		     (ent->d_name[1] == '.' && ent->d_name[2] == '\0')))
+			continue;	/* skip '.' and '..' */
+
+		if (lstat(ent->d_name, &st) < 0) {
+			fprintf(stderr, "rmrf_cwd: lstat(%s): %s\n",
+				ent->d_name, strerror(errno));
+			continue;
+		}
+
+		if (cwd_st && cwd_st->st_dev != st.st_dev)
+			continue;	/* do not cross devices */
+		else if (S_ISDIR(st.st_mode)) {
+			switch (safe_chdir(ent->d_name, &st)) {
+			case -1: continue;
+			case -2: break;
+			default: rmrf_cwd(&st); break;
+			}
+
+			if (fchdir(dirfd(cwd)) < 0) {
+				perror("rmrf_cwd: fchdir()");
+				goto err;
+			}
+
+			if (rmdir(ent->d_name) < 0) {
+				fprintf(stderr, "rmrf_cwd: rmdir(%s): %s\n",
+					ent->d_name, strerror(errno));
+				continue;
+			}
+		} else if (unlink(ent->d_name) < 0) {
+			fprintf(stderr, "rmrf_cwd: unlink(%s): %s\n",
+				ent->d_name, strerror(errno));
+			continue;
+		}
+	}
+
+	rc = 0;
+err:
+	closedir(cwd);
+	return rc;
+}
+
+static int
+safe_chdir_subpath(char const *path_c, size_t path_len)
+{
+	char		path[path_len+1];
+	char		*ptr = path;
+	int		rc = 0;
+
+	if (path_len == 0)
+		return 0;
+
+	strncpy(path, path_c, path_len);
+	path[path_len] = '\0';
+
+	while (ptr) {
+		char		*new_ptr = strsep(&ptr, "/");
+		struct stat	st;
+
+		if (*new_ptr == '\0')
+			continue;	/* skip empty path components
+					 * (e.g. double /) */
+		
+		if (lstat(new_ptr, &st) < 0) {
+			fprintf(stderr, "stat(%s): %s\n",
+				new_ptr, strerror(errno));
+			rc = -1;
+		} else if (!S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)) {
+			fprintf(stderr, "safe_chdir_subpath(): invalid mode of '%s': %04x\n",
+				new_ptr, st.st_mode);
+			rc = -1;
+		} else
+			rc = safe_chdir(new_ptr, &st);
+
+		if (rc < 0)
+			break;
+	}
+
+	return rc;
+}
+
+/* Usage: rmrf <dir> */
+static int
+do_rmrf(int argc, char *argv[], bool remove_parent_dir)
+{
+	char const	*dir;
+	size_t		dir_len;
+	char const	*last_path;
+	int		parent_fd;
+	
+	if (argc != 2) {
+		fprintf(stderr, "wrong number of parameters for 'rmrf' operation\n");
+		return EXIT_FAILURE;
+	}
+
+	dir       = argv[1];
+
+	/* strip leading MOCK_ROOT; it's a little bit hacky but required to
+	 * keep backward compatibility */
+	if (strncmp(dir, MOCK_ROOT, sizeof(MOCK_ROOT)-1) == 0)
+		dir += sizeof(MOCK_ROOT)-1;
+
+	while (*dir == '/')
+		++dir;			/* strip leading '/' */
+	
+	dir_len   = strlen(dir);
+	while (dir_len>0 && dir[dir_len-1] == '/')
+		--dir_len;		/* strip trailing '/' */
+
+	last_path = dir + dir_len;
+	while (last_path > dir && last_path[-1] != '/')
+		--last_path;
+
+	if (dir_len == 0) {
+		fprintf(stderr, "do_rmrf(): empty path\n");
+		return EXIT_FAILURE;
+	}
+	if (dir_len > 255) {
+		fprintf(stderr, "pathname too long\n");
+		return EXIT_FAILURE;
+	}
+
+	
+	/* real work begins here... */
+
+	if (chdir(MOCK_ROOT) < 0) {
+		perror("chdir(<MOCK_ROOT>)");
+		return EXIT_FAILURE;
+	}
+
+	if (last_path > dir &&		/* else, it would be a noop */
+	    safe_chdir_subpath(dir, last_path - dir) < 0)
+		return EXIT_FAILURE;
+
+	parent_fd = open(".", O_RDONLY|O_DIRECTORY);
+	if (parent_fd < 0) {
+		perror("open(<MOCK_ROOT>)");
+		return EXIT_FAILURE;
+	}
+
+	if (safe_chdir_subpath(last_path, dir+dir_len - last_path + 1) < 0)
+		return EXIT_FAILURE;
+
+	/* we are now *in* the given path */
+	if (rmrf_cwd(NULL) < 0)
+		return EXIT_FAILURE;
+
+	if (fchdir(parent_fd) < 0) {
+		perror("fchdir(<parent>)");
+		return EXIT_FAILURE;
+	}
+
+	if (remove_parent_dir &&
+	    rmdir(last_path) < 0)
+		return EXIT_FAILURE;
+
+	return EXIT_SUCCESS;
+}
+
+int main(int argc, char *argv[])
+{
+	if (argc < 2) {
+		fprintf(stderr, "not enough parameters\n");
+		return EXIT_FAILURE;
+	}
+
+	if (strcmp(argv[1], "rmrf") == 0)
+		return do_rmrf(argc-1, argv+1, true);
+	else if (strcmp(argv[1], "rmtree") == 0)
+		return do_rmrf(argc-1, argv+1, false);
+	else
+		fprintf(stderr, "unknown argument\n");
+
+	return EXIT_FAILURE;
+}




More information about the Fedora-buildsys-list mailing list