From b47f60b3fa6e71d58f76825f256125b8dcdff562 Mon Sep 17 00:00:00 2001 From: Timo Sirainen Date: Wed, 3 Jun 2026 18:10:36 +0300 Subject: [PATCH] [PATCH 3/5] lib: Add t_openat_safe_dir() Gbp-Pq: Name 0003-lib-Add-t_openat_safe_dir.patch --- src/lib/path-util.c | 19 +++++++++++++++ src/lib/path-util.h | 7 ++++++ src/lib/test-path-util.c | 52 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/src/lib/path-util.c b/src/lib/path-util.c index 3b8c154..59ea492 100644 --- a/src/lib/path-util.c +++ b/src/lib/path-util.c @@ -528,6 +528,25 @@ int t_openat_safe(int base_fd, const char *path, int flags, return path_openat_safe_walk(base_fd, path, flags, error_r); } +int t_openat_safe_dir(const char *base_dir, const char *path, int flags, + const char **error_r) +{ + i_assert(base_dir != NULL); + i_assert(path != NULL); + i_assert(error_r != NULL); + + int dir_fd = open(base_dir, + O_DIRECTORY | O_RDONLY | O_CLOEXEC | + (flags & O_NOFOLLOW)); + if (dir_fd < 0) { + *error_r = t_strdup_printf("open(%s) failed: %m", base_dir); + return -1; + } + int fd = t_openat_safe(dir_fd, path, flags, error_r); + i_close_fd(&dir_fd); + return fd; +} + bool t_binary_abspath(const char **binpath, const char **error_r) { const char *path_env, *const *paths; diff --git a/src/lib/path-util.h b/src/lib/path-util.h index ec64ba6..ceeb483 100644 --- a/src/lib/path-util.h +++ b/src/lib/path-util.h @@ -86,6 +86,13 @@ int t_readlink(const char *path, const char **dest_r, const char **error_r); int t_openat_safe(int base_fd, const char *path, int flags, const char **error_r); +/* Convenience wrapper: opens base_dir as a directory fd, then delegates to + t_openat_safe(). If O_NOFOLLOW is present in flags it is also applied to + the open() of base_dir itself, refusing a symlink as its final component. + Returns the new fd on success, -1 on failure. */ +int t_openat_safe_dir(const char *base_dir, const char *path, int flags, + const char **error_r); + /* Update binpath to be absolute: * a) begins with '/' -> no change * b) contains '/' -> assume relative to working directory diff --git a/src/lib/test-path-util.c b/src/lib/test-path-util.c index 66c551a..3e96702 100644 --- a/src/lib/test-path-util.c +++ b/src/lib/test-path-util.c @@ -356,6 +356,57 @@ static void test_openat_safe(void) i_error("rmdir(%s) failed: %m", subdir); } +static void test_openat_safe_dir(void) +{ + const char *error; + int fd; + + /* Plain file: success. */ + const char *plain = t_strconcat(tmpdir, "/plain2", NULL); + int wfd = creat(plain, 0600); + if (wfd < 0) + i_fatal("creat(%s) failed: %m", plain); + i_close_fd(&wfd); + + fd = t_openat_safe_dir(tmpdir, "plain2", O_RDONLY, &error); + test_assert(fd >= 0); + i_close_fd(&fd); + + /* Nonexistent base_dir. */ + errno = 0; + fd = t_openat_safe_dir(t_strconcat(tmpdir, "/no-such-dir", NULL), + "plain2", O_RDONLY, &error); + test_assert(fd == -1); + test_assert(errno == ENOENT); + test_assert(error != NULL); + + /* Absolute symlink in relative part: refused. */ + const char *abs_link2 = t_strconcat(tmpdir, "/abs-link2", NULL); + if (symlink("/etc/hostname", abs_link2) < 0) + i_fatal("symlink failed: %m"); + errno = 0; + fd = t_openat_safe_dir(tmpdir, "abs-link2", O_RDONLY, &error); + test_assert(fd == -1); + test_assert(errno == ELOOP); + + /* O_NOFOLLOW: symlink as base_dir final component refused. */ + const char *dir_link = t_strconcat(tmpdir, "/dir-link", NULL); + if (symlink(tmpdir, dir_link) < 0) + i_fatal("symlink failed: %m"); + errno = 0; + fd = t_openat_safe_dir(dir_link, "plain2", + O_RDONLY | O_NOFOLLOW, &error); + test_assert(fd == -1); + /* Without O_NOFOLLOW the same symlinked base_dir succeeds. */ + fd = t_openat_safe_dir(dir_link, "plain2", O_RDONLY, &error); + test_assert(fd >= 0); + i_close_fd(&fd); + + i_unlink(plain); + i_unlink(abs_link2); + i_unlink(dir_link); +} + static void test_cleanup(void) { const char *error; @@ -396,6 +447,7 @@ void test_path_util(void) test_link_alloc(); test_link_alloc2(); test_openat_safe(); + test_openat_safe_dir(); test_cleanup(); alarm(0); test_end(); -- 2.39.5