resolv: Lift domain search list limits [BZ #19569] [BZ #21475]
diff mbox

Message ID 20170630193737.8D4F2439942F0@oldenburg.str.redhat.com
State New
Headers show

Commit Message

Florian Weimer June 30, 2017, 7:37 p.m. UTC
This change uses the extended resolver state in struct resolv_conf to
store the search list.  If applications have not patched the _res
object directly, this extended search list will be used by the stub
resolver during name resolution.

2017-06-30  Florian Weimer  <fweimer@redhat.com>

	[BZ #19569]
	Support an arbitrary number of search domains.
	* resolv/resolv_context.h (__resolv_context_search_list): New.
	* resolv/resolv_conf.h (struct resolv_conf): Add search_list,
	search_list_size members.
	* resolv/resolv_conf.c (resolv_conf_matches): Compare search list.
	(__resolv_conf_allocate): Allocate and and copy search list.
	(update_from_conf): Copy the search list.
	* resolv/res_init.c (struct search_list): Define using dynarray.
	(struct resolv_conf_parser): Define.
	(resolv_conf_parser_init, resolv_conf_parser_free)
	(domain_from_hostname): New functions.
	(res_vinit_1): Add struct resolv_conf_parser * parameter.  Use
	struct search_list to collect search list entries.  Call
	domain_from_hostname to obtain the fallback domain name.
	(__res_vinit): Create and destroy parser object.  Pass search list
	to __resolv_conf_allocate.
	* resolv/res_query.c (__res_context_search): Use
	__resolv_context_search_list to obtain search list entries.
	* resolv/tst-resolv-res_init-skeleton.c (print_resp): Print data
	from extended resolver context.
	(test_cases): Update.

Patch
diff mbox

diff --git a/NEWS b/NEWS
index df6f394..bf2b5df 100644
--- a/NEWS
+++ b/NEWS
@@ -237,6 +237,14 @@  Version 2.26
 * The _res_opcodes variable has been removed from libresolv.  It had been
   exported by accident.
 
+* The glibc DNS stub resolver now supports an arbitary number of search
+  domains (configured using the “search” directive in /etc/resolv.conf).
+  Most applications will automatically benefit from this change, but for
+  backwards compatibility reasons, applications which directly modify _res
+  objects (which contain the resolver state, including the search list
+  array, which is limited to six entries) will only use the first six search
+  domains, as before.
+
 Security related changes:
 
 * The DNS stub resolver limits the advertised UDP buffer size to 1200 bytes,
diff --git a/resolv/res_init.c b/resolv/res_init.c
index 659d3ea..5941d37 100644
--- a/resolv/res_init.c
+++ b/resolv/res_init.c
@@ -124,18 +124,75 @@  is_sort_mask (char ch)
   return ch == '/' || ch == '&';
 }
 
+/* Array of strings for the search array.  The backing store is
+   managed separately.  */
+#define DYNARRAY_STRUCT search_list
+#define DYNARRAY_ELEMENT const char *
+#define DYNARRAY_INITIAL_SIZE 4
+#define DYNARRAY_PREFIX search_list_
+#include <malloc/dynarray-skeleton.c>
+
+/* resolv.conf parser state and results.  */
+struct resolv_conf_parser
+{
+  char *buffer;            /* Temporary buffer for reading lines.  */
+  char *search_list_store; /* Backing storage for search list entries.  */
+  struct search_list search_list; /* Points into search_list_store.  */
+};
+
+static void
+resolv_conf_parser_init (struct resolv_conf_parser *parser)
+{
+  parser->buffer = NULL;
+  parser->search_list_store = NULL;
+  search_list_init (&parser->search_list);
+}
+
+static void
+resolv_conf_parser_free (struct resolv_conf_parser *parser)
+{
+  free (parser->buffer);
+  free (parser->search_list_store);
+  search_list_free (&parser->search_list);
+}
+
+/* Try to obtain the domain name from the host name and store it in
+   *RESULT.  Return false on memory allocation failure.  If the domain
+   name cannot be determined for any other reason, write NULL to
+   *RESULT and return true.  */
+static bool
+domain_from_hostname (char **result)
+{
+  char buf[256];
+  /* gethostbyname may not terminate the buffer.  */
+  buf[sizeof (buf) - 1] = '\0';
+  if (__gethostname (buf, sizeof (buf) - 1) == 0)
+    {
+      char *dot = strchr (buf, '.');
+      if (dot != NULL)
+        {
+          *result = __strdup (dot + 1);
+          if (*result == NULL)
+            return false;
+          return true;
+        }
+    }
+  *result = NULL;
+  return true;
+}
+
 /* Internal helper function for __res_vinit, to aid with resource
    deallocation and error handling.  Return true on success, false on
    failure.  */
 static bool
-res_vinit_1 (res_state statp, bool preinit, FILE *fp, char **buffer)
+res_vinit_1 (res_state statp, bool preinit, FILE *fp,
+             struct resolv_conf_parser *parser)
 {
-  char *cp, **pp;
+  char *cp;
   size_t buffer_size = 0;
   int nserv = 0;    /* Number of nameservers read from file.  */
   bool have_serv6 = false;
   bool haveenv = false;
-  bool havesearch = false;
   int nsort = 0;
   char *net;
 
@@ -162,39 +219,40 @@  res_vinit_1 (res_state statp, bool preinit, FILE *fp, char **buffer)
   /* Allow user to override the local domain definition.  */
   if ((cp = getenv ("LOCALDOMAIN")) != NULL)
     {
-      strncpy (statp->defdname, cp, sizeof (statp->defdname) - 1);
-      statp->defdname[sizeof (statp->defdname) - 1] = '\0';
+      /* The code below splits the string in place.  */
+      cp = __strdup (cp);
+      if (cp == NULL)
+        return false;
+      free (parser->search_list_store);
+      parser->search_list_store = cp;
       haveenv = true;
 
+      /* The string will be truncated as needed below.  */
+      search_list_add (&parser->search_list, cp);
+
       /* Set search list to be blank-separated strings from rest of
          env value.  Permits users of LOCALDOMAIN to still have a
          search list, and anyone to set the one that they want to use
          as an individual (even more important now that the rfc1535
          stuff restricts searches).  */
-      cp = statp->defdname;
-      pp = statp->dnsrch;
-      *pp++ = cp;
-      for (int n = 0; *cp && pp < statp->dnsrch + MAXDNSRCH; cp++)
+      for (bool in_name = true; *cp != '\0'; cp++)
         {
           if (*cp == '\n')
-            break;
+            {
+              *cp = '\0';
+              break;
+            }
           else if (*cp == ' ' || *cp == '\t')
             {
-              *cp = 0;
-              n = 1;
+              *cp = '\0';
+              in_name = false;
             }
-          else if (n > 0)
+          else if (!in_name)
             {
-              *pp++ = cp;
-              n = 0;
-              havesearch = true;
+              search_list_add (&parser->search_list, cp);
+              in_name = true;
             }
         }
-      /* Null terminate last domain if there are excess.  */
-      while (*cp != '\0' && *cp != ' ' && *cp != '\t' && *cp != '\n')
-        cp++;
-      *cp = '\0';
-      *pp++ = 0;
     }
 
 #define MATCH(line, name)                       \
@@ -210,7 +268,7 @@  res_vinit_1 (res_state statp, bool preinit, FILE *fp, char **buffer)
       while (true)
         {
           {
-            ssize_t ret = __getline (buffer, &buffer_size, fp);
+            ssize_t ret = __getline (&parser->buffer, &buffer_size, fp);
             if (ret <= 0)
               {
                 if (_IO_ferror_unlocked (fp))
@@ -221,73 +279,82 @@  res_vinit_1 (res_state statp, bool preinit, FILE *fp, char **buffer)
           }
 
           /* Skip comments.  */
-          if (**buffer == ';' || **buffer == '#')
+          if (*parser->buffer == ';' || *parser->buffer == '#')
             continue;
           /* Read default domain name.  */
-          if (MATCH (*buffer, "domain"))
+          if (MATCH (parser->buffer, "domain"))
             {
               if (haveenv)
                 /* LOCALDOMAIN overrides the configuration file.  */
                 continue;
-              cp = *buffer + sizeof ("domain") - 1;
+              cp = parser->buffer + sizeof ("domain") - 1;
               while (*cp == ' ' || *cp == '\t')
                 cp++;
               if ((*cp == '\0') || (*cp == '\n'))
                 continue;
-              strncpy (statp->defdname, cp, sizeof (statp->defdname) - 1);
-              statp->defdname[sizeof (statp->defdname) - 1] = '\0';
-              if ((cp = strpbrk (statp->defdname, " \t\n")) != NULL)
+
+              cp = __strdup (cp);
+              if (cp == NULL)
+                return false;
+              free (parser->search_list_store);
+              parser->search_list_store = cp;
+              search_list_clear (&parser->search_list);
+              search_list_add (&parser->search_list, cp);
+              /* Replace trailing whitespace.  */
+              if ((cp = strpbrk (cp, " \t\n")) != NULL)
                 *cp = '\0';
-              havesearch = false;
               continue;
             }
           /* Set search list.  */
-          if (MATCH (*buffer, "search"))
+          if (MATCH (parser->buffer, "search"))
             {
               if (haveenv)
                 /* LOCALDOMAIN overrides the configuration file.  */
                 continue;
-              cp = *buffer + sizeof ("search") - 1;
+              cp = parser->buffer + sizeof ("search") - 1;
               while (*cp == ' ' || *cp == '\t')
                 cp++;
               if ((*cp == '\0') || (*cp == '\n'))
                 continue;
-              strncpy (statp->defdname, cp, sizeof (statp->defdname) - 1);
-              statp->defdname[sizeof (statp->defdname) - 1] = '\0';
-              if ((cp = strchr (statp->defdname, '\n')) != NULL)
-                *cp = '\0';
+
+              {
+                char *p = strchr (cp, '\n');
+                if (p != NULL)
+                  *p = '\0';
+              }
+              cp = __strdup (cp);
+              if (cp == NULL)
+                return false;
+              free (parser->search_list_store);
+              parser->search_list_store = cp;
+
+              /* The string is truncated below.  */
+              search_list_clear (&parser->search_list);
+              search_list_add (&parser->search_list, cp);
+
               /* Set search list to be blank-separated strings on rest
                  of line.  */
-              cp = statp->defdname;
-              pp = statp->dnsrch;
-              *pp++ = cp;
-              for (int n = 0; *cp && pp < statp->dnsrch + MAXDNSRCH; cp++)
+              for (bool in_name = true; *cp != '\0'; cp++)
                 {
                   if (*cp == ' ' || *cp == '\t')
                     {
-                      *cp = 0;
-                      n = 1;
+                      *cp = '\0';
+                      in_name = false;
                     }
-                  else if (n)
+                  else if (!in_name)
                     {
-                      *pp++ = cp;
-                      n = 0;
+                      search_list_add (&parser->search_list, cp);
+                      in_name = true;
                     }
                 }
-              /* Null terminate last domain if there are excess.  */
-              while (*cp != '\0' && *cp != ' ' && *cp != '\t')
-                cp++;
-              *cp = '\0';
-              *pp++ = 0;
-              havesearch = true;
               continue;
             }
           /* Read nameservers to query.  */
-          if (MATCH (*buffer, "nameserver") && nserv < MAXNS)
+          if (MATCH (parser->buffer, "nameserver") && nserv < MAXNS)
             {
               struct in_addr a;
 
-              cp = *buffer + sizeof ("nameserver") - 1;
+              cp = parser->buffer + sizeof ("nameserver") - 1;
               while (*cp == ' ' || *cp == '\t')
                 cp++;
               if ((*cp != '\0') && (*cp != '\n') && __inet_aton (cp, &a))
@@ -335,11 +402,11 @@  res_vinit_1 (res_state statp, bool preinit, FILE *fp, char **buffer)
                 }
               continue;
             }
-          if (MATCH (*buffer, "sortlist"))
+          if (MATCH (parser->buffer, "sortlist"))
             {
               struct in_addr a;
 
-              cp = *buffer + sizeof ("sortlist") - 1;
+              cp = parser->buffer + sizeof ("sortlist") - 1;
               while (nsort < MAXRESOLVSORT)
                 {
                   while (*cp == ' ' || *cp == '\t')
@@ -379,9 +446,9 @@  res_vinit_1 (res_state statp, bool preinit, FILE *fp, char **buffer)
                 }
               continue;
             }
-          if (MATCH (*buffer, "options"))
+          if (MATCH (parser->buffer, "options"))
             {
-              res_setoptions (statp, *buffer + sizeof ("options") - 1);
+              res_setoptions (statp, parser->buffer + sizeof ("options") - 1);
               continue;
             }
         }
@@ -399,25 +466,29 @@  res_vinit_1 (res_state statp, bool preinit, FILE *fp, char **buffer)
       statp->nsaddr.sin_port = htons (NAMESERVER_PORT);
       statp->nscount = 1;
     }
-  if (statp->defdname[0] == 0)
-    {
-      char buf[sizeof (statp->defdname)];
-      if (__gethostname (buf, sizeof (statp->defdname) - 1) == 0
-          && (cp = strchr (buf, '.')) != NULL)
-        strcpy (statp->defdname, cp + 1);
-    }
 
-  /* Find components of local domain that might be searched.  */
-  if (!havesearch)
+  if (search_list_size (&parser->search_list) == 0)
     {
-      pp = statp->dnsrch;
-      *pp++ = statp->defdname;
-      *pp = NULL;
-
+      char *domain;
+      if (!domain_from_hostname (&domain))
+        return false;
+      if (domain != NULL)
+        {
+          free (parser->search_list_store);
+          parser->search_list_store = domain;
+          search_list_add (&parser->search_list, domain);
+        }
     }
 
   if ((cp = getenv ("RES_OPTIONS")) != NULL)
     res_setoptions (statp, cp);
+
+  if (search_list_has_failed (&parser->search_list))
+    {
+      __set_errno (ENOMEM);
+      return false;
+    }
+
   statp->options |= RES_INIT;
   return true;
 }
@@ -453,13 +524,17 @@  __res_vinit (res_state statp, int preinit)
         return -1;
       }
 
-  char *buffer = NULL;
-  bool ok = res_vinit_1 (statp, preinit, fp, &buffer);
-  free (buffer);
+  struct resolv_conf_parser parser;
+  resolv_conf_parser_init (&parser);
+  bool ok = res_vinit_1 (statp, preinit, fp, &parser);
 
   if (ok)
     {
-      struct resolv_conf init = { 0 }; /* No data yet.  */
+      struct resolv_conf init =
+        {
+          .search_list = search_list_begin (&parser.search_list),
+          .search_list_size = search_list_size (&parser.search_list),
+        };
       struct resolv_conf *conf = __resolv_conf_allocate (&init);
       if (conf == NULL)
         ok = false;
@@ -469,6 +544,7 @@  __res_vinit (res_state statp, int preinit)
           __resolv_conf_put (conf);
         }
     }
+  resolv_conf_parser_free (&parser);
 
   if (!ok)
     {
diff --git a/resolv/res_query.c b/resolv/res_query.c
index 33249e3..ebbe5a6 100644
--- a/resolv/res_query.c
+++ b/resolv/res_query.c
@@ -326,7 +326,7 @@  __res_context_search (struct resolv_context *ctx,
 		      int *nanswerp2, int *resplen2, int *answerp2_malloced)
 {
 	struct __res_state *statp = ctx->resp;
-	const char *cp, * const *domain;
+	const char *cp;
 	HEADER *hp = (HEADER *) answer;
 	char tmp[NS_MAXDNAME];
 	u_int dots;
@@ -392,10 +392,11 @@  __res_context_search (struct resolv_context *ctx,
 	    (dots && !trailing_dot && (statp->options & RES_DNSRCH) != 0)) {
 		int done = 0;
 
-		for (domain = (const char * const *)statp->dnsrch;
-		     *domain && !done;
-		     domain++) {
-			const char *dname = domain[0];
+		for (size_t domain_index = 0; !done; ++domain_index) {
+			const char *dname = __resolv_context_search_list
+			  (ctx, domain_index);
+			if (dname == NULL)
+			  break;
 			searched = 1;
 
 			/* __res_context_querydoman concatenates name
diff --git a/resolv/resolv_conf.c b/resolv/resolv_conf.c
index ebf6e5e..3a730ff 100644
--- a/resolv/resolv_conf.c
+++ b/resolv/resolv_conf.c
@@ -111,6 +111,34 @@  static bool
 resolv_conf_matches (const struct __res_state *resp,
                      const struct resolv_conf *conf)
 {
+  /* Check that the search list in *RESP has not been modified by the
+     application.  */
+  {
+    if (!(resp->dnsrch[0] == resp->defdname
+          && resp->dnsrch[MAXDNSRCH] == NULL))
+      return false;
+    size_t search_list_size = 0;
+    for (size_t i = 0; i < conf->search_list_size; ++i)
+      {
+        if (resp->dnsrch[i] != NULL)
+          {
+            search_list_size += strlen (resp->dnsrch[i]) + 1;
+            if (strcmp (resp->dnsrch[i], conf->search_list[i]) != 0)
+              return false;
+          }
+        else
+          {
+            /* resp->dnsrch is truncated if the number of elements
+               exceeds MAXDNSRCH, or if the combined storage space for
+               the search list exceeds what can be stored in
+               resp->defdname.  */
+            if (i == MAXDNSRCH || search_list_size > sizeof (resp->dnsrch))
+              break;
+            /* Otherwise, a mismatch indicates a match failure.  */
+            return false;
+          }
+      }
+  }
   return true;
 }
 
@@ -144,10 +172,17 @@  __resolv_conf_put (struct resolv_conf *conf)
 struct resolv_conf *
 __resolv_conf_allocate (const struct resolv_conf *init)
 {
+  /* Space needed by the strings.  */
+  size_t string_space = 0;
+  for (size_t i = 0; i < init->search_list_size; ++i)
+    string_space += strlen (init->search_list[i]) + 1;
+
   /* Allocate the buffer.  */
   void *ptr;
   struct alloc_buffer buffer = alloc_buffer_allocate
-    (sizeof (struct resolv_conf),
+    (sizeof (struct resolv_conf)
+     + init->search_list_size * sizeof (init->search_list[0])
+     + string_space,
      &ptr);
   struct resolv_conf *conf
     = alloc_buffer_alloc (&buffer, struct resolv_conf);
@@ -160,6 +195,16 @@  __resolv_conf_allocate (const struct resolv_conf *init)
   conf->__refcount = 1;
   conf->initstamp = __res_initstamp;
 
+  /* Allocate and fill the search list array.  */
+  {
+    conf->search_list_size = init->search_list_size;
+    const char **array = alloc_buffer_alloc_array
+      (&buffer, const char *, init->search_list_size);
+    conf->search_list = array;
+    for (size_t i = 0; i < init->search_list_size; ++i)
+      array[i] = alloc_buffer_copy_string (&buffer, init->search_list[i]);
+  }
+
   assert (!alloc_buffer_has_failed (&buffer));
   return conf;
 }
@@ -168,6 +213,24 @@  __resolv_conf_allocate (const struct resolv_conf *init)
 static __attribute__ ((nonnull (1, 2), warn_unused_result)) bool
 update_from_conf (struct __res_state *resp, const struct resolv_conf *conf)
 {
+  /* Fill in the prefix of the search list.  It is truncated either at
+     MAXDNSRCH, or if reps->defdname has insufficient space.  */
+  {
+    struct alloc_buffer buffer
+      = alloc_buffer_create (resp->defdname, sizeof (resp->defdname));
+    size_t size = conf->search_list_size;
+    size_t i;
+    for (i = 0; i < size && i < MAXDNSRCH; ++i)
+      {
+        resp->dnsrch[i] = alloc_buffer_copy_string
+          (&buffer, conf->search_list[i]);
+        if (resp->dnsrch[i] == NULL)
+          /* No more space in resp->defdname.  Truncate.  */
+          break;
+      }
+    resp->dnsrch[i] = NULL;
+  }
+
   /* The overlapping parts of both configurations should agree after
      initialization.  */
   assert (resolv_conf_matches (resp, conf));
diff --git a/resolv/resolv_conf.h b/resolv/resolv_conf.h
index 48f92d6..80a0b93 100644
--- a/resolv/resolv_conf.h
+++ b/resolv/resolv_conf.h
@@ -35,6 +35,10 @@  struct resolv_conf
   /* Reference counter.  The object is deallocated once it reaches
      zero.  For internal use within resolv_conf only.  */
   size_t __refcount;
+
+  /* The domain names forming the search list.  */
+  const char *const *search_list;
+  size_t search_list_size;
 };
 
 /* The functions below are for use by the res_init resolv.conf parser
diff --git a/resolv/resolv_context.h b/resolv/resolv_context.h
index ff9ef2c..0f4d47d 100644
--- a/resolv/resolv_context.h
+++ b/resolv/resolv_context.h
@@ -93,6 +93,25 @@  struct resolv_context *__resolv_context_get_override (struct __res_state *)
   __attribute__ ((nonnull (1), warn_unused_result));
 libc_hidden_proto (__resolv_context_get_override)
 
+/* Return the search path entry at INDEX, or NULL if there are fewer
+   than INDEX entries.  */
+static __attribute__ ((nonnull (1), unused)) const char *
+__resolv_context_search_list (const struct resolv_context *ctx, size_t index)
+{
+  if (ctx->conf != NULL)
+    {
+      if (index < ctx->conf->search_list_size)
+        return ctx->conf->search_list[index];
+      else
+        return NULL;
+    }
+  /* Fallback.  ctx->resp->dnsrch is a NULL-terminated array.  */
+  for (size_t i = 0; ctx->resp->dnsrch[i] != NULL && i < MAXDNSRCH; ++i)
+    if (i == index)
+      return ctx->resp->dnsrch[i];
+  return NULL;
+}
+
 /* Called during thread shutdown to free the associated resolver
    context (mostly in response to cancellation, otherwise the
    __resolv_context_get/__resolv_context_put pairing will already have
diff --git a/resolv/tst-resolv-res_init-skeleton.c b/resolv/tst-resolv-res_init-skeleton.c
index b5fe2cf..d377a6b 100644
--- a/resolv/tst-resolv-res_init-skeleton.c
+++ b/resolv/tst-resolv-res_init-skeleton.c
@@ -25,6 +25,7 @@ 
 #include <gnu/lib-names.h>
 #include <netdb.h>
 #include <resolv/resolv-internal.h> /* For DEPRECATED_RES_USE_INET6.  */
+#include <resolv/resolv_context.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <support/capture_subprocess.h>
@@ -116,6 +117,11 @@  print_option_flag (FILE *fp, int *options, int flag, const char *name)
 static void
 print_resp (FILE *fp, res_state resp)
 {
+  struct resolv_context *ctx = __resolv_context_get_override (resp);
+  TEST_VERIFY_EXIT (ctx != NULL);
+  if (ctx->conf == NULL)
+    fprintf (fp, "; extended resolver state missing\n");
+
   /* The options directive.  */
   {
     /* RES_INIT is used internally for tracking initialization.  */
@@ -165,6 +171,19 @@  print_resp (FILE *fp, res_state resp)
   else if (resp->defdname[0] != '\0')
     fprintf (fp, "domain %s\n", resp->defdname);
 
+  /* The extended search path.  */
+  {
+    size_t i = 0;
+    while (true)
+      {
+        const char *name = __resolv_context_search_list (ctx, i);
+        if (name == NULL)
+          break;
+        fprintf (fp, "; search[%zu]: %s\n", i, name);
+        ++i;
+      }
+  }
+
   /* The sortlist directive.  */
   if (resp->nsort > 0)
     {
@@ -224,6 +243,8 @@  print_resp (FILE *fp, res_state resp)
     }
 
   TEST_VERIFY (!ferror (fp));
+
+  __resolv_context_put (ctx);
 }
 
 /* Parameters of one test case.  */
@@ -368,11 +389,13 @@  struct test_case test_cases[] =
     {.name = "empty file",
      .conf = "",
      .expected = "search example.com\n"
+     "; search[0]: example.com\n"
      "nameserver 127.0.0.1\n"
     },
     {.name = "empty file with LOCALDOMAIN",
      .conf = "",
      .expected = "search example.net\n"
+     "; search[0]: example.net\n"
      "nameserver 127.0.0.1\n",
      .localdomain = "example.net",
     },
@@ -380,6 +403,7 @@  struct test_case test_cases[] =
      .conf = "",
      .expected = "options attempts:5 edns0\n"
      "search example.com\n"
+     "; search[0]: example.com\n"
      "nameserver 127.0.0.1\n",
      .res_options = "edns0 attempts:5",
     },
@@ -387,6 +411,7 @@  struct test_case test_cases[] =
      .conf = "",
      .expected = "options attempts:5 edns0\n"
      "search example.org\n"
+     "; search[0]: example.org\n"
      "nameserver 127.0.0.1\n",
      .localdomain = "example.org",
      .res_options = "edns0 attempts:5",
@@ -396,6 +421,8 @@  struct test_case test_cases[] =
      "search corp.example.com example.com\n"
      "nameserver 192.0.2.1\n",
      .expected = "search corp.example.com example.com\n"
+     "; search[0]: corp.example.com\n"
+     "; search[1]: example.com\n"
      "nameserver 192.0.2.1\n"
     },
     {.name = "whitespace",
@@ -403,16 +430,46 @@  struct test_case test_cases[] =
      " (trailing whitespace,\n"
      "# missing newline at end of file).\n"
      "\n"
-     "domain  example.net\n"
      ";search commented out\n"
-     "search corp.example.com\texample.com\n"
+     "search corp.example.com\texample.com \n"
      "#nameserver 192.0.2.3\n"
      "nameserver 192.0.2.1 \n"
      "nameserver 192.0.2.2",    /* No \n at end of file.  */
      .expected = "search corp.example.com example.com\n"
+     "; search[0]: corp.example.com\n"
+     "; search[1]: example.com\n"
      "nameserver 192.0.2.1\n"
      "nameserver 192.0.2.2\n"
     },
+    {.name = "domain",
+     .conf = "domain example.net\n"
+     "nameserver 192.0.2.1\n",
+     .expected = "search example.net\n"
+     "; search[0]: example.net\n"
+     "nameserver 192.0.2.1\n"
+    },
+    {.name = "domain space",
+     .conf = "domain example.net \n"
+     "nameserver 192.0.2.1\n",
+     .expected = "search example.net\n"
+     "; search[0]: example.net\n"
+     "nameserver 192.0.2.1\n"
+    },
+    {.name = "domain tab",
+     .conf = "domain example.net\t\n"
+     "nameserver 192.0.2.1\n",
+     .expected = "search example.net\n"
+     "; search[0]: example.net\n"
+     "nameserver 192.0.2.1\n"
+    },
+    {.name = "domain override",
+     .conf = "search example.com example.org\n"
+     "nameserver 192.0.2.1\n"
+     "domain example.net",      /* No \n at end of file.  */
+     .expected = "search example.net\n"
+     "; search[0]: example.net\n"
+     "nameserver 192.0.2.1\n"
+    },
     {.name = "option values, multiple servers",
      .conf = "options\tinet6\tndots:3 edns0\tattempts:5\ttimeout:19\n"
      "domain  example.net\n"
@@ -423,6 +480,8 @@  struct test_case test_cases[] =
      "nameserver 192.0.2.2\n",
      .expected = "options ndots:3 timeout:19 attempts:5 inet6 edns0\n"
      "search corp.example.com example.com\n"
+     "; search[0]: corp.example.com\n"
+     "; search[1]: example.com\n"
      "nameserver 192.0.2.1\n"
      "nameserver ::1\n"
      "nameserver 192.0.2.2\n"
@@ -432,6 +491,7 @@  struct test_case test_cases[] =
      "search example.com\n",
      .expected = "options ndots:15 timeout:30 attempts:5 use-vc\n"
      "search example.com\n"
+     "; search[0]: example.com\n"
      "nameserver 127.0.0.1\n"
     },
     {.name = "repeated directives",
@@ -443,6 +503,7 @@  struct test_case test_cases[] =
      "search\n",
      .expected = "options ndots:2 use-vc edns0\n"
      "search example.org\n"
+     "; search[0]: example.org\n"
      "nameserver 127.0.0.1\n"
     },
     {.name = "many name servers, sortlist",
@@ -459,6 +520,10 @@  struct test_case test_cases[] =
      "nameserver 192.0.2.8\n",
      .expected = "options single-request\n"
      "search example.org example.com example.net corp.example.com\n"
+     "; search[0]: example.org\n"
+     "; search[1]: example.com\n"
+     "; search[2]: example.net\n"
+     "; search[3]: corp.example.com\n"
      "sortlist 192.0.2.0/255.255.255.0\n"
      "nameserver 192.0.2.1\n"
      "nameserver 192.0.2.2\n"
@@ -480,6 +545,11 @@  struct test_case test_cases[] =
      .expected = "options single-request\n"
      "search example.org example.com example.net corp.example.com"
      " legacy.example.com\n"
+     "; search[0]: example.org\n"
+     "; search[1]: example.com\n"
+     "; search[2]: example.net\n"
+     "; search[3]: corp.example.com\n"
+     "; search[4]: legacy.example.com\n"
      "sortlist 192.0.2.0/255.255.255.0\n"
      "nameserver 192.0.2.1\n"
      "nameserver 2001:db8::2\n"
@@ -490,6 +560,7 @@  struct test_case test_cases[] =
      "nameserver 192.0.2.2:5353\n"
      "nameserver 192.0.2.3 5353\n",
      .expected = "search example.com\n"
+     "; search[0]: example.com\n"
      "nameserver 192.0.2.1\n"
      "nameserver 192.0.2.3\n"
     },
@@ -498,9 +569,42 @@  struct test_case test_cases[] =
      "nameserver 192.0.2.1\n",
      .expected = "options ndots:3 timeout:7 attempts:5 use-vc edns0\n"
      "search example.com\n"
+     "; search[0]: example.com\n"
      "nameserver 192.0.2.1\n",
      .res_options = "attempts:5 ndots:3 edns0 ",
     },
+    {.name = "many search list entries (bug 19569)",
+     .conf = "nameserver 192.0.2.1\n"
+     "search corp.example.com support.example.com"
+     " community.example.org wan.example.net vpn.example.net"
+     " example.com example.org example.net\n",
+     .expected = "search corp.example.com support.example.com"
+     " community.example.org wan.example.net vpn.example.net example.com\n"
+     "; search[0]: corp.example.com\n"
+     "; search[1]: support.example.com\n"
+     "; search[2]: community.example.org\n"
+     "; search[3]: wan.example.net\n"
+     "; search[4]: vpn.example.net\n"
+     "; search[5]: example.com\n"
+     "; search[6]: example.org\n"
+     "; search[7]: example.net\n"
+     "nameserver 192.0.2.1\n",
+    },
+    {.name = "very long search list entries (bug 21475)",
+     .conf = "nameserver 192.0.2.1\n"
+     "search example.com "
+#define H63 "this-host-name-is-longer-than-yours-yes-I-really-really-mean-it"
+#define D63 "this-domain-name-is-as-long-as-the-previous-name--63-characters"
+     " " H63 "." D63 ".example.org"
+     " " H63 "." D63 ".example.net\n",
+     .expected = "search example.com " H63 "." D63 ".example.org\n"
+     "; search[0]: example.com\n"
+     "; search[1]: " H63 "." D63 ".example.org\n"
+     "; search[2]: " H63 "." D63 ".example.net\n"
+#undef H63
+#undef D63
+     "nameserver 192.0.2.1\n",
+    },
     { NULL }
   };