* Re: [PATCH v3] seunshare: fix the frail tmpdir cleanup [not found] <CAEjxPJ6QGZcAO zjdajOFTaDc tuh56Vao3niO37udEr=UfAgg@mail.gmail.com> @ 2025-08-01 18:21 ` Rahul Sandhu 2025-08-01 18:29 ` Stephen Smalley 0 siblings, 1 reply; 12+ messages in thread From: Rahul Sandhu @ 2025-08-01 18:21 UTC (permalink / raw) To: stephen.smalley.work; +Cc: nvraxn, selinux > I would think the idiomatic C way of doing this would be to use fts(3) > or nftw(3). > setfiles/restorecon originally used nftw(3) but later switched to > fts(3) for reasons I don't recall. It's worth noting that fts (3) is not actually mandated by POSIX from a portability perspective, however we do already depend on it for other parts of the userspace[1] (as you mentioned), and shims exist for other libcs, for example musl[2], so I don't think this is a huge deal. I can't speak for setfiles and restorecon, but nftw's api is pretty crap compared to fts, so, like them, I'd rather use fts for this than nftw even though nftw is part of posix[3]. I think dirfds are fine to use here, however I'm happy to send a v2 using fts if you wish - would you like me to do so? [1] https://github.com/SELinuxProject/selinux/blob/main/libselinux/src/selinux_restorecon.c#L17 [2] https://github.com/void-linux/musl-fts [3] https://pubs.opengroup.org/onlinepubs/9699919799/functions/nftw.html ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3] seunshare: fix the frail tmpdir cleanup 2025-08-01 18:21 ` [PATCH v3] seunshare: fix the frail tmpdir cleanup Rahul Sandhu @ 2025-08-01 18:29 ` Stephen Smalley 2025-08-14 17:47 ` James Carter 0 siblings, 1 reply; 12+ messages in thread From: Stephen Smalley @ 2025-08-01 18:29 UTC (permalink / raw) To: Rahul Sandhu; +Cc: selinux On Fri, Aug 1, 2025 at 2:21 PM Rahul Sandhu <nvraxn@gmail.com> wrote: > > > I would think the idiomatic C way of doing this would be to use fts(3) > > or nftw(3). > > setfiles/restorecon originally used nftw(3) but later switched to > > fts(3) for reasons I don't recall. > > It's worth noting that fts (3) is not actually mandated by POSIX from a > portability perspective, however we do already depend on it for other > parts of the userspace[1] (as you mentioned), and shims exist for other > libcs, for example musl[2], so I don't think this is a huge deal. I > can't speak for setfiles and restorecon, but nftw's api is pretty crap > compared to fts, so, like them, I'd rather use fts for this than nftw > even though nftw is part of posix[3]. > > I think dirfds are fine to use here, however I'm happy to send a v2 > using fts if you wish - would you like me to do so? Maybe wait and see what other SELinux userspace maintainers think - I am just one opinion here. > > [1] https://github.com/SELinuxProject/selinux/blob/main/libselinux/src/selinux_restorecon.c#L17 > [2] https://github.com/void-linux/musl-fts > [3] https://pubs.opengroup.org/onlinepubs/9699919799/functions/nftw.html ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v3] seunshare: fix the frail tmpdir cleanup 2025-08-01 18:29 ` Stephen Smalley @ 2025-08-14 17:47 ` James Carter 2025-09-25 5:38 ` [PATCH v4] " Rahul Sandhu 0 siblings, 1 reply; 12+ messages in thread From: James Carter @ 2025-08-14 17:47 UTC (permalink / raw) To: Stephen Smalley; +Cc: Rahul Sandhu, selinux On Fri, Aug 1, 2025 at 2:29 PM Stephen Smalley <stephen.smalley.work@gmail.com> wrote: > > On Fri, Aug 1, 2025 at 2:21 PM Rahul Sandhu <nvraxn@gmail.com> wrote: > > > > > I would think the idiomatic C way of doing this would be to use fts(3) > > > or nftw(3). > > > setfiles/restorecon originally used nftw(3) but later switched to > > > fts(3) for reasons I don't recall. > > > > It's worth noting that fts (3) is not actually mandated by POSIX from a > > portability perspective, however we do already depend on it for other > > parts of the userspace[1] (as you mentioned), and shims exist for other > > libcs, for example musl[2], so I don't think this is a huge deal. I > > can't speak for setfiles and restorecon, but nftw's api is pretty crap > > compared to fts, so, like them, I'd rather use fts for this than nftw > > even though nftw is part of posix[3]. > > > > I think dirfds are fine to use here, however I'm happy to send a v2 > > using fts if you wish - would you like me to do so? > > Maybe wait and see what other SELinux userspace maintainers think - I > am just one opinion here. > Since we use both fts and dirfds in other places, I am ok with either one. I don't think you need to redo the patch just to use fts. Jim > > > > [1] https://github.com/SELinuxProject/selinux/blob/main/libselinux/src/selinux_restorecon.c#L17 > > [2] https://github.com/void-linux/musl-fts > > [3] https://pubs.opengroup.org/onlinepubs/9699919799/functions/nftw.html > ^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v4] seunshare: fix the frail tmpdir cleanup 2025-08-14 17:47 ` James Carter @ 2025-09-25 5:38 ` Rahul Sandhu 2025-10-06 15:57 ` Stephen Smalley 0 siblings, 1 reply; 12+ messages in thread From: Rahul Sandhu @ 2025-09-25 5:38 UTC (permalink / raw) To: jwcart2; +Cc: nvraxn, selinux, stephen.smalley.work For some reason, rm is invoked via system (3) to cleanup the runtime temp directory. This really isn't all that robust, *especially* given that seunshare is supposed to be a security boundary. Instead do this using libc, the API designed to be used within C programs. Also, don't bother trying to delete the contents of the tmpdir and then trying to delete the parent directory with rmdir later - for some... undocumented reason currently we attempt to delete the contents of the dir with dropped perms, *then* elevate perms, *then* delete the tmpdir. This doesn't really make all that much sense as far as I can tell. We should be the only ones using the tmpdir, so we may as well just delete the entire thing using the rm_rf () function with elevated permissions. Signed-off-by: Rahul Sandhu <nvraxn@gmail.com> --- sandbox/seunshare.c | 77 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 12 deletions(-) v2: don't use else after return v3: don't follow symlinks in rm_rf (). This is pretty important as we we are operating on an untrusted directory, which may have symlinks pointed to privileged content. However, as we only really need to operate on the contents of the tmpdir, we can ignore symlinks. v4: fix spelling in commit message diff --git a/sandbox/seunshare.c b/sandbox/seunshare.c index 106f625f..c46db90f 100644 --- a/sandbox/seunshare.c +++ b/sandbox/seunshare.c @@ -403,6 +403,66 @@ err: return rc; } +/* + * Recursively delete a directory. + * SAFTEY: This function will NOT follow symbolic links (AT_SYMLINK_NOFOLLOW). + * As a result, this function can be run safely on a directory owned by + * a non-root user: symbolic links to root paths (such as /root) will + * not be followed. + */ +static bool rm_rf(int targetfd, const char *path) { + struct stat statbuf; + + if (fstatat(targetfd, path, &statbuf, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno == ENOENT) { + return true; + } + perror("fstatat"); + return false; + } + + if (S_ISDIR(statbuf.st_mode)) { + const int newfd = openat(targetfd, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (newfd < 0) { + perror("openat"); + return false; + } + + DIR *dir = fdopendir(newfd); + if (!dir) { + perror("fdopendir"); + close(newfd); + return false; + } + + struct dirent *entry; + int rc = true; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + if (rm_rf(dirfd(dir), entry->d_name) != 0) { + rc = false; + } + } + + closedir(dir); + + if (unlinkat(targetfd, path, AT_REMOVEDIR) < 0) { + perror("unlinkat"); + rc = false; + } + + return rc; + } + if (unlinkat(targetfd, path, 0) < 0) { + perror("unlinkat"); + return false; + } + return true; +} + /** * Clean up runtime temporary directory. Returns 0 if no problem was detected, * >0 if some error was detected, but errors here are treated as non-fatal and @@ -428,24 +488,17 @@ static int cleanup_tmpdir(const char *tmpdir, const char *src, free(cmdbuf); cmdbuf = NULL; } - /* remove files from the runtime temporary directory */ - if (asprintf(&cmdbuf, "/bin/rm -r '%s/' 2>/dev/null", tmpdir) == -1) { - fprintf(stderr, _("Out of memory\n")); - cmdbuf = NULL; + if ((uid_t)setfsuid(0) != 0) { + /* setfsuid does not return error, but this check makes code checkers happy */ rc++; } - /* this may fail if there's root-owned file left in the runtime tmpdir */ - if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) rc++; - free(cmdbuf); cmdbuf = NULL; - /* remove runtime temporary directory */ - if ((uid_t)setfsuid(0) != 0) { - /* setfsuid does not return error, but this check makes code checkers happy */ + /* Recursively remove the runtime temp directory. */ + if (!rm_rf(AT_FDCWD, tmpdir)) { + fprintf(stderr, _("Failed to recursively remove directory %s\n"), tmpdir); rc++; } - if (pwd->pw_uid != 0 && rmdir(tmpdir) == -1) - fprintf(stderr, _("Failed to remove directory %s: %s\n"), tmpdir, strerror(errno)); if ((uid_t)setfsuid(pwd->pw_uid) != 0) { fprintf(stderr, _("unable to switch back to user after clearing tmp dir\n")); rc++; -- 2.50.1 ^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v4] seunshare: fix the frail tmpdir cleanup 2025-09-25 5:38 ` [PATCH v4] " Rahul Sandhu @ 2025-10-06 15:57 ` Stephen Smalley 2025-10-06 16:31 ` Stephen Smalley 0 siblings, 1 reply; 12+ messages in thread From: Stephen Smalley @ 2025-10-06 15:57 UTC (permalink / raw) To: Rahul Sandhu; +Cc: jwcart2, selinux On Thu, Sep 25, 2025 at 1:38 AM Rahul Sandhu <nvraxn@gmail.com> wrote: > > For some reason, rm is invoked via system (3) to cleanup the runtime > temp directory. This really isn't all that robust, *especially* given > that seunshare is supposed to be a security boundary. Instead do this > using libc, the API designed to be used within C programs. > > Also, don't bother trying to delete the contents of the tmpdir and then > trying to delete the parent directory with rmdir later - for some... > undocumented reason currently we attempt to delete the contents of the > dir with dropped perms, *then* elevate perms, *then* delete the tmpdir. I think this was for security/safety reasons, to prevent unlinking any files linked from the tmpdir that weren't actually owned by the user. However, in theory, your rewrite makes that obsolete. > > This doesn't really make all that much sense as far as I can tell. We > should be the only ones using the tmpdir, so we may as well just delete > the entire thing using the rm_rf () function with elevated permissions. > > Signed-off-by: Rahul Sandhu <nvraxn@gmail.com> > --- > sandbox/seunshare.c | 77 ++++++++++++++++++++++++++++++++++++++------- > 1 file changed, 65 insertions(+), 12 deletions(-) > > v2: don't use else after return > v3: don't follow symlinks in rm_rf (). This is pretty important as we > we are operating on an untrusted directory, which may have symlinks > pointed to privileged content. However, as we only really need to > operate on the contents of the tmpdir, we can ignore symlinks. > v4: fix spelling in commit message > > diff --git a/sandbox/seunshare.c b/sandbox/seunshare.c > index 106f625f..c46db90f 100644 > --- a/sandbox/seunshare.c > +++ b/sandbox/seunshare.c > @@ -403,6 +403,66 @@ err: > return rc; > } > > +/* > + * Recursively delete a directory. > + * SAFTEY: This function will NOT follow symbolic links (AT_SYMLINK_NOFOLLOW). Spelling here too: SAFETY > + * As a result, this function can be run safely on a directory owned by > + * a non-root user: symbolic links to root paths (such as /root) will > + * not be followed. > + */ > +static bool rm_rf(int targetfd, const char *path) { > + struct stat statbuf; > + > + if (fstatat(targetfd, path, &statbuf, AT_SYMLINK_NOFOLLOW) < 0) { > + if (errno == ENOENT) { > + return true; > + } > + perror("fstatat"); > + return false; > + } > + > + if (S_ISDIR(statbuf.st_mode)) { > + const int newfd = openat(targetfd, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); > + if (newfd < 0) { > + perror("openat"); > + return false; > + } > + > + DIR *dir = fdopendir(newfd); > + if (!dir) { > + perror("fdopendir"); > + close(newfd); > + return false; > + } > + > + struct dirent *entry; > + int rc = true; > + while ((entry = readdir(dir)) != NULL) { > + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { > + continue; > + } > + > + if (rm_rf(dirfd(dir), entry->d_name) != 0) { > + rc = false; > + } > + } > + > + closedir(dir); > + > + if (unlinkat(targetfd, path, AT_REMOVEDIR) < 0) { > + perror("unlinkat"); > + rc = false; > + } > + > + return rc; > + } > + if (unlinkat(targetfd, path, 0) < 0) { > + perror("unlinkat"); > + return false; > + } > + return true; > +} > + > /** > * Clean up runtime temporary directory. Returns 0 if no problem was detected, > * >0 if some error was detected, but errors here are treated as non-fatal and > @@ -428,24 +488,17 @@ static int cleanup_tmpdir(const char *tmpdir, const char *src, > free(cmdbuf); cmdbuf = NULL; > } > > - /* remove files from the runtime temporary directory */ > - if (asprintf(&cmdbuf, "/bin/rm -r '%s/' 2>/dev/null", tmpdir) == -1) { > - fprintf(stderr, _("Out of memory\n")); > - cmdbuf = NULL; > + if ((uid_t)setfsuid(0) != 0) { > + /* setfsuid does not return error, but this check makes code checkers happy */ > rc++; > } > - /* this may fail if there's root-owned file left in the runtime tmpdir */ > - if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) rc++; > - free(cmdbuf); cmdbuf = NULL; > > - /* remove runtime temporary directory */ > - if ((uid_t)setfsuid(0) != 0) { > - /* setfsuid does not return error, but this check makes code checkers happy */ > + /* Recursively remove the runtime temp directory. */ > + if (!rm_rf(AT_FDCWD, tmpdir)) { > + fprintf(stderr, _("Failed to recursively remove directory %s\n"), tmpdir); > rc++; > } > > - if (pwd->pw_uid != 0 && rmdir(tmpdir) == -1) > - fprintf(stderr, _("Failed to remove directory %s: %s\n"), tmpdir, strerror(errno)); > if ((uid_t)setfsuid(pwd->pw_uid) != 0) { > fprintf(stderr, _("unable to switch back to user after clearing tmp dir\n")); > rc++; > -- > 2.50.1 > ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v4] seunshare: fix the frail tmpdir cleanup 2025-10-06 15:57 ` Stephen Smalley @ 2025-10-06 16:31 ` Stephen Smalley 2025-10-06 18:40 ` Stephen Smalley 0 siblings, 1 reply; 12+ messages in thread From: Stephen Smalley @ 2025-10-06 16:31 UTC (permalink / raw) To: Rahul Sandhu; +Cc: jwcart2, selinux On Mon, Oct 6, 2025 at 11:57 AM Stephen Smalley <stephen.smalley.work@gmail.com> wrote: > > On Thu, Sep 25, 2025 at 1:38 AM Rahul Sandhu <nvraxn@gmail.com> wrote: > > > > For some reason, rm is invoked via system (3) to cleanup the runtime > > temp directory. This really isn't all that robust, *especially* given > > that seunshare is supposed to be a security boundary. Instead do this > > using libc, the API designed to be used within C programs. > > > > Also, don't bother trying to delete the contents of the tmpdir and then > > trying to delete the parent directory with rmdir later - for some... > > undocumented reason currently we attempt to delete the contents of the > > dir with dropped perms, *then* elevate perms, *then* delete the tmpdir. > > I think this was for security/safety reasons, to prevent unlinking any > files linked from the tmpdir that weren't actually owned by the user. > However, in theory, your rewrite makes that obsolete. > > > > > This doesn't really make all that much sense as far as I can tell. We > > should be the only ones using the tmpdir, so we may as well just delete > > the entire thing using the rm_rf () function with elevated permissions. > > > > Signed-off-by: Rahul Sandhu <nvraxn@gmail.com> > > --- > > sandbox/seunshare.c | 77 ++++++++++++++++++++++++++++++++++++++------- > > 1 file changed, 65 insertions(+), 12 deletions(-) > > > > v2: don't use else after return > > v3: don't follow symlinks in rm_rf (). This is pretty important as we > > we are operating on an untrusted directory, which may have symlinks > > pointed to privileged content. However, as we only really need to > > operate on the contents of the tmpdir, we can ignore symlinks. > > v4: fix spelling in commit message > > > > diff --git a/sandbox/seunshare.c b/sandbox/seunshare.c > > index 106f625f..c46db90f 100644 > > --- a/sandbox/seunshare.c > > +++ b/sandbox/seunshare.c > > @@ -403,6 +403,66 @@ err: > > return rc; > > } > > > > +/* > > + * Recursively delete a directory. > > + * SAFTEY: This function will NOT follow symbolic links (AT_SYMLINK_NOFOLLOW). > > Spelling here too: SAFETY > > > + * As a result, this function can be run safely on a directory owned by > > + * a non-root user: symbolic links to root paths (such as /root) will > > + * not be followed. > > + */ > > +static bool rm_rf(int targetfd, const char *path) { > > + struct stat statbuf; > > + > > + if (fstatat(targetfd, path, &statbuf, AT_SYMLINK_NOFOLLOW) < 0) { > > + if (errno == ENOENT) { > > + return true; > > + } > > + perror("fstatat"); > > + return false; > > + } > > + > > + if (S_ISDIR(statbuf.st_mode)) { > > + const int newfd = openat(targetfd, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); > > + if (newfd < 0) { > > + perror("openat"); > > + return false; > > + } > > + > > + DIR *dir = fdopendir(newfd); > > + if (!dir) { > > + perror("fdopendir"); > > + close(newfd); > > + return false; > > + } > > + > > + struct dirent *entry; > > + int rc = true; > > + while ((entry = readdir(dir)) != NULL) { > > + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { > > + continue; > > + } > > + > > + if (rm_rf(dirfd(dir), entry->d_name) != 0) { > > + rc = false; > > + } > > + } > > + > > + closedir(dir); > > + > > + if (unlinkat(targetfd, path, AT_REMOVEDIR) < 0) { > > + perror("unlinkat"); > > + rc = false; > > + } > > + > > + return rc; > > + } > > + if (unlinkat(targetfd, path, 0) < 0) { > > + perror("unlinkat"); > > + return false; > > + } > > + return true; > > +} > > + > > /** > > * Clean up runtime temporary directory. Returns 0 if no problem was detected, > > * >0 if some error was detected, but errors here are treated as non-fatal and > > @@ -428,24 +488,17 @@ static int cleanup_tmpdir(const char *tmpdir, const char *src, > > free(cmdbuf); cmdbuf = NULL; > > } > > > > - /* remove files from the runtime temporary directory */ > > - if (asprintf(&cmdbuf, "/bin/rm -r '%s/' 2>/dev/null", tmpdir) == -1) { > > - fprintf(stderr, _("Out of memory\n")); > > - cmdbuf = NULL; > > + if ((uid_t)setfsuid(0) != 0) { > > + /* setfsuid does not return error, but this check makes code checkers happy */ I'm a little confused by this - further down there is a setfsuid(pwd->pw_uid) call which seems to suggest that we are already running as uid-0 at this point? If so, why do we need this setfsuid(0) call? > > rc++; > > } > > - /* this may fail if there's root-owned file left in the runtime tmpdir */ > > - if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) rc++; > > - free(cmdbuf); cmdbuf = NULL; > > > > - /* remove runtime temporary directory */ > > - if ((uid_t)setfsuid(0) != 0) { > > - /* setfsuid does not return error, but this check makes code checkers happy */ > > + /* Recursively remove the runtime temp directory. */ > > + if (!rm_rf(AT_FDCWD, tmpdir)) { > > + fprintf(stderr, _("Failed to recursively remove directory %s\n"), tmpdir); > > rc++; > > } > > > > - if (pwd->pw_uid != 0 && rmdir(tmpdir) == -1) > > - fprintf(stderr, _("Failed to remove directory %s: %s\n"), tmpdir, strerror(errno)); > > if ((uid_t)setfsuid(pwd->pw_uid) != 0) { > > fprintf(stderr, _("unable to switch back to user after clearing tmp dir\n")); > > rc++; > > -- > > 2.50.1 > > ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v4] seunshare: fix the frail tmpdir cleanup 2025-10-06 16:31 ` Stephen Smalley @ 2025-10-06 18:40 ` Stephen Smalley 2025-10-07 9:29 ` [PATCH v5] " Rahul Sandhu 0 siblings, 1 reply; 12+ messages in thread From: Stephen Smalley @ 2025-10-06 18:40 UTC (permalink / raw) To: Rahul Sandhu; +Cc: jwcart2, selinux On Mon, Oct 6, 2025 at 12:31 PM Stephen Smalley <stephen.smalley.work@gmail.com> wrote: > > On Mon, Oct 6, 2025 at 11:57 AM Stephen Smalley > <stephen.smalley.work@gmail.com> wrote: > > > > On Thu, Sep 25, 2025 at 1:38 AM Rahul Sandhu <nvraxn@gmail.com> wrote: > > > > > > For some reason, rm is invoked via system (3) to cleanup the runtime > > > temp directory. This really isn't all that robust, *especially* given > > > that seunshare is supposed to be a security boundary. Instead do this > > > using libc, the API designed to be used within C programs. > > > > > > Also, don't bother trying to delete the contents of the tmpdir and then > > > trying to delete the parent directory with rmdir later - for some... > > > undocumented reason currently we attempt to delete the contents of the > > > dir with dropped perms, *then* elevate perms, *then* delete the tmpdir. > > > > I think this was for security/safety reasons, to prevent unlinking any > > files linked from the tmpdir that weren't actually owned by the user. > > However, in theory, your rewrite makes that obsolete. > > > > > > > > This doesn't really make all that much sense as far as I can tell. We > > > should be the only ones using the tmpdir, so we may as well just delete > > > the entire thing using the rm_rf () function with elevated permissions. > > > > > > Signed-off-by: Rahul Sandhu <nvraxn@gmail.com> > > > --- > > > sandbox/seunshare.c | 77 ++++++++++++++++++++++++++++++++++++++------- > > > 1 file changed, 65 insertions(+), 12 deletions(-) > > > > > > v2: don't use else after return > > > v3: don't follow symlinks in rm_rf (). This is pretty important as we > > > we are operating on an untrusted directory, which may have symlinks > > > pointed to privileged content. However, as we only really need to > > > operate on the contents of the tmpdir, we can ignore symlinks. > > > v4: fix spelling in commit message > > > > > > diff --git a/sandbox/seunshare.c b/sandbox/seunshare.c > > > index 106f625f..c46db90f 100644 > > > --- a/sandbox/seunshare.c > > > +++ b/sandbox/seunshare.c > > > @@ -403,6 +403,66 @@ err: > > > return rc; > > > } > > > > > > +/* > > > + * Recursively delete a directory. > > > + * SAFTEY: This function will NOT follow symbolic links (AT_SYMLINK_NOFOLLOW). > > > > Spelling here too: SAFETY > > > > > + * As a result, this function can be run safely on a directory owned by > > > + * a non-root user: symbolic links to root paths (such as /root) will > > > + * not be followed. > > > + */ > > > +static bool rm_rf(int targetfd, const char *path) { > > > + struct stat statbuf; > > > + > > > + if (fstatat(targetfd, path, &statbuf, AT_SYMLINK_NOFOLLOW) < 0) { > > > + if (errno == ENOENT) { > > > + return true; > > > + } > > > + perror("fstatat"); > > > + return false; > > > + } > > > + > > > + if (S_ISDIR(statbuf.st_mode)) { > > > + const int newfd = openat(targetfd, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); > > > + if (newfd < 0) { > > > + perror("openat"); > > > + return false; > > > + } > > > + > > > + DIR *dir = fdopendir(newfd); > > > + if (!dir) { > > > + perror("fdopendir"); > > > + close(newfd); > > > + return false; > > > + } > > > + > > > + struct dirent *entry; > > > + int rc = true; > > > + while ((entry = readdir(dir)) != NULL) { > > > + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { > > > + continue; > > > + } > > > + > > > + if (rm_rf(dirfd(dir), entry->d_name) != 0) { > > > + rc = false; > > > + } > > > + } > > > + > > > + closedir(dir); > > > + > > > + if (unlinkat(targetfd, path, AT_REMOVEDIR) < 0) { > > > + perror("unlinkat"); > > > + rc = false; > > > + } > > > + > > > + return rc; > > > + } > > > + if (unlinkat(targetfd, path, 0) < 0) { > > > + perror("unlinkat"); > > > + return false; > > > + } > > > + return true; > > > +} > > > + > > > /** > > > * Clean up runtime temporary directory. Returns 0 if no problem was detected, > > > * >0 if some error was detected, but errors here are treated as non-fatal and > > > @@ -428,24 +488,17 @@ static int cleanup_tmpdir(const char *tmpdir, const char *src, > > > free(cmdbuf); cmdbuf = NULL; > > > } > > > > > > - /* remove files from the runtime temporary directory */ > > > - if (asprintf(&cmdbuf, "/bin/rm -r '%s/' 2>/dev/null", tmpdir) == -1) { > > > - fprintf(stderr, _("Out of memory\n")); > > > - cmdbuf = NULL; > > > + if ((uid_t)setfsuid(0) != 0) { > > > + /* setfsuid does not return error, but this check makes code checkers happy */ > > I'm a little confused by this - further down there is a > setfsuid(pwd->pw_uid) call which seems to suggest that we are already > running as uid-0 at this point? If so, why do we need this setfsuid(0) > call? Never mind, I see it now. So just fix the earlier spelling mistake and we'll call it done. > > > > rc++; > > > } > > > - /* this may fail if there's root-owned file left in the runtime tmpdir */ > > > - if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) rc++; > > > - free(cmdbuf); cmdbuf = NULL; > > > > > > - /* remove runtime temporary directory */ > > > - if ((uid_t)setfsuid(0) != 0) { > > > - /* setfsuid does not return error, but this check makes code checkers happy */ > > > + /* Recursively remove the runtime temp directory. */ > > > + if (!rm_rf(AT_FDCWD, tmpdir)) { > > > + fprintf(stderr, _("Failed to recursively remove directory %s\n"), tmpdir); > > > rc++; > > > } > > > > > > - if (pwd->pw_uid != 0 && rmdir(tmpdir) == -1) > > > - fprintf(stderr, _("Failed to remove directory %s: %s\n"), tmpdir, strerror(errno)); > > > if ((uid_t)setfsuid(pwd->pw_uid) != 0) { > > > fprintf(stderr, _("unable to switch back to user after clearing tmp dir\n")); > > > rc++; > > > -- > > > 2.50.1 > > > ^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v5] seunshare: fix the frail tmpdir cleanup 2025-10-06 18:40 ` Stephen Smalley @ 2025-10-07 9:29 ` Rahul Sandhu 2025-10-07 12:46 ` Stephen Smalley 0 siblings, 1 reply; 12+ messages in thread From: Rahul Sandhu @ 2025-10-07 9:29 UTC (permalink / raw) To: stephen.smalley.work; +Cc: jwcart2, nvraxn, selinux For some reason, rm is invoked via system (3) to cleanup the runtime temp directory. This really isn't all that robust, *especially* given that seunshare is supposed to be a security boundary. Instead do this using libc, the API designed to be used within C programs. Also, don't bother trying to delete the contents of the tmpdir and then trying to delete the parent directory with rmdir later - for some... undocumented reason currently we attempt to delete the contents of the dir with dropped perms, *then* elevate perms, *then* delete the tmpdir. This doesn't really make all that much sense as far as I can tell. We should be the only ones using the tmpdir, so we may as well just delete the entire thing using the rm_rf () function with elevated permissions. Signed-off-by: Rahul Sandhu <nvraxn@gmail.com> --- sandbox/seunshare.c | 77 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 12 deletions(-) v2: don't use else after return v3: don't follow symlinks in rm_rf (). This is pretty important as we we are operating on an untrusted directory, which may have symlinks pointed to privileged content. However, as we only really need to operate on the contents of the tmpdir, we can ignore symlinks. v4: fix spelling in commit message v5: fix spelling in comment diff --git a/sandbox/seunshare.c b/sandbox/seunshare.c index 106f625f..55e62620 100644 --- a/sandbox/seunshare.c +++ b/sandbox/seunshare.c @@ -403,6 +403,66 @@ err: return rc; } +/* + * Recursively delete a directory. + * SAFETY: This function will NOT follow symbolic links (AT_SYMLINK_NOFOLLOW). + * As a result, this function can be run safely on a directory owned by + * a non-root user: symbolic links to root paths (such as /root) will + * not be followed. + */ +static bool rm_rf(int targetfd, const char *path) { + struct stat statbuf; + + if (fstatat(targetfd, path, &statbuf, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno == ENOENT) { + return true; + } + perror("fstatat"); + return false; + } + + if (S_ISDIR(statbuf.st_mode)) { + const int newfd = openat(targetfd, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (newfd < 0) { + perror("openat"); + return false; + } + + DIR *dir = fdopendir(newfd); + if (!dir) { + perror("fdopendir"); + close(newfd); + return false; + } + + struct dirent *entry; + int rc = true; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + if (rm_rf(dirfd(dir), entry->d_name) != 0) { + rc = false; + } + } + + closedir(dir); + + if (unlinkat(targetfd, path, AT_REMOVEDIR) < 0) { + perror("unlinkat"); + rc = false; + } + + return rc; + } + if (unlinkat(targetfd, path, 0) < 0) { + perror("unlinkat"); + return false; + } + return true; +} + /** * Clean up runtime temporary directory. Returns 0 if no problem was detected, * >0 if some error was detected, but errors here are treated as non-fatal and @@ -428,24 +488,17 @@ static int cleanup_tmpdir(const char *tmpdir, const char *src, free(cmdbuf); cmdbuf = NULL; } - /* remove files from the runtime temporary directory */ - if (asprintf(&cmdbuf, "/bin/rm -r '%s/' 2>/dev/null", tmpdir) == -1) { - fprintf(stderr, _("Out of memory\n")); - cmdbuf = NULL; + if ((uid_t)setfsuid(0) != 0) { + /* setfsuid does not return error, but this check makes code checkers happy */ rc++; } - /* this may fail if there's root-owned file left in the runtime tmpdir */ - if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) rc++; - free(cmdbuf); cmdbuf = NULL; - /* remove runtime temporary directory */ - if ((uid_t)setfsuid(0) != 0) { - /* setfsuid does not return error, but this check makes code checkers happy */ + /* Recursively remove the runtime temp directory. */ + if (!rm_rf(AT_FDCWD, tmpdir)) { + fprintf(stderr, _("Failed to recursively remove directory %s\n"), tmpdir); rc++; } - if (pwd->pw_uid != 0 && rmdir(tmpdir) == -1) - fprintf(stderr, _("Failed to remove directory %s: %s\n"), tmpdir, strerror(errno)); if ((uid_t)setfsuid(pwd->pw_uid) != 0) { fprintf(stderr, _("unable to switch back to user after clearing tmp dir\n")); rc++; -- 2.50.1 ^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v5] seunshare: fix the frail tmpdir cleanup 2025-10-07 9:29 ` [PATCH v5] " Rahul Sandhu @ 2025-10-07 12:46 ` Stephen Smalley 2025-10-07 14:09 ` Stephen Smalley 0 siblings, 1 reply; 12+ messages in thread From: Stephen Smalley @ 2025-10-07 12:46 UTC (permalink / raw) To: Rahul Sandhu; +Cc: jwcart2, selinux On Tue, Oct 7, 2025 at 5:30 AM Rahul Sandhu <nvraxn@gmail.com> wrote: > > For some reason, rm is invoked via system (3) to cleanup the runtime > temp directory. This really isn't all that robust, *especially* given > that seunshare is supposed to be a security boundary. Instead do this > using libc, the API designed to be used within C programs. > > Also, don't bother trying to delete the contents of the tmpdir and then > trying to delete the parent directory with rmdir later - for some... > undocumented reason currently we attempt to delete the contents of the > dir with dropped perms, *then* elevate perms, *then* delete the tmpdir. > > This doesn't really make all that much sense as far as I can tell. We > should be the only ones using the tmpdir, so we may as well just delete > the entire thing using the rm_rf () function with elevated permissions. > > Signed-off-by: Rahul Sandhu <nvraxn@gmail.com> With this patch applied, I see the following output when I use sandbox -M and create anything in the tmpdir. $ sandbox -M bash bash: cannot set terminal process group (-1): Inappropriate ioctl for device bash: no job control in this shell bash-5.2$ mkdir /tmp/foobar bash-5.2$ exit Failed to recursively remove directory /tmp/.sandbox-sdsmall-chjRXi > --- > sandbox/seunshare.c | 77 ++++++++++++++++++++++++++++++++++++++------- > 1 file changed, 65 insertions(+), 12 deletions(-) > > v2: don't use else after return > v3: don't follow symlinks in rm_rf (). This is pretty important as we > we are operating on an untrusted directory, which may have symlinks > pointed to privileged content. However, as we only really need to > operate on the contents of the tmpdir, we can ignore symlinks. > v4: fix spelling in commit message > v5: fix spelling in comment > > diff --git a/sandbox/seunshare.c b/sandbox/seunshare.c > index 106f625f..55e62620 100644 > --- a/sandbox/seunshare.c > +++ b/sandbox/seunshare.c > @@ -403,6 +403,66 @@ err: > return rc; > } > > +/* > + * Recursively delete a directory. > + * SAFETY: This function will NOT follow symbolic links (AT_SYMLINK_NOFOLLOW). > + * As a result, this function can be run safely on a directory owned by > + * a non-root user: symbolic links to root paths (such as /root) will > + * not be followed. > + */ > +static bool rm_rf(int targetfd, const char *path) { > + struct stat statbuf; > + > + if (fstatat(targetfd, path, &statbuf, AT_SYMLINK_NOFOLLOW) < 0) { > + if (errno == ENOENT) { > + return true; > + } > + perror("fstatat"); > + return false; > + } > + > + if (S_ISDIR(statbuf.st_mode)) { > + const int newfd = openat(targetfd, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); > + if (newfd < 0) { > + perror("openat"); > + return false; > + } > + > + DIR *dir = fdopendir(newfd); > + if (!dir) { > + perror("fdopendir"); > + close(newfd); > + return false; > + } > + > + struct dirent *entry; > + int rc = true; > + while ((entry = readdir(dir)) != NULL) { > + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { > + continue; > + } > + > + if (rm_rf(dirfd(dir), entry->d_name) != 0) { > + rc = false; > + } > + } > + > + closedir(dir); > + > + if (unlinkat(targetfd, path, AT_REMOVEDIR) < 0) { > + perror("unlinkat"); > + rc = false; > + } > + > + return rc; > + } > + if (unlinkat(targetfd, path, 0) < 0) { > + perror("unlinkat"); > + return false; > + } > + return true; > +} > + > /** > * Clean up runtime temporary directory. Returns 0 if no problem was detected, > * >0 if some error was detected, but errors here are treated as non-fatal and > @@ -428,24 +488,17 @@ static int cleanup_tmpdir(const char *tmpdir, const char *src, > free(cmdbuf); cmdbuf = NULL; > } > > - /* remove files from the runtime temporary directory */ > - if (asprintf(&cmdbuf, "/bin/rm -r '%s/' 2>/dev/null", tmpdir) == -1) { > - fprintf(stderr, _("Out of memory\n")); > - cmdbuf = NULL; > + if ((uid_t)setfsuid(0) != 0) { > + /* setfsuid does not return error, but this check makes code checkers happy */ > rc++; > } > - /* this may fail if there's root-owned file left in the runtime tmpdir */ > - if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) rc++; > - free(cmdbuf); cmdbuf = NULL; > > - /* remove runtime temporary directory */ > - if ((uid_t)setfsuid(0) != 0) { > - /* setfsuid does not return error, but this check makes code checkers happy */ > + /* Recursively remove the runtime temp directory. */ > + if (!rm_rf(AT_FDCWD, tmpdir)) { > + fprintf(stderr, _("Failed to recursively remove directory %s\n"), tmpdir); > rc++; > } > > - if (pwd->pw_uid != 0 && rmdir(tmpdir) == -1) > - fprintf(stderr, _("Failed to remove directory %s: %s\n"), tmpdir, strerror(errno)); > if ((uid_t)setfsuid(pwd->pw_uid) != 0) { > fprintf(stderr, _("unable to switch back to user after clearing tmp dir\n")); > rc++; > -- > 2.50.1 > ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v5] seunshare: fix the frail tmpdir cleanup 2025-10-07 12:46 ` Stephen Smalley @ 2025-10-07 14:09 ` Stephen Smalley 2025-10-07 16:59 ` Rahul Sandhu 0 siblings, 1 reply; 12+ messages in thread From: Stephen Smalley @ 2025-10-07 14:09 UTC (permalink / raw) To: Rahul Sandhu; +Cc: jwcart2, selinux On Tue, Oct 7, 2025 at 8:46 AM Stephen Smalley <stephen.smalley.work@gmail.com> wrote: > > On Tue, Oct 7, 2025 at 5:30 AM Rahul Sandhu <nvraxn@gmail.com> wrote: > > > > For some reason, rm is invoked via system (3) to cleanup the runtime > > temp directory. This really isn't all that robust, *especially* given > > that seunshare is supposed to be a security boundary. Instead do this > > using libc, the API designed to be used within C programs. > > > > Also, don't bother trying to delete the contents of the tmpdir and then > > trying to delete the parent directory with rmdir later - for some... > > undocumented reason currently we attempt to delete the contents of the > > dir with dropped perms, *then* elevate perms, *then* delete the tmpdir. > > > > This doesn't really make all that much sense as far as I can tell. We > > should be the only ones using the tmpdir, so we may as well just delete Also, to be clear, "should be the only ones using the tmpdir above" is not a safe assumption against the user spawning a detached process that can try to create symlinks in parallel with the recursive removal, but that should be mitigated by your rm_rf implementation IIUC. > > the entire thing using the rm_rf () function with elevated permissions. > > > > Signed-off-by: Rahul Sandhu <nvraxn@gmail.com> > > With this patch applied, I see the following output when I use sandbox > -M and create anything in the tmpdir. > > $ sandbox -M bash > bash: cannot set terminal process group (-1): Inappropriate ioctl for device > bash: no job control in this shell > bash-5.2$ mkdir /tmp/foobar > bash-5.2$ exit > Failed to recursively remove directory /tmp/.sandbox-sdsmall-chjRXi > > > --- > > sandbox/seunshare.c | 77 ++++++++++++++++++++++++++++++++++++++------- > > 1 file changed, 65 insertions(+), 12 deletions(-) > > > > v2: don't use else after return > > v3: don't follow symlinks in rm_rf (). This is pretty important as we > > we are operating on an untrusted directory, which may have symlinks > > pointed to privileged content. However, as we only really need to > > operate on the contents of the tmpdir, we can ignore symlinks. > > v4: fix spelling in commit message > > v5: fix spelling in comment > > > > diff --git a/sandbox/seunshare.c b/sandbox/seunshare.c > > index 106f625f..55e62620 100644 > > --- a/sandbox/seunshare.c > > +++ b/sandbox/seunshare.c > > @@ -403,6 +403,66 @@ err: > > return rc; > > } > > > > +/* > > + * Recursively delete a directory. > > + * SAFETY: This function will NOT follow symbolic links (AT_SYMLINK_NOFOLLOW). > > + * As a result, this function can be run safely on a directory owned by > > + * a non-root user: symbolic links to root paths (such as /root) will > > + * not be followed. > > + */ > > +static bool rm_rf(int targetfd, const char *path) { > > + struct stat statbuf; > > + > > + if (fstatat(targetfd, path, &statbuf, AT_SYMLINK_NOFOLLOW) < 0) { > > + if (errno == ENOENT) { > > + return true; > > + } > > + perror("fstatat"); > > + return false; > > + } > > + > > + if (S_ISDIR(statbuf.st_mode)) { > > + const int newfd = openat(targetfd, path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); > > + if (newfd < 0) { > > + perror("openat"); > > + return false; > > + } > > + > > + DIR *dir = fdopendir(newfd); > > + if (!dir) { > > + perror("fdopendir"); > > + close(newfd); > > + return false; > > + } > > + > > + struct dirent *entry; > > + int rc = true; > > + while ((entry = readdir(dir)) != NULL) { > > + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { > > + continue; > > + } > > + > > + if (rm_rf(dirfd(dir), entry->d_name) != 0) { > > + rc = false; > > + } > > + } > > + > > + closedir(dir); > > + > > + if (unlinkat(targetfd, path, AT_REMOVEDIR) < 0) { > > + perror("unlinkat"); > > + rc = false; > > + } > > + > > + return rc; > > + } > > + if (unlinkat(targetfd, path, 0) < 0) { > > + perror("unlinkat"); > > + return false; > > + } > > + return true; > > +} > > + > > /** > > * Clean up runtime temporary directory. Returns 0 if no problem was detected, > > * >0 if some error was detected, but errors here are treated as non-fatal and > > @@ -428,24 +488,17 @@ static int cleanup_tmpdir(const char *tmpdir, const char *src, > > free(cmdbuf); cmdbuf = NULL; > > } > > > > - /* remove files from the runtime temporary directory */ > > - if (asprintf(&cmdbuf, "/bin/rm -r '%s/' 2>/dev/null", tmpdir) == -1) { > > - fprintf(stderr, _("Out of memory\n")); > > - cmdbuf = NULL; > > + if ((uid_t)setfsuid(0) != 0) { > > + /* setfsuid does not return error, but this check makes code checkers happy */ > > rc++; > > } > > - /* this may fail if there's root-owned file left in the runtime tmpdir */ > > - if (cmdbuf && spawn_command(cmdbuf, pwd->pw_uid) != 0) rc++; > > - free(cmdbuf); cmdbuf = NULL; > > > > - /* remove runtime temporary directory */ > > - if ((uid_t)setfsuid(0) != 0) { > > - /* setfsuid does not return error, but this check makes code checkers happy */ > > + /* Recursively remove the runtime temp directory. */ > > + if (!rm_rf(AT_FDCWD, tmpdir)) { > > + fprintf(stderr, _("Failed to recursively remove directory %s\n"), tmpdir); > > rc++; > > } > > > > - if (pwd->pw_uid != 0 && rmdir(tmpdir) == -1) > > - fprintf(stderr, _("Failed to remove directory %s: %s\n"), tmpdir, strerror(errno)); > > if ((uid_t)setfsuid(pwd->pw_uid) != 0) { > > fprintf(stderr, _("unable to switch back to user after clearing tmp dir\n")); > > rc++; > > -- > > 2.50.1 > > ^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v5] seunshare: fix the frail tmpdir cleanup 2025-10-07 14:09 ` Stephen Smalley @ 2025-10-07 16:59 ` Rahul Sandhu 0 siblings, 0 replies; 12+ messages in thread From: Rahul Sandhu @ 2025-10-07 16:59 UTC (permalink / raw) To: stephen.smalley.work; +Cc: jwcart2, nvraxn, selinux > Also, to be clear, "should be the only ones using the tmpdir above" is > not a safe assumption against the user spawning a detached process > that can try to create symlinks in parallel with the recursive > removal, but that should be mitigated by your rm_rf implementation > IIUC. Yes, sorry I forgot to update the commit message. Rahul ^ permalink raw reply [flat|nested] 12+ messages in thread
[parent not found: <CAEjxPJ4wxMi0kXc7wDi qwboAcz1Y0UvzDoZMZrpUgcNH_cNRg@mail.gmail.com>]
* Re: [PATCH v5] seunshare: fix the frail tmpdir cleanup [not found] <CAEjxPJ4wxMi0kXc7wDi qwboAcz1Y0UvzDoZMZrpUgcNH_cNRg@mail.gmail.com> @ 2025-10-07 17:02 ` Rahul Sandhu 0 siblings, 0 replies; 12+ messages in thread From: Rahul Sandhu @ 2025-10-07 17:02 UTC (permalink / raw) To: stephen.smalley.work; +Cc: jwcart2, nvraxn, selinux > With this patch applied, I see the following output when I use sandbox > -M and create anything in the tmpdir. > > $ sandbox -M bash > bash: cannot set terminal process group (-1): Inappropriate ioctl for device > bash: no job control in this shell > bash-5.2$ mkdir /tmp/foobar > bash-5.2$ exit > Failed to recursively remove directory /tmp/.sandbox-sdsmall-chjRXi Some idiot just spent a few hours trying to reproduce this and failing, because they changed the function signature to an int to a bool, and although they commited that, turns out that this was unstaged: - if (rm_rf(dirfd(dir), entry->d_name) != 0) { + if (!rm_rf(dirfd(dir), entry->d_name)) { ... I'm really sorry, new patch coming soon. Regards, Rahul ^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2025-10-07 17:02 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
[not found] <CAEjxPJ6QGZcAO zjdajOFTaDc tuh56Vao3niO37udEr=UfAgg@mail.gmail.com>
2025-08-01 18:21 ` [PATCH v3] seunshare: fix the frail tmpdir cleanup Rahul Sandhu
2025-08-01 18:29 ` Stephen Smalley
2025-08-14 17:47 ` James Carter
2025-09-25 5:38 ` [PATCH v4] " Rahul Sandhu
2025-10-06 15:57 ` Stephen Smalley
2025-10-06 16:31 ` Stephen Smalley
2025-10-06 18:40 ` Stephen Smalley
2025-10-07 9:29 ` [PATCH v5] " Rahul Sandhu
2025-10-07 12:46 ` Stephen Smalley
2025-10-07 14:09 ` Stephen Smalley
2025-10-07 16:59 ` Rahul Sandhu
[not found] <CAEjxPJ4wxMi0kXc7wDi qwboAcz1Y0UvzDoZMZrpUgcNH_cNRg@mail.gmail.com>
2025-10-07 17:02 ` Rahul Sandhu
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for NNTP newsgroup(s).