SPDX: Mass-update to GPL-2.0-or-later
[exim.git] / src / src / search.c
index 3dfd80e32820de87c3b5f80426cf0d10ce657da2..2b6e5d37f6b9d44bc8a0ea4185d80a48522439e8 100644 (file)
@@ -1,11 +1,11 @@
-/* $Cambridge: exim/src/src/search.c,v 1.1 2004/10/07 10:39:01 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2004 */
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* A set of functions to search databases in various formats. An open
 database is represented by a void * value which is returned from a lookup-
@@ -43,7 +43,7 @@ static int open_filecount = 0;
 
 /* Allow us to reset store used for lookups and lookup caching */
 
-static void *search_reset_point = NULL;
+static rmark search_reset_point = NULL;
 
 
 
@@ -64,14 +64,12 @@ Returns:     +ve => valid lookup name; value is offset in lookup_list
 */
 
 int
-search_findtype(uschar *name, int len)
+search_findtype(const uschar * name, int len)
 {
-int bot = 0;
-int top = lookup_list_count;
-while (top > bot)
+for (int bot = 0, top = lookup_list_count; top > bot; )
   {
   int mid = (top + bot)/2;
-  int c = Ustrncmp(name, lookup_list[mid].name, len);
+  int c = Ustrncmp(name, lookup_list[mid]->name, len);
 
   /* If c == 0 we have matched the incoming name with the start of the search
   type name. However, some search types are substrings of others (e.g. nis and
@@ -81,9 +79,9 @@ while (top > bot)
   are testing. By leaving c == 0 when the lengths are different, and doing a
   > 0 test below, this all falls out correctly. */
 
-  if (c == 0 && Ustrlen(lookup_list[mid].name) == len)
+  if (c == 0 && Ustrlen(lookup_list[mid]->name) == len)
     {
-    if (lookup_list[mid].find != NULL) return mid;
+    if (lookup_list[mid]->find != NULL) return mid;
     search_error_message  = string_sprintf("lookup type \"%.*s\" is not "
       "available (not in the binary - check buildtime LOOKUP configuration)",
       len, name);
@@ -93,7 +91,7 @@ while (top > bot)
   if (c > 0) bot = mid + 1; else top = mid;
   }
 
-search_error_message = string_sprintf("unknown lookup type \"%.*s\"",len,name);
+search_error_message = string_sprintf("unknown lookup type \"%.*s\"", len, name);
 return -1;
 }
 
@@ -117,18 +115,20 @@ Arguments:
                  otherwise it's a literal string
   afflen       the length of the affix
   starflags    where to put the SEARCH_STAR and SEARCH_STARAT flags
+  opts        where to put the options
 
 Returns:     +ve => valid lookup name; value is offset in lookup_list
              -ve => invalid name; message in search_error_message.
 */
 
 int
-search_findtype_partial(uschar *name, int *ptypeptr, uschar **ptypeaff,
-  int *afflen, int *starflags)
+search_findtype_partial(const uschar *name, int *ptypeptr, const uschar **ptypeaff,
+  int *afflen, int *starflags, const uschar ** opts)
 {
 int len, stype;
 int pv = -1;
-uschar *ss = name;
+const uschar *ss = name;
+const uschar * t;
 
 *starflags = 0;
 *ptypeaff = NULL;
@@ -169,38 +169,102 @@ if (Ustrncmp(name, "partial", 7) == 0)
     }
   }
 
-/* Now we are left with a lookup name, possibly followed by * or *@. */
+/* Now we are left with a lookup name, possibly followed by * or *@,
+and then by options starting with a "," */
 
 len = Ustrlen(ss);
-if (len >= 2 && Ustrncmp(ss + len - 2, "*@", 2) == 0)
+if ((t = Ustrchr(ss, '*')))
   {
-  *starflags |= SEARCH_STARAT;
-  len -= 2;
+  len = t - ss;
+  *starflags |= (t[1] == '@' ? SEARCH_STARAT : SEARCH_STAR);
   }
-else if (len >= 1 && ss[len-1]  == '*')
+else
+  t = ss;
+
+if ((t = Ustrchr(t, ',')))
   {
-  *starflags |= SEARCH_STAR;
-  len--;
+  int l = t - ss;
+  if (l < len) len = l;
+  *opts = string_copy(t+1);
   }
+else
+  *opts = NULL;
 
 /* Check for the individual search type. Only those that are actually in the
-binary are valid. For query-style types, "partial" is an error. */
+binary are valid. For query-style types, "partial" and default types are
+erroneous. */
 
 stype = search_findtype(ss, len);
-if (pv >= 0 && mac_islookup(stype, lookup_querystyle))
+if (stype >= 0 && mac_islookup(stype, lookup_querystyle))
   {
-  search_error_message = string_sprintf("\"partial\" is not permitted "
-    "for lookup type \"%s\"", ss);
-  return -1;
+  if (pv >= 0)
+    {
+    search_error_message = string_sprintf("\"partial\" is not permitted "
+      "for lookup type \"%s\"", ss);
+    return -1;
+    }
+  if ((*starflags & (SEARCH_STAR|SEARCH_STARAT)) != 0)
+    {
+    search_error_message = string_sprintf("defaults using \"*\" or \"*@\" are "
+      "not permitted for lookup type \"%s\"", ss);
+    return -1;
+    }
   }
 
-/* All is well; pass back the partial type and return the lookup type. */
-
 *ptypeptr = pv;
 return stype;
 }
 
 
+/* Set the parameters for the three different kinds of lookup.
+Arguments:
+ search_type   the search-type code
+ search                the search-type string
+ query         argument for the search; filename or query
+ fnamep                pointer to return filename
+ opts          options
+
+Return:        keyquery        the search-type (for single-key) or query (for query-type)
+ */
+uschar *
+search_args(int search_type, uschar * search, uschar * query, uschar ** fnamep,
+  const uschar * opts)
+{
+Uskip_whitespace(&query);
+if (mac_islookup(search_type, lookup_absfilequery))
+  {                                    /* query-style but with file (sqlite) */
+  int sep = ',';
+
+  /* Check options first for new-style file spec */
+  if (opts) for (uschar * s; s = string_nextinlist(&opts, &sep, NULL, 0); )
+    if (Ustrncmp(s, "file=", 5) == 0)
+      {
+      *fnamep = s+5;
+      return query;
+      }
+
+  /* If no filename from options, use old-tyle space-sep prefix on query */
+  if (*query == '/')
+    {
+    uschar * s = query;
+    while (*query && !isspace(*query)) query++;
+    *fnamep = string_copyn(s, query - s);
+    Uskip_whitespace(&query);
+    }
+  else
+    *fnamep = NULL;
+  return query;                /* remainder after file skipped */
+  }
+if (!mac_islookup(search_type, lookup_querystyle))
+  {                                    /* single-key */
+  *fnamep = query;
+  return search;       /* modifiers important so use "keyquery" for them */
+  }
+*fnamep = NULL;                                /* else query-style */
+return query;
+}
+
+
 
 /*************************************************
 *               Release cached resources         *
@@ -224,13 +288,11 @@ Returns:     nothing
 static void
 tidyup_subtree(tree_node *t)
 {
-search_cache *c = (search_cache *)(t->data.ptr);
-if (t->left != NULL) tidyup_subtree(t->left);
-if (t->right != NULL) tidyup_subtree(t->right);
-if (c != NULL &&
-    c->handle != NULL &&
-    lookup_list[c->search_type].close != NULL)
-  lookup_list[c->search_type].close(c->handle);
+search_cache * c = (search_cache *)(t->data.ptr);
+if (t->left)  tidyup_subtree(t->left);
+if (t->right) tidyup_subtree(t->right);
+if (c && c->handle && lookup_list[c->search_type]->close)
+  lookup_list[c->search_type]->close(c->handle);
 }
 
 
@@ -243,15 +305,14 @@ Returns:  nothing
 void
 search_tidyup(void)
 {
-int i;
 int old_pool = store_pool;
 
-DEBUG(D_lookup) debug_printf("search_tidyup called\n");
+DEBUG(D_lookup) debug_printf_indent("search_tidyup called\n");
 
 /* Close individually each cached open file. */
 
 store_pool = POOL_SEARCH;
-if (search_tree != NULL)
+if (search_tree)
   {
   tidyup_subtree(search_tree);
   search_tree = NULL;
@@ -261,11 +322,10 @@ open_filecount = 0;
 
 /* Call the general tidyup entry for any drivers that have one. */
 
-for (i = 0; i < lookup_list_count; i++)
-  if (lookup_list[i].tidy != NULL) (lookup_list[i].tidy)();
+for (int i = 0; i < lookup_list_count; i++) if (lookup_list[i]->tidy)
+  (lookup_list[i]->tidy)();
 
-if (search_reset_point != NULL) store_reset(search_reset_point);
-search_reset_point = NULL;
+if (search_reset_point) search_reset_point = store_reset(search_reset_point);
 store_pool = old_pool;
 }
 
@@ -321,23 +381,30 @@ Returns:         an identifying handle for the open database;
 */
 
 void *
-search_open(uschar *filename, int search_type, int modemask, uid_t *owners,
-  gid_t *owngroups)
+search_open(const uschar * filename, int search_type, int modemask,
+  uid_t * owners, gid_t * owngroups)
 {
 void *handle;
 tree_node *t;
 search_cache *c;
-lookup_info *lk = lookup_list + search_type;
+lookup_info *lk = lookup_list[search_type];
 uschar keybuffer[256];
 int old_pool = store_pool;
 
+if (filename && is_tainted(filename))
+  {
+  log_write(0, LOG_MAIN|LOG_PANIC,
+    "Tainted filename for search: '%s'", filename);
+  return NULL;
+  }
+
 /* Change to the search store pool and remember our reset point */
 
 store_pool = POOL_SEARCH;
-if (search_reset_point == NULL) search_reset_point = store_get(0);
+if (!search_reset_point) search_reset_point = store_mark();
 
-DEBUG(D_lookup) debug_printf("search_open: %s \"%s\"\n", lk->name,
-  (filename == NULL)? US"NULL" : filename);
+DEBUG(D_lookup) debug_printf_indent("search_open: %s \"%s\"\n", lk->name,
+  filename ? filename : US"NULL");
 
 /* See if we already have this open for this type of search, and if so,
 pass back the tree block as the handle. The key for the tree node is the search
@@ -345,18 +412,17 @@ type plus '0' concatenated with the file name. There may be entries in the tree
 with closed files if a lot of files have been opened. */
 
 sprintf(CS keybuffer, "%c%.254s", search_type + '0',
-  (filename == NULL)? US"" : filename);
+  filename ? filename : US"");
 
-if ((t = tree_search(search_tree, keybuffer)) != NULL)
+if ((t = tree_search(search_tree, keybuffer)))
   {
-  c = (search_cache *)(t->data.ptr);
-  if (c->handle != NULL)
+  if ((c = (search_cache *)t->data.ptr)->handle)
     {
-    DEBUG(D_lookup) debug_printf("  cached open\n");
+    DEBUG(D_lookup) debug_printf_indent("  cached open\n");
     store_pool = old_pool;
     return t;
     }
-  DEBUG(D_lookup) debug_printf("  cached closed\n");
+  DEBUG(D_lookup) debug_printf_indent("  cached closed\n");
   }
 
 /* Otherwise, we need to open the file or database - each search type has its
@@ -366,39 +432,35 @@ we are holding open in the cache. If the limit is reached, close the least
 recently used one. */
 
 if (lk->type == lookup_absfile && open_filecount >= lookup_open_max)
-  {
-  if (open_bot == NULL)
+  if (!open_bot)
     log_write(0, LOG_MAIN|LOG_PANIC, "too many lookups open, but can't find "
       "one to close");
   else
     {
     search_cache *c = (search_cache *)(open_bot->data.ptr);
-    DEBUG(D_lookup) debug_printf("Too many lookup files open\n  closing %s\n",
+    DEBUG(D_lookup) debug_printf_indent("Too many lookup files open\n  closing %s\n",
       open_bot->name);
-    open_bot = c->up;
-    if (open_bot != NULL)
+    if ((open_bot = c->up))
       ((search_cache *)(open_bot->data.ptr))->down = NULL;
     else
       open_top = NULL;
-    ((lookup_list + c->search_type)->close)(c->handle);
+    ((lookup_list[c->search_type])->close)(c->handle);
     c->handle = NULL;
     open_filecount--;
     }
-  }
 
 /* If opening is successful, call the file-checking function if there is one,
 and if all is still well, enter the open database into the tree. */
 
-handle = lk->open(filename, &search_error_message);
-if (handle == NULL)
+if (!(handle = (lk->open)(filename, &search_error_message)))
   {
   store_pool = old_pool;
   return NULL;
   }
 
-if (lk->check != NULL &&
-   !lk->check(handle, filename, modemask, owners, owngroups,
-     &search_error_message))
+if (  lk->check
+   && !lk->check(handle, filename, modemask, owners, owngroups,
+        &search_error_message))
   {
   lk->close(handle);
   store_pool = old_pool;
@@ -413,10 +475,10 @@ if (lk->type == lookup_absfile) open_filecount++;
 insert a new entry. On re-use, leave any cached lookup data and the lookup
 count alone. */
 
-if (t == NULL)
+if (!t)
   {
-  t = store_get(sizeof(tree_node) + Ustrlen(keybuffer));
-  t->data.ptr = c = store_get(sizeof(search_cache));
+  t = store_get(sizeof(tree_node) + Ustrlen(keybuffer), GET_UNTAINTED);
+  t->data.ptr = c = store_get(sizeof(search_cache), GET_UNTAINTED);
   c->item_cache = NULL;
   Ustrcpy(t->name, keybuffer);
   tree_insertnode(&search_tree, t);
@@ -448,6 +510,8 @@ Arguments:
                NULL for query-style searches
   keystring    the keystring for single-key+file lookups, or
                the querystring for query-style lookups
+  cache_rd     FALSE to avoid lookup in cache layer
+  opts        type-specific options
 
 Returns:       a pointer to a dynamic string containing the answer,
                or NULL if the query failed or was deferred; in the
@@ -456,11 +520,13 @@ Returns:       a pointer to a dynamic string containing the answer,
 */
 
 static uschar *
-internal_search_find(void *handle, uschar *filename, uschar *keystring)
+internal_search_find(void * handle, const uschar * filename, uschar * keystring,
+  BOOL cache_rd, const uschar * opts)
 {
-tree_node *t = (tree_node *)handle;
-search_cache *c = (search_cache *)(t->data.ptr);
-uschar *data = NULL;
+tree_node * t = (tree_node *)handle;
+search_cache * c = (search_cache *)(t->data.ptr);
+expiring_data * e = NULL;      /* compiler quietening */
+uschar * data = NULL;
 int search_type = t->name[0] - '0';
 int old_pool = store_pool;
 
@@ -468,11 +534,12 @@ int old_pool = store_pool;
 the callers don't have to test for NULL, set an empty string. */
 
 search_error_message = US"";
-search_find_defer = FALSE;
+f.search_find_defer = FALSE;
 
-DEBUG(D_lookup) debug_printf("internal_search_find: file=\"%s\"\n  "
-  "type=%s key=\"%s\"\n", filename,
-  lookup_list[search_type].name, keystring);
+DEBUG(D_lookup) debug_printf_indent("internal_search_find: file=\"%s\"\n  "
+  "type=%s key=\"%s\" opts=%s%s%s\n", filename,
+  lookup_list[search_type]->name, keystring,
+  opts ? "\"" : "", opts, opts ? "\"" : "");
 
 /* Insurance. If the keystring is empty, just fail. */
 
@@ -483,85 +550,138 @@ if (keystring[0] == 0) return NULL;
 store_pool = POOL_SEARCH;
 
 /* Look up the data for the key, unless it is already in the cache for this
-file. No need to check c->item_cache for NULL, tree_search will do so. */
-
-if ((t = tree_search(c->item_cache, keystring)) == NULL)
+file. No need to check c->item_cache for NULL, tree_search will do so. Check
+whether we want to use the cache entry last so that we can always replace it. */
+
+if (  (t = tree_search(c->item_cache, keystring))
+   && (!(e = t->data.ptr)->expiry || e->expiry > time(NULL))
+   && (!opts && !e->opts  ||  opts && e->opts && Ustrcmp(opts, e->opts) == 0)
+   && cache_rd
+   )
+  { /* Data was in the cache already; set the pointer from the tree node */
+  data = e->data.ptr;
+  DEBUG(D_lookup) debug_printf_indent("cached data used for lookup of %s%s%s\n",
+    keystring,
+    filename ? US"\n  in " : US"", filename ? filename : US"");
+  }
+else
   {
-  BOOL do_cache = TRUE;
+  uint do_cache = UINT_MAX;
   int keylength = Ustrlen(keystring);
 
   DEBUG(D_lookup)
     {
-    if (filename != NULL)
-      debug_printf("file lookup required for %s\n  in %s\n",
-        keystring, filename);
-    else
-      debug_printf("database lookup required for %s\n", keystring);
+    if (t)
+      debug_printf_indent("cached data found but %s; ",
+       e->expiry && e->expiry <= time(NULL) ? "out-of-date"
+       : cache_rd ? "wrong opts" : "no_rd option set");
+    debug_printf_indent("%s lookup required for %s%s%s\n",
+      filename ? US"file" : US"database",
+      keystring,
+      filename ? US"\n  in " : US"", filename ? filename : US"");
+    if (!filename && is_tainted(keystring))
+      {
+      debug_printf_indent("                             ");
+      debug_print_taint(keystring);
+      }
+    }
+
+  /* Check that the query, for query-style lookups,
+  is either untainted or properly quoted for the lookup type.
+
+  XXX Should we this move into lf_sqlperform() ?  The server-taint check is there.
+  */
+
+  if (  !filename && lookup_list[search_type]->quote
+     && is_tainted(keystring) && !is_quoted_like(keystring, search_type))
+    {
+    uschar * s = acl_current_verb();
+    if (!s) s = authenticator_current_name();  /* must be before transport */
+    if (!s) s = transport_current_name();      /* must be before router */
+    if (!s) s = router_current_name(); /* GCC ?: would be good, but not in clang */
+    if (!s) s = US"";
+#ifdef enforce_quote_protection_notyet
+    search_error_message = string_sprintf(
+      "tainted search query is not properly quoted%s: %s%s",
+      s, keystring);
+    f.search_find_defer = TRUE;
+#else
+     {
+      int q = quoter_for_address(keystring);
+      /* If we're called from a transport, no privs to open the paniclog;
+      the logging punts to using stderr - and that seems to stop the debug
+      stream. */
+      log_write(0,
+       transport_name ? LOG_MAIN : LOG_MAIN|LOG_PANIC,
+       "tainted search query is not properly quoted%s: %s", s, keystring);
+
+      DEBUG(D_lookup) debug_printf_indent("search_type %d (%s) quoting %d (%s)\n",
+       search_type, lookup_list[search_type]->name,
+       q, is_real_quoter(q) ? lookup_list[q]->name : US"none");
+     }
+#endif
     }
 
   /* Call the code for the different kinds of search. DEFER is handled
   like FAIL, except that search_find_defer is set so the caller can
   distinguish if necessary. */
 
-  if (lookup_list[search_type].find(c->handle, filename, keystring, keylength,
-      &data, &search_error_message, &do_cache) == DEFER)
-    {
-    search_find_defer = TRUE;
-    }
+  if (lookup_list[search_type]->find(c->handle, filename, keystring, keylength,
+         &data, &search_error_message, &do_cache, opts) == DEFER)
+    f.search_find_defer = TRUE;
 
   /* A record that has been found is now in data, which is either NULL
   or points to a bit of dynamic store. Cache the result of the lookup if
   caching is permitted. Lookups can disable caching, when they did something
   that changes their data. The mysql and pgsql lookups do this when an
-  UPDATE/INSERT query was executed. */
+  UPDATE/INSERT query was executed.  Lookups can also set a TTL for the
+  cache entry; the dnsdb lookup does.
+  Finally, the caller can request no caching by setting an option. */
 
   else if (do_cache)
     {
-    int len = keylength + 1;
-    t = store_get(sizeof(tree_node) + len);
-    memcpy(t->name, keystring, len);
-    t->data.ptr = data;
-    tree_insertnode(&c->item_cache, t);
+    DEBUG(D_lookup) debug_printf_indent("%s cache entry\n",
+      t ? "replacing old" : "creating new");
+    if (!t)    /* No existing entry.  Create new one. */
+      {
+      int len = keylength + 1;
+      /* The cache node value should never be expanded so use tainted mem */
+      e = store_get(sizeof(expiring_data) + sizeof(tree_node) + len, GET_TAINTED);
+      t = (tree_node *)(e+1);
+      memcpy(t->name, keystring, len);
+      t->data.ptr = e;
+      tree_insertnode(&c->item_cache, t);
+      }
+      /* Else previous, out-of-date cache entry.  Update with the */
+      /* new result and forget the old one */
+    e->expiry = do_cache == UINT_MAX ? 0 : time(NULL)+do_cache;
+    e->opts = opts ? string_copy(opts) : NULL;
+    e->data.ptr = data;
     }
 
-  /* If caching was disabled, empty the cache tree. We just set the cache
-  pointer to NULL here, because we cannot release the store at this stage. */
+/* If caching was disabled, empty the cache tree. We just set the cache
+pointer to NULL here, because we cannot release the store at this stage. */
 
   else
     {
-    DEBUG(D_lookup) debug_printf("lookup forced cache cleanup\n");
-    c->item_cache = NULL;
+    DEBUG(D_lookup) debug_printf_indent("lookup forced cache cleanup\n");
+    c->item_cache = NULL;      /* forget all lookups on this connection */
     }
   }
 
-/* Data was in the cache already; set the pointer from the tree node */
-
-else
-  {
-  data = US t->data.ptr;
-  DEBUG(D_lookup) debug_printf("cached data used for lookup of %s%s%s\n",
-    keystring,
-    (filename == NULL)? US"" : US"\n  in ",
-    (filename == NULL)? US"" : filename);
-  }
-
-/* Debug: output the answer */
-
 DEBUG(D_lookup)
   {
-  if (data == NULL)
-    {
-    if (search_find_defer) debug_printf("lookup deferred: %s\n",
-      search_error_message);
-    else debug_printf("lookup failed\n");
-    }
-  else debug_printf("lookup yielded: %s\n", data);
+  if (data)
+    debug_printf_indent("lookup yielded: %s\n", data);
+  else if (f.search_find_defer)
+    debug_printf_indent("lookup deferred: %s\n", search_error_message);
+  else debug_printf_indent("lookup failed\n");
   }
 
 /* Return it in new dynamic store in the regular pool */
 
 store_pool = old_pool;
-return (data == NULL)? NULL : string_copy(data);
+return data ? string_copy(data) : NULL;
 }
 
 
@@ -587,6 +707,7 @@ Arguments:
   starflags      SEARCH_STAR and SEARCH_STARAT flags
   expand_setup   pointer to offset for setting up expansion strings;
                  don't do any if < 0
+  opts          type-specific options
 
 Returns:         a pointer to a dynamic string containing the answer,
                  or NULL if the query failed or was deferred; in the
@@ -594,27 +715,47 @@ Returns:         a pointer to a dynamic string containing the answer,
 */
 
 uschar *
-search_find(void *handle, uschar *filename, uschar *keystring, int partial,
-  uschar *affix, int affixlen, int starflags, int *expand_setup)
+search_find(void * handle, const uschar * filename, uschar * keystring,
+  int partial, const uschar * affix, int affixlen, int starflags,
+  int * expand_setup, const uschar * opts)
 {
-tree_node *t = (tree_node *)handle;
-BOOL set_null_wild = FALSE;
-uschar *yield;
+tree_node * t = (tree_node *)handle;
+BOOL set_null_wild = FALSE, cache_rd = TRUE, ret_key = FALSE;
+uschar * yield;
 
 DEBUG(D_lookup)
   {
   if (partial < 0) affixlen = 99;   /* So that "NULL" prints */
-  debug_printf("search_find: file=\"%s\"\n  key=\"%s\" "
-    "partial=%d affix=%.*s starflags=%x\n",
-    (filename == NULL)? US"NULL" : filename,
-    keystring, partial, affixlen, affix, starflags);
+  debug_printf_indent("search_find: file=\"%s\"\n  key=\"%s\" "
+    "partial=%d affix=%.*s starflags=%x opts=%s%s%s\n",
+    filename ? filename : US"NULL",
+    keystring, partial, affixlen, affix, starflags,
+    opts ? "\"" : "", opts, opts ? "\"" : "");
+
+  }
+
+/* Parse global lookup options. Also, create a new options list with
+the global options dropped so that the cache-modifiers are not
+used in the cache key. */
+
+if (opts)
+  {
+  int sep = ',';
+  gstring * g = NULL;
+
+  for (uschar * ele; ele = string_nextinlist(&opts, &sep, NULL, 0); )
+    if (Ustrcmp(ele, "ret=key") == 0) ret_key = TRUE;
+    else if (Ustrcmp(ele, "cache=no_rd") == 0) cache_rd = FALSE;
+    else g = string_append_listele(g, ',', ele);
+
+  opts = string_from_gstring(g);
   }
 
 /* Arrange to put this database at the top of the LRU chain if it is a type
 that opens real files. */
 
-if (open_top != (tree_node *)handle &&
-    lookup_list[t->name[0]-'0'].type == lookup_absfile)
+if (  open_top != (tree_node *)handle 
+   && lookup_list[t->name[0]-'0']->type == lookup_absfile)
   {
   search_cache *c = (search_cache *)(t->data.ptr);
   tree_node *up = c->up;
@@ -624,32 +765,32 @@ if (open_top != (tree_node *)handle &&
   Otherwise there will be a non-NULL up pointer, since we checked above that
   this block isn't already at the top of the list. */
 
-  if (up != NULL)
+  if (up)
     {
     ((search_cache *)(up->data.ptr))->down = down;
-    if (down != NULL)
+    if (down)
       ((search_cache *)(down->data.ptr))->up = up;
-    else open_bot = up;
+    else
+      open_bot = up;
     }
 
   /* Now put it at the head of the list. */
 
   c->up = NULL;
   c->down = open_top;
-  if (open_top == NULL) open_bot = t; else
-    ((search_cache *)(open_top->data.ptr))->up = t;
+  if (!open_top) open_bot = t;
+  else ((search_cache *)(open_top->data.ptr))->up = t;
   open_top = t;
   }
 
 DEBUG(D_lookup)
   {
-  tree_node *t = open_top;
-  debug_printf("LRU list:\n");
-  while (t != NULL)
+  debug_printf_indent("LRU list:\n");
+  for (tree_node *t = open_top; t; )
     {
     search_cache *c = (search_cache *)(t->data.ptr);
-    debug_printf("  %s\n", t->name);
-    if (t == open_bot) debug_printf("  End\n");
+    debug_printf_indent("  %s\n", t->name);
+    if (t == open_bot) debug_printf_indent("  End\n");
     t = c->down;
     }
   }
@@ -657,9 +798,10 @@ DEBUG(D_lookup)
 /* First of all, try to match the key string verbatim. If matched a complete
 entry but could have been partial, flag to set up variables. */
 
-yield = internal_search_find(handle, filename, keystring);
-if (search_find_defer) return NULL;
-if (yield != NULL) { if (partial >= 0) set_null_wild = TRUE; }
+yield = internal_search_find(handle, filename, keystring, cache_rd, opts);
+if (f.search_find_defer) return NULL;
+
+if (yield) { if (partial >= 0) set_null_wild = TRUE; }
 
 /* Not matched a complete entry; handle partial lookups, but only if the full
 search didn't defer. Don't use string_sprintf() to construct the initial key,
@@ -676,18 +818,19 @@ else if (partial >= 0)
 
   if (affixlen == 0) keystring2 = keystring; else
     {
-    keystring2 = store_get(len + affixlen + 1);
+    keystring2 = store_get(len + affixlen + 1,
+         is_tainted(keystring) || is_tainted(affix) ? GET_TAINTED : GET_UNTAINTED);
     Ustrncpy(keystring2, affix, affixlen);
     Ustrcpy(keystring2 + affixlen, keystring);
-    DEBUG(D_lookup) debug_printf("trying partial match %s\n", keystring2);
-    yield = internal_search_find(handle, filename, keystring2);
-    if (search_find_defer) return NULL;
+    DEBUG(D_lookup) debug_printf_indent("trying partial match %s\n", keystring2);
+    yield = internal_search_find(handle, filename, keystring2, cache_rd, opts);
+    if (f.search_find_defer) return NULL;
     }
 
   /* The key in its entirety did not match a wild entry; try chopping off
   leading components. */
 
-  if (yield == NULL)
+  if (!yield)
     {
     int dotcount = 0;
     uschar *keystring3 = keystring2 + affixlen;
@@ -717,15 +860,16 @@ else if (partial >= 0)
         if (affixlen > 0) Ustrncpy(keystring3, affix, affixlen);
         }
 
-      DEBUG(D_lookup) debug_printf("trying partial match %s\n", keystring3);
-      yield = internal_search_find(handle, filename, keystring3);
-      if (search_find_defer) return NULL;
-      if (yield != NULL)
+      DEBUG(D_lookup) debug_printf_indent("trying partial match %s\n", keystring3);
+      yield = internal_search_find(handle, filename, keystring3,
+               cache_rd, opts);
+      if (f.search_find_defer) return NULL;
+      if (yield)
         {
         /* First variable is the wild part; second is the fixed part. Take care
         to get it right when keystring3 is just "*". */
 
-        if (expand_setup != NULL && *expand_setup >= 0)
+        if (expand_setup && *expand_setup >= 0)
           {
           int fixedlength = Ustrlen(keystring3) - affixlen;
           int wildlength = Ustrlen(keystring) - fixedlength - 1;
@@ -746,10 +890,10 @@ else if (partial >= 0)
   }
 
 /* If nothing has been matched, but the option to look for "*@" is set, try
-replacing everthing to the left of @ by *. After a match, the wild part
+replacing everything to the left of @ by *. After a match, the wild part
 is set to the string to the left of the @. */
 
-if (yield == NULL && (starflags & SEARCH_STARAT) != 0)
+if (!yield  &&  starflags & SEARCH_STARAT)
   {
   uschar *atat = Ustrrchr(keystring, '@');
   if (atat != NULL && atat > keystring)
@@ -758,12 +902,12 @@ if (yield == NULL && (starflags & SEARCH_STARAT) != 0)
     savechar = *(--atat);
     *atat = '*';
 
-    DEBUG(D_lookup) debug_printf("trying default match %s\n", atat);
-    yield = internal_search_find(handle, filename, atat);
+    DEBUG(D_lookup) debug_printf_indent("trying default match %s\n", atat);
+    yield = internal_search_find(handle, filename, atat, cache_rd, opts);
     *atat = savechar;
-    if (search_find_defer) return NULL;
+    if (f.search_find_defer) return NULL;
 
-    if (yield != NULL && expand_setup != NULL && *expand_setup >= 0)
+    if (yield && expand_setup && *expand_setup >= 0)
       {
       *expand_setup += 1;
       expand_nstring[*expand_setup] = keystring;
@@ -779,11 +923,11 @@ if (yield == NULL && (starflags & SEARCH_STARAT) != 0)
 try that. If we do match, the first variable (the wild part) is the whole key,
 and the second is empty. */
 
-if (yield == NULL && (starflags & (SEARCH_STAR|SEARCH_STARAT)) != 0)
+if (!yield  &&  starflags & (SEARCH_STAR|SEARCH_STARAT))
   {
-  DEBUG(D_lookup) debug_printf("trying to match *\n");
-  yield = internal_search_find(handle, filename, US"*");
-  if (yield != NULL && expand_setup != NULL && *expand_setup >= 0)
+  DEBUG(D_lookup) debug_printf_indent("trying to match *\n");
+  yield = internal_search_find(handle, filename, US"*", cache_rd, opts);
+  if (yield && expand_setup && *expand_setup >= 0)
     {
     *expand_setup += 1;
     expand_nstring[*expand_setup] = keystring;
@@ -801,7 +945,7 @@ chopping off any of the domain components, set up the expansion variables
 fixed part of the domain. The set_null_wild flag is set only when yield is not
 NULL. */
 
-if (set_null_wild && expand_setup != NULL && *expand_setup >= 0)
+if (set_null_wild && expand_setup && *expand_setup >= 0)
   {
   *expand_setup += 1;
   expand_nstring[*expand_setup] = keystring;
@@ -811,6 +955,13 @@ if (set_null_wild && expand_setup != NULL && *expand_setup >= 0)
   expand_nlength[*expand_setup] = Ustrlen(keystring);
   }
 
+/* If we have a result, check the options to see if the key was wanted rather
+than the result.  Return a de-tainted version of the key on the grounds that
+it have been validated by the lookup. */
+
+if (yield && ret_key)
+  yield = string_copy_taint(keystring, GET_UNTAINTED);
+
 return yield;
 }