diff -up sudo-1.8.6p3/src/sesh.c.sudoedit-selinux sudo-1.8.6p3/src/sesh.c --- sudo-1.8.6p3/src/sesh.c.sudoedit-selinux 2012-09-18 15:56:30.000000000 +0200 +++ sudo-1.8.6p3/src/sesh.c 2012-09-25 16:06:33.408584649 +0200 @@ -34,6 +34,10 @@ # include "compat/stdbool.h" #endif /* HAVE_STDBOOL_H */ +#include +#include +#include + #include "missing.h" #include "alloc.h" #include "error.h" @@ -43,6 +47,16 @@ #include "sudo_exec.h" #include "sudo_plugin.h" +/* + * Return codes: + * EXIT_FAILURE ... unspecified error + * 0 ... everything ok + * 30 ... invalid -e arg value + * 31 ... odd number of paths + * 32 ... copy operation failed, no files copied + * 33 ... copy operation failed, some files copied + */ + sudo_conv_t sudo_conv; /* NULL in non-plugin */ /* @@ -77,19 +91,134 @@ main(int argc, char *argv[], char *envp[ if ((cp = strrchr(argv[0], '-')) != NULL && cp != argv[0]) noexec = strcmp(cp, "-noexec") == 0; - /* Shift argv and make a copy of the command to execute. */ - argv++; - argc--; - cmnd = estrdup(argv[0]); - - /* If invoked as a login shell, modify argv[0] accordingly. */ - if (argv[-1][0] == '-') { - if ((cp = strrchr(argv[0], '/')) == NULL) - cp = argv[0]; - *cp = '-'; + /* check the first argument, if it's `-e' then we are in sudoedit mode */ + if (strncmp(argv[1], "-e", 3) == 0) { + int fd_src, fd_dst, post, n, ret = -1; + ssize_t nread, nwritten; + char *path_src, *path_dst, buf[BUFSIZ]; + + if (argc < 3) + return EXIT_FAILURE; + + /* + * We need to know whether we are performing the copy operation + * before or after the editing. Without this we would not know + * which files are temporary and which are the originals. + * post = 0 ... before + * post = 1 ... after + */ + if (strncmp(argv[2], "0", 2) == 0) + post = 0; + else if (strncmp(argv[2], "1", 2) == 0) + post = 1; + else /* invalid value */ + return 30; + + /* align argv & argc to the beggining of the file list */ + argv += 3; + argc -= 3; + + /* no files specified, nothing to do */ + if (argc == 0) + return 0; + /* odd number of paths specified */ + if (argc % 2 == 1) + return 31; + + for (n = 0; n < argc - 1; n += 2) { + path_src = argv[n]; + path_dst = argv[n+1]; + /* + * Try to open the source file for reading. If it + * doesn't exist, it's ok, we'll create an empty + * destination file. + */ + if ((fd_src = open(path_src, O_RDONLY, 0600)) < 0) { + if (errno == ENOENT) { + /* new file */ + } else { + warning(_("open(%s)"), path_src); + if (post) { + ret = 33; + goto nocleanup; + } else + goto cleanup_0; + } + } + + /* + * Use O_EXCL if we are not in the post editing stage + * so that it's ensured that the temporary files are + * created by us and that we are not opening any sym- + * links. + */ + if ((fd_dst = open(path_dst, (post ? 0 : O_EXCL) | + O_WRONLY|O_TRUNC|O_CREAT, post ? 0644 : 0600)) < 0) + { + /* error - cleanup */ + warning(_("open(%s%s)"), path_dst, post ? "" : ", O_EXCL"); + if (post) { + ret = 33; + goto nocleanup; + } else + goto cleanup_0; + } + + if (fd_src != -1) { + while ((nread = read(fd_src, buf, sizeof(buf))) > 0) { + if ((nwritten = write(fd_dst, buf, nread)) != nread) { + warning(_("write")); + if (post) { + ret = 33; + goto nocleanup; + } else + goto cleanup_0; + } + } + } + + if (fd_dst != -1) + close(fd_dst); + if (fd_src != -1) + close(fd_src); + fd_dst = fd_src = -1; + } + + ret = 0; + /* remove temporary files (post=1) */ + for (n = 0; n < argc - 1; n += 2) + unlink(argv[n]); +nocleanup: + if (fd_dst != -1) + close(fd_dst); + if (fd_src != -1) + close(fd_src); + _exit(ret); +cleanup_0: + /* remove temporary files (post=0) */ + for (n = 0; n < argc - 1; n += 2) + unlink(argv[n+1]); + if (fd_dst != -1) + close(fd_dst); + if (fd_src != -1) + close(fd_src); + _exit(32); + } else { + + /* Shift argv and make a copy of the command to execute. */ + argv++; + argc--; + cmnd = estrdup(argv[0]); + + /* If invoked as a login shell, modify argv[0] accordingly. */ + if (argv[-1][0] == '-') { + if ((cp = strrchr(argv[0], '/')) == NULL) + cp = argv[0]; + *cp = '-'; + } + sudo_execve(cmnd, argv, envp, noexec); + warning(_("unable to execute %s"), argv[0]); + sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, EXIT_FAILURE); } - sudo_execve(cmnd, argv, envp, noexec); - warning(_("unable to execute %s"), argv[0]); - sudo_debug_exit_int(__func__, __FILE__, __LINE__, sudo_debug_subsys, EXIT_FAILURE); _exit(EXIT_FAILURE); } diff -up sudo-1.8.6p3/src/sudo.c.sudoedit-selinux sudo-1.8.6p3/src/sudo.c --- sudo-1.8.6p3/src/sudo.c.sudoedit-selinux 2012-09-18 15:57:43.000000000 +0200 +++ sudo-1.8.6p3/src/sudo.c 2012-09-25 16:04:36.687422997 +0200 @@ -915,6 +915,10 @@ exec_setup(struct command_details *detai if (selinux_setup(details->selinux_role, details->selinux_type, ptyname ? ptyname : user_details.tty, ptyfd) == -1) goto done; + if (details->flags & CD_SUDOEDIT_COPY) { + rval = true; + goto done; + } } #endif @@ -1116,6 +1120,8 @@ run_command(struct command_details *deta break; case CMD_WSTATUS: /* Command ran, exited or was killed. */ + if (details->flags & CD_SUDOEDIT_COPY) + break; sudo_debug_printf(SUDO_DEBUG_DEBUG, "calling policy close with wait status %d", cstat.val); policy_close(&policy_plugin, cstat.val, 0); diff -up sudo-1.8.6p3/src/sudo_edit.c.sudoedit-selinux sudo-1.8.6p3/src/sudo_edit.c --- sudo-1.8.6p3/src/sudo_edit.c.sudoedit-selinux 2012-09-18 15:56:30.000000000 +0200 +++ sudo-1.8.6p3/src/sudo_edit.c 2012-09-25 16:06:19.108564255 +0200 @@ -49,11 +49,284 @@ #if TIME_WITH_SYS_TIME # include #endif +#ifdef HAVE_SELINUX +# include +#endif #include "sudo.h" #if defined(HAVE_SETRESUID) || defined(HAVE_SETREUID) || defined(HAVE_SETEUID) +struct tempfile { + char *tfile; + char *ofile; + struct timeval omtim; + off_t osize; +}; + +static int +selinux_edit_copy(struct command_details *command_details, struct tempfile *tf, char **files, int nfiles, const char *tmpdir, int tmplen, int tval_isset) +{ + char **sesh_args; + int i, sesh_nargs, ret; + struct command_details sesh_details; + debug_decl(selinux_edit_copy, SUDO_DEBUG_EDIT); + + /* Prepare selinux stuff (setexeccon) */ + if (selinux_setup(command_details->selinux_role, + command_details->selinux_type, NULL, -1) != 0) + return -1; + + if (nfiles < 1) + return 1; + + /* Construct common args for sesh */ + memcpy(&sesh_details, command_details, sizeof(sesh_details)); + sesh_details.command = _PATH_SUDO_SESH; + sesh_details.flags |= CD_SUDOEDIT_COPY; + + sesh_nargs = (nfiles * 2) + 4 + 1; + sesh_args = (char **)emalloc2(sesh_nargs, sizeof(char *)); + sesh_args++; + sesh_args[0] = "sesh"; + sesh_args[1] = "-e"; + + if (files != NULL) { + sesh_args[2] = "0"; + + for (i = 2; i < nfiles+2; ++i) { + sesh_args[2*i-1] = files[i-2]; + tf[i-2].ofile = files[i-2]; + /* + * O_CREAT | O_EXCL is used in the sesh helper, so the + * usage of the tempnam function here is safe. + */ + sesh_args[2*i] = tempnam(tmpdir, "sudo."); + tf[i-2].tfile = sesh_args[2*i]; + //tf[i-2].omtim = 0; + tf[i-2].osize = 0; + } + + sesh_args[2*i-1] = NULL; + + /* Run sesh -e 0 ... */ + sesh_details.argv = sesh_args; + switch(run_command(&sesh_details)) { + case 0: + break; + case 31: + error(1, _("sesh: internal error: odd number of paths")); + case 32: + error(1, _("sesh: unable to create temporary files")); + } + + /* Chown to user's UID so he can edit the temporary files */ + for (i = 2; i < nfiles+2; ++i) { + if (chown(tf[i-2].tfile, user_details.uid, user_details.gid) != 0) { + warning("Unable to chown(%s) to %d:%d for editing", + tf[i-2].tfile, user_details.uid, user_details.gid); + } + } + } else { + sesh_args[2] = "1"; + + /* Construct args for sesh -e 1 */ + for (i = 2; i < nfiles+2; ++i) { + sesh_args[2*i-1] = tf[i-2].tfile; + sesh_args[2*i] = tf[i-2].ofile; + + if (chown(tf[i-2].tfile, sesh_details.uid, sesh_details.gid) != 0) { + warning("Unable to chown(%s) back to %d:%d", + tf[i-2].tfile, sesh_details.uid, sesh_details.gid); + } + } + + sesh_args[2*i-1] = NULL; + + /* Run sesh -e 1 ... */ + sesh_details.argv = sesh_args; + switch(run_command(&sesh_details)) { + case 0: + break; + case 32: + warning(_("Copying the temporary files back to its original place failed. The files were left in %s"), tmpdir); + break; + case 33: + warning(_("Copying of some of the temporary files back to its original place failed and they were left in %s"), + tmpdir); + break; + } + } + + return (nfiles); +} + +static void switch_user(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups); + +static int sudo_edit_copy(struct command_details *command_details, struct tempfile *tf, char **files, int nfiles, const char *tmpdir, int tmplen, int tval_isset) +{ + int i, j, tfd, ofd, rc; + char *cp, *suff, buf[BUFSIZ]; + ssize_t nwritten, nread; + struct stat sb; + struct timeval tv; + debug_decl(sudo_edit_copy, SUDO_DEBUG_EDIT); + + if (files != NULL) { + /* Create temporary copies */ + for (i = 0, j = 0; i < nfiles; i++) { + rc = -1; + switch_user(command_details->euid, command_details->egid, + command_details->ngroups, command_details->groups); + if ((ofd = open(files[i], O_RDONLY, 0644)) != -1 || errno == ENOENT) { + if (ofd == -1) { + zero_bytes(&sb, sizeof(sb)); /* new file */ + rc = 0; + } else { + rc = fstat(ofd, &sb); + } + } + switch_user(ROOT_UID, user_details.egid, + user_details.ngroups, user_details.groups); + if (rc || (ofd != -1 && !S_ISREG(sb.st_mode))) { + if (rc) + warning("%s", files[i]); + else + warningx(_("%s: not a regular file"), files[i]); + if (ofd != -1) + close(ofd); + continue; + } + tf[j].ofile = files[i]; + tf[j].osize = sb.st_size; + mtim_get(&sb, &tf[j].omtim); + if ((cp = strrchr(tf[j].ofile, '/')) != NULL) + cp++; + else + cp = tf[j].ofile; + suff = strrchr(cp, '.'); + if (suff != NULL) { + easprintf(&tf[j].tfile, "%.*s/%.*sXXXXXXXX%s", tmplen, tmpdir, + (int)(size_t)(suff - cp), cp, suff); + } else { + easprintf(&tf[j].tfile, "%.*s/%s.XXXXXXXX", tmplen, tmpdir, cp); + } + if (seteuid(user_details.uid) != 0) + error(1, "seteuid(%d)", (int)user_details.uid); + tfd = mkstemps(tf[j].tfile, suff ? strlen(suff) : 0); + if (seteuid(ROOT_UID) != 0) + error(1, "seteuid(ROOT_UID)"); + if (tfd == -1) { + warning("mkstemps"); + goto cleanup; + } + if (ofd != -1) { + while ((nread = read(ofd, buf, sizeof(buf))) != 0) { + if ((nwritten = write(tfd, buf, nread)) != nread) { + if (nwritten == -1) + warning("%s", tf[j].tfile); + else + warningx(_("%s: short write"), tf[j].tfile); + goto cleanup; + } + } + close(ofd); + } + /* + * We always update the stashed mtime because the time + * resolution of the filesystem the temporary file is on may + * not match that of the filesystem where the file to be edited + * resides. It is OK if touch() fails since we only use the info + * to determine whether or not a file has been modified. + */ + (void) touch(tfd, NULL, &tf[j].omtim); + rc = fstat(tfd, &sb); + if (!rc) + mtim_get(&sb, &tf[j].omtim); + close(tfd); + j++; + } + if ((nfiles = j) == 0) + goto cleanup; /* no files readable, you lose */ + } else { + /* Copy contents of temp files to real ones */ + for (i = 0; i < nfiles; i++) { + rc = -1; + if (seteuid(user_details.uid) != 0) + error(1, "seteuid(%d)", (int)user_details.uid); + if ((tfd = open(tf[i].tfile, O_RDONLY, 0644)) != -1) { + rc = fstat(tfd, &sb); + } + if (seteuid(ROOT_UID) != 0) + error(1, "seteuid(ROOT_UID)"); + if (rc || !S_ISREG(sb.st_mode)) { + if (rc) + warning("%s", tf[i].tfile); + else + warningx(_("%s: not a regular file"), tf[i].tfile); + warningx(_("%s left unmodified"), tf[i].ofile); + if (tfd != -1) + close(tfd); + continue; + } + mtim_get(&sb, &tv); + if (tf[i].osize == sb.st_size && timevalcmp(&tf[i].omtim, &tv, ==)) { + /* + * If mtime and size match but the user spent no measurable + * time in the editor we can't tell if the file was changed. + */ + if (tval_isset) { + warningx(_("%s unchanged"), tf[i].ofile); + unlink(tf[i].tfile); + close(tfd); + continue; + } + } + switch_user(command_details->euid, command_details->egid, + command_details->ngroups, command_details->groups); + ofd = open(tf[i].ofile, O_WRONLY|O_TRUNC|O_CREAT, 0644); + switch_user(ROOT_UID, user_details.egid, + user_details.ngroups, user_details.groups); + if (ofd == -1) { + warning(_("unable to write to %s"), tf[i].ofile); + warningx(_("contents of edit session left in %s"), tf[i].tfile); + close(tfd); + continue; + } + while ((nread = read(tfd, buf, sizeof(buf))) > 0) { + if ((nwritten = write(ofd, buf, nread)) != nread) { + if (nwritten == -1) + warning("%s", tf[i].ofile); + else + warningx(_("%s: short write"), tf[i].ofile); + break; + } + } + if (nread == 0) { + /* success, got EOF */ + unlink(tf[i].tfile); + } else if (nread < 0) { + warning(_("unable to read temporary file")); + warningx(_("contents of edit session left in %s"), tf[i].tfile); + } else { + warning(_("unable to write to %s"), tf[i].ofile); + warningx(_("contents of edit session left in %s"), tf[i].tfile); + } + close(ofd); + } + j = 0; + } + + debug_return_int(j); +cleanup: + for (i = 0; i < nfiles; i++) { + if (tf[i].tfile != NULL) + unlink(tf[i].tfile); + } + + debug_return_int(-1); +} + static void switch_user(uid_t euid, gid_t egid, int ngroups, GETGROUPS_T *groups) { @@ -87,20 +360,17 @@ int sudo_edit(struct command_details *command_details) { struct command_details editor_details; - ssize_t nread, nwritten; const char *tmpdir; - char *cp, *suff, **nargv, **ap, **files = NULL; - char buf[BUFSIZ]; - int rc, i, j, ac, ofd, tfd, nargc, rval, tmplen; - int editor_argc = 0, nfiles = 0; + char **ap; + char **nargv, **files = NULL; + int editor_argc = 0; + int i, ac, nargc, rval, nfiles = 0, tmplen; struct stat sb; - struct timeval tv, tv1, tv2; - struct tempfile { - char *tfile; - char *ofile; - struct timeval omtim; - off_t osize; - } *tf = NULL; + struct timeval tv1, tv2; + struct tempfile *tf; +#ifdef HAVE_SELINUX + int rbac_enabled; +#endif debug_decl(sudo_edit, SUDO_DEBUG_EDIT) /* @@ -109,7 +379,7 @@ sudo_edit(struct command_details *comman */ if (setuid(ROOT_UID) != 0) { warning(_("unable to change uid to root (%u)"), ROOT_UID); - goto cleanup; + return 1; } /* @@ -127,6 +397,9 @@ sudo_edit(struct command_details *comman while (tmplen > 0 && tmpdir[tmplen - 1] == '/') tmplen--; +#ifdef HAVE_SELINUX + rbac_enabled = is_selinux_enabled() > 0 && command_details->selinux_role != NULL; +#endif /* * The user's editor must be separated from the files to be * edited by a "--" option. @@ -141,7 +414,7 @@ sudo_edit(struct command_details *comman } if (nfiles == 0) { warningx(_("plugin error: missing file list for sudoedit")); - goto cleanup; + return 1; } /* @@ -150,81 +423,18 @@ sudo_edit(struct command_details *comman */ tf = emalloc2(nfiles, sizeof(*tf)); zero_bytes(tf, nfiles * sizeof(*tf)); - for (i = 0, j = 0; i < nfiles; i++) { - rc = -1; - switch_user(command_details->euid, command_details->egid, - command_details->ngroups, command_details->groups); - if ((ofd = open(files[i], O_RDONLY, 0644)) != -1 || errno == ENOENT) { - if (ofd == -1) { - zero_bytes(&sb, sizeof(sb)); /* new file */ - rc = 0; - } else { - rc = fstat(ofd, &sb); - } - } - switch_user(ROOT_UID, user_details.egid, - user_details.ngroups, user_details.groups); - if (rc || (ofd != -1 && !S_ISREG(sb.st_mode))) { - if (rc) - warning("%s", files[i]); - else - warningx(_("%s: not a regular file"), files[i]); - if (ofd != -1) - close(ofd); - continue; - } - tf[j].ofile = files[i]; - tf[j].osize = sb.st_size; - mtim_get(&sb, &tf[j].omtim); - if ((cp = strrchr(tf[j].ofile, '/')) != NULL) - cp++; - else - cp = tf[j].ofile; - suff = strrchr(cp, '.'); - if (suff != NULL) { - easprintf(&tf[j].tfile, "%.*s/%.*sXXXXXXXX%s", tmplen, tmpdir, - (int)(size_t)(suff - cp), cp, suff); - } else { - easprintf(&tf[j].tfile, "%.*s/%s.XXXXXXXX", tmplen, tmpdir, cp); - } - if (seteuid(user_details.uid) != 0) - error(1, "seteuid(%d)", (int)user_details.uid); - tfd = mkstemps(tf[j].tfile, suff ? strlen(suff) : 0); - if (seteuid(ROOT_UID) != 0) - error(1, "seteuid(ROOT_UID)"); - if (tfd == -1) { - warning("mkstemps"); - goto cleanup; - } - if (ofd != -1) { - while ((nread = read(ofd, buf, sizeof(buf))) != 0) { - if ((nwritten = write(tfd, buf, nread)) != nread) { - if (nwritten == -1) - warning("%s", tf[j].tfile); - else - warningx(_("%s: short write"), tf[j].tfile); - goto cleanup; - } - } - close(ofd); - } - /* - * We always update the stashed mtime because the time - * resolution of the filesystem the temporary file is on may - * not match that of the filesystem where the file to be edited - * resides. It is OK if touch() fails since we only use the info - * to determine whether or not a file has been modified. - */ - (void) touch(tfd, NULL, &tf[j].omtim); - rc = fstat(tfd, &sb); - if (!rc) - mtim_get(&sb, &tf[j].omtim); - close(tfd); - j++; - } - if ((nfiles = j) == 0) - goto cleanup; /* no files readable, you lose */ + + /* Make temporary copies of the original files */ + if (!rbac_enabled) + nfiles = sudo_edit_copy(command_details, tf, files, nfiles, tmpdir, tmplen, 0); + else + nfiles = selinux_edit_copy(command_details, tf, files, nfiles, tmpdir, tmplen, 0); + if (nfiles <= 0) + return 1; + + switch_user(ROOT_UID, user_details.egid, + user_details.ngroups, user_details.groups); /* * Allocate space for the new argument vector and fill it in. * We concatenate the editor with its args and the file list @@ -253,84 +463,18 @@ sudo_edit(struct command_details *comman editor_details.argv = nargv; rval = run_command(&editor_details); gettimeofday(&tv2, NULL); + timevalsub(&tv1, &tv2); - /* Copy contents of temp files to real ones */ - for (i = 0; i < nfiles; i++) { - rc = -1; - if (seteuid(user_details.uid) != 0) - error(1, "seteuid(%d)", (int)user_details.uid); - if ((tfd = open(tf[i].tfile, O_RDONLY, 0644)) != -1) { - rc = fstat(tfd, &sb); - } - if (seteuid(ROOT_UID) != 0) - error(1, "seteuid(ROOT_UID)"); - if (rc || !S_ISREG(sb.st_mode)) { - if (rc) - warning("%s", tf[i].tfile); - else - warningx(_("%s: not a regular file"), tf[i].tfile); - warningx(_("%s left unmodified"), tf[i].ofile); - if (tfd != -1) - close(tfd); - continue; - } - mtim_get(&sb, &tv); - if (tf[i].osize == sb.st_size && timevalcmp(&tf[i].omtim, &tv, ==)) { - /* - * If mtime and size match but the user spent no measurable - * time in the editor we can't tell if the file was changed. - */ - timevalsub(&tv1, &tv2); - if (timevalisset(&tv2)) { - warningx(_("%s unchanged"), tf[i].ofile); - unlink(tf[i].tfile); - close(tfd); - continue; - } - } - switch_user(command_details->euid, command_details->egid, - command_details->ngroups, command_details->groups); - ofd = open(tf[i].ofile, O_WRONLY|O_TRUNC|O_CREAT, 0644); - switch_user(ROOT_UID, user_details.egid, - user_details.ngroups, user_details.groups); - if (ofd == -1) { - warning(_("unable to write to %s"), tf[i].ofile); - warningx(_("contents of edit session left in %s"), tf[i].tfile); - close(tfd); - continue; - } - while ((nread = read(tfd, buf, sizeof(buf))) > 0) { - if ((nwritten = write(ofd, buf, nread)) != nread) { - if (nwritten == -1) - warning("%s", tf[i].ofile); - else - warningx(_("%s: short write"), tf[i].ofile); - break; - } - } - if (nread == 0) { - /* success, got EOF */ - unlink(tf[i].tfile); - } else if (nread < 0) { - warning(_("unable to read temporary file")); - warningx(_("contents of edit session left in %s"), tf[i].tfile); - } else { - warning(_("unable to write to %s"), tf[i].ofile); - warningx(_("contents of edit session left in %s"), tf[i].tfile); - } - close(ofd); - } + switch_user(ROOT_UID, user_details.egid, + user_details.ngroups, user_details.groups); + + /* Copy the temporary files back to originals */ + if (!rbac_enabled) + nfiles = sudo_edit_copy(command_details, tf, NULL, nfiles, NULL, 0, timevalisset(&tv2)); + else + nfiles = selinux_edit_copy(command_details, tf, NULL, nfiles, NULL, 0, timevalisset(&tv2)); + debug_return_int(rval); - -cleanup: - /* Clean up temp files and return. */ - if (tf != NULL) { - for (i = 0; i < nfiles; i++) { - if (tf[i].tfile != NULL) - unlink(tf[i].tfile); - } - } - debug_return_int(1); } #else /* HAVE_SETRESUID || HAVE_SETREUID || HAVE_SETEUID */ diff -up sudo-1.8.6p3/src/sudo.h.sudoedit-selinux sudo-1.8.6p3/src/sudo.h --- sudo-1.8.6p3/src/sudo.h.sudoedit-selinux 2012-09-18 15:56:30.000000000 +0200 +++ sudo-1.8.6p3/src/sudo.h 2012-09-25 16:04:36.690423001 +0200 @@ -130,6 +130,7 @@ struct user_details { #define CD_RBAC_ENABLED 0x0800 #define CD_USE_PTY 0x1000 #define CD_SET_UTMP 0x2000 +#define CD_SUDOEDIT_COPY 0x4000 struct command_details { uid_t uid;