summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Waugh <twaugh@redhat.com>2015-01-28 17:10:23 +0000
committerAndreas Gruenbacher <agruen@gnu.org>2015-01-31 22:13:44 +0100
commit025a54b789bd88ed15430f8633514e296826983e (patch)
tree13eb60f2fe8202f628839c03d4f87214ae25fc47
parent6a56d401d29e30eaad31a7e9cf9e493a5a535f8a (diff)
downloadpatch-025a54b789bd88ed15430f8633514e296826983e.tar.gz
Add symlink-safe system call replacements
Add wrappers around system calls that traverse relative pathnames without following symlinks. Written by Tim Waugh <twaugh@redhat.com> and Andreas Gruenbacher <agruenba@redhat.com>. * src/safe.h: Declare functions here. * src/safe.c: Implement safe_* system call replacements that do not follow symlinks along pathnames. Pathname components are resolved with openat(). Lookup results are cached to keep the overhead reasonably low. * tests/deep-directories: New path traversal cache test. * src/Makefile.am (patch_SOURCES): Add safe.[ch]. * tests/Makefile.am (TESTS): Add new test.
-rw-r--r--src/Makefile.am2
-rw-r--r--src/safe.c463
-rw-r--r--src/safe.h33
-rw-r--r--tests/Makefile.am3
-rwxr-xr-xtests/deep-directories28
5 files changed, 528 insertions, 1 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index 4c5fc53..7690760 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -27,6 +27,8 @@ patch_SOURCES = \
patch.c \
pch.c \
pch.h \
+ safe.c \
+ safe.h \
util.c \
util.h \
version.c \
diff --git a/src/safe.c b/src/safe.c
new file mode 100644
index 0000000..eec6ce5
--- /dev/null
+++ b/src/safe.c
@@ -0,0 +1,463 @@
+/* safe path traversal functions for 'patch' */
+
+/* Copyright (C) 2015 Free Software Foundation, Inc.
+
+ Written by Tim Waugh <twaugh@redhat.com> and
+ Andreas Gruenbacher <agruenba@redhat.com>.
+
+ 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/>. */
+
+#include <config.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/resource.h>
+#include <sys/time.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+#include <alloca.h>
+#include <safe.h>
+#include "dirname.h"
+
+#include <hash.h>
+#include <xalloc.h>
+#include <minmax.h>
+
+#define XTERN extern
+#include "common.h"
+
+/* Path lookup results are cached in a hash table + LRU list. When the
+ cache is full, the oldest entries are removed. */
+
+unsigned long dirfd_cache_misses;
+
+struct cached_dirfd {
+ /* lru list */
+ struct cached_dirfd *prev, *next;
+
+ /* key (openat arguments) */
+ int dirfd;
+ char *name;
+
+ /* value (openat result) */
+ int fd;
+};
+
+static Hash_table *cached_dirfds = NULL;
+static size_t max_cached_fds;
+static struct cached_dirfd lru_list = {
+ .prev = &lru_list,
+ .next = &lru_list,
+};
+
+static size_t hash_cached_dirfd (const void *entry, size_t table_size)
+{
+ const struct cached_dirfd *d = entry;
+ size_t strhash = hash_string (d->name, table_size);
+ return (strhash * 31 + d->dirfd) % table_size;
+}
+
+static bool compare_cached_dirfds (const void *_a,
+ const void *_b)
+{
+ const struct cached_dirfd *a = _a;
+ const struct cached_dirfd *b = _b;
+
+ return (a->dirfd == b->dirfd &&
+ !strcmp (a->name, b->name));
+}
+
+static void free_cached_dirfd (struct cached_dirfd *d)
+{
+ close (d->fd);
+ free (d->name);
+ free (d);
+}
+
+static void init_dirfd_cache (void)
+{
+ struct rlimit nofile;
+
+ max_cached_fds = 8;
+ if (getrlimit (RLIMIT_NOFILE, &nofile) == 0)
+ max_cached_fds = MAX (nofile.rlim_cur / 4, max_cached_fds);
+
+ cached_dirfds = hash_initialize (max_cached_fds,
+ NULL,
+ hash_cached_dirfd,
+ compare_cached_dirfds,
+ NULL);
+
+ if (!cached_dirfds)
+ xalloc_die ();
+}
+
+static void lru_list_add (struct cached_dirfd *entry, struct cached_dirfd *head)
+{
+ struct cached_dirfd *next = head->next;
+ entry->prev = head;
+ entry->next = next;
+ head->next = next->prev = entry;
+}
+
+static void lru_list_del (struct cached_dirfd *entry)
+{
+ struct cached_dirfd *prev = entry->prev;
+ struct cached_dirfd *next = entry->next;
+ prev->next = next;
+ next->prev = prev;
+}
+
+static struct cached_dirfd *lookup_cached_dirfd (int dirfd, const char *name)
+{
+ struct cached_dirfd *entry = NULL;
+
+ if (cached_dirfds)
+ {
+ struct cached_dirfd key;
+ key.dirfd = dirfd;
+ key.name = (char *) name;
+ entry = hash_lookup (cached_dirfds, &key);
+ if (entry)
+ {
+ /* Move this most recently used entry to the head of the lru list */
+ lru_list_del (entry);
+ lru_list_add (entry, &lru_list);
+ }
+ }
+
+ return entry;
+}
+
+static void remove_cached_dirfd (struct cached_dirfd *entry)
+{
+ lru_list_del (entry);
+ hash_delete (cached_dirfds, entry);
+ free_cached_dirfd (entry);
+}
+
+static void insert_cached_dirfd (struct cached_dirfd *entry, int keepfd)
+{
+ if (cached_dirfds == NULL)
+ init_dirfd_cache ();
+
+ /* Trim off the least recently used entries */
+ while (hash_get_n_entries (cached_dirfds) >= max_cached_fds)
+ {
+ struct cached_dirfd *last = lru_list.prev;
+ assert (last != &lru_list);
+ if (last->fd == keepfd)
+ {
+ last = last->prev;
+ assert (last != &lru_list);
+ }
+ remove_cached_dirfd (last);
+ }
+
+ assert (hash_insert (cached_dirfds, entry) == entry);
+ lru_list_add (entry, &lru_list);
+}
+
+static void invalidate_cached_dirfd (int dirfd, const char *name)
+{
+ struct cached_dirfd key, *entry;
+ if (!cached_dirfds)
+ return;
+
+ key.dirfd = dirfd;
+ key.name = (char *) name;
+ entry = hash_lookup (cached_dirfds, &key);
+ if (entry)
+ remove_cached_dirfd (entry);
+}
+
+static int openat_cached (int dirfd, const char *name, int keepfd)
+{
+ int fd;
+ struct cached_dirfd *entry = lookup_cached_dirfd (dirfd, name);
+
+ if (entry)
+ return entry->fd;
+ dirfd_cache_misses++;
+
+ /* Actually get the new directory file descriptor. Don't follow
+ symbolic links. */
+ fd = openat (dirfd, name, O_DIRECTORY | O_NOFOLLOW);
+
+ /* Don't cache errors. */
+ if (fd < 0)
+ return fd;
+
+ /* Store new cache entry */
+ entry = xmalloc (sizeof (struct cached_dirfd));
+ entry->dirfd = dirfd;
+ entry->name = xstrdup (name);
+ entry->fd = fd;
+ insert_cached_dirfd (entry, keepfd);
+
+ return fd;
+}
+
+/* Resolve the next path component in PATH inside DIRFD. */
+static int traverse_next (int dirfd, const char **path, int keepfd)
+{
+ const char *p = *path;
+ char *name;
+
+ while (*p && ! ISSLASH (*p))
+ p++;
+ if (**path == '.' && *path + 1 == p)
+ goto skip;
+ name = alloca (p - *path + 1);
+ memcpy(name, *path, p - *path);
+ name[p - *path] = 0;
+
+ dirfd = openat_cached (dirfd, name, keepfd);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ {
+ *path = p;
+ return -1;
+ }
+skip:
+ while (ISSLASH (*p))
+ p++;
+ *path = p;
+ return dirfd;
+}
+
+/* Traverse PATHNAME. Updates PATHNAME to point to the last path component and
+ returns a file descriptor to its parent directory (which can be AT_FDCWD).
+ When KEEPFD is given, make sure that the cache entry for DIRFD is not
+ removed from the cache (and KEEPFD remains open) even if the cache grows
+ beyond MAX_CACHED_FDS entries. */
+static int traverse_another_path (const char **pathname, int keepfd)
+{
+ unsigned long misses = dirfd_cache_misses;
+ const char *path = *pathname, *last;
+ int dirfd = AT_FDCWD;
+
+ if (! *path || IS_ABSOLUTE_FILE_NAME (path))
+ return dirfd;
+
+ /* Find the last pathname component */
+ last = strrchr (path, 0) - 1;
+ if (ISSLASH (*last))
+ {
+ while (last != path)
+ if (! ISSLASH (*--last))
+ break;
+ }
+ while (last != path && ! ISSLASH (*(last - 1)))
+ last--;
+ if (last == path)
+ return dirfd;
+
+ if (debug & 32)
+ printf ("Resolving path \"%.*s\"", (int) (last - path), path);
+
+ while (path != last)
+ {
+ dirfd = traverse_next (dirfd, &path, keepfd);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ {
+ if (debug & 32)
+ {
+ printf (" (failed)\n");
+ fflush (stdout);
+ }
+ if (errno == ELOOP)
+ {
+ fprintf (stderr, "Refusing to follow symbolic link %.*s\n",
+ (int) (path - *pathname), *pathname);
+ fatal_exit (0);
+ }
+ return dirfd;
+ }
+ }
+ *pathname = last;
+ if (debug & 32)
+ {
+ misses = dirfd_cache_misses - misses;
+ if (! misses)
+ printf(" (cached)\n");
+ else
+ printf (" (%lu miss%s)\n", misses, misses == 1 ? "" : "es");
+ fflush (stdout);
+ }
+ return dirfd;
+}
+
+/* Just traverse PATHNAME; see traverse_another_path(). */
+static int traverse_path (const char **pathname)
+{
+ return traverse_another_path (pathname, -1);
+}
+
+static int safe_xstat (const char *pathname, struct stat *buf, int flags)
+{
+ int dirfd;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return fstatat (dirfd, pathname, buf, flags);
+}
+
+/* Replacement for stat() */
+int safe_stat (const char *pathname, struct stat *buf)
+{
+ return safe_xstat (pathname, buf, 0);
+}
+
+/* Replacement for lstat() */
+int safe_lstat (const char *pathname, struct stat *buf)
+{
+ return safe_xstat (pathname, buf, AT_SYMLINK_NOFOLLOW);
+}
+
+/* Replacement for open() */
+int safe_open (const char *pathname, int flags, mode_t mode)
+{
+ int dirfd;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return openat (dirfd, pathname, flags, mode);
+}
+
+/* Replacement for rename() */
+int safe_rename (const char *oldpath, const char *newpath)
+{
+ int olddirfd, newdirfd;
+ int ret;
+
+ olddirfd = traverse_path (&oldpath);
+ if (olddirfd != AT_FDCWD && olddirfd < 0)
+ return olddirfd;
+
+ newdirfd = traverse_another_path (&newpath, olddirfd);
+ if (newdirfd != AT_FDCWD && newdirfd < 0)
+ return newdirfd;
+
+ ret = renameat (olddirfd, oldpath, newdirfd, newpath);
+ invalidate_cached_dirfd (olddirfd, oldpath);
+ invalidate_cached_dirfd (newdirfd, newpath);
+ return ret;
+}
+
+/* Replacement for mkdir() */
+int safe_mkdir (const char *pathname, mode_t mode)
+{
+ int dirfd;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return mkdirat (dirfd, pathname, mode);
+}
+
+/* Replacement for rmdir() */
+int safe_rmdir (const char *pathname)
+{
+ int dirfd;
+ int ret;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+
+ ret = unlinkat (dirfd, pathname, AT_REMOVEDIR);
+ invalidate_cached_dirfd (dirfd, pathname);
+ return ret;
+}
+
+/* Replacement for unlink() */
+int safe_unlink (const char *pathname)
+{
+ int dirfd;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return unlinkat (dirfd, pathname, 0);
+}
+
+/* Replacement for symlink() */
+int safe_symlink (const char *target, const char *linkpath)
+{
+ int dirfd;
+
+ dirfd = traverse_path (&linkpath);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return symlinkat (target, dirfd, linkpath);
+}
+
+/* Replacement for chmod() */
+int safe_chmod (const char *pathname, mode_t mode)
+{
+ int dirfd;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return fchmodat (dirfd, pathname, mode, 0);
+}
+
+/* Replacement for lchown() */
+int safe_lchown (const char *pathname, uid_t owner, gid_t group)
+{
+ int dirfd;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return fchownat (dirfd, pathname, owner, group, AT_SYMLINK_NOFOLLOW);
+}
+
+/* Replacement for lutimens() */
+int safe_lutimens (const char *pathname, struct timespec const times[2])
+{
+ int dirfd;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return utimensat (dirfd, pathname, times, AT_SYMLINK_NOFOLLOW);
+}
+
+/* Replacement for readlink() */
+ssize_t safe_readlink(const char *pathname, char *buf, size_t bufsiz)
+{
+ int dirfd;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return readlinkat (dirfd, pathname, buf, bufsiz);
+}
+
+/* Replacement for access() */
+int safe_access(const char *pathname, int mode)
+{
+ int dirfd;
+
+ dirfd = traverse_path (&pathname);
+ if (dirfd < 0 && dirfd != AT_FDCWD)
+ return dirfd;
+ return faccessat (dirfd, pathname, mode, 0);
+}
diff --git a/src/safe.h b/src/safe.h
new file mode 100644
index 0000000..1a0ff70
--- /dev/null
+++ b/src/safe.h
@@ -0,0 +1,33 @@
+/* safe path traversal functions for 'patch' */
+
+/* Copyright (C) 2015 Free Software Foundation, Inc.
+
+ Written by Tim Waugh <twaugh@redhat.com> and
+ Andreas Gruenbacher <agruenba@redhat.com>.
+
+ 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/>. */
+
+int safe_stat (const char *pathname, struct stat *buf);
+int safe_lstat (const char *pathname, struct stat *buf);
+int safe_open (const char *pathname, int flags, mode_t mode);
+int safe_rename (const char *oldpath, const char *newpath);
+int safe_mkdir (const char *pathname, mode_t mode);
+int safe_rmdir (const char *pathname);
+int safe_unlink (const char *pathname);
+int safe_symlink (const char *target, const char *linkpath);
+int safe_chmod (const char *pathname, mode_t mode);
+int safe_lchown (const char *pathname, uid_t owner, gid_t group);
+int safe_lutimens (const char *pathname, struct timespec const times[2]);
+ssize_t safe_readlink(const char *pathname, char *buf, size_t bufsiz);
+int safe_access(const char *pathname, int mode);
diff --git a/tests/Makefile.am b/tests/Makefile.am
index cfc4f37..7c9efa9 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -52,7 +52,8 @@ TESTS = \
remember-reject-files \
remove-directories \
symlinks \
- unmodified-files
+ unmodified-files \
+ deep-directories
XFAIL_TESTS = \
dash-o-append
diff --git a/tests/deep-directories b/tests/deep-directories
new file mode 100755
index 0000000..89e66be
--- /dev/null
+++ b/tests/deep-directories
@@ -0,0 +1,28 @@
+# Copyright (C) 2015 Free Software Foundation, Inc.
+#
+# Copying and distribution of this file, with or without modification,
+# in any medium, are permitted without royalty provided the copyright
+# notice and this notice are preserved.
+
+. $srcdir/test-lib.sh
+
+require_cat
+use_local_patch
+use_tmpdir
+
+# ==============================================================
+# Exercise the directory file descriptor cache
+
+# Artificially limit to 8 cache entries
+ulimit -n 32 >& /dev/null || exit 77
+
+cat > ab.diff <<EOF
+--- /dev/null
++++ b/1/2/3/4/5/6/7/8/9/foo
+@@ -0,0 +1 @@
++foo
+EOF
+
+check 'patch -p1 < ab.diff || echo Status: $?' <<EOF
+patching file 1/2/3/4/5/6/7/8/9/foo
+EOF