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 d29ba6f650..fa84f865e5 100644 --- a/doc/doc-txt/ChangeLog +++ b/doc/doc-txt/ChangeLog @@ -125,6 +125,18 @@ 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. +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 ----------------- 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 ----------------- 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/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/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 diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c index 74439bfc87..7e719b9894 100644 --- a/src/src/lookups/dsearch.c +++ b/src/src/lookups/dsearch.c @@ -15,75 +15,135 @@ 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 /************************************************* * 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) { -DIR * dp = exim_opendir(dirname); -if (!dp) +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); + 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 = 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; +#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 * *************************************************/ -/* 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 * handle, const uschar * filename, int modemask, +dsearch_check(void * handle, const uschar * UNUSED(dirname), 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; -*errmsg = string_sprintf("dirname '%s' for dsearch is not absolute", filename); -return FALSE; +ds_handle *h = handle; +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) +{ +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 /************************************************* * 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) - -/* 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 a single lstat() or +stat() or fstatat() syscall, instead of reading the directory (which takes at +least 3 syscalls). */ 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; +ifmt_set_t filter_by_type = 0; +int allowdots = 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; +#endif +const uschar *full_path; +int stat_result; if (Ustrchr(keystring, '/') != 0) { @@ -98,41 +158,128 @@ if (opts) uschar * ele; while ((ele = string_nextinlist(&opts, &sep, NULL, 0))) - if (Ustrcmp(ele, "ret=full") == 0) - flags |= 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 + /* 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 + 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) - 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 ".." */ + 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); + return DEFER; + } + } + else if (Ustrcmp(ele, "follow") == 0) + follow_symlink = 1; + else if (Ustrcmp(ele, "ignorekey") == 0) + ignore_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] != '.' - ) ) ) ) +if (ignore_key) + keystring = ""; +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%sfilter_set=%04jx ret=%s key=%s\n", + follow_symlink ? "follow, " : "", + 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 (! allowdots + && keystring[0] == '.' + && (keystring[1] == 0 + || keystring[1] == '.' && keystring[2] == 0)) + return FAIL; + +#ifdef USE_AT_FILE +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 +full_path = ignore_key ? dirname + : string_sprintf("%s/%s", dirname, keystring); +if (follow_symlink) + stat_result = Ustat(full_path, &statbuf); +else + stat_result = Ulstat(full_path, &statbuf); +#endif +if (stat_result >= 0) { - /* 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); - return OK; + if (!filter_by_type + || filter_by_type & S_IFMT_to_set(statbuf.st_mode)) + { + 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. */ + 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", + dirname, keystring, S_IFMT_to_long_name(statbuf.st_mode)); + errno = ERRNO_MISMATCH; + return DEFER; } 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 +291,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) +{ +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 */ +} +#else +static void +dsearch_close(void * UNUSED(handle)) { -handle = handle; /* Avoid compiler warning */ } +#endif /************************************************* @@ -178,7 +334,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 diff --git a/src/src/lookups/lf_check_file.c b/src/src/lookups/lf_check_file.c index c4c05e44de..1426b6b682 100644 --- a/src/src/lookups/lf_check_file.c +++ b/src/src/lookups/lf_check_file.c @@ -11,7 +11,112 @@ #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) }, + { 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) }, + #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); + +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 1UL << ni_map[i].index; +return 0; +} +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]; +} /************************************************* * Check a file's credentials * @@ -55,17 +160,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..c31a78ea26 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,57 @@ 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 & S_IFMT_to_set(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. + */ + +/* 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 ) + +typedef unsigned long ifmt_set_t; + +#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 */ + +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 */ 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 */ 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; }