From 1b406bb57217f7b5f7136f683e21e1d74dba7a83 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Tue, 23 Aug 2022 17:23:17 +1000 Subject: [PATCH 01/17] Multiple improvements to dsearch --- src/src/exim.h | 7 ++ src/src/lookups/dsearch.c | 191 +++++++++++++++++++++++++++++--------- 2 files changed, 154 insertions(+), 44 deletions(-) diff --git a/src/src/exim.h b/src/src/exim.h index ccf14f0fd0..be16e597b9 100644 --- a/src/src/exim.h +++ b/src/src/exim.h @@ -668,5 +668,12 @@ not get the TIME_WAIT */ # define SERVERSIDE_CLOSE_NOWAIT #endif +/* Stand-in for an unused function parameter */ +#ifdef __GNUC__ +#define UNUSED(NAME) NAME __attribute__((unused)) +#else +#define UNUSED(NAME) NAME +#endif + #endif /* End of exim.h */ diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index 74439bfc87..e655977c1c 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -15,6 +15,20 @@ lstat()) rather than a directory scan). */ #include "../exim.h" #include "lf_functions.h" +#if !defined USE_AT_FILE \ + && !defined NO_AT_FILE \ + && ( defined O_PATH && defined O_DIRECTORY ) \ + && ( _XOPEN_SOURCE >= 700 || _POSIX_C_SOURCE >= 200809L \ + || defined _ATFILE_SOURCE ) +#define USE_AT_FILE +#endif + +#ifdef USE_AT_FILE +/* Have fstatat() */ +typedef struct { + int dir_fd; +} ds_handle; +#endif /************************************************* @@ -29,17 +43,37 @@ actually scanning through the list of files. */ static void * dsearch_open(const uschar * dirname, uschar ** errmsg) { -DIR * dp = exim_opendir(dirname); -if (!dp) +if (is_tainted(dirname)) + { + log_write(0, LOG_MAIN|LOG_PANIC, "Tainted dirname '%s'", dirname); + errno = EACCES; + *errmsg = string_open_failed("%s for directory search", dirname); + return NULL; + } +#ifdef USE_AT_FILE +int dir_fd = open(dirname, O_PATH|O_DIRECTORY); +if (dir_fd<0) { *errmsg = string_open_failed("%s for directory search", dirname); return NULL; } -closedir(dp); -return (void *)(-1); +ds_handle *h = malloc(sizeof (ds_handle)); +h->dir_fd = dir_fd; +return h; +#else +if (f.running_in_test_harness) + /* Dereferencing (void*)(-1) will intentionally cause an immediate abort on + * most modern architectures. However we only use this return statement + * during regression testing, as it has "undefined behaviour" according to + * ISO-9899, and in theory could misbehave on exotic architectures even + * during normal operation; in particular (void*)(-1)==NULL is allowable. + */ + return (void*)(-1); +static char ignored_handle[1]; +return ignored_handle; +#endif } - /************************************************* * Check entry point * *************************************************/ @@ -48,10 +82,9 @@ return (void *)(-1); integer as this gives warnings on 64-bit systems. */ static BOOL -dsearch_check(void * handle, const uschar * filename, int modemask, +dsearch_check(void * UNUSED(handle), const uschar * filename, int modemask, uid_t * owners, gid_t * owngroups, uschar ** errmsg) { -handle = handle; if (*filename == '/') return lf_check_file(-1, filename, S_IFDIR, modemask, owners, owngroups, "dsearch", errmsg) == 0; @@ -64,26 +97,36 @@ return FALSE; * Find entry point * *************************************************/ -#define RET_FULL BIT(0) -#define FILTER_TYPE BIT(1) -#define FILTER_ALL BIT(1) -#define FILTER_FILE BIT(2) -#define FILTER_DIR BIT(3) -#define FILTER_SUBDIR BIT(4) +#define FILTER_BY(TYPE) BIT( S_I##TYPE / (S_IFMT&-S_IFMT) ) /* X&-X gives the bottom bit of X */ -/* See local README for interface description. We use lstat() instead of -scanning the directory, as it is hopefully faster to let the OS do the scanning -for us. */ +/* See local README for interface description. We use lstat() or fstatat() +instead of reading the directory, as it is hopefully faster to let the OS do +the scanning for us. */ static int -dsearch_find(void * handle, const uschar * dirname, const uschar * keystring, - int length, uschar ** result, uschar ** errmsg, uint * do_cache, - const uschar * opts) +dsearch_find( + #ifdef USE_AT_FILE + void * handle, + #else + void * UNUSED(handle), + #endif + const uschar * dirname, const uschar * keystring, int length, + uschar ** result, uschar ** errmsg, uint * do_cache, const uschar * opts) { struct stat statbuf; int save_errno; -uschar * filename; -unsigned flags = 0; +unsigned filter_by_type = 0; +int exclude_dotdotdot = 0; +int ret_full = 0; +int follow_symlink = 0; +int empty_key = 0; +#ifdef USE_AT_FILE +ds_handle *h = handle; +int statat_flags = 0; +#else +const uschar *filename; +#endif +int stat_result; if (Ustrchr(keystring, '/') != 0) { @@ -99,40 +142,91 @@ if (opts) while ((ele = string_nextinlist(&opts, &sep, NULL, 0))) if (Ustrcmp(ele, "ret=full") == 0) - flags |= RET_FULL; + ret_full = 1; else if (Ustrncmp(ele, "filter=", 7) == 0) { ele += 7; - if (Ustrcmp(ele, "file") == 0) - flags |= FILTER_TYPE | FILTER_FILE; - else if (Ustrcmp(ele, "dir") == 0) - flags |= FILTER_TYPE | FILTER_DIR; - else if (Ustrcmp(ele, "subdir") == 0) - flags |= FILTER_TYPE | FILTER_SUBDIR; /* like dir but not "." or ".." */ + if (Ustrcmp(ele, "file") == 0) filter_by_type |= FILTER_BY(FREG); + else if (Ustrcmp(ele, "dir") == 0) filter_by_type |= FILTER_BY(FDIR); + else if (Ustrcmp(ele, "subdir") == 0) filter_by_type |= FILTER_BY(FDIR), exclude_dotdotdot = 1; /* dir but not "." or ".." */ + else if (Ustrcmp(ele, "pipe") == 0 + || Ustrcmp(ele, "fifo") == 0) filter_by_type |= FILTER_BY(FIFO); + else if (Ustrcmp(ele, "socket") == 0) filter_by_type |= FILTER_BY(FSOCK); + else if (Ustrcmp(ele, "link") == 0 + || Ustrcmp(ele, "symlink") == 0) filter_by_type |= FILTER_BY(FLNK); + else if (Ustrcmp(ele, "bdev") == 0) filter_by_type |= FILTER_BY(FBLK); + else if (Ustrcmp(ele, "cdev") == 0) filter_by_type |= FILTER_BY(FCHR); + else + { + *errmsg = string_sprintf("unknown filter option for dsearch lookup: %s", ele); + return DEFER; + } + } + else if (Ustrcmp(ele, "follow") == 0) + follow_symlink = 1; + else if (Ustrcmp(ele, "checkpath") == 0) + empty_key = follow_symlink = ret_full = 1; + else if (Ustrcmp(ele, "emptykey") == 0) + empty_key = 1; + else + { + *errmsg = string_sprintf("unknown option for dsearch lookup: %s", ele); + return DEFER; } } -filename = string_sprintf("%s/%s", dirname, keystring); -if ( Ulstat(filename, &statbuf) >= 0 - && ( !(flags & FILTER_TYPE) - || (flags & FILTER_FILE && S_ISREG(statbuf.st_mode)) - || ( flags & (FILTER_DIR | FILTER_SUBDIR) - && S_ISDIR(statbuf.st_mode) - && ( flags & FILTER_DIR - || keystring[0] != '.' - || keystring[1] && keystring[1] != '.' - ) ) ) ) +/* exclude "." and ".." when {filter=subdir} included */ +if (exclude_dotdotdot + && keystring[0] == '.' + && (keystring[1] == 0 + || keystring[1] == '.' && keystring[2] == 0)) + return FAIL; + +if (empty_key) + { + if (keystring && keystring[0] != '\0') + { + *errmsg = US "non-empty key for dsearch pathcheck"; + return DEFER; + } + keystring = ""; + } +else if (keystring[0] == 0) /* in case lstat treats "/dir/" the same as "/dir/." */ + return FAIL; + +#ifdef USE_AT_FILE +if (!follow_symlink) statat_flags |= AT_SYMLINK_NOFOLLOW; +if (empty_key) statat_flags |= AT_EMPTY_PATH; +stat_result = fstatat(h->dir_fd, CCS keystring, &statbuf, statat_flags); +#else +filename = empty_key ? dirname + : string_sprintf("%s/%s", dirname, keystring); +if (follow_symlink) + stat_result = Ustat(filename, &statbuf); +else + stat_result = Ulstat(filename, &statbuf); +#endif +if (stat_result >= 0 + && ( !filter_by_type + || filter_by_type & BIT((statbuf.st_mode & S_IFMT) / (S_IFMT&-S_IFMT)))) { + if (ret_full) + #ifdef USE_AT_FILE + keystring = string_sprintf("%s/%s", dirname, keystring); + #else + keystring = filename; + #endif + /* Since the filename exists in the filesystem, we can return a non-tainted result. */ - *result = string_copy_taint(flags & RET_FULL ? filename : keystring, GET_UNTAINTED); + *result = string_copy_taint(keystring, GET_UNTAINTED); return OK; } if (errno == ENOENT || errno == 0) return FAIL; save_errno = errno; -*errmsg = string_sprintf("%s: lstat: %s", filename, strerror(errno)); +*errmsg = string_sprintf("%s/%s: lstat: %s", dirname, keystring, strerror(errno)); errno = save_errno; return DEFER; } @@ -144,11 +238,20 @@ return DEFER; /* See local README for interface description */ -void -static dsearch_close(void *handle) +#ifdef USE_AT_FILE +static void +dsearch_close(void *handle) { -handle = handle; /* Avoid compiler warning */ +ds_handle *h = handle; +close(h->dir_fd); /* ignore error */ +free(h); } +#else +static void +dsearch_close(void * UNUSED(handle)) +{ +} +#endif /************************************************* @@ -178,7 +281,7 @@ static lookup_info _lookup_info = { .close = dsearch_close, /* close function */ .tidy = NULL, /* no tidy function */ .quote = NULL, /* no quoting function */ - .version_report = dsearch_version_report /* version reporting */ + .version_report = dsearch_version_report /* version reporting */ }; #ifdef DYNLOOKUP From d6eb4f8bb78c4e739ec6f9dda254399e63783f55 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Tue, 30 Aug 2022 11:25:52 +1000 Subject: [PATCH 02/17] =?UTF-8?q?Rename=20emptykey=E2=86=92ignorekey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/src/lookups/dsearch.c | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index e655977c1c..09de28cdde 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -119,7 +119,7 @@ unsigned filter_by_type = 0; int exclude_dotdotdot = 0; int ret_full = 0; int follow_symlink = 0; -int empty_key = 0; +int ignore_key = 0; #ifdef USE_AT_FILE ds_handle *h = handle; int statat_flags = 0; @@ -165,9 +165,9 @@ if (opts) else if (Ustrcmp(ele, "follow") == 0) follow_symlink = 1; else if (Ustrcmp(ele, "checkpath") == 0) - empty_key = follow_symlink = ret_full = 1; - else if (Ustrcmp(ele, "emptykey") == 0) - empty_key = 1; + ignore_key = follow_symlink = ret_full = 1; + else if (Ustrcmp(ele, "ignorekey") == 0) + ignore_key = 1; else { *errmsg = string_sprintf("unknown option for dsearch lookup: %s", ele); @@ -175,6 +175,11 @@ if (opts) } } +if (ignore_key) + keystring = ""; +else if (keystring == NULL || keystring[0] == 0) /* in case lstat treats "/dir/" the same as "/dir/." */ + return FAIL; + /* exclude "." and ".." when {filter=subdir} included */ if (exclude_dotdotdot && keystring[0] == '.' @@ -182,25 +187,13 @@ if (exclude_dotdotdot || keystring[1] == '.' && keystring[2] == 0)) return FAIL; -if (empty_key) - { - if (keystring && keystring[0] != '\0') - { - *errmsg = US "non-empty key for dsearch pathcheck"; - return DEFER; - } - keystring = ""; - } -else if (keystring[0] == 0) /* in case lstat treats "/dir/" the same as "/dir/." */ - return FAIL; - #ifdef USE_AT_FILE if (!follow_symlink) statat_flags |= AT_SYMLINK_NOFOLLOW; -if (empty_key) statat_flags |= AT_EMPTY_PATH; +if (ignore_key) statat_flags |= AT_EMPTY_PATH; stat_result = fstatat(h->dir_fd, CCS keystring, &statbuf, statat_flags); #else -filename = empty_key ? dirname - : string_sprintf("%s/%s", dirname, keystring); +filename = ignore_key ? dirname + : string_sprintf("%s/%s", dirname, keystring); if (follow_symlink) stat_result = Ustat(filename, &statbuf); else From f5a8e8a1388828a17b907b6f7d8e313e9c4c139f Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Sat, 27 Aug 2022 20:42:51 +1000 Subject: [PATCH 03/17] Add ERRNO_MISMATCH --- src/src/macros.h | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/src/macros.h b/src/src/macros.h index c55276332f..886c5c16b6 100644 --- a/src/src/macros.h +++ b/src/src/macros.h @@ -561,19 +561,20 @@ table exim_errstrings[] in log.c */ #define ERRNO_UTF8_FWD (-49) /* target not supporting SMTPUTF8 */ #define ERRNO_HOST_IS_LOCAL (-50) /* Transport refuses to talk to localhost */ #define ERRNO_TAINT (-51) /* Transport refuses to talk use tainted filename */ +#define ERRNO_MISMATCH (-52) /* Filename or Object not of requested type */ /* These must be last, so all retry deferments can easily be identified */ -#define ERRNO_RETRY_BASE (-52) /* Base to test against */ -#define ERRNO_RRETRY (-52) /* Not time for routing */ +#define ERRNO_RETRY_BASE (-53) /* Base to test against */ +#define ERRNO_RRETRY (-53) /* Not time for routing */ -#define ERRNO_WARN_BASE (-53) /* Base to test against */ -#define ERRNO_LRETRY (-53) /* Not time for local delivery */ -#define ERRNO_HRETRY (-54) /* Not time for any remote host */ -#define ERRNO_LOCAL_ONLY (-55) /* Local-only delivery */ -#define ERRNO_QUEUE_DOMAIN (-56) /* Domain in queue_domains */ -#define ERRNO_TRETRY (-57) /* Transport concurrency limit */ -#define ERRNO_EVENT (-58) /* Event processing request alternate response */ +#define ERRNO_WARN_BASE (-54) /* Base to test against */ +#define ERRNO_LRETRY (-54) /* Not time for local delivery */ +#define ERRNO_HRETRY (-55) /* Not time for any remote host */ +#define ERRNO_LOCAL_ONLY (-56) /* Local-only delivery */ +#define ERRNO_QUEUE_DOMAIN (-57) /* Domain in queue_domains */ +#define ERRNO_TRETRY (-58) /* Transport concurrency limit */ +#define ERRNO_EVENT (-59) /* Event processing request alternate response */ From 033542378a95fd4cbbb57c322fb0258dd36527b4 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Mon, 29 Aug 2022 19:48:20 +1000 Subject: [PATCH 04/17] Add ret=dir --- src/src/lookups/dsearch.c | 69 ++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index 09de28cdde..c984060c66 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -117,15 +117,18 @@ struct stat statbuf; int save_errno; unsigned filter_by_type = 0; int exclude_dotdotdot = 0; -int ret_full = 0; +enum { + RET_KEY, /* return the key */ + RET_DIR, /* return the dir without the key */ + RET_FULL /* return the path comprising both combined */ +} ret_mode = RET_KEY; int follow_symlink = 0; int ignore_key = 0; #ifdef USE_AT_FILE ds_handle *h = handle; int statat_flags = 0; -#else -const uschar *filename; #endif +const uschar *full_path; int stat_result; if (Ustrchr(keystring, '/') != 0) @@ -142,7 +145,7 @@ if (opts) while ((ele = string_nextinlist(&opts, &sep, NULL, 0))) if (Ustrcmp(ele, "ret=full") == 0) - ret_full = 1; + ret_mode = RET_FULL; else if (Ustrncmp(ele, "filter=", 7) == 0) { ele += 7; @@ -165,7 +168,10 @@ if (opts) else if (Ustrcmp(ele, "follow") == 0) follow_symlink = 1; else if (Ustrcmp(ele, "checkpath") == 0) - ignore_key = follow_symlink = ret_full = 1; + { + ignore_key = follow_symlink = 1; + ret_mode = RET_DIR; + } else if (Ustrcmp(ele, "ignorekey") == 0) ignore_key = 1; else @@ -192,28 +198,45 @@ if (!follow_symlink) statat_flags |= AT_SYMLINK_NOFOLLOW; if (ignore_key) statat_flags |= AT_EMPTY_PATH; stat_result = fstatat(h->dir_fd, CCS keystring, &statbuf, statat_flags); #else -filename = ignore_key ? dirname - : string_sprintf("%s/%s", dirname, keystring); +full_path = ignore_key ? dirname + : string_sprintf("%s/%s", dirname, keystring); if (follow_symlink) - stat_result = Ustat(filename, &statbuf); + stat_result = Ustat(full_path, &statbuf); else - stat_result = Ulstat(filename, &statbuf); + stat_result = Ulstat(full_path, &statbuf); #endif -if (stat_result >= 0 - && ( !filter_by_type - || filter_by_type & BIT((statbuf.st_mode & S_IFMT) / (S_IFMT&-S_IFMT)))) +if (stat_result >= 0) { - if (ret_full) - #ifdef USE_AT_FILE - keystring = string_sprintf("%s/%s", dirname, keystring); - #else - keystring = filename; - #endif - - /* Since the filename exists in the filesystem, we can return a - non-tainted result. */ - *result = string_copy_taint(keystring, GET_UNTAINTED); - return OK; + if (!filter_by_type + || filter_by_type & BIT((statbuf.st_mode & S_IFMT) / (S_IFMT&-S_IFMT))) + { + switch (ret_mode) + { + default: + case RET_KEY: + full_path = keystring; + break; + case RET_DIR: + full_path = dirname; + break; + case RET_FULL: + #ifdef USE_AT_FILE + full_path = string_sprintf("%s/%s", dirname, keystring); + #else + full_path = full_path; + #endif + break; + } + + /* Since the filename exists in the filesystem, we can return a + non-tainted result. */ + *result = string_copy_taint(full_path, GET_UNTAINTED); + return OK; + } + *errmsg = string_sprintf("%s/%s is of unexpected type %s", + dirname, keystring, S_IFMT_to_long_name(statbuf.st_mode)); + errno = ERRNO_MISMATCH; + return DEFER; } if (errno == ENOENT || errno == 0) return FAIL; From c53174f329859fad4a32c19a038b422b664a6bdc Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Sat, 27 Aug 2022 20:42:51 +1000 Subject: [PATCH 05/17] =?UTF-8?q?Move=20file-type=20naming=20into=20lf=5Ff?= =?UTF-8?q?unctions.h;=20filename=E2=86=92dirname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/src/log.c | 1 + src/src/lookups/dsearch.c | 65 +++++++++------ src/src/lookups/lf_check_file.c | 137 ++++++++++++++++++++++++++++++-- src/src/lookups/lf_functions.h | 44 +++++++++- 4 files changed, 215 insertions(+), 32 deletions(-) diff --git a/src/src/log.c b/src/src/log.c index 08ece6158e..fcd0ffed23 100644 --- a/src/src/log.c +++ b/src/src/log.c @@ -106,6 +106,7 @@ static const uschar * exim_errstrings[] = { [- ERRNO_UTF8_FWD] = US"target not supporting SMTPUTF8", [- ERRNO_HOST_IS_LOCAL] = US"host is local", [- ERRNO_TAINT] = US"tainted filename", + [- ERRNO_MISMATCH] = US"not requested type", [- ERRNO_RRETRY] = US"Not time for routing", diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index c984060c66..c587d45af7 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -78,27 +78,33 @@ return ignored_handle; * Check entry point * *************************************************/ -/* The handle will always be (void *)(-1), but don't try casting it to an -integer as this gives warnings on 64-bit systems. */ - +#ifdef USE_AT_FILE static BOOL -dsearch_check(void * UNUSED(handle), const uschar * filename, int modemask, +dsearch_check(void * handle, const uschar * UNUSED(dirname), int modemask, uid_t * owners, gid_t * owngroups, uschar ** errmsg) { -if (*filename == '/') - return lf_check_file(-1, filename, S_IFDIR, modemask, owners, owngroups, +ds_handle *h = handle; +return lf_check_file(h->dir_fd, NULL, S_IFDIR, modemask, owners, owngroups, + "dsearch", errmsg) == 0; +} +#else +static BOOL +dsearch_check(void * UNUSED(handle), const uschar * dirname, int modemask, + uid_t * owners, gid_t * owngroups, uschar ** errmsg) +{ +if (*dirname == '/') + return lf_check_file(-1, dirname, S_IFDIR, modemask, owners, owngroups, "dsearch", errmsg) == 0; -*errmsg = string_sprintf("dirname '%s' for dsearch is not absolute", filename); +*errmsg = string_sprintf("dirname '%s' for dsearch is not absolute", dirname); return FALSE; } +#endif /************************************************* * Find entry point * *************************************************/ -#define FILTER_BY(TYPE) BIT( S_I##TYPE / (S_IFMT&-S_IFMT) ) /* X&-X gives the bottom bit of X */ - /* See local README for interface description. We use lstat() or fstatat() instead of reading the directory, as it is hopefully faster to let the OS do the scanning for us. */ @@ -144,24 +150,34 @@ if (opts) uschar * ele; while ((ele = string_nextinlist(&opts, &sep, NULL, 0))) - if (Ustrcmp(ele, "ret=full") == 0) - ret_mode = RET_FULL; + if (Ustrncmp(ele, "ret=", 4) == 0) + { + ele += 4; + if (Ustrcmp(ele, "full") == 0) + ret_mode = RET_FULL; + else if (Ustrcmp(ele, "dir") == 0) + ret_mode = RET_DIR; + #if 0 + /* XXX ret=key is excluded from opts by special-case code in by search_find() */ + else if (Ustrcmp(ele, "key") == 0) + ret_mode = RET_KEY; + #endif + else + { + *errmsg = string_sprintf("unknown parameter for dsearch lookup: %s", ele-=4); + return DEFER; + } + } else if (Ustrncmp(ele, "filter=", 7) == 0) { ele += 7; - if (Ustrcmp(ele, "file") == 0) filter_by_type |= FILTER_BY(FREG); - else if (Ustrcmp(ele, "dir") == 0) filter_by_type |= FILTER_BY(FDIR); - else if (Ustrcmp(ele, "subdir") == 0) filter_by_type |= FILTER_BY(FDIR), exclude_dotdotdot = 1; /* dir but not "." or ".." */ - else if (Ustrcmp(ele, "pipe") == 0 - || Ustrcmp(ele, "fifo") == 0) filter_by_type |= FILTER_BY(FIFO); - else if (Ustrcmp(ele, "socket") == 0) filter_by_type |= FILTER_BY(FSOCK); - else if (Ustrcmp(ele, "link") == 0 - || Ustrcmp(ele, "symlink") == 0) filter_by_type |= FILTER_BY(FLNK); - else if (Ustrcmp(ele, "bdev") == 0) filter_by_type |= FILTER_BY(FBLK); - else if (Ustrcmp(ele, "cdev") == 0) filter_by_type |= FILTER_BY(FCHR); + int i = S_IFMTix_from_name(ele); + if (i>=0) + filter_by_type |= BIT(i); + else if (Ustrcmp(ele, "subdir") == 0) filter_by_type |= BIT(S_IFMT_to_index(S_IFDIR)), exclude_dotdotdot = 1; /* dir but not "." or ".." */ else { - *errmsg = string_sprintf("unknown filter option for dsearch lookup: %s", ele); + *errmsg = string_sprintf("unknown parameter for dsearch lookup: %s", ele-=7); return DEFER; } } @@ -169,7 +185,8 @@ if (opts) follow_symlink = 1; else if (Ustrcmp(ele, "checkpath") == 0) { - ignore_key = follow_symlink = 1; + /* shortcut for follow,ret=dir */ + follow_symlink = 1; ret_mode = RET_DIR; } else if (Ustrcmp(ele, "ignorekey") == 0) @@ -208,7 +225,7 @@ else if (stat_result >= 0) { if (!filter_by_type - || filter_by_type & BIT((statbuf.st_mode & S_IFMT) / (S_IFMT&-S_IFMT))) + || filter_by_type & BIT(S_IFMT_to_index(statbuf.st_mode))) { switch (ret_mode) { diff --git a/src/src/lookups/lf_check_file.c b/src/src/lookups/lf_check_file.c index c4c05e44de..3c43768884 100644 --- a/src/src/lookups/lf_check_file.c +++ b/src/src/lookups/lf_check_file.c @@ -11,7 +11,127 @@ #include "../exim.h" #include "lf_functions.h" +const uschar * S_IF_longnames[] = { + #ifdef S_IFBLK + [S_IFMT_to_index(S_IFBLK)] = CUS "block device", + #endif + #ifdef S_IFCHR + [S_IFMT_to_index(S_IFCHR)] = CUS "serial device", + #endif + #ifdef S_IFLNK + [S_IFMT_to_index(S_IFLNK)] = CUS "symbolic link", + #endif + #ifdef S_IFIFO + [S_IFMT_to_index(S_IFIFO)] = CUS "named pipe", + #endif + #ifdef S_IFSOCK + [S_IFMT_to_index(S_IFSOCK)] = CUS "local socket", + #endif + [S_IFMT_to_index(S_IFDIR)] = CUS "directory", + [S_IFMT_to_index(S_IFREG)] = CUS "regular file" +}; +const uschar * S_IF_names[] = { + #ifdef S_IFBLK + [S_IFMT_to_index(S_IFBLK)] = CUS "bdev", + #endif + #ifdef S_IFCHR + [S_IFMT_to_index(S_IFCHR)] = CUS "cdev", + #endif + #ifdef S_IFLNK + [S_IFMT_to_index(S_IFLNK)] = CUS "link", + #endif + #ifdef S_IFIFO + [S_IFMT_to_index(S_IFIFO)] = CUS "fifo", + #endif + #ifdef S_IFSOCK + [S_IFMT_to_index(S_IFSOCK)] = CUS "sock", + #endif + [S_IFMT_to_index(S_IFDIR)] = CUS "dir", + [S_IFMT_to_index(S_IFREG)] = CUS "file" +}; +const uschar * S_IF_ucnames[] = { + #ifdef S_IFBLK + [S_IFMT_to_index(S_IFBLK)] = CUS "BDEV", + #endif + #ifdef S_IFCHR + [S_IFMT_to_index(S_IFCHR)] = CUS "CDEV", + #endif + #ifdef S_IFLNK + [S_IFMT_to_index(S_IFLNK)] = CUS "LINK", + #endif + #ifdef S_IFIFO + [S_IFMT_to_index(S_IFIFO)] = CUS "FIFO", + #endif + #ifdef S_IFSOCK + [S_IFMT_to_index(S_IFSOCK)] = CUS "SOCK", + #endif + [S_IFMT_to_index(S_IFDIR)] = CUS "DIR", + [S_IFMT_to_index(S_IFREG)] = CUS "FILE" +}; +const size_t num_S_IF_names = nelem(S_IF_names); + +static const struct { + const uschar *name; + int index; +} ni_map[] = { + /* sorted in descending order of likelihood */ + { CUS "file", S_IFMT_to_index(S_IFREG) }, + { CUS "dir", S_IFMT_to_index(S_IFDIR) }, + #ifdef S_IFLNK + { CUS "symlink", S_IFMT_to_index(S_IFLNK) }, + { CUS "link", S_IFMT_to_index(S_IFLNK) }, + #endif + #ifdef S_IFIFO + { CUS "fifo", S_IFMT_to_index(S_IFIFO) }, + { CUS "pipe", S_IFMT_to_index(S_IFIFO) }, + #endif + #ifdef S_IFSOCK + { CUS "socket", S_IFMT_to_index(S_IFSOCK) }, + { CUS "sock", S_IFMT_to_index(S_IFSOCK) }, + #endif + #ifdef S_IFCHR + { CUS "cdev", S_IFMT_to_index(S_IFCHR) }, + { CUS "tty", S_IFMT_to_index(S_IFCHR) }, + #endif + #ifdef S_IFBLK + { CUS "bdev", S_IFMT_to_index(S_IFBLK) }, + #endif + { CUS "reg", S_IFMT_to_index(S_IFREG) } +}; +static const size_t num_ni_map = nelem(ni_map); +int +S_IFMTix_from_name(const uschar *name) +{ +for (int i=0 ; i < num_ni_map ; ++i) + if (Ustrcmp(ni_map[i].name, name) == 0) + return ni_map[i].index; +return -1; +} + +const uschar * +S_IFMTix_to_long_name(int index) +{ +if (index < 0 || index >= num_S_IF_names) + return NULL; /* invalid file type */ +return S_IF_longnames[index]; +} + +const uschar * +S_IFMTix_to_name(int index) +{ +if (index < 0 || index >= num_S_IF_names) + return NULL; /* invalid file type */ +return S_IF_names[index]; +} + +const uschar * +S_IFMTix_to_ucname(int index) +{ +if (index < 0 || index >= num_S_IF_names) + return NULL; /* invalid file type */ +return S_IF_ucnames[index]; +} /************************************************* * Check a file's credentials * @@ -55,17 +175,20 @@ if ((fd < 0 ? Ustat(filename, &statbuf) : fstat(fd, &statbuf)) != 0) if ((statbuf.st_mode & S_IFMT) != s_type) { - if (s_type == S_IFREG) + const uschar *t = S_IFMT_to_long_name(s_type); + if (t) { - *errmsg = string_sprintf("%s is not a regular file (%s lookup)", - filename, type); - errno = ERRNO_NOTREGULAR; + *errmsg = string_sprintf("%s is not a %s (%s lookup)", + filename, t, type); + errno = s_type == S_IFDIR ? ERRNO_NOTDIRECTORY : + /* s_type != S_IFREG ? ERRNO_MISMATCH : */ /* TODO - decide whether to use this? */ + ERRNO_NOTREGULAR; } else { - *errmsg = string_sprintf("%s is not a directory (%s lookup)", - filename, type); - errno = ERRNO_NOTDIRECTORY; + *errmsg = string_sprintf("%s is not a type#%d name (%s lookup)", + filename, S_IFMT_to_index(s_type), type); + errno = ERRNO_BADMODE; } return +1; } diff --git a/src/src/lookups/lf_functions.h b/src/src/lookups/lf_functions.h index b7acbb5a90..99c0aa9a1d 100644 --- a/src/src/lookups/lf_functions.h +++ b/src/src/lookups/lf_functions.h @@ -3,7 +3,7 @@ *************************************************/ /* Copyright (c) University of Cambridge 1995 - 2018 */ -/* Copyright (c) The Exim Maintainers 2020 */ +/* Copyright (c) The Exim Maintainers 2022 */ /* See the file NOTICE for conditions of use and distribution. */ /* SPDX-License-Identifier: GPL-2.0-or-later */ @@ -18,4 +18,46 @@ extern int lf_sqlperform(const uschar *, const uschar *, const uschar *, int(*)(const uschar *, uschar *, uschar **, uschar **, BOOL *, uint *, const uschar *)); +/* + * The st_mode field returned by stat() indicates the type of a file; these + * functions help to interpret that. + * + * Usage: + * struct stat s; + * const uschar *t; + * stat(&s, filename); + * if ((t = S_IFMT_to_long_name(s.st_mode))) + * printf("%s is a %s\n", filename, t); + * else + * printf("%s is unknown\n", filename); + * if ( allowed_filetype_mask & BIT(S_IFMT_to_index(s.st_mode)) ) + * printf("\tALLOWED\n"); + * else + * printf("\tFORBIDDEN\n"); + * + * S_IFMT_to_index shifts this to remove bits that are not part of S_IFMT, + * making a "small" number suitable for an array index; S_IFMT_from_index does + * the reverse. These indeces can be used with the S_IFMTix_* variants; the + * others use the mode masked with S_IFMT directly. + * + * The _ucname and _name versions are the same, differing only in case; the + * _long_name versions provide human-readable forms suitable for logging. + * + * Macros for use in static initialisers; take care to avoid side effects in + * parameters + */ +#define S_IFMT_scale (S_IFMT & -S_IFMT) /* X&-X comprises the least significant 1-bit of X, all other bits 0 */ +#define S_IFMT_to_index(S) ( (S)<0 ? -1 : (S_IFMT & (S)) / S_IFMT_scale ) +#define S_IFMT_from_index(I) ( (I)<0 ? -1 : S_IFMT & (I) * S_IFMT_scale ) + +extern const uschar *S_IFMTix_to_name(int index); /* NULL on error */ +extern const uschar *S_IFMTix_to_long_name(int index); /* NULL on error */ +extern const uschar *S_IFMTix_to_ucname(int index); /* NULL on error */ +extern int S_IFMTix_from_name(const uschar *name); /* negative on error */ + +static inline const uschar *S_IFMT_to_name(int index) { return S_IFMTix_to_name( S_IFMT_to_index(index)); } /* NULL on error */ +static inline const uschar *S_IFMT_to_long_name(int index) { return S_IFMTix_to_long_name(S_IFMT_to_index(index)); } /* NULL on error */ +static inline const uschar *S_IFMT_to_ucname(int index) { return S_IFMTix_to_ucname( S_IFMT_to_index(index)); } /* NULL on error */ +static inline int S_IFMT_from_name(const uschar *name) { return S_IFMT_from_index( S_IFMTix_from_name(name)); } /* negative on error */ + /* End of lf_functions.h */ From a2c12c7badb13cd98fdb2b46548a7cc0332987e0 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Sat, 27 Aug 2022 13:33:59 +1000 Subject: [PATCH 06/17] Do abs path check before taint check --- src/src/lookups/dsearch.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index c587d45af7..6e09858d9e 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -43,6 +43,11 @@ actually scanning through the list of files. */ static void * dsearch_open(const uschar * dirname, uschar ** errmsg) { +if (*dirname != '/') + { + *errmsg = string_sprintf("dirname '%s' for dsearch is not absolute", dirname); + return NULL; + } if (is_tainted(dirname)) { log_write(0, LOG_MAIN|LOG_PANIC, "Tainted dirname '%s'", dirname); @@ -92,11 +97,8 @@ static BOOL dsearch_check(void * UNUSED(handle), const uschar * dirname, int modemask, uid_t * owners, gid_t * owngroups, uschar ** errmsg) { -if (*dirname == '/') - return lf_check_file(-1, dirname, S_IFDIR, modemask, owners, owngroups, - "dsearch", errmsg) == 0; -*errmsg = string_sprintf("dirname '%s' for dsearch is not absolute", dirname); -return FALSE; +return lf_check_file(-1, dirname, S_IFDIR, modemask, owners, owngroups, + "dsearch", errmsg) == 0; } #endif From 7c0d5d4c4d6df520b6235bd9c12d20f92285f972 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Mon, 29 Aug 2022 20:13:24 +1000 Subject: [PATCH 07/17] Add filter=nodots --- src/src/lookups/dsearch.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index 6e09858d9e..07097967cb 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -177,6 +177,7 @@ if (opts) if (i>=0) filter_by_type |= BIT(i); else if (Ustrcmp(ele, "subdir") == 0) filter_by_type |= BIT(S_IFMT_to_index(S_IFDIR)), exclude_dotdotdot = 1; /* dir but not "." or ".." */ + else if (Ustrcmp(ele, "nodots") == 0) exclude_dotdotdot = 1; /* any but "." or ".." */ else { *errmsg = string_sprintf("unknown parameter for dsearch lookup: %s", ele-=7); From ff23f6421f690e70b9b948ea28a25a3d6130d9df Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Tue, 30 Aug 2022 10:56:04 +1000 Subject: [PATCH 08/17] Make function comments match code --- src/src/lookups/dsearch.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index 07097967cb..9a36a90a12 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -35,10 +35,11 @@ typedef struct { * Open entry point * *************************************************/ -/* See local README for interface description. We open the directory to test -whether it exists and whether it is searchable. However, we don't need to keep -it open, because the "search" can be done by a call to lstat() rather than -actually scanning through the list of files. */ +/* See local README for interface description. We checks that the proposed +directory is untainted and absolute. On systems where fstatat is supported, we +also opens the directory in O_PATH mode, meaning that fstatat does not have to +re-parse the entire path when it subsequently checks for the existence of a +file within it. */ static void * dsearch_open(const uschar * dirname, uschar ** errmsg) @@ -107,9 +108,9 @@ return lf_check_file(-1, dirname, S_IFDIR, modemask, owners, owngroups, * Find entry point * *************************************************/ -/* See local README for interface description. We use lstat() or fstatat() -instead of reading the directory, as it is hopefully faster to let the OS do -the scanning for us. */ +/* See local README for interface description. We use a single lstat() or +stat() or fstatat() syscall, instead of reading the directory (which takes at +least 3 syscalls). */ static int dsearch_find( From 5445e6df1de436f6671d4e419a5c91f5906d8d4f Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Tue, 30 Aug 2022 11:00:26 +1000 Subject: [PATCH 09/17] Add dsearch DEBUG --- src/src/lookups/dsearch.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index 9a36a90a12..0335fd10f6 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -65,6 +65,7 @@ if (dir_fd<0) } ds_handle *h = malloc(sizeof (ds_handle)); h->dir_fd = dir_fd; +DEBUG(D_lookup) debug_printf_indent(" dsearch_open: dirname=%s -> fd=%d h=%p\n", dirname, dir_fd, h); return h; #else if (f.running_in_test_harness) @@ -90,16 +91,20 @@ dsearch_check(void * handle, const uschar * UNUSED(dirname), int modemask, uid_t * owners, gid_t * owngroups, uschar ** errmsg) { ds_handle *h = handle; -return lf_check_file(h->dir_fd, NULL, S_IFDIR, modemask, owners, owngroups, +BOOL r = lf_check_file(h->dir_fd, NULL, S_IFDIR, modemask, owners, owngroups, "dsearch", errmsg) == 0; +DEBUG(D_lookup) debug_printf_indent(" dsearch_check: h=%p fd=%d -> %d\n", h, h->dir_fd, r); +return r; } #else static BOOL dsearch_check(void * UNUSED(handle), const uschar * dirname, int modemask, uid_t * owners, gid_t * owngroups, uschar ** errmsg) { -return lf_check_file(-1, dirname, S_IFDIR, modemask, owners, owngroups, +BOOL r = lf_check_file(-1, dirname, S_IFDIR, modemask, owners, owngroups, "dsearch", errmsg) == 0; +DEBUG(D_lookup) debug_printf_indent(" dsearch_check: dirname=%s -> %d\n", dirname, r); +return r; } #endif @@ -207,6 +212,13 @@ if (ignore_key) else if (keystring == NULL || keystring[0] == 0) /* in case lstat treats "/dir/" the same as "/dir/." */ return FAIL; +DEBUG(D_lookup) debug_printf_indent(" dsearch_find: %s%sfilterbits=%#x ret=%s key=%s\n", + follow_symlink ? "follow, " : "", + exclude_dotdotdot ? "filter=nodots, " : "", + filter_by_type, + ret_mode == RET_FULL ? "full" : ret_mode == RET_DIR ? "dir" : "key", + keystring); + /* exclude "." and ".." when {filter=subdir} included */ if (exclude_dotdotdot && keystring[0] == '.' @@ -251,7 +263,9 @@ if (stat_result >= 0) /* Since the filename exists in the filesystem, we can return a non-tainted result. */ + full_path = *result = string_copy_taint(full_path, GET_UNTAINTED); + DEBUG(D_lookup) debug_printf_indent(" dsearch_find: res=%s", full_path); return OK; } *errmsg = string_sprintf("%s/%s is of unexpected type %s", @@ -280,6 +294,7 @@ static void dsearch_close(void *handle) { ds_handle *h = handle; +DEBUG(D_lookup) debug_printf_indent(" dsearch_close: h=%p fd=%d\n", h, h->dir_fd); close(h->dir_fd); /* ignore error */ free(h); } From 8309162737ccb6c7d6b177633e7f6e315b1970ea Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Sun, 28 Aug 2022 12:36:29 +1000 Subject: [PATCH 10/17] Add dsearch changes to changelog & readme --- doc/doc-txt/ChangeLog | 7 +++++++ doc/doc-txt/NewStuff | 21 ++++++++++++++++++++- src/README.UPDATING | 9 +++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog index d29ba6f650..8e2929b3d8 100644 --- a/doc/doc-txt/ChangeLog +++ b/doc/doc-txt/ChangeLog @@ -125,6 +125,13 @@ JH/26 For a ${readsocket } in TLS mode, send a TLS Close Alert before the TCP JH/27 Fix ${srs_encode ..}. Previously it would give a bad result for one day every 1024 days. +MK/08 Numerous changes to dsearch lookups: (a) now performs taint check before + other validations to prevent an attacker from making inferences from the + varying error messages; (b) no longer require "read" permission on the + directory (so you can now chmod a-r the dir to improve system security); + (c) uses fstatat on systems that support it (reducing the syscall count); + (d) additional filter options to support matching all inode types; (e) + new ret=dir option. See documentation for full list. Exim version 4.96 ----------------- diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff index 991bcf7418..e695f24f2d 100644 --- a/doc/doc-txt/NewStuff +++ b/doc/doc-txt/NewStuff @@ -9,7 +9,7 @@ the documentation is updated, this file is reduced to a short list. Version 4.97 ------------ - 1. The expansion-test faciility (exim -be) can set variables. + 1. The expansion-test facility (exim -be) can set variables. 2. An event on a failing SMTP AUTH, for both client and server operations. @@ -32,6 +32,25 @@ Version 4.97 11. An option for the ${readsocket } expansion to set an SNI for TLS. 12. The ACL remove_header modifier can take a pattern. + 3. Dsearch lookup will now check for taint violations before doing anything + else (this prevents an attacker from inferring the locations of files from + differences in error messages). + + 4. Dsearch lookup no longer requires read access to directories; consider + revoking read access using chmod go-r /path/to/containing/dir/. + + 5. Dsearch lookup will use fstatat() on systems that support it. + + 6. Dsearch lookup has new option "ret=dir" to return only the name of the + containing directory without the target (this simplifies some kinds of + nested loopup expansions). + + 7. Dsearch lookup now has filter= parameters to match all common inode types + (file, dir, symlink, pipe, socket, tty, & bdev). + + 8. Dsearch lookup has new option "filter=nodots" to exclude "." and ".." + even when directory matching is not required; equivalent to "filter=subdir" + but without requiring that the target actually be a directory. Version 4.96 ------------ diff --git a/src/README.UPDATING b/src/README.UPDATING index 72bc970214..caa8de844b 100644 --- a/src/README.UPDATING +++ b/src/README.UPDATING @@ -26,6 +26,15 @@ The rest of this document contains information about changes in 4.xx releases that might affect a running system. +Exim version 4.97 +----------------- + +The dsearch lookup type no longer requires read permission on its target +directories; scan permission alone now suffices. If you were relying on +unreadable directories to block dsearch lookups, you should now make other +arrangements. Conversely, you may now tighten up the permissions on any +directories that used to require world or group read access. + Exim version 4.95 ----------------- From c7c5ce5c21317d4a0d9319875bac6c674f1ec4d8 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Sun, 28 Aug 2022 13:20:24 +1000 Subject: [PATCH 11/17] Adjust dsearch documentation --- doc/doc-docbook/spec.xfpt | 30 +++++++++++++++++++----------- doc/doc-txt/ChangeLog | 5 +++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt index 86f8dda630..e9dc2e024c 100644 --- a/doc/doc-docbook/spec.xfpt +++ b/doc/doc-docbook/spec.xfpt @@ -6830,20 +6830,28 @@ each element starting with a tag name and an equals. Two options are supported, for the return value and for filtering match candidates. -The "ret" option requests an alternate result value of -the entire path for the entry. Example: +The "ret=" options request alternative result values: "ret=key" is the default, +and just returns the key (filename), "ret=full" requests the entire path for +the entry, and "ret=dir" requests the directory alone. +Example: .code +${lookup {passwd} dsearch,ret=key {/etc}} ${lookup {passwd} dsearch,ret=full {/etc}} +${lookup {passwd} dsearch,ret=dir {/etc}} .endd -The default result is just the requested entry. -The "filter" option requests that only directory entries of a given type -are matched. The match value is one of "file", "dir" or "subdir" (the latter -not matching "." or ".."). Example: -.code -${lookup {passwd} dsearch,filter=file {/etc}} -.endd -The default matching is for any entry type, including directories -and symlinks. +gives "passwd", "/etc/passwd" and "/etc". + +By default all directory entries are matched, regardless of type. The "filter" +option requests that only directory entries of the given type(s) are matched: +"file", "dir", "symlink", "pipe", "socket", "tty", "bdev". The filter option +may be repeated to allow multiple types. + +In addition, "filter=nodots" excludes "." and "..", and "filter=subdir" is +shorthand for "filter=dir,filter=nodots". + +The "follow" option follows any symlinks before applies other checks, and +dangling symlinks will report as nonexistent; otherwise symlinks are rejected +if any other file-type filter is applied. An example of how this lookup can be used to support virtual domains is given in section diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog index 8e2929b3d8..fa84f865e5 100644 --- a/doc/doc-txt/ChangeLog +++ b/doc/doc-txt/ChangeLog @@ -132,6 +132,11 @@ MK/08 Numerous changes to dsearch lookups: (a) now performs taint check before (c) uses fstatat on systems that support it (reducing the syscall count); (d) additional filter options to support matching all inode types; (e) new ret=dir option. See documentation for full list. +MK/09 Bug 2916: dsearch lookups no longer requires "read" permission on the + directory. + +MK/10 New dsearch lookup features: (a) ret=dir option; (b) filter options for + all inode types; (c) can use fstatat(2) where supported. Exim version 4.96 ----------------- From 95dcd84413ec656abfd8fcd46c8db0ce11926f24 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Thu, 22 Sep 2022 19:04:29 +1000 Subject: [PATCH 12/17] Use macro args exactly once --- src/src/lookups/lf_functions.h | 87 +++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/src/src/lookups/lf_functions.h b/src/src/lookups/lf_functions.h index 99c0aa9a1d..be3a6ab834 100644 --- a/src/src/lookups/lf_functions.h +++ b/src/src/lookups/lf_functions.h @@ -19,45 +19,54 @@ extern int lf_sqlperform(const uschar *, const uschar *, const uschar *, uschar **, BOOL *, uint *, const uschar *)); /* - * The st_mode field returned by stat() indicates the type of a file; these - * functions help to interpret that. - * - * Usage: - * struct stat s; - * const uschar *t; - * stat(&s, filename); - * if ((t = S_IFMT_to_long_name(s.st_mode))) - * printf("%s is a %s\n", filename, t); - * else - * printf("%s is unknown\n", filename); - * if ( allowed_filetype_mask & BIT(S_IFMT_to_index(s.st_mode)) ) - * printf("\tALLOWED\n"); - * else - * printf("\tFORBIDDEN\n"); - * - * S_IFMT_to_index shifts this to remove bits that are not part of S_IFMT, - * making a "small" number suitable for an array index; S_IFMT_from_index does - * the reverse. These indeces can be used with the S_IFMTix_* variants; the - * others use the mode masked with S_IFMT directly. - * - * The _ucname and _name versions are the same, differing only in case; the - * _long_name versions provide human-readable forms suitable for logging. - * - * Macros for use in static initialisers; take care to avoid side effects in - * parameters + The st_mode field returned by stat() indicates the type of a file; these + functions help to interpret that. + + Usage: + struct stat s; + const uschar *t; + stat(&s, filename); + if ((t = S_IFMT_to_long_name(s.st_mode))) + printf("%s is a %s\n", filename, t); + else + printf("%s is unknown\n", filename); + if ( allowed_filetype_mask & BIT(S_IFMT_to_index(s.st_mode)) ) + printf("\tALLOWED\n"); + else + printf("\tFORBIDDEN\n"); + + S_IFMT_to_index shifts this to remove bits that are not part of S_IFMT, + making a "small" number suitable for an array index; S_IFMT_from_index does + the reverse. These indeces can be used with the S_IFMTix_* variants; the + others use the mode masked with S_IFMT directly. + + The _ucname and _name versions are the same, differing only in case; the + _long_name versions provide human-readable forms suitable for logging. */ -#define S_IFMT_scale (S_IFMT & -S_IFMT) /* X&-X comprises the least significant 1-bit of X, all other bits 0 */ -#define S_IFMT_to_index(S) ( (S)<0 ? -1 : (S_IFMT & (S)) / S_IFMT_scale ) -#define S_IFMT_from_index(I) ( (I)<0 ? -1 : S_IFMT & (I) * S_IFMT_scale ) - -extern const uschar *S_IFMTix_to_name(int index); /* NULL on error */ -extern const uschar *S_IFMTix_to_long_name(int index); /* NULL on error */ -extern const uschar *S_IFMTix_to_ucname(int index); /* NULL on error */ -extern int S_IFMTix_from_name(const uschar *name); /* negative on error */ - -static inline const uschar *S_IFMT_to_name(int index) { return S_IFMTix_to_name( S_IFMT_to_index(index)); } /* NULL on error */ -static inline const uschar *S_IFMT_to_long_name(int index) { return S_IFMTix_to_long_name(S_IFMT_to_index(index)); } /* NULL on error */ -static inline const uschar *S_IFMT_to_ucname(int index) { return S_IFMTix_to_ucname( S_IFMT_to_index(index)); } /* NULL on error */ -static inline int S_IFMT_from_name(const uschar *name) { return S_IFMT_from_index( S_IFMTix_from_name(name)); } /* negative on error */ + +/* In all traditional Unix systems S_IFMT == 0xf000, so S_IFMT_scale == 0x1000 + == (1<<12); however this method will work with any other "reasonable" + values, in particular any positive value for S_IFMT, provided + ( 1 << S_IFMT / S_IFMT_scale ) fits inside an unsigned int without rollover - in + general, so it spans no more than 5 bits. +*/ +#define S_IFMT_scale (S_IFMT & -S_IFMT) /* X&-X computes the greatest power of 2 that divides into X */ +/* static_assert( S_IFMT > 0 && S_IFMT / S_IFMT_scale < 32 ); */ + +/* These need to be macros (rather than functions) to allow them to be used in + static initialisers */ +#define S_IFMT_to_index(S) ( (S_IFMT & (S)) / S_IFMT_scale ) +#define S_IFMT_from_index(I) ( S_IFMT & (I) * S_IFMT_scale ) + + +extern const uschar *S_IFMTix_to_name(int index); /* NULL on error */ +extern const uschar *S_IFMTix_to_long_name(int index); /* NULL on error */ +extern const uschar *S_IFMTix_to_ucname(int index); /* NULL on error */ +extern int S_IFMTix_from_name(const uschar *name); /* negative on error */ + +static inline const uschar *S_IFMT_to_name(int index) { int i = S_IFMT_to_index(index); return i<0 ? NULL : S_IFMTix_to_name(i); } /* NULL on error */ +static inline const uschar *S_IFMT_to_long_name(int index) { int i = S_IFMT_to_index(index); return i<0 ? NULL : S_IFMTix_to_long_name(i); } /* NULL on error */ +static inline const uschar *S_IFMT_to_ucname(int index) { int i = S_IFMT_to_index(index); return i<0 ? NULL : S_IFMTix_to_ucname(i); } /* NULL on error */ +static inline int S_IFMT_from_name(const uschar *name) { int i = S_IFMTix_from_name(name); return i<0 ? -1 : S_IFMT_from_index(i); } /* negative on error */ /* End of lf_functions.h */ From 04c8a28566057dbe45c60d78aea59ec91b3b7753 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Thu, 22 Sep 2022 19:23:45 +1000 Subject: [PATCH 13/17] Make locate.pl finish faster --- test/src/locate.pl | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/src/locate.pl b/test/src/locate.pl index ba74e030b6..809b34b895 100644 --- a/test/src/locate.pl +++ b/test/src/locate.pl @@ -36,6 +36,21 @@ sub locate { my ($tool, @dirs) = @_; + my $n = 0; + for ( my @look_in = map { $_.'/' } @dirs; @look_in ;) { + my $d = shift @look_in; + printf STDERR "\r%7u %s\e[K", ++$n, $d; + my $p = "$d/$tool"; + -x $p && -f _ and do { + printf STDERR "\r\e[K"; + return $p; + }; + push @look_in, glob $d.'*/' + unless $d =~ m{bin/$}; + } + + return; + # use die to break out of the find as soon # as we found it my $cwd = cwd; @@ -51,5 +66,5 @@ sub locate { }; chdir $cwd; - return (ref $@ eq ref {} and $@->{found}) ? $@->{found} : undef; + return ref $@ eq 'HASH' && $@->{found} || undef; } From 3c90e86db931d326c5f1be44a3f1a2c468e2aafc Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Thu, 22 Sep 2022 19:42:10 +1000 Subject: [PATCH 14/17] malloc->store_get --- src/src/lookups/dsearch.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index 0335fd10f6..b1225efe2f 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -63,7 +63,7 @@ if (dir_fd<0) *errmsg = string_open_failed("%s for directory search", dirname); return NULL; } -ds_handle *h = malloc(sizeof (ds_handle)); +ds_handle *h = store_get(sizeof (ds_handle), GET_UNTAINTED); h->dir_fd = dir_fd; DEBUG(D_lookup) debug_printf_indent(" dsearch_open: dirname=%s -> fd=%d h=%p\n", dirname, dir_fd, h); return h; @@ -296,7 +296,6 @@ dsearch_close(void *handle) ds_handle *h = handle; DEBUG(D_lookup) debug_printf_indent(" dsearch_close: h=%p fd=%d\n", h, h->dir_fd); close(h->dir_fd); /* ignore error */ -free(h); } #else static void From f93d685e53402e0f26d78cfc40804949a6c8005d Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Thu, 22 Sep 2022 19:49:52 +1000 Subject: [PATCH 15/17] Remove proposed checkpath option --- src/src/lookups/dsearch.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index b1225efe2f..de064a93e5 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -192,12 +192,6 @@ if (opts) } else if (Ustrcmp(ele, "follow") == 0) follow_symlink = 1; - else if (Ustrcmp(ele, "checkpath") == 0) - { - /* shortcut for follow,ret=dir */ - follow_symlink = 1; - ret_mode = RET_DIR; - } else if (Ustrcmp(ele, "ignorekey") == 0) ignore_key = 1; else From bea4bbb790a9b7dad4cf94c50e5133b857d84b14 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Thu, 22 Sep 2022 21:19:40 +1000 Subject: [PATCH 16/17] lookup/*.c add build dependency on lf_functions.h --- src/src/lookups/Makefile | 66 ++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/src/lookups/Makefile b/src/src/lookups/Makefile index 19585bf31a..066084a5d8 100644 --- a/src/src/lookups/Makefile +++ b/src/src/lookups/Makefile @@ -30,47 +30,47 @@ lf_check_file.o: $(HDRS) lf_check_file.c lf_functions.h lf_quote.o: $(HDRS) lf_quote.c lf_functions.h lf_sqlperform.o: $(HDRS) lf_sqlperform.c lf_functions.h -cdb.o: $(HDRS) cdb.c -dbmdb.o: $(HDRS) dbmdb.c -dnsdb.o: $(HDRS) dnsdb.c -dsearch.o: $(HDRS) dsearch.c -ibase.o: $(HDRS) ibase.c -ldap.o: $(HDRS) ldap.c +cdb.o: $(HDRS) cdb.c lf_functions.h +dbmdb.o: $(HDRS) dbmdb.c lf_functions.h +dnsdb.o: $(HDRS) dnsdb.c lf_functions.h +dsearch.o: $(HDRS) dsearch.c lf_functions.h +ibase.o: $(HDRS) ibase.c lf_functions.h +ldap.o: $(HDRS) ldap.c lf_functions.h lmdb.o: $(HDRS) lmdb.c -json.o: $(HDRS) json.c -lsearch.o: $(HDRS) lsearch.c -mysql.o: $(HDRS) mysql.c -nis.o: $(HDRS) nis.c -nisplus.o: $(HDRS) nisplus.c +json.o: $(HDRS) json.c lf_functions.h +lsearch.o: $(HDRS) lsearch.c lf_functions.h +mysql.o: $(HDRS) mysql.c lf_functions.h +nis.o: $(HDRS) nis.c lf_functions.h +nisplus.o: $(HDRS) nisplus.c lf_functions.h oracle.o: $(HDRS) oracle.c passwd.o: $(HDRS) passwd.c -pgsql.o: $(HDRS) pgsql.c -readsock.o: $(HDRS) readsock.c -redis.o: $(HDRS) redis.c -spf.o: $(HDRS) spf.c -sqlite.o: $(HDRS) sqlite.c -testdb.o: $(HDRS) testdb.c +pgsql.o: $(HDRS) pgsql.c lf_functions.h +readsock.o: $(HDRS) readsock.c lf_functions.h +redis.o: $(HDRS) redis.c lf_functions.h +spf.o: $(HDRS) spf.c lf_functions.h +sqlite.o: $(HDRS) sqlite.c lf_functions.h +testdb.o: $(HDRS) testdb.c lf_functions.h whoson.o: $(HDRS) whoson.c -cdb.so: $(HDRS) cdb.c -dbmdb.so: $(HDRS) dbmdb.c -dnsdb.so: $(HDRS) dnsdb.c -dsearch.so: $(HDRS) dsearch.c -ibase.so: $(HDRS) ibase.c -json.so: $(HDRS) json.c +cdb.so: $(HDRS) cdb.c lf_functions.h +dbmdb.so: $(HDRS) dbmdb.c lf_functions.h +dnsdb.so: $(HDRS) dnsdb.c lf_functions.h +dsearch.so: $(HDRS) dsearch.c lf_functions.h +ibase.so: $(HDRS) ibase.c lf_functions.h +json.so: $(HDRS) json.c lf_functions.h ldap.so: $(HDRS) ldap.c -lmdb.so: $(HDRS) lmdb.c -lsearch.so: $(HDRS) lsearch.c -mysql.so: $(HDRS) mysql.c -nis.so: $(HDRS) nis.c -nisplus.so: $(HDRS) nisplus.c +lmdb.so: $(HDRS) lmdb.c lf_functions.h +lsearch.so: $(HDRS) lsearch.c lf_functions.h +mysql.so: $(HDRS) mysql.c lf_functions.h +nis.so: $(HDRS) nis.c lf_functions.h +nisplus.so: $(HDRS) nisplus.c lf_functions.h oracle.so: $(HDRS) oracle.c passwd.so: $(HDRS) passwd.c -pgsql.so: $(HDRS) pgsql.c -redis.so: $(HDRS) redis.c -spf.so: $(HDRS) spf.c -sqlite.so: $(HDRS) sqlite.c -testdb.so: $(HDRS) testdb.c +pgsql.so: $(HDRS) pgsql.c lf_functions.h +redis.so: $(HDRS) redis.c lf_functions.h +spf.so: $(HDRS) spf.c lf_functions.h +sqlite.so: $(HDRS) sqlite.c lf_functions.h +testdb.so: $(HDRS) testdb.c lf_functions.h whoson.so: $(HDRS) whoson.c # End From 38e83d2dccf1fc432125b3be0245ffb56f93e8a5 Mon Sep 17 00:00:00 2001 From: Martin D Kealey Date: Thu, 22 Sep 2022 21:35:16 +1000 Subject: [PATCH 17/17] Remove unused S_IFMT_* funcs; flip nodots to allowdots; deprecate "filter=dir implies allowdots" --- src/src/lookups/dsearch.c | 34 ++++++++++++++++++++------------- src/src/lookups/lf_check_file.c | 25 +++++------------------- src/src/lookups/lf_functions.h | 18 +++++++++-------- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index de064a93e5..7e719b9894 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -129,8 +129,8 @@ dsearch_find( { struct stat statbuf; int save_errno; -unsigned filter_by_type = 0; -int exclude_dotdotdot = 0; +ifmt_set_t filter_by_type = 0; +int allowdots = 0; enum { RET_KEY, /* return the key */ RET_DIR, /* return the dir without the key */ @@ -166,7 +166,7 @@ if (opts) else if (Ustrcmp(ele, "dir") == 0) ret_mode = RET_DIR; #if 0 - /* XXX ret=key is excluded from opts by special-case code in by search_find() */ + /* NOTE ret=key is excluded from opts by special-case code in by search_find() */ else if (Ustrcmp(ele, "key") == 0) ret_mode = RET_KEY; #endif @@ -179,11 +179,19 @@ if (opts) else if (Ustrncmp(ele, "filter=", 7) == 0) { ele += 7; - int i = S_IFMTix_from_name(ele); - if (i>=0) - filter_by_type |= BIT(i); - else if (Ustrcmp(ele, "subdir") == 0) filter_by_type |= BIT(S_IFMT_to_index(S_IFDIR)), exclude_dotdotdot = 1; /* dir but not "." or ".." */ - else if (Ustrcmp(ele, "nodots") == 0) exclude_dotdotdot = 1; /* any but "." or ".." */ + ifmt_set_t m = S_IFMTset_from_name(ele); + if (m) + { + filter_by_type |= m; +/* XXX issue immediate deprecation warning */ +#ifndef NO_DIR_IMPLIES_ALLOWDOTS + /* allow "." or ".." when "dir" rather than "subdir" */ + if (m == S_IFMT_to_set(S_IFDIR) && ele[0] == 'd') + allowdots = 1; +#endif + } + else if (Ustrcmp(ele, "allowdots") == 0) + allowdots = 1; /* allow "." or ".." */ else { *errmsg = string_sprintf("unknown parameter for dsearch lookup: %s", ele-=7); @@ -206,15 +214,15 @@ if (ignore_key) else if (keystring == NULL || keystring[0] == 0) /* in case lstat treats "/dir/" the same as "/dir/." */ return FAIL; -DEBUG(D_lookup) debug_printf_indent(" dsearch_find: %s%sfilterbits=%#x ret=%s key=%s\n", +DEBUG(D_lookup) debug_printf_indent(" dsearch_find: %s%sfilter_set=%04jx ret=%s key=%s\n", follow_symlink ? "follow, " : "", - exclude_dotdotdot ? "filter=nodots, " : "", - filter_by_type, + allowdots ? "filter=allowdots, " : "", + (uintmax_t) filter_by_type, ret_mode == RET_FULL ? "full" : ret_mode == RET_DIR ? "dir" : "key", keystring); /* exclude "." and ".." when {filter=subdir} included */ -if (exclude_dotdotdot +if (! allowdots && keystring[0] == '.' && (keystring[1] == 0 || keystring[1] == '.' && keystring[2] == 0)) @@ -235,7 +243,7 @@ else if (stat_result >= 0) { if (!filter_by_type - || filter_by_type & BIT(S_IFMT_to_index(statbuf.st_mode))) + || filter_by_type & S_IFMT_to_set(statbuf.st_mode)) { switch (ret_mode) { diff --git a/src/src/lookups/lf_check_file.c b/src/src/lookups/lf_check_file.c index 3c43768884..1426b6b682 100644 --- a/src/src/lookups/lf_check_file.c +++ b/src/src/lookups/lf_check_file.c @@ -77,6 +77,7 @@ static const struct { /* sorted in descending order of likelihood */ { CUS "file", S_IFMT_to_index(S_IFREG) }, { CUS "dir", S_IFMT_to_index(S_IFDIR) }, + { CUS "subdir", S_IFMT_to_index(S_IFDIR) }, #ifdef S_IFLNK { CUS "symlink", S_IFMT_to_index(S_IFLNK) }, { CUS "link", S_IFMT_to_index(S_IFLNK) }, @@ -100,13 +101,13 @@ static const struct { }; static const size_t num_ni_map = nelem(ni_map); -int -S_IFMTix_from_name(const uschar *name) +ifmt_set_t +S_IFMTset_from_name(const uschar *name) { for (int i=0 ; i < num_ni_map ; ++i) if (Ustrcmp(ni_map[i].name, name) == 0) - return ni_map[i].index; -return -1; + return 1UL << ni_map[i].index; +return 0; } const uschar * @@ -117,22 +118,6 @@ if (index < 0 || index >= num_S_IF_names) return S_IF_longnames[index]; } -const uschar * -S_IFMTix_to_name(int index) -{ -if (index < 0 || index >= num_S_IF_names) - return NULL; /* invalid file type */ -return S_IF_names[index]; -} - -const uschar * -S_IFMTix_to_ucname(int index) -{ -if (index < 0 || index >= num_S_IF_names) - return NULL; /* invalid file type */ -return S_IF_ucnames[index]; -} - /************************************************* * Check a file's credentials * *************************************************/ diff --git a/src/src/lookups/lf_functions.h b/src/src/lookups/lf_functions.h index be3a6ab834..c31a78ea26 100644 --- a/src/src/lookups/lf_functions.h +++ b/src/src/lookups/lf_functions.h @@ -30,7 +30,7 @@ extern int lf_sqlperform(const uschar *, const uschar *, const uschar *, printf("%s is a %s\n", filename, t); else printf("%s is unknown\n", filename); - if ( allowed_filetype_mask & BIT(S_IFMT_to_index(s.st_mode)) ) + if ( allowed_filetype_mask & S_IFMT_to_set(s.st_mode) ) printf("\tALLOWED\n"); else printf("\tFORBIDDEN\n"); @@ -58,15 +58,17 @@ extern int lf_sqlperform(const uschar *, const uschar *, const uschar *, #define S_IFMT_to_index(S) ( (S_IFMT & (S)) / S_IFMT_scale ) #define S_IFMT_from_index(I) ( S_IFMT & (I) * S_IFMT_scale ) +typedef unsigned long ifmt_set_t; -extern const uschar *S_IFMTix_to_name(int index); /* NULL on error */ +#define S_IFMT_to_set(S) (1UL << S_IFMT_to_index(S)) + +extern ifmt_set_t S_IFMTset_from_name(const uschar *name); /* zero on error */ extern const uschar *S_IFMTix_to_long_name(int index); /* NULL on error */ -extern const uschar *S_IFMTix_to_ucname(int index); /* NULL on error */ -extern int S_IFMTix_from_name(const uschar *name); /* negative on error */ -static inline const uschar *S_IFMT_to_name(int index) { int i = S_IFMT_to_index(index); return i<0 ? NULL : S_IFMTix_to_name(i); } /* NULL on error */ -static inline const uschar *S_IFMT_to_long_name(int index) { int i = S_IFMT_to_index(index); return i<0 ? NULL : S_IFMTix_to_long_name(i); } /* NULL on error */ -static inline const uschar *S_IFMT_to_ucname(int index) { int i = S_IFMT_to_index(index); return i<0 ? NULL : S_IFMTix_to_ucname(i); } /* NULL on error */ -static inline int S_IFMT_from_name(const uschar *name) { int i = S_IFMTix_from_name(name); return i<0 ? -1 : S_IFMT_from_index(i); } /* negative on error */ +static inline const uschar * +S_IFMT_to_long_name(int ifmt) { /* NULL on error */ +int i = S_IFMT_to_index(ifmt); +return i<0 ? NULL : S_IFMTix_to_long_name(i); +} /* End of lf_functions.h */