]> granicus.if.org Git - sudo/commitdiff
Add new fdexec sudoers setting to allow choose whether execve() or
authorTodd C. Miller <Todd.Miller@courtesan.com>
Mon, 23 Jan 2017 02:56:16 +0000 (18:56 -0800)
committerTodd C. Miller <Todd.Miller@courtesan.com>
Mon, 23 Jan 2017 02:56:16 +0000 (18:56 -0800)
fexecve() is used.

doc/sudoers.cat
doc/sudoers.man.in
doc/sudoers.mdoc.in
plugins/sudoers/def_data.c
plugins/sudoers/def_data.h
plugins/sudoers/def_data.in
plugins/sudoers/defaults.c
plugins/sudoers/match.c

index ffad399c187e178064252d007aca6716f7110e2d..10ebc14227d71e324a79f4cb3b0578bb9272c1f4 100644 (file)
@@ -416,7 +416,9 @@ S\bSU\bUD\bDO\bOE\bER\bRS\bS F\bFI\bIL\bLE\bE F\bFO\bOR\bRM\bMA\bAT\bT
      command after the digest check has been performed but before the command
      is executed.  A similar race condition exists on systems that lack the
      fexecve(2) system call when the directory in which the command is located
-     is writable by the user.
+     is writable by the user.  See the description of the _\bf_\bd_\be_\bx_\be_\bc setting for
+     more information on how s\bsu\bud\bdo\bo executes commands that have an associated
+     digest.
 
      Command digests are only supported by version 1.8.7 or higher.
 
@@ -1728,6 +1730,34 @@ S\bSU\bUD\bDO\bOE\bER\bRS\bS O\bOP\bPT\bTI\bIO\bON\bNS\bS
                    requirements.  The group name specified should not include
                    a % prefix.  This is not set by default.
 
+     fdexec        Determines whether s\bsu\bud\bdo\bo will execute a command by its path
+                   or by an open file descriptor.  It has the following
+                   possible values:
+
+                   always  Always execute by file descriptor.
+
+                   never   Never execute by file descriptor.
+
+                   digest_only
+                           Only execute by file descriptor if the command has
+                           an associated digest in the _\bs_\bu_\bd_\bo_\be_\br_\bs file.
+
+                   The default value is _\bd_\bi_\bg_\be_\bs_\bt_\b__\bo_\bn_\bl_\by.  This avoids a time of
+                   check versus time of use race condition when the command is
+                   located in a directory writable by the invoking user.
+
+                   Note that _\bf_\bd_\be_\bx_\be_\bc will change the first element of the
+                   argument vector for scripts ($0 in the shell) due to the
+                   way the kernel runs script interpreters.  Instead of being
+                   a normal path, it will refer to a file descriptor.  For
+                   example, _\b/_\bd_\be_\bv_\b/_\bf_\bd_\b/_\b4 on Solaris and _\b/_\bp_\br_\bo_\bc_\b/_\bs_\be_\bl_\bf_\b/_\bf_\bd_\b/_\b4 on Linux.
+                   A workaround is to use the SUDO_COMMAND environment
+                   variable instead.
+
+                   This setting is only supported by version 1.8.20 or higher.
+                   If the operating system does not support the fexecve(2)
+                   system call, this setting has no effect.
+
      group_plugin  A string containing a s\bsu\bud\bdo\boe\ber\brs\bs group plugin with optional
                    arguments.  The string should consist of the plugin path,
                    either fully-qualified or relative to the
index bae06012ce9981e9e692ce4fb45f3e6603f561c2..9ab794eef42b5d297382fb013b3a6ba1f402ab7a 100644 (file)
@@ -890,6 +890,11 @@ A similar race condition exists on systems that lack the
 fexecve(2)
 system call when the directory in which the command is located
 is writable by the user.
+See the description of the
+\fIfdexec\fR
+setting for more information on how
+\fBsudo\fR
+executes commands that have an associated digest.
 .PP
 Command digests are only supported by version 1.8.7 or higher.
 .SS "Defaults"
@@ -3485,6 +3490,53 @@ The group name specified should not include a
 prefix.
 This is not set by default.
 .TP 14n
+fdexec
+Determines whether
+\fBsudo\fR
+will execute a command by its path or by an open file descriptor.
+It has the following possible values:
+.PP
+.RS 14n
+.PD 0
+.TP 8n
+always
+Always execute by file descriptor.
+.PD
+.TP 8n
+never
+Never execute by file descriptor.
+.TP 8n
+digest_only
+Only execute by file descriptor if the command has an associated digest
+in the
+\fIsudoers\fR
+file.
+.PP
+The default value is
+\fIdigest_only\fR.
+This avoids a time of check versus time of use race condition when
+the command is located in a directory writable by the invoking user.
+.sp
+Note that
+\fIfdexec\fR
+will change the first element of the argument vector for scripts
+($0 in the shell) due to the way the kernel runs script interpreters.
+Instead of being a normal path, it will refer to a file descriptor.
+For example,
+\fI/dev/fd/4\fR
+on Solaris and
+\fI/proc/self/fd/4\fR
+on Linux.
+A workaround is to use the
+\fRSUDO_COMMAND\fR
+environment variable instead.
+.sp
+This setting is only supported by version 1.8.20 or higher.
+If the operating system does not support the
+fexecve(2)
+system call, this setting has no effect.
+.RE
+.TP 14n
 group_plugin
 A string containing a
 \fBsudoers\fR
index 4a9508d8fe91b28651f2a22047a77c8081126efa..f343fa15f61fd74e4ecb721b5ccca86caa6d1657 100644 (file)
@@ -847,6 +847,11 @@ A similar race condition exists on systems that lack the
 .Xr fexecve 2
 system call when the directory in which the command is located
 is writable by the user.
+See the description of the
+.Em fdexec
+setting for more information on how
+.Nm sudo
+executes commands that have an associated digest.
 .Pp
 Command digests are only supported by version 1.8.7 or higher.
 .Ss Defaults
@@ -3254,6 +3259,46 @@ The group name specified should not include a
 .Li %
 prefix.
 This is not set by default.
+.It fdexec
+Determines whether
+.Nm sudo
+will execute a command by its path or by an open file descriptor.
+It has the following possible values:
+.Bl -tag -width 6n
+.It always
+Always execute by file descriptor.
+.It never
+Never execute by file descriptor.
+.It digest_only
+Only execute by file descriptor if the command has an associated digest
+in the
+.Em sudoers
+file.
+.El
+.Pp
+The default value is
+.Em digest_only .
+This avoids a time of check versus time of use race condition when
+the command is located in a directory writable by the invoking user.
+.Pp
+Note that
+.Em fdexec
+will change the first element of the argument vector for scripts
+($0 in the shell) due to the way the kernel runs script interpreters.
+Instead of being a normal path, it will refer to a file descriptor.
+For example,
+.Pa /dev/fd/4
+on Solaris and
+.Pa /proc/self/fd/4
+on Linux.
+A workaround is to use the
+.Dv SUDO_COMMAND
+environment variable instead.
+.Pp
+This setting is only supported by version 1.8.20 or higher.
+If the operating system does not support the
+.Xr fexecve 2
+system call, this setting has no effect.
 .It group_plugin
 A string containing a
 .Nm sudoers
index 00caa8b452603716eb513023a594fedc04eb3696..489cb23a978b0d5ec74e9d9bd614dfc46d804a2a 100644 (file)
@@ -21,6 +21,13 @@ static struct def_values def_data_verifypw[] = {
     { NULL, 0 },
 };
 
+static struct def_values def_data_fdexec[] = {
+    { "never", never },
+    { "digest_only", digest_only },
+    { "always", always },
+    { NULL, 0 },
+};
+
 struct sudo_defs_types sudo_defs_table[] = {
     {
        "syslog", T_LOGFAC|T_BOOL,
@@ -434,6 +441,10 @@ struct sudo_defs_types sudo_defs_table[] = {
        "iolog_mode", T_MODE,
        N_("File mode to use for the I/O log files: 0%o"),
        NULL,
+    }, {
+       "fdexec", T_TUPLE|T_BOOL,
+       N_("Execute commands by file descriptor instead of by path: %s"),
+       def_data_fdexec,
     }, {
        NULL, 0, NULL
     }
index d83d2c3b6a56fd251901307df0cd4d9d4391b2ab..8b798a541aa9903008a1e818ea6feec3c4bf6e7c 100644 (file)
 #define def_iolog_group         (sudo_defs_table[I_IOLOG_GROUP].sd_un.str)
 #define I_IOLOG_MODE            102
 #define def_iolog_mode          (sudo_defs_table[I_IOLOG_MODE].sd_un.mode)
+#define I_FDEXEC                103
+#define def_fdexec              (sudo_defs_table[I_FDEXEC].sd_un.tuple)
 
 enum def_tuple {
        never,
        once,
        always,
        any,
-       all
+       all,
+       digest_only
 };
index 9f069f1eec45c04303fa30878b8a8100b03e6bc3..000b3a926da7606f2b073ea9e40e1a39ed1f27bc 100644 (file)
@@ -322,3 +322,7 @@ iolog_group
 iolog_mode
        T_MODE
        "File mode to use for the I/O log files: 0%o"
+fdexec
+       T_TUPLE|T_BOOL
+       "Execute commands by file descriptor instead of by path: %s"
+       never digest_only always
index 5eaf8ea9358ce6aa183ac57124f2d3ecd447f382..70f81f66926d6c40e511d38cf1b4f6dc550917ca 100644 (file)
@@ -533,6 +533,7 @@ init_defaults(void)
     def_netgroup_tuple = false;
     def_sudoedit_checkdir = true;
     def_iolog_mode = S_IRUSR|S_IWUSR;
+    def_fdexec = digest_only;
 
     /* Syslog options need special care since they both strings and ints */
 #if (LOGGING & SLOG_SYSLOG)
index a994beea72388bde126248246f88cc0cbc672695..008d7a7c923908b9fc5a0ec3d301b9be20556ac3 100644 (file)
 # include "compat/sha2.h"
 #endif
 
+#if !defined(O_SEARCH) && defined(O_PATH)
+# define O_SEARCH O_PATH
+#endif
+
 static struct member_list empty = TAILQ_HEAD_INITIALIZER(empty);
 
 static bool command_matches_dir(const char *sudoers_dir, size_t dlen, const struct sudo_digest *digest);
@@ -83,7 +87,7 @@ static bool command_matches_glob(const char *sudoers_cmnd, const char *sudoers_a
 #endif
 static bool command_matches_fnmatch(const char *sudoers_cmnd, const char *sudoers_args, const struct sudo_digest *digest);
 static bool command_matches_normal(const char *sudoers_cmnd, const char *sudoers_args, const struct sudo_digest *digest);
-static bool digest_matches(const char *file, const struct sudo_digest *sd, int *fd);
+static bool digest_matches(int fd, const char *file, const struct sudo_digest *sd);
 
 /*
  * Returns true if string 's' contains meta characters.
@@ -433,10 +437,73 @@ done:
     debug_return_bool(rc);
 }
 
+/*
+ * Stat file by fd is possible, else by path.
+ * Returns true on success, else false.
+ */
+static bool
+do_stat(int fd, const char *path, struct stat *sb)
+{
+    debug_decl(do_stat, SUDOERS_DEBUG_MATCH)
+
+    if (fd != -1)
+       debug_return_bool(fstat(fd, sb) == 0);
+    debug_return_bool(stat(path, sb) == 0);
+}
+
+/*
+ * Open path if fdexec is enabled or if a digest is present.
+ * Returns false on error, else true.
+ */
+static bool
+open_cmnd(const char *path, const struct sudo_digest *digest, int *fdp)
+{
+    int fd = -1;
+    bool is_script = false;
+    debug_decl(open_cmnd, SUDOERS_DEBUG_MATCH)
+
+    /* Only open the file for fdexec or for digest matching. */
+    if (def_fdexec != always && digest == NULL)
+       debug_return_bool(true);
+
+    fd = open(path, O_RDONLY|O_NONBLOCK);
+# ifdef O_SEARCH
+    if (fd == -1 && errno == EACCES && digest == NULL) {
+       /* Try again with O_SEARCH if no digest is specified. */
+       const int saved_errno = errno;
+       if ((fd = open(path, O_SEARCH)) == -1)
+           errno = saved_errno;
+    }
+# endif
+    if (fd == -1)
+       debug_return_bool(false);
+
+#ifdef HAVE_FEXECVE
+    do {
+       /* Check for #! cookie and set is_script. */
+       char magic[2];
+       if (read(fd, magic, 2) == 2) {
+           if (magic[0] == '#' && magic[1] == '!')
+               is_script = true;
+       }
+       (void) lseek(fd, (off_t)0, SEEK_SET);
+    } while (0);
+    /*
+     * Shell scripts go through namei twice and so we can't set the close
+     * on exec flag on the fd for fexecve(2).
+     */
+    if (!is_script)
+       (void)fcntl(fd, F_SETFD, FD_CLOEXEC);
+#endif /* HAVE_FEXECVE */
+    *fdp = fd;
+    debug_return_bool(true);
+}
+
 static bool
 command_matches_fnmatch(const char *sudoers_cmnd, const char *sudoers_args,
     const struct sudo_digest *digest)
 {
+    struct stat sb; /* XXX - unused */
     debug_decl(command_matches_fnmatch, SUDOERS_DEBUG_MATCH)
 
     /*
@@ -453,11 +520,22 @@ command_matches_fnmatch(const char *sudoers_cmnd, const char *sudoers_args,
            close(cmnd_fd);
            cmnd_fd = -1;
        }
+       /* Open the file for fdexec or for digest matching. */
+       if (!open_cmnd(user_cmnd, digest, &cmnd_fd))
+           goto bad;
+       if (!do_stat(cmnd_fd, user_cmnd, &sb))
+           goto bad;
        /* Check digest of user_cmnd since sudoers_cmnd is a pattern. */
-       if (digest != NULL && !digest_matches(user_cmnd, digest, &cmnd_fd))
-           debug_return_bool(false);
+       if (digest != NULL && !digest_matches(cmnd_fd, user_cmnd, digest))
+           goto bad;
        /* No need to set safe_cmnd since user_cmnd matches sudoers_cmnd */
        debug_return_bool(true);
+bad:
+       if (cmnd_fd != -1) {
+           close(cmnd_fd);
+           cmnd_fd = -1;
+       }
+       debug_return_bool(false);
     }
     debug_return_bool(false);
 }
@@ -502,17 +580,22 @@ command_matches_glob(const char *sudoers_cmnd, const char *sudoers_args,
     /* If user_cmnd is fully-qualified, check for an exact match. */
     if (user_cmnd[0] == '/') {
        for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) {
-           if (strcmp(cp, user_cmnd) != 0 || stat(cp, &sudoers_stat) == -1)
+           if (fd != -1) {
+               close(fd);
+               fd = -1;
+           }
+           if (strcmp(cp, user_cmnd) != 0)
+               continue;
+           /* Open the file for fdexec or for digest matching. */
+           if (!open_cmnd(cp, digest, &fd))
+               continue;
+           if (!do_stat(fd, cp, &sudoers_stat))
                continue;
            if (user_stat == NULL ||
                (user_stat->st_dev == sudoers_stat.st_dev &&
                user_stat->st_ino == sudoers_stat.st_ino)) {
-               if (fd != -1) {
-                   close(fd);
-                   fd = -1;
-               }
                /* There could be multiple matches, check digest early. */
-               if (digest != NULL && !digest_matches(cp, digest, &fd)) {
+               if (digest != NULL && !digest_matches(fd, cp, digest)) {
                    bad_digest = true;
                    continue;
                }
@@ -532,6 +615,11 @@ command_matches_glob(const char *sudoers_cmnd, const char *sudoers_args,
     /* No exact match, compare basename, st_dev and st_ino. */
     if (!bad_digest) {
        for (ap = gl.gl_pathv; (cp = *ap) != NULL; ap++) {
+           if (fd != -1) {
+               close(fd);
+               fd = -1;
+           }
+
            /* If it ends in '/' it is a directory spec. */
            dlen = strlen(cp);
            if (cp[dlen - 1] == '/') {
@@ -545,17 +633,18 @@ command_matches_glob(const char *sudoers_cmnd, const char *sudoers_args,
                base++;
            else
                base = cp;
-           if (strcmp(user_base, base) != 0 ||
-               stat(cp, &sudoers_stat) == -1)
+           if (strcmp(user_base, base) != 0)
+               continue;
+
+           /* Open the file for fdexec or for digest matching. */
+           if (!open_cmnd(cp, digest, &fd))
+               continue;
+           if (!do_stat(fd, cp, &sudoers_stat))
                continue;
            if (user_stat == NULL ||
                (user_stat->st_dev == sudoers_stat.st_dev &&
                user_stat->st_ino == sudoers_stat.st_ino)) {
-               if (fd != -1) {
-                   close(fd);
-                   fd = -1;
-               }
-               if (digest != NULL && !digest_matches(cp, digest, &fd))
+               if (digest != NULL && !digest_matches(fd, cp, digest))
                    continue;
                free(safe_cmnd);
                if ((safe_cmnd = strdup(cp)) == NULL) {
@@ -655,19 +744,16 @@ static struct digest_function {
 };
 
 static bool
-digest_matches(const char *file, const struct sudo_digest *sd, int *fd)
+digest_matches(int fd, const char *file, const struct sudo_digest *sd)
 {
     unsigned char file_digest[SHA512_DIGEST_LENGTH];
     unsigned char sudoers_digest[SHA512_DIGEST_LENGTH];
     unsigned char buf[32 * 1024];
     struct digest_function *func = NULL;
-#ifdef HAVE_FEXECVE
-    bool first = true;
-    bool is_script = false;
-#endif /* HAVE_FEXECVE */
     size_t nread;
     SHA2_CTX ctx;
     FILE *fp;
+    int fd2;
     unsigned int i;
     debug_decl(digest_matches, SUDOERS_DEBUG_MATCH)
 
@@ -700,22 +786,20 @@ digest_matches(const char *file, const struct sudo_digest *sd, int *fd)
        }
     }
 
-    if ((fp = fopen(file, "r")) == NULL) {
+    if ((fd2 = dup(fd)) == -1) {
+       sudo_debug_printf(SUDO_DEBUG_INFO, "unable to dup %s: %s",
+           file, strerror(errno));
+       debug_return_bool(false);
+    }
+    if ((fp = fdopen(fd2, "r")) == NULL) {
        sudo_debug_printf(SUDO_DEBUG_INFO, "unable to open %s: %s",
            file, strerror(errno));
+       close(fd2);
        debug_return_bool(false);
     }
 
     func->init(&ctx);
     while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0) {
-#ifdef HAVE_FEXECVE
-       /* Check for #! cookie and set is_script. */
-       if (first) {
-           first = false;
-           if (nread >= 2 && buf[0] == '#' && buf[1] == '!')
-               is_script = true;
-       }
-#endif /* HAVE_FEXECVE */
        func->update(&ctx, buf, nread);
     }
     if (ferror(fp)) {
@@ -733,24 +817,6 @@ digest_matches(const char *file, const struct sudo_digest *sd, int *fd)
        debug_return_bool(false);
     }
 
-#ifdef HAVE_FEXECVE
-    /*
-     * On systems with fexecve(2) we can use that to execute the
-     * matching command even when the directory is writable.
-     */
-    if ((*fd = dup(fileno(fp))) == -1) {
-       sudo_debug_printf(SUDO_DEBUG_INFO, "unable to dup %s: %s",
-           file, strerror(errno));
-       fclose(fp);
-       debug_return_bool(false);
-    }
-    /*
-     * Shell scripts go through namei twice and so we can't set the close
-     * on exec flag on the fd for fexecve(2).
-     */
-    if (!is_script)
-       (void)fcntl(*fd, F_SETFD, FD_CLOEXEC);
-#endif /* HAVE_FEXECVE */
     fclose(fp);
     debug_return_bool(true);
 bad_format:
@@ -765,6 +831,7 @@ command_matches_normal(const char *sudoers_cmnd, const char *sudoers_args, const
     struct stat sudoers_stat;
     const char *base;
     size_t dlen;
+    int fd = -1;
     debug_decl(command_matches_normal, SUDOERS_DEBUG_MATCH)
 
     /* If it ends in '/' it is a directory spec. */
@@ -777,10 +844,15 @@ command_matches_normal(const char *sudoers_cmnd, const char *sudoers_args, const
        base = sudoers_cmnd;
     else
        base++;
-    if (strcmp(user_base, base) != 0 ||
-       stat(sudoers_cmnd, &sudoers_stat) == -1)
+    if (strcmp(user_base, base) != 0)
        debug_return_bool(false);
 
+    /* Open the file for fdexec or for digest matching. */
+    if (!open_cmnd(sudoers_cmnd, digest, &fd))
+       goto bad;
+    if (!do_stat(fd, sudoers_cmnd, &sudoers_stat))
+       goto bad;
+
     /*
      * Return true if inode/device matches AND
      *  a) there are no args in sudoers OR
@@ -791,23 +863,38 @@ command_matches_normal(const char *sudoers_cmnd, const char *sudoers_args, const
     if (user_stat != NULL &&
        (user_stat->st_dev != sudoers_stat.st_dev ||
        user_stat->st_ino != sudoers_stat.st_ino))
-       debug_return_bool(false);
+       goto bad;
     if (!command_args_match(sudoers_cmnd, sudoers_args))
-       debug_return_bool(false);
-    if (cmnd_fd != -1) {
-       close(cmnd_fd);
-       cmnd_fd = -1;
-    }
-    if (digest != NULL && !digest_matches(sudoers_cmnd, digest, &cmnd_fd)) {
+       goto bad;
+    if (digest != NULL && !digest_matches(fd, sudoers_cmnd, digest)) {
        /* XXX - log functions not available but we should log very loudly */
-       debug_return_bool(false);
+       goto bad;
     }
     free(safe_cmnd);
     if ((safe_cmnd = strdup(sudoers_cmnd)) == NULL) {
        sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
-       debug_return_bool(false);
+       goto bad;
+    }
+    if (cmnd_fd != -1) {
+       close(cmnd_fd);
+       cmnd_fd = -1;
+    }
+#ifdef HAVE_FEXECVE
+    /* Stash away fd if we are going to use fexecve(2) */
+    if (def_fdexec == always || (digest != NULL && def_fdexec == digest_only)) {
+       cmnd_fd = fd;
+    } else
+#endif /* HAVE_FEXECVE */
+    {
+       /* Either fdexec is not in use or fexecve(2) is not present. */
+       if (fd != -1)
+           close(fd);
     }
     debug_return_bool(true);
+bad:
+    if (fd != -1)
+       close(fd);
+    debug_return_bool(false);
 }
 #endif /* SUDOERS_NAME_MATCH */
 
@@ -851,23 +938,30 @@ command_matches_dir(const char *sudoers_dir, size_t dlen,
        debug_return_bool(false);
     }
     while ((dent = readdir(dirp)) != NULL) {
+       if (fd != -1) {
+           close(fd);
+           fd = -1;
+       }
+
        /* ignore paths > PATH_MAX (XXX - log) */
        buf[dlen] = '\0';
        if (strlcat(buf, dent->d_name, sizeof(buf)) >= sizeof(buf))
            continue;
 
        /* only stat if basenames are the same */
-       if (strcmp(user_base, dent->d_name) != 0 ||
-           stat(buf, &sudoers_stat) == -1)
+       if (strcmp(user_base, dent->d_name) != 0)
+           continue;
+
+       /* Open the file for fdexec or for digest matching. */
+       if (!open_cmnd(buf, digest, &fd))
+           continue;
+       if (!do_stat(fd, buf, &sudoers_stat))
            continue;
+
        if (user_stat == NULL ||
            (user_stat->st_dev == sudoers_stat.st_dev &&
            user_stat->st_ino == sudoers_stat.st_ino)) {
-           if (fd != -1) {
-               close(fd);
-               fd = -1;
-           }
-           if (digest != NULL && !digest_matches(buf, digest, &fd))
+           if (digest != NULL && !digest_matches(fd, buf, digest))
                continue;
            free(safe_cmnd);
            if ((safe_cmnd = strdup(buf)) == NULL) {