On Tue, Aug 26, 2014 at 08:07:00PM -0700, Josh Triplett wrote: > Package: safe-rm > Severity: normal > > [As discussed at DebConf 14.] > > I'm working on making it possible to merge /bin and /sbin into /usr/bin > and /usr/sbin respectively. As a first step towards that, I'm planning > to propose a Debian Policy change to prohibit conflicts between > /bin/$foo and /usr/bin/$foo, and likewise for sbin. safe-rm is one of > only two packages that contains such a conflict: it installs > /usr/bin/rm, which would conflict with /bin/rm. > > In order to fix this while preserving safe-rm's default of automatic > protection on installation, safe-rm will need to divert and replace > /bin/rm. This will require quite a bit of care to do safely; see dash's > maintainer scripts for a safe procedure. > > Since safe-rm currently uses Perl, this change will also require one of > two approaches: either provide a small wrapper C program that attempts > to run /usr/bin/safe-rm and falls back to /bin/rm if that fails, or > rewrite safe-rm in C.
Here's a rewrite in C (currently modified to prepend /bin/echo to the program being run, for testing purposes). I did not understand the "Prepare for actually deleting the file" part of safe-rm, so I did not translate it (it was resetting IFS and PATH and stuff, that's not really needed here). Otherwise it should be an exact match. Now we basically only need to add a diversion and change the code to call the diverted rm binary instead of /bin/rm. -- Julian Andres Klode - Debian Developer, Ubuntu Member See http://wiki.debian.org/JulianAndresKlode and http://jak-linux.org/. Be friendly, do not top-post, and follow RFC 1855 "Netiquette". - If you don't I might ignore you.
/* safe-rm.c - wrapper around the rm command to prevent accidental deletions * * Copyright (C) 2008-2014 Francois Marier * Copyright (C) 2015 Julian Andres Klode <[email protected]> (C translation) * * 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, either version 3 of the License, or * (at your option) any later version. * * 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 #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <search.h> #include <string.h> #include <errno.h> #include <stdlib.h> #include <glob.h> #include <stdio.h> /** DEFAULT_PROTECTED_DIRS - Directories protected by default */ static const char *DEFAULT_PROTECTED_DIRS[] = { "/bin", "/boot", "/dev", "/etc", "/home", "/initrd", "/lib", "/lib32", "/lib64", "/proc", "/root", "/sbin", "/sys", "/usr", "/usr/bin", "/usr/include", "/usr/lib", "/usr/local", "/usr/local/bin", "/usr/local/include", "/usr/local/sbin", "/usr/local/share", "/usr/sbin", "/usr/share", "/usr/src", "/var", NULL, }; /** * protected_dirs - A binary search tree of protected directories * * This is used with tsearch() and friends */ void *protected_dirs; /** * strcmpvoid - Check whether string @a matches string @b * @a: the first string * @b: the second string * * Returns: strcmp(a, b). */ int strcmpvoid(const void *a, const void *b) { return strcmp(a, b); } /** * strany - Check if any string is not NULL. * @a: the first string * @b: the second string * * This can be used with tfind to check for emptyness: * tfind(NULL, &rootp, strany) * will return the first element in the tree. * * Returns: 0 if a or b is not NULL, 1 otherwise. */ int strany(const void *a, const void *b) { return (a != NULL || b != NULL) ? 0 : 1; } /** * rtrim - Trim @value * @value: The string that shall be trimmed. * @totrim: Characters that shall be trimmed * * Replaces all characters from @totrim on the right hand side of @value. */ void rtrim(char *value, char *totrim) { size_t len = strlen(value); while (--len > 0 && strchr(totrim, value[len]) != NULL) { value[len] = 0; } } /** * read_config_file - Read a configuration file * @path: The path to the configuration file * */ void read_config_file(const char *path) { struct stat buf; FILE *infile; char *line = NULL; size_t line_alloc = 0; glob_t globbed; if (stat(path, &buf) != 0) return; infile = fopen(path, "r"); if (infile == NULL) { fprintf(stderr, "Could not open configuration file: %s\n", path); return; } while (getline(&line, &line_alloc, infile) != -1) { rtrim(line, "\n\r\t "); switch (glob(line, 0, NULL, &globbed)) { case GLOB_NOMATCH: case 0: break; default: fprintf(stderr, "Cannot glob() for line %s\n", line); exit(1); } /* Insert the matches into our tree of paths */ for (size_t i = 0; i < globbed.gl_pathc; i++) tsearch(globbed.gl_pathv[i], &protected_dirs, strcmpvoid); } } /** * join - Join two paths * @a: First path * @b: Second path * * Returns: @a/@b */ char *join(const char *a, const char *b) { char *out; if (asprintf(&out, "%s/%s", a, b) < 1) { fprintf(stderr, "safe-rm: Internal error: Cannot join %s and %s: %s", a, b, strerror(errno)); exit(1); } return out; } int main(int argc, char *argv[]) { const char *HOME = getenv("HOME") ? : ""; const char *XDG_CONFIG_HOME = getenv("XDG_CONFIG_HOME") ? : join(HOME, ".config"); const char *LEGACY_CONFIG_FILE = join(HOME, ".safe-rm"); const char *USER_CONFIG_FILE = join(XDG_CONFIG_HOME, "safe-rm"); const char *GLOBAL_CONFIG_FILE = "/etc/safe-rm.conf"; read_config_file(GLOBAL_CONFIG_FILE); read_config_file(LEGACY_CONFIG_FILE); read_config_file(USER_CONFIG_FILE); /* Insert defaults if no configuration option was read. */ if (tfind(NULL, &protected_dirs, strany) == NULL) { for (int i = 0; DEFAULT_PROTECTED_DIRS[i] != NULL; i++) tsearch(DEFAULT_PROTECTED_DIRS[i], &protected_dirs, strcmpvoid); } /* Build the array of allowed arguments */ char **allowed_args = calloc(argc + 2, sizeof(*allowed_args)); int allowed = 0; // TODO: Disable the echo test. allowed_args[allowed++] = "/bin/echo"; allowed_args[allowed++] = "/bin/rm"; for (int i = 1; i < argc; i++) { struct stat buf; char *pathname = argv[i]; /* Normalize the pathname */ char *normalized_pathname = pathname; buf.st_mode = 0; if (strchr(normalized_pathname, '/') != NULL || stat(normalized_pathname, &buf) == 0) { /* Convert to an absolute path (e.g. remove "..") */ char *cpath = realpath(pathname, NULL); if (cpath != NULL) normalized_pathname = cpath; } /* Trim trailing slashes */ rtrim(normalized_pathname, "/"); /* Check against the blacklist */ if (tfind(normalized_pathname, &protected_dirs, strcmpvoid) != NULL && !S_ISLNK(buf.st_mode)) { fprintf(stderr, "safe-rm: skipping %s\n", pathname); } else { allowed_args[allowed++] = normalized_pathname; } } allowed_args[allowed] = NULL; // TODO(?): Prepare for actually deleting the file /* Make sure we're not calling ourselves recursively */ if (strcmp(realpath(allowed_args[0], NULL), realpath(argv[0], NULL)) == 0) { fprintf(stderr, "safe-rm cannot find the real \"rm\" binary\n"); exit(1); } /* Run the real rm command, returning with the same error code */ if (execv(allowed_args[0], allowed_args) != 0) { fprintf(stderr, "safe-rm: Cannot execute the real \"rm\" binary: %s\n", strerror(errno)); exit(2); } }

