On Wed, Aug 05, 2015 at 05:49:59PM +0200, Julian Andres Klode wrote:
> On Wed, Aug 05, 2015 at 05:23:48PM +0200, Julian Andres Klode wrote:
> > On Wed, Aug 05, 2015 at 05:09:50PM +0200, Julian Andres Klode wrote:
> > > On Tue, Aug 26, 2014 at 08:07:00PM -0700, Josh Triplett wrote:
> > > > 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.
> >
>
I noticed a bug in v3, namely symbolic links. Now it should work
correctly.
Changes since v3:
- Only call realpath() for files that are not symbolic links, so
symbolic links can be blacklisted.
Changes since v2:
read_config_file:
- Rework existence check to check for ENOENT from fopen()
main:
- reorder
- use an lstat() for the S_ISLNK check and remove the previous
stat(), as it was unneeded
--
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 _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#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)
{
FILE *infile;
char *line = NULL;
size_t line_alloc = 0;
glob_t globbed;
infile = fopen(path, "r");
if (infile == NULL) {
if (errno != ENOENT) {
fprintf(stderr, "Could not open configuration file %s: %s\n", path,
strerror(errno));
}
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)
{
size_t lena;
size_t lenb;
char *out;
lena = strlen(a);
lenb = strlen(b);
out = malloc(lena + 1 /* separator */ + lenb + 1 /* 0 byte */ );
strncpy(out, a, lena);
strncpy(out + lena, "/", 1);
strncpy(out + lena + 1, b, lenb);
out[lena + 1 + lenb] = '\0';
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";
/* Allocate one more parameter than necessary, so we can quickly
* prepend something for testing. */
char **allowed_args = calloc(argc + 2, sizeof(*allowed_args));
int allowed = 0;
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 (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;
/* stat the file */
buf.st_mode = 0;
lstat(pathname, &buf);
/* Try looking up the real path */
if (!S_ISLNK(buf.st_mode)) {
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) {
fprintf(stderr, "safe-rm: skipping %s\n", pathname);
} else {
allowed_args[allowed++] = normalized_pathname;
}
}
allowed_args[allowed] = NULL;
/* 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);
}
}