From: Timo Sirainen Date: Mon, 4 May 2026 13:10:13 +0000 (+0000) Subject: [PATCH 2/2] lib-sieve: storage: file - Validate script stat path through dir_fd X-Git-Tag: archive/raspbian/1%2.4.1+dfsg1-6+rpi1+deb13u7^2~94 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=f122d8933686e8477686d8b02e8d819eecfc9ee4;p=dovecot.git [PATCH 2/2] lib-sieve: storage: file - Validate script stat path through dir_fd Extend the symlink-escape protection added in the previous commit to the stat performed by sieve_file_script_open(): an "include :personal" lookup or any other indirect path that triggers sieve_file_script_stat() also needs to refuse a symlink whose target leaves the personal storage directory, otherwise the existence check succeeds and the file is opened later via the safe path with an unhelpful "permission denied". Add sieve_file_script_stat_safe(), which uses fstatat(AT_SYMLINK_NOFOLLOW) to obtain the entry's own stat (lnk_st) and then, only if the entry is a symlink, opens it through sieve_file_storage_open_safe() to validate the target stays inside dir_fd and to fetch the resolved target's stat (st) via fstat(). Non-symlink entries skip the open entirely. Use the new helper in sieve_file_script_open() whenever fstorage->dir_fd is available, falling back to the unsafe lstat+stat variant for non-personal or single-file storages. Gbp-Pq: Name 0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch --- diff --git a/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c b/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c index 52e752a..0136c3a 100644 --- a/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c +++ b/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c @@ -285,6 +285,57 @@ sieve_file_script_stat(const char *path, struct stat *st, struct stat *lnk_st) return 0; } +/* TOCTOU-safe variant of sieve_file_script_stat() for directory storages. + Resolves the script entry beneath fstorage->dir_fd, refusing symlinks + whose target escapes the storage directory. *st gets the (resolved) + target's stat; *lnk_st gets the directory entry's own stat (i.e. + AT_SYMLINK_NOFOLLOW), matching the semantics of the unsafe variant. */ +static int +sieve_file_script_stat_safe(struct sieve_file_storage *fstorage, + const char *filename, struct stat *st, + struct stat *lnk_st, const char **error_r) +{ + int fd, saved_errno; + + i_assert(fstorage->dir_fd >= 0); + + if (fstatat(fstorage->dir_fd, filename, lnk_st, + AT_SYMLINK_NOFOLLOW) < 0) { + *error_r = t_strdup_printf( + "fstatat(%s/%s) failed: %m", + fstorage->path, filename); + return -1; + } + + if (!S_ISLNK(lnk_st->st_mode)) { + *st = *lnk_st; + return 0; + } + + /* Symlink: open it via the safe walker (which refuses any target + that leaves fstorage->dir_fd) and read st from the resulting fd. + The fd is only used to fetch the resolved stat and is closed + immediately. Open with O_NONBLOCK so that a target which resolves + to a FIFO or other special file inside the storage directory only + yields its stat instead of blocking the delivery process. */ + if (sieve_file_storage_open_safe(fstorage, filename, + O_RDONLY | O_NONBLOCK, + &fd, error_r) < 0) + return -1; + + if (fstat(fd, st) < 0) { + saved_errno = errno; + *error_r = t_strdup_printf( + "fstat() failed for '%s/%s': %m", + fstorage->path, filename); + i_close_fd(&fd); + errno = saved_errno; + return -1; + } + i_close_fd(&fd); + return 0; +} + static const char * path_split_filename(const char *path, const char **dir_path_r) { @@ -353,7 +404,25 @@ static int sieve_file_script_open(struct sieve_script *script) dir_path = path; path = sieve_file_storage_path_extend(fstorage, filename); - ret = sieve_file_script_stat(path, &st, &lnk_st); + if (fstorage->dir_fd >= 0) { + const char *serror; + + ret = sieve_file_script_stat_safe( + fstorage, filename, &st, + &lnk_st, &serror); + if (ret < 0 && errno == ELOOP) { + sieve_script_set_critical( + script, + "Failed to open sieve script: %s", + serror); + script->storage->error_code = + SIEVE_ERROR_NO_PERMISSION; + success = FALSE; + } + } else { + ret = sieve_file_script_stat( + path, &st, &lnk_st); + } } } else { @@ -451,8 +520,11 @@ sieve_file_script_get_stream(struct sieve_script *script, /* For directory-based storage, open the script via the storage directory fd so that path resolution refuses to follow symlinks - whose (recursive) target leaves the storage directory. - Single-file storages have no dir_fd, so fall back to plain open(). */ + whose (recursive) target leaves the storage directory. The fd-based + walk also makes this TOCTOU-safe: even if intermediate path + components are mutated between the stat in sieve_file_script_open() + and this call, the resolution stays within the original directory. + Single-file storages have no dir_fd, so fall back to a plain open(). */ if (fstorage->dir_fd >= 0 && fscript->filename != NULL && *fscript->filename != '\0') { if (sieve_file_storage_open_safe(fstorage, fscript->filename,