Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 38 additions & 73 deletions syscall.c
Original file line number Diff line number Diff line change
Expand Up @@ -1677,34 +1677,8 @@ int do_open_nofollow(const char *pathname, int flags)
flag name, picked up by the same #ifdef;
flag value differs from FreeBSD)
Other systems fall back to the per-component O_NOFOLLOW walk below.

The relpath must also not contain any ../ elements in the path.
*/

/* Returns 1 if path has any "/"-separated component that is exactly
* "..", 0 otherwise. Used by secure_relative_open's front-door
* validation to reject inputs that the per-component walk fallback
* would otherwise resolve through ".." -- e.g. bare "..", "foo/..",
* "subdir/.." -- which RESOLVE_BENEATH-equivalent kernels reject in
* the kernel but the per-component fallback (NetBSD/OpenBSD/Solaris/
* Cygwin/pre-5.6 Linux) does not. */
static int path_has_dotdot_component(const char *path)
{
const char *p = path;

while (*p) {
const char *q;
if (*p == '/') { p++; continue; }
q = p;
while (*q && *q != '/')
q++;
if (q - p == 2 && p[0] == '.' && p[1] == '.')
return 1;
p = q;
}
return 0;
}

#if defined(__linux__) && defined(HAVE_OPENAT2)
static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode)
{
Expand Down Expand Up @@ -1787,37 +1761,23 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
errno = EINVAL;
return -1;
}
/* Reject any path with a literal ".." component (bare "..",
* "../foo", "foo/..", "foo/../bar", "subdir/.."). The previous
* substring-based check caught only "../" prefix and "/../"
* substring; bare ".." and trailing "/.." escape on the per-
* component walk fallback used by NetBSD/OpenBSD/Solaris/Cygwin
* and pre-5.6 Linux. RESOLVE_BENEATH on Linux/FreeBSD/macOS
* catches some of these in-kernel with EXDEV, but the front
* door must reject them consistently with EINVAL across all
* platforms so callers can rely on the validation. */
if (path_has_dotdot_component(relpath)) {
errno = EINVAL;
return -1;
}
if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {
errno = EINVAL;
return -1;
}

#if defined(__linux__) && defined(HAVE_OPENAT2)
{
/* openat2(2) can fail with -EAGAIN if the path contains a ".." component
* and there was a rename or mount on the system, so on busy machines it is
* necessary to retry this a few times. Based on experiments in libpathrs,
* ~256 iterations is enough to ensure that ~50k openat2(2) runs on a very
* rename-heavy system never fail. */
for (int tries = 0; tries < 256; tries++) {
int fd = secure_relative_open_linux(basedir, relpath, flags, mode);
/* ENOSYS = kernel < 5.6 doesn't have the syscall even though
* glibc/kernel-headers do; fall through to the portable path.
* (Built unconditionally unless --disable-openat2, which forces
* the portable resolver below so that tier is exercised.) */
if (fd != -1 || errno != ENOSYS)
if (fd != -1)
return fd;
if (errno == ENOSYS)
break; // fallback to portable path
if (errno != EAGAIN)
return -1;
}
#endif

#ifdef O_RESOLVE_BENEATH
#elif defined(O_RESOLVE_BENEATH)
return secure_relative_open_resolve_beneath(basedir, relpath, flags, mode);
#endif

Expand All @@ -1830,56 +1790,61 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
pathjoin(fullpath, sizeof fullpath, basedir, relpath);
return open(fullpath, flags, mode);
#else
int dirfd = AT_FDCWD;
int dirfd = AT_FDCWD, retfd = -1;
char *path_copy = NULL;
if (basedir != NULL) {
if (basedir[0] == '/') {
/* Absolute basedir: operator-trusted, plain openat. */
dirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY);
if (dirfd == -1) {
if (dirfd == -1)
return -1;
}
} else {
/* Relative basedir: walk it component-by-component
* with O_NOFOLLOW. This is the per-component
* RESOLVE_BENEATH equivalent for platforms without
* kernel-supported confinement, and matches the
* relpath walk below. Symlinks in basedir are
* rejected outright on this fallback path; the
* Linux openat2 / O_RESOLVE_BENEATH paths above
* still allow within-tree symlinks. */
/* Relative basedir: walk it component-by-component with
* O_NOFOLLOW. This is the per-component RESOLVE_BENEATH equivalent
* for platforms without kernel-supported confinement, and matches
* the relpath walk below. Symlinks and ".." components in basedir
* are rejected outright on this fallback path; the Linux openat2 /
* O_RESOLVE_BENEATH paths above still allow them as long as they
* stay within the tree.
* */
char *bcopy = my_strdup(basedir, __FILE__, __LINE__);
if (!bcopy)
return -1;
for (const char *part = strtok(bcopy, "/");
part != NULL;
part = strtok(NULL, "/"))
{
if (!strcmp(part, "..")) {
free(bcopy);
errno = EXDEV; // emulate RESOLVE_BENEATH
goto cleanup;
}
int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
if (next_fd == -1) {
int save_errno = errno;
if (dirfd != AT_FDCWD) close(dirfd);
int save_errno = errno; // free only saves errno on glibc >= 2.33
free(bcopy);
errno = save_errno;
return -1;
goto cleanup;
}
if (dirfd != AT_FDCWD) close(dirfd);
dirfd = next_fd;
}
free(bcopy);
}
}
int retfd = -1;

char *path_copy = my_strdup(relpath, __FILE__, __LINE__);
if (!path_copy) {
if (dirfd != AT_FDCWD) close(dirfd);
return -1;
}

path_copy = my_strdup(relpath, __FILE__, __LINE__);
if (!path_copy)
goto cleanup;

for (const char *part = strtok(path_copy, "/");
part != NULL;
part = strtok(NULL, "/"))
{
if (!strcmp(part, "..")) {
errno = EXDEV; // emulate RESOLVE_BENEATH
goto cleanup;
}
int next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
if (next_fd == -1 && errno == ENOTDIR) {
if (strtok(NULL, "/") != NULL) {
Expand Down
30 changes: 14 additions & 16 deletions t_chmod_secure.c
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,21 @@ static int errs = 0;
static int kernel_resolve_beneath_supported(void)
{
int fd;
#ifdef __linux__
{
struct open_how how;
memset(&how, 0, sizeof how);
how.flags = O_RDONLY | O_DIRECTORY;
how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
fd = syscall(SYS_openat2, AT_FDCWD, ".", &how, sizeof how);
if (fd >= 0) {
close(fd);
return 1;
}
/* ENOSYS = kernel < 5.6. Fall through to the O_RESOLVE_BENEATH
* probe in case we're a Linux build running on a kernel that
* gained O_RESOLVE_BENEATH via some out-of-tree backport. */
#if defined __linux__
struct open_how how;
memset(&how, 0, sizeof how);
how.flags = O_RDONLY | O_DIRECTORY;
how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
fd = syscall(SYS_openat2, AT_FDCWD, ".", &how, sizeof how);
if (fd >= 0) {
close(fd);
return 1;
}
#endif
#ifdef O_RESOLVE_BENEATH
/* O_RESOLVE_BENEATH is not defined on Linux, and even if it were, Linux's
* openat(2) does not return -EINVAL for unknown flag bits and so if
* O_RESOLVE_BENEATH happened to get defined somehow, the following
* fallback would always return success. */
#elif defined O_RESOLVE_BENEATH
fd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY | O_RESOLVE_BENEATH);
if (fd >= 0) {
close(fd);
Expand Down