From ce64025414eb7287dd09b977a6604524da997ed7 Mon Sep 17 00:00:00 2001 From: Enrico Scholz Date: Sat, 27 Oct 2007 10:39:32 +0200 Subject: [PATCH] added koji-helper setuid program This patch adds a 'koji-helper' setuid program which implements the following methods: * koji-helper rmrf removes everything under , inclusive . It does not cross filesystem borders * koji-helper rmtree removes everything under , but not 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 --- Makefile | 15 +++- builder/kojid | 53 +++-------- koji.spec | 3 +- src/koji-helper.c | 260 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 286 insertions(+), 45 deletions(-) create mode 100644 src/koji-helper.c diff --git a/Makefile b/Makefile index 7432f31..3589a25 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 @@ -103,14 +108,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 0fca59b..197ccb5 100755 --- a/builder/kojid +++ b/builder/kojid @@ -160,35 +160,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" @@ -637,7 +619,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: @@ -646,15 +628,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)) @@ -1213,8 +1187,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 57bb8e8..23eccc4 100644 --- a/koji.spec +++ b/koji.spec @@ -16,7 +16,6 @@ Group: Applications/System URL: http://fedorahosted.org/koji Source: https://fedorahosted.org/koji/attachment/wiki/KojiRelease/%{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 @@ -96,6 +95,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 @@ -135,6 +135,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 + * + * 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 . + */ + +#define _GNU_SOURCE + +#ifndef MOCK_ROOT +# define MOCK_ROOT "/var/lib/mock" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 */ +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()"); + 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()"); + 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()"); + 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; +} -- 1.5.5.1