]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Merge pull request #10984 from fbuihuu/tmpfiles-be-more-explicit-with-unsafe-transition
authorLennart Poettering <lennart@poettering.net>
Mon, 10 Dec 2018 11:31:56 +0000 (12:31 +0100)
committerGitHub <noreply@github.com>
Mon, 10 Dec 2018 11:31:56 +0000 (12:31 +0100)
tmpfiles: be more explicit when an unsafe path transition is met

1  2 
src/basic/fs-util.c
src/core/service.c
src/test/test-fs-util.c
src/tmpfiles/tmpfiles.c

diff --combined src/basic/fs-util.c
index f70878fb8e11c22061e9d4e9e54bdf32bc45691e,ac97c803d08ab2c8c8d9b79f737217c87a79bfb9..06bae8decf438150a16cb93b885111803cf12426
@@@ -13,7 -13,9 +13,8 @@@
  #include "alloc-util.h"
  #include "dirent-util.h"
  #include "fd-util.h"
 -#include "fileio.h"
  #include "fs-util.h"
+ #include "locale-util.h"
  #include "log.h"
  #include "macro.h"
  #include "missing.h"
@@@ -26,7 -28,6 +27,7 @@@
  #include "string-util.h"
  #include "strv.h"
  #include "time-util.h"
 +#include "tmpfile-util.h"
  #include "user-util.h"
  #include "util.h"
  
@@@ -211,62 -212,31 +212,62 @@@ int readlink_and_make_absolute(const ch
  }
  
  int chmod_and_chown(const char *path, mode_t mode, uid_t uid, gid_t gid) {
 +        char fd_path[STRLEN("/proc/self/fd/") + DECIMAL_STR_MAX(int) + 1];
 +        _cleanup_close_ int fd = -1;
          assert(path);
  
 -        /* Under the assumption that we are running privileged we
 -         * first change the access mode and only then hand out
 +        /* Under the assumption that we are running privileged we first change the access mode and only then hand out
           * ownership to avoid a window where access is too open. */
  
 -        if (mode != MODE_INVALID)
 -                if (chmod(path, mode) < 0)
 +        fd = open(path, O_PATH|O_CLOEXEC|O_NOFOLLOW); /* Let's acquire an O_PATH fd, as precaution to change mode/owner
 +                                                       * on the same file */
 +        if (fd < 0)
 +                return -errno;
 +
 +        xsprintf(fd_path, "/proc/self/fd/%i", fd);
 +
 +        if (mode != MODE_INVALID) {
 +
 +                if ((mode & S_IFMT) != 0) {
 +                        struct stat st;
 +
 +                        if (stat(fd_path, &st) < 0)
 +                                return -errno;
 +
 +                        if ((mode & S_IFMT) != (st.st_mode & S_IFMT))
 +                                return -EINVAL;
 +                }
 +
 +                if (chmod(fd_path, mode & 07777) < 0)
                          return -errno;
 +        }
  
          if (uid != UID_INVALID || gid != GID_INVALID)
 -                if (chown(path, uid, gid) < 0)
 +                if (chown(fd_path, uid, gid) < 0)
                          return -errno;
  
          return 0;
  }
  
  int fchmod_and_chown(int fd, mode_t mode, uid_t uid, gid_t gid) {
 -        /* Under the assumption that we are running privileged we
 -         * first change the access mode and only then hand out
 +        /* Under the assumption that we are running privileged we first change the access mode and only then hand out
           * ownership to avoid a window where access is too open. */
  
 -        if (mode != MODE_INVALID)
 -                if (fchmod(fd, mode) < 0)
 +        if (mode != MODE_INVALID) {
 +
 +                if ((mode & S_IFMT) != 0) {
 +                        struct stat st;
 +
 +                        if (fstat(fd, &st) < 0)
 +                                return -errno;
 +
 +                        if ((mode & S_IFMT) != (st.st_mode & S_IFMT))
 +                                return -EINVAL;
 +                }
 +
 +                if (fchmod(fd, mode & 0777) < 0)
                          return -errno;
 +        }
  
          if (uid != UID_INVALID || gid != GID_INVALID)
                  if (fchown(fd, uid, gid) < 0)
@@@ -294,6 -264,7 +295,6 @@@ int fchmod_opath(int fd, mode_t m) 
           * fchownat() does. */
  
          xsprintf(procfs_path, "/proc/self/fd/%i", fd);
 -
          if (chmod(procfs_path, m) < 0)
                  return -errno;
  
@@@ -663,15 -634,42 +664,42 @@@ int inotify_add_watch_fd(int fd, int wh
          return r;
  }
  
- static bool safe_transition(const struct stat *a, const struct stat *b) {
+ static bool unsafe_transition(const struct stat *a, const struct stat *b) {
          /* Returns true if the transition from a to b is safe, i.e. that we never transition from unprivileged to
           * privileged files or directories. Why bother? So that unprivileged code can't symlink to privileged files
           * making us believe we read something safe even though it isn't safe in the specific context we open it in. */
  
          if (a->st_uid == 0) /* Transitioning from privileged to unprivileged is always fine */
-                 return true;
+                 return false;
  
-         return a->st_uid == b->st_uid; /* Otherwise we need to stay within the same UID */
+         return a->st_uid != b->st_uid; /* Otherwise we need to stay within the same UID */
+ }
+ static int log_unsafe_transition(int a, int b, const char *path, unsigned flags) {
+         _cleanup_free_ char *n1 = NULL, *n2 = NULL;
+         if (!FLAGS_SET(flags, CHASE_WARN))
+                 return -ENOLINK;
+         (void) fd_get_path(a, &n1);
+         (void) fd_get_path(b, &n2);
+         return log_warning_errno(SYNTHETIC_ERRNO(ENOLINK),
+                                  "Detected unsafe path transition %s %s %s during canonicalization of %s.",
+                                  n1, special_glyph(ARROW), n2, path);
+ }
+ static int log_autofs_mount_point(int fd, const char *path, unsigned flags) {
+         _cleanup_free_ char *n1 = NULL;
+         if (!FLAGS_SET(flags, CHASE_WARN))
+                 return -EREMOTE;
+         (void) fd_get_path(fd, &n1);
+         return log_warning_errno(SYNTHETIC_ERRNO(EREMOTE),
+                                  "Detected autofs mount point %s during canonicalization of %s.",
+                                  n1, path);
  }
  
  int chase_symlinks(const char *path, const char *original_root, unsigned flags, char **ret) {
           *    path is fully normalized, and == 0 for each normalization step. This may be combined with
           *    CHASE_NONEXISTENT, in which case 1 is returned when a component is not found.
           *
+          * 4. With CHASE_SAFE: in this case the path must not contain unsafe transitions, i.e. transitions from
+          *    unprivileged to privileged files or directories. In such cases the return value is -ENOLINK. If
+          *    CHASE_WARN is also set a warning describing the unsafe transition is emitted.
+          *
+          * 5. With CHASE_NO_AUTOFS: in this case if an autofs mount point is encountered, the path normalization is
+          *    aborted and -EREMOTE is returned. If CHASE_WARN is also set a warning showing the path of the mount point
+          *    is emitted.
+          *
           * */
  
          /* A root directory of "/" or "" is identical to none */
                                  if (fstat(fd_parent, &st) < 0)
                                          return -errno;
  
-                                 if (!safe_transition(&previous_stat, &st))
-                                         return -EPERM;
+                                 if (unsafe_transition(&previous_stat, &st))
+                                         return log_unsafe_transition(fd, fd_parent, path, flags);
  
                                  previous_stat = st;
                          }
                  if (fstat(child, &st) < 0)
                          return -errno;
                  if ((flags & CHASE_SAFE) &&
-                     !safe_transition(&previous_stat, &st))
-                         return -EPERM;
+                     unsafe_transition(&previous_stat, &st))
+                         return log_unsafe_transition(fd, child, path, flags);
  
                  previous_stat = st;
  
                  if ((flags & CHASE_NO_AUTOFS) &&
                      fd_is_fs_type(child, AUTOFS_SUPER_MAGIC) > 0)
-                         return -EREMOTE;
+                         return log_autofs_mount_point(child, path, flags);
  
                  if (S_ISLNK(st.st_mode) && !((flags & CHASE_NOFOLLOW) && isempty(todo))) {
                          char *joined;
                                          if (fstat(fd, &st) < 0)
                                                  return -errno;
  
-                                         if (!safe_transition(&previous_stat, &st))
-                                                 return -EPERM;
+                                         if (unsafe_transition(&previous_stat, &st))
+                                                 return log_unsafe_transition(child, fd, path, flags);
  
                                          previous_stat = st;
                                  }
diff --combined src/core/service.c
index 76f1e160697f7d22f53c4554c9d4645f67368505,1fafb33f2371dc6cbbf0d1d1fd3dd04f1863da5a..d631687205e048b46e0791d366adb2d235667a77
@@@ -12,7 -12,6 +12,7 @@@
  #include "bus-kernel.h"
  #include "bus-util.h"
  #include "dbus-service.h"
 +#include "dbus-unit.h"
  #include "def.h"
  #include "env-util.h"
  #include "escape.h"
@@@ -935,8 -934,8 +935,8 @@@ static int service_load_pid_file(Servic
          prio = may_warn ? LOG_INFO : LOG_DEBUG;
  
          fd = chase_symlinks(s->pid_file, NULL, CHASE_OPEN|CHASE_SAFE, NULL);
-         if (fd == -EPERM) {
-                 log_unit_full(UNIT(s), LOG_DEBUG, fd, "Permission denied while opening PID file or potentially unsafe symlink chain, will now retry with relaxed checks: %s", s->pid_file);
+         if (fd == -ENOLINK) {
+                 log_unit_full(UNIT(s), LOG_DEBUG, fd, "Potentially unsafe symlink chain, will now retry with relaxed checks: %s", s->pid_file);
  
                  questionable_pid_file = true;
  
@@@ -1036,9 -1035,6 +1036,9 @@@ static void service_set_state(Service *
  
          assert(s);
  
 +        if (s->state != state)
 +                bus_unit_send_pending_change_signal(UNIT(s), false);
 +
          table = s->type == SERVICE_IDLE ? state_translation_table_idle : state_translation_table;
  
          old_state = s->state;
diff --combined src/test/test-fs-util.c
index 75466a1f1c4a35e45916ae06b4203ae7393b7d69,26980d939f4736a55ec3f9435d531bcdfb3e8495..b3a4b1749c23157a7dcdf427f2ed65c2a852a2b1
@@@ -4,6 -4,8 +4,6 @@@
  
  #include "alloc-util.h"
  #include "fd-util.h"
 -#include "fd-util.h"
 -#include "fileio.h"
  #include "fs-util.h"
  #include "id128-util.h"
  #include "macro.h"
@@@ -14,7 -16,6 +14,7 @@@
  #include "string-util.h"
  #include "strv.h"
  #include "tests.h"
 +#include "tmpfile-util.h"
  #include "user-util.h"
  #include "util.h"
  #include "virt.h"
@@@ -248,11 -249,11 +248,11 @@@ static void test_chase_symlinks(void) 
                  assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) >= 0);
  
                  assert_se(chown(q, 0, 0) >= 0);
-                 assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) == -EPERM);
+                 assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) == -ENOLINK);
  
                  assert_se(rmdir(q) >= 0);
                  assert_se(symlink("/etc/passwd", q) >= 0);
-                 assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) == -EPERM);
+                 assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) == -ENOLINK);
  
                  assert_se(chown(p, 0, 0) >= 0);
                  assert_se(chase_symlinks(q, NULL, CHASE_SAFE, NULL) >= 0);
diff --combined src/tmpfiles/tmpfiles.c
index 9cd317e97b1a98b3bafdd5ca597808deb6f0721c,810b03567e61f78d9000cdbeb21fdb7e384dcd2d..08c57a3fb166f27af2ca57433799589f91e3ad97
  #include "label.h"
  #include "log.h"
  #include "macro.h"
 +#include "main-func.h"
  #include "missing.h"
  #include "mkdir.h"
 -#include "mount-util.h"
 +#include "mountpoint-util.h"
  #include "pager.h"
  #include "parse-util.h"
  #include "path-lookup.h"
@@@ -173,13 -172,6 +173,13 @@@ static char *arg_replace = NULL
  static OrderedHashmap *items = NULL, *globs = NULL;
  static Set *unix_sockets = NULL;
  
 +STATIC_DESTRUCTOR_REGISTER(items, ordered_hashmap_freep);
 +STATIC_DESTRUCTOR_REGISTER(globs, ordered_hashmap_freep);
 +STATIC_DESTRUCTOR_REGISTER(unix_sockets, set_free_freep);
 +STATIC_DESTRUCTOR_REGISTER(arg_include_prefixes, freep);
 +STATIC_DESTRUCTOR_REGISTER(arg_exclude_prefixes, freep);
 +STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
 +
  static int specifier_machine_id_safe(char specifier, void *data, void *userdata, char **ret);
  static int specifier_directory(char specifier, void *data, void *userdata, char **ret);
  
@@@ -380,39 -372,48 +380,39 @@@ static struct Item* find_glob(OrderedHa
          return NULL;
  }
  
 -static void load_unix_sockets(void) {
 +static int load_unix_sockets(void) {
 +        _cleanup_set_free_free_ Set *sockets = NULL;
          _cleanup_fclose_ FILE *f = NULL;
          int r;
  
          if (unix_sockets)
 -                return;
 +                return 0;
  
          /* We maintain a cache of the sockets we found in /proc/net/unix to speed things up a little. */
  
 -        unix_sockets = set_new(&path_hash_ops);
 -        if (!unix_sockets) {
 -                log_oom();
 -                return;
 -        }
 +        sockets = set_new(&path_hash_ops);
 +        if (!sockets)
 +                return log_oom();
  
          f = fopen("/proc/net/unix", "re");
 -        if (!f) {
 -                log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
 -                               "Failed to open /proc/net/unix, ignoring: %m");
 -                goto fail;
 -        }
 +        if (!f)
 +                return log_full_errno(errno == ENOENT ? LOG_DEBUG : LOG_WARNING, errno,
 +                                      "Failed to open /proc/net/unix, ignoring: %m");
  
          /* Skip header */
          r = read_line(f, LONG_LINE_MAX, NULL);
 -        if (r < 0) {
 -                log_warning_errno(r, "Failed to skip /proc/net/unix header line: %m");
 -                goto fail;
 -        }
 -        if (r == 0) {
 -                log_warning("Premature end of file reading /proc/net/unix.");
 -                goto fail;
 -        }
 +        if (r < 0)
 +                return log_warning_errno(r, "Failed to skip /proc/net/unix header line: %m");
 +        if (r == 0)
 +                return log_warning_errno(SYNTHETIC_ERRNO(EIO), "Premature end of file reading /proc/net/unix.");
  
          for (;;) {
 -                _cleanup_free_ char *line = NULL;
 -                char *p, *s;
 +                _cleanup_free_ char *line = NULL, *s = NULL;
 +                char *p;
  
                  r = read_line(f, LONG_LINE_MAX, &line);
 -                if (r < 0) {
 -                        log_warning_errno(r, "Failed to read /proc/net/unix line, ignoring: %m");
 -                        goto fail;
 -                }
 +                if (r < 0)
 +                        return log_warning_errno(r, "Failed to read /proc/net/unix line, ignoring: %m");
                  if (r == 0) /* EOF */
                          break;
  
                          continue;
  
                  s = strdup(p);
 -                if (!s) {
 -                        log_oom();
 -                        goto fail;
 -                }
 +                if (!s)
 +                        return log_oom();
  
                  path_simplify(s, false);
  
 -                r = set_consume(unix_sockets, s);
 -                if (r < 0 && r != -EEXIST) {
 -                        log_warning_errno(r, "Failed to add AF_UNIX socket to set, ignoring: %m");
 -                        goto fail;
 -                }
 -        }
 +                r = set_consume(sockets, s);
 +                if (r == -EEXIST)
 +                        continue;
 +                if (r < 0)
 +                        return log_warning_errno(r, "Failed to add AF_UNIX socket to set, ignoring: %m");
  
 -        return;
 +                TAKE_PTR(s);
 +        }
  
 -fail:
 -        unix_sockets = set_free_free(unix_sockets);
 +        unix_sockets = TAKE_PTR(sockets);
 +        return 1;
  }
  
  static bool unix_socket_alive(const char *fn) {
          assert(fn);
  
 -        load_unix_sockets();
 -
 -        if (unix_sockets)
 -                return !!set_get(unix_sockets, (char*) fn);
 +        if (load_unix_sockets() < 0)
 +                return true;     /* We don't know, so assume yes */
  
 -        /* We don't know, so assume yes */
 -        return true;
 +        return !!set_get(unix_sockets, (char*) fn);
  }
  
  static int dir_is_mount_point(DIR *d, const char *subdir) {
@@@ -855,10 -861,8 +855,8 @@@ static int path_open_parent_safe(const 
          if (!dn)
                  return log_oom();
  
-         fd = chase_symlinks(dn, NULL, CHASE_OPEN|CHASE_SAFE, NULL);
-         if (fd == -EPERM)
-                 return log_error_errno(fd, "Unsafe symlinks encountered in %s, refusing.", path);
-         if (fd < 0)
+         fd = chase_symlinks(dn, NULL, CHASE_OPEN|CHASE_SAFE|CHASE_WARN, NULL);
+         if (fd < 0 && fd != -ENOLINK)
                  return log_error_errno(fd, "Failed to validate path %s: %m", path);
  
          return fd;
@@@ -878,10 -882,8 +876,8 @@@ static int path_open_safe(const char *p
                                         "Failed to open invalid path '%s'.",
                                         path);
  
-         fd = chase_symlinks(path, NULL, CHASE_OPEN|CHASE_SAFE|CHASE_NOFOLLOW, NULL);
-         if (fd == -EPERM)
-                 return log_error_errno(fd, "Unsafe symlinks encountered in %s, refusing.", path);
-         if (fd < 0)
+         fd = chase_symlinks(path, NULL, CHASE_OPEN|CHASE_SAFE|CHASE_WARN|CHASE_NOFOLLOW, NULL);
+         if (fd < 0 && fd != -ENOLINK)
                  return log_error_errno(fd, "Failed to validate path %s: %m", path);
  
          return fd;
@@@ -1838,7 -1840,7 +1834,7 @@@ static int item_do(Item *i, int fd, con
                          else {
                                  _cleanup_free_ char *de_path = NULL;
  
 -                                de_path = path_join(NULL, path, de->d_name);
 +                                de_path = path_join(path, de->d_name);
                                  if (!de_path)
                                          q = log_oom();
                                  else
@@@ -2259,11 -2261,12 +2255,12 @@@ static int process_item(Item *i, Operat
  
          i->done |= operation;
  
-         r = chase_symlinks(i->path, NULL, CHASE_NO_AUTOFS, NULL);
+         r = chase_symlinks(i->path, NULL, CHASE_NO_AUTOFS|CHASE_WARN, NULL);
          if (r == -EREMOTE) {
-                 log_debug_errno(r, "Item '%s' is below autofs, skipping.", i->path);
+                 log_notice_errno(r, "Skipping %s", i->path);
                  return 0;
-         } else if (r < 0)
+         }
+         if (r < 0)
                  log_debug_errno(r, "Failed to determine whether '%s' is below autofs, ignoring: %m", i->path);
  
          r = FLAGS_SET(operation, OPERATION_CREATE) ? create_item(i) : 0;
@@@ -2326,18 -2329,18 +2323,18 @@@ static void item_free_contents(Item *i
  #endif
  }
  
 -static void item_array_free(ItemArray *a) {
 +static ItemArray* item_array_free(ItemArray *a) {
          size_t n;
  
          if (!a)
 -                return;
 +                return NULL;
  
          for (n = 0; n < a->n_items; n++)
                  item_free_contents(a->items + n);
  
          set_free(a->children);
          free(a->items);
 -        free(a);
 +        return mfree(a);
  }
  
  static int item_compare(const Item *a, const Item *b) {
@@@ -2630,21 -2633,24 +2627,21 @@@ static int parse_line(const char *fname
                  break;
  
          case CREATE_CHAR_DEVICE:
 -        case CREATE_BLOCK_DEVICE: {
 -                unsigned major, minor;
 -
 +        case CREATE_BLOCK_DEVICE:
                  if (!i.argument) {
                          *invalid_config = true;
                          log_error("[%s:%u] Device file requires argument.", fname, line);
                          return -EBADMSG;
                  }
  
 -                if (sscanf(i.argument, "%u:%u", &major, &minor) != 2) {
 +                r = parse_dev(i.argument, &i.major_minor);
 +                if (r < 0) {
                          *invalid_config = true;
 -                        log_error("[%s:%u] Can't parse device file major/minor '%s'.", fname, line, i.argument);
 +                        log_error_errno(r, "[%s:%u] Can't parse device file major/minor '%s'.", fname, line, i.argument);
                          return -EBADMSG;
                  }
  
 -                i.major_minor = makedev(major, minor);
                  break;
 -        }
  
          case SET_XATTR:
          case RECURSIVE_SET_XATTR:
@@@ -3157,11 -3163,9 +3154,11 @@@ static int link_parent(ItemArray *a) 
          return 0;
  }
  
 -int main(int argc, char *argv[]) {
 +DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(item_array_hash_ops, char, string_hash_func, string_compare_func,
 +                                              ItemArray, item_array_free);
 +
 +static int run(int argc, char *argv[]) {
          _cleanup_strv_free_ char **config_dirs = NULL;
 -        int r, k, r_process = 0;
          bool invalid_config = false;
          Iterator iterator;
          ItemArray *a;
                  PHASE_CREATE,
                  _PHASE_MAX
          } phase;
 +        int r, k;
  
          r = parse_argv(argc, argv);
          if (r <= 0)
 -                goto finish;
 +                return r;
  
          log_setup_service();
  
          if (arg_user) {
                  r = user_config_paths(&config_dirs);
 -                if (r < 0) {
 -                        log_error_errno(r, "Failed to initialize configuration directory list: %m");
 -                        goto finish;
 -                }
 +                if (r < 0)
 +                        return log_error_errno(r, "Failed to initialize configuration directory list: %m");
          } else {
                  config_dirs = strv_split_nulstr(CONF_PATHS_NULSTR("tmpfiles.d"));
 -                if (!config_dirs) {
 -                        r = log_oom();
 -                        goto finish;
 -                }
 +                if (!config_dirs)
 +                        return log_oom();
          }
  
          if (DEBUG_LOGGING) {
          if (arg_cat_config) {
                  (void) pager_open(arg_pager_flags);
  
 -                r = cat_config(config_dirs, argv + optind);
 -                goto finish;
 +                return cat_config(config_dirs, argv + optind);
          }
  
          umask(0022);
  
          mac_selinux_init();
  
 -        items = ordered_hashmap_new(&string_hash_ops);
 -        globs = ordered_hashmap_new(&string_hash_ops);
 -
 -        if (!items || !globs) {
 -                r = log_oom();
 -                goto finish;
 -        }
 +        items = ordered_hashmap_new(&item_array_hash_ops);
 +        globs = ordered_hashmap_new(&item_array_hash_ops);
 +        if (!items || !globs)
 +                return log_oom();
  
          /* If command line arguments are specified along with --replace, read all
           * configuration files and insert the positional arguments at the specified
          else
                  r = parse_arguments(config_dirs, argv + optind, &invalid_config);
          if (r < 0)
 -                goto finish;
 +                return r;
  
          /* Let's now link up all child/parent relationships */
          ORDERED_HASHMAP_FOREACH(a, items, iterator) {
                  r = link_parent(a);
                  if (r < 0)
 -                        goto finish;
 +                        return r;
          }
          ORDERED_HASHMAP_FOREACH(a, globs, iterator) {
                  r = link_parent(a);
                  if (r < 0)
 -                        goto finish;
 +                        return r;
          }
  
          /* If multiple operations are requested, let's first run the remove/clean operations, and only then the create
                  /* The non-globbing ones usually create things, hence we apply them first */
                  ORDERED_HASHMAP_FOREACH(a, items, iterator) {
                          k = process_item_array(a, op);
 -                        if (k < 0 && r_process == 0)
 -                                r_process = k;
 +                        if (k < 0 && r >= 0)
 +                                r = k;
                  }
  
                  /* The globbing ones usually alter things, hence we apply them second. */
                  ORDERED_HASHMAP_FOREACH(a, globs, iterator) {
                          k = process_item_array(a, op);
 -                        if (k < 0 && r_process == 0)
 -                                r_process = k;
 +                        if (k < 0 && r >= 0)
 +                                r = k;
                  }
          }
  
 -finish:
 -        pager_close();
 -
 -        ordered_hashmap_free_with_destructor(items, item_array_free);
 -        ordered_hashmap_free_with_destructor(globs, item_array_free);
 -
 -        free(arg_include_prefixes);
 -        free(arg_exclude_prefixes);
 -        free(arg_root);
 -
 -        set_free_free(unix_sockets);
 -
 -        mac_selinux_finish();
 -
 -        if (r < 0 || ERRNO_IS_RESOURCE(-r_process))
 -                return EXIT_FAILURE;
 -        else if (invalid_config)
 +        if (ERRNO_IS_RESOURCE(-r))
 +                return r;
 +        if (invalid_config)
                  return EX_DATAERR;
 -        else if (r_process < 0)
 +        if (r < 0)
                  return EX_CANTCREAT;
 -        else
 -                return EXIT_SUCCESS;
 +        return 0;
  }
 +
 +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);