Handle validation when more one key is needed.
diff --git a/src/config.h b/src/config.h
index c0e699d..b564b8b 100644
--- a/src/config.h
+++ b/src/config.h
@@ -19,6 +19,7 @@
 #define CHILD_LIFETIME 150 /* secs 'till terminated (RFC1035 suggests > 120s) */
 #define EDNS_PKTSZ 4096 /* default max EDNS.0 UDP packet from RFC5625 */
 #define KEYBLOCK_LEN 35 /* choose to mininise fragmentation when storing DNSSEC keys */
+#define DNSSEC_WORK 50 /* Max number of queries to validate one question */
 #define TIMEOUT 10 /* drop UDP queries after TIMEOUT seconds */
 #define FORWARD_TEST 50 /* try all servers every 50 queries */
 #define FORWARD_TIME 20 /* or 20 seconds */
diff --git a/src/dnsmasq.h b/src/dnsmasq.h
index a84ac36..204c104 100644
--- a/src/dnsmasq.h
+++ b/src/dnsmasq.h
@@ -560,7 +560,7 @@
   time_t time;
   unsigned char *hash[HASH_SIZE];
 #ifdef HAVE_DNSSEC 
-  int class;
+  int class, work_counter;
   struct blockdata *stash; /* Saved reply, whilst we validate */
   size_t stash_len;
   struct frec *dependent; /* Query awaiting internally-generated DNSKEY or DS query */
diff --git a/src/forward.c b/src/forward.c
index 2088f98..5163e87 100644
--- a/src/forward.c
+++ b/src/forward.c
@@ -331,6 +331,9 @@
 	    forward->flags |= FREC_NOREBIND;
 	  if (header->hb4 & HB4_CD)
 	    forward->flags |= FREC_CHECKING_DISABLED;
+#ifdef HAVE_DNSSEC
+	  forward->work_counter = DNSSEC_WORK;
+#endif
 
 	  header->id = htons(forward->new_id);
 	  
@@ -772,15 +775,31 @@
 	    status = dnssec_validate_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class);
 	  else
 	    status = dnssec_validate_reply(now, header, n, daemon->namebuff, daemon->keyname, &forward->class);
-	    
+
 	  /* Can't validate, as we're missing key data. Put this
 	     answer aside, whilst we get that. */     
 	  if (status == STAT_NEED_DS || status == STAT_NEED_KEY)
 	    {
-	      struct frec *new;
+	      struct frec *new, *orig;
 	      
-	      if ((new = get_new_frec(now, NULL, 1)))
+	      /* Free any saved query */
+	      if (forward->stash)
+		blockdata_free(forward->stash);
+	      
+	      /* Now save reply pending receipt of key data */
+	      if (!(forward->stash = blockdata_alloc((char *)header, n)))
+		return;
+	      forward->stash_len = n;
+	      
+	    anotherkey:	      
+	      /* Find the original query that started it all.... */
+	      for (orig = forward; orig->dependent; orig = orig->dependent);
+
+	      if (--orig->work_counter == 0 || !(new = get_new_frec(now, NULL, 1)))
+		status = STAT_INSECURE;
+	      else
 		{
+		  int fd;
 		  struct frec *next = new->next;
 		  *new = *forward; /* copy everything, then overwrite */
 		  new->next = next;
@@ -791,80 +810,67 @@
 #endif
 		  new->flags &= ~(FREC_DNSKEY_QUERY | FREC_DS_QUERY);
 		  
-		  /* Free any saved query */
-		  if (forward->stash)
-		    blockdata_free(forward->stash);
+		  new->dependent = forward; /* to find query awaiting new one. */
+		  forward->blocking_query = new; /* for garbage cleaning */
+		  /* validate routines leave name of required record in daemon->keyname */
+		  if (status == STAT_NEED_KEY)
+		    {
+		      new->flags |= FREC_DNSKEY_QUERY; 
+		      nn = dnssec_generate_query(header, ((char *) header) + daemon->packet_buff_sz,
+						 daemon->keyname, forward->class, T_DNSKEY, &server->addr);
+		    }
+		  else 
+		    {
+		      new->flags |= FREC_DS_QUERY;
+		      nn = dnssec_generate_query(header,((char *) header) + daemon->packet_buff_sz,
+						 daemon->keyname, forward->class, T_DS, &server->addr);
+		    }
+		  if ((hash = hash_questions(header, nn, daemon->namebuff)))
+		    memcpy(new->hash, hash, HASH_SIZE);
+		  new->new_id = get_id();
+		  header->id = htons(new->new_id);
+		  /* Save query for retransmission */
+		  new->stash = blockdata_alloc((char *)header, nn);
+		  new->stash_len = nn;
 		  
-		  /* Now save reply pending receipt of key data */
-		  if (!(forward->stash = blockdata_alloc((char *)header, n)))
-		    free_frec(new); /* malloc failure, unwind */
+		  /* Don't resend this. */
+		  daemon->srv_save = NULL;
+		  
+		  if (server->sfd)
+		    fd = server->sfd->fd;
 		  else
 		    {
-		      int fd;
-		      
-		      forward->stash_len = n;
-		      
-		      new->dependent = forward; /* to find query awaiting new one. */
-		      forward->blocking_query = new; /* for garbage cleaning */
-		      /* validate routines leave name of required record in daemon->keyname */
-		      if (status == STAT_NEED_KEY)
-			{
-			  new->flags |= FREC_DNSKEY_QUERY; 
-			  nn = dnssec_generate_query(header, ((char *) header) + daemon->packet_buff_sz,
-						     daemon->keyname, forward->class, T_DNSKEY, &server->addr);
-			}
-		      else 
-			{
-			  new->flags |= FREC_DS_QUERY;
-			  nn = dnssec_generate_query(header,((char *) header) + daemon->packet_buff_sz,
-						     daemon->keyname, forward->class, T_DS, &server->addr);
-			}
-		      if ((hash = hash_questions(header, nn, daemon->namebuff)))
-			memcpy(new->hash, hash, HASH_SIZE);
-		      new->new_id = get_id();
-		      header->id = htons(new->new_id);
-		      /* Save query for retransmission */
-		      new->stash = blockdata_alloc((char *)header, nn);
-		      new->stash_len = nn;
-
-		      /* Don't resend this. */
-		      daemon->srv_save = NULL;
-	
-		      if (server->sfd)
-			fd = server->sfd->fd;
-		      else
-			{
-			  fd = -1;
+		      fd = -1;
 #ifdef HAVE_IPV6
-			  if (server->addr.sa.sa_family == AF_INET6)
-			    {
-			      if (new->rfd6 || (new->rfd6 = allocate_rfd(AF_INET6)))
-				fd = new->rfd6->fd;
-			    }
-			  else
-#endif
-			    {
-			      if (new->rfd4 || (new->rfd4 = allocate_rfd(AF_INET)))
-				fd = new->rfd4->fd;
-			    }
-			}
-		      
-		      if (fd != -1)
+		      if (server->addr.sa.sa_family == AF_INET6)
 			{
-			  while (sendto(fd, (char *)header, nn, 0, &server->addr.sa, sa_len(&server->addr)) == -1 && retry_send()); 
-			  server->queries++;
+			  if (new->rfd6 || (new->rfd6 = allocate_rfd(AF_INET6)))
+			    fd = new->rfd6->fd;
+			}
+		      else
+#endif
+			{
+			  if (new->rfd4 || (new->rfd4 = allocate_rfd(AF_INET)))
+			    fd = new->rfd4->fd;
 			}
 		    }
+		  
+		  if (fd != -1)
+		    {
+		      while (sendto(fd, (char *)header, nn, 0, &server->addr.sa, sa_len(&server->addr)) == -1 && retry_send()); 
+		      server->queries++;
+		    }
+		  
+		  return;
 		}
-	     
-	      return;
 	    }
 	  
 	  /* Ok, we reached far enough up the chain-of-trust that we can validate something.
 	     Now wind back down, pulling back answers which wouldn't previously validate
-	     and validate them with the new data. Failure to find needed data here is an internal error.
-	     Once we get to the original answer (FREC_DNSSEC_QUERY not set) and it validates,
-	     return it to the original requestor. */
+	     and validate them with the new data. Note that if an answer needs multiple
+	     keys to validate, we may find another key is needed, in which case we set off
+	     down another branch of the tree. Once we get to the original answer 
+	     (FREC_DNSSEC_QUERY not set) and it validates, return it to the original requestor. */
 	  while (forward->dependent)
 	    {
 	      struct frec *prev = forward->dependent;
@@ -884,18 +890,23 @@
 		    status = dnssec_validate_reply(now, header, n, daemon->namebuff, daemon->keyname, &forward->class);	
 		  
 		  if (status == STAT_NEED_DS || status == STAT_NEED_KEY)
-		    {
-		      my_syslog(LOG_ERR, _("Unexpected missing data for DNSSEC validation"));
-		      status = STAT_INSECURE;
-		    }
+		    goto anotherkey;
 		}
 	    }
 	  
 	  if (status == STAT_TRUNCATED)
 	    header->hb3 |= HB3_TC;
 	  else
-	    log_query(F_KEYTAG | F_SECSTAT, "result", NULL, 
-		      status == STAT_SECURE ? "SECURE" : (status == STAT_INSECURE ? "INSECURE" : "BOGUS"));
+	    {
+	      char *result;
+	      
+	      if (forward->work_counter == 0)
+		result = "ABANDONED";
+	      else
+		result = (status == STAT_SECURE ? "SECURE" : (status == STAT_INSECURE ? "INSECURE" : "BOGUS"));
+	      
+	      log_query(F_KEYTAG | F_SECSTAT, "result", NULL, result);
+	    }
 	  
 	  no_cache_dnssec = 0;
 	  
@@ -1173,60 +1184,73 @@
 }
 
 #ifdef HAVE_DNSSEC
-static int tcp_key_recurse(time_t now, int status, int class, char *keyname, struct server *server)
+static int tcp_key_recurse(time_t now, int status, struct dns_header *header, size_t n, 
+			   int class, char *name, char *keyname, struct server *server, int *keycount)
 {
   /* Recurse up the key heirarchy */
-  size_t n; 
-  unsigned char *packet = whine_malloc(65536 + MAXDNAME + RRFIXEDSZ + sizeof(u16));
-  unsigned char *payload = &packet[2];
-  struct dns_header *header = (struct dns_header *)payload;
-  u16 *length = (u16 *)packet;
   int new_status;
-  unsigned char c1, c2;
 
-  n = dnssec_generate_query(header, ((char *) header) + 65536, keyname, class, 
-			    status == STAT_NEED_KEY ? T_DNSKEY : T_DS, &server->addr);
+  /* limit the amount of work we do, to avoid cycling forever on loops in the DNS */
+  if (--(*keycount) == 0)
+    return STAT_INSECURE;
   
-  *length = htons(n);
-  
-  if (!read_write(server->tcpfd, packet, n + sizeof(u16), 0) ||
-      !read_write(server->tcpfd, &c1, 1, 1) ||
-      !read_write(server->tcpfd, &c2, 1, 1) ||
-      !read_write(server->tcpfd, payload, (c1 << 8) | c2, 1))
-    {
-      close(server->tcpfd);
-      server->tcpfd = -1;
-      new_status = STAT_INSECURE;
-    }
+  if (status == STAT_NEED_KEY)
+    new_status = dnssec_validate_by_ds(now, header, n, name, keyname, class);
+  else if (status == STAT_NEED_DS)
+    new_status = dnssec_validate_ds(now, header, n, name, keyname, class);
   else
+    new_status = dnssec_validate_reply(now, header, n, name, keyname, &class);
+  
+  /* Can't validate because we need a key/DS whose name now in keyname.
+     Make query for same, and recurse to validate */
+  if (new_status == STAT_NEED_DS || new_status == STAT_NEED_KEY)
     {
-      n = (c1 << 8) | c2;
+      size_t m; 
+      unsigned char *packet = whine_malloc(65536 + MAXDNAME + RRFIXEDSZ + sizeof(u16));
+      unsigned char *payload = &packet[2];
+      struct dns_header *new_header = (struct dns_header *)payload;
+      u16 *length = (u16 *)packet;
+      unsigned char c1, c2;
+       
+      if (!packet)
+	return STAT_INSECURE;
+
+    another_tcp_key:
+      m = dnssec_generate_query(new_header, ((char *) new_header) + 65536, keyname, class, 
+				new_status == STAT_NEED_KEY ? T_DNSKEY : T_DS, &server->addr);
       
-      if (status == STAT_NEED_KEY)
-	new_status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, class);
+      *length = htons(m);
+      
+      if (!read_write(server->tcpfd, packet, m + sizeof(u16), 0) ||
+	  !read_write(server->tcpfd, &c1, 1, 1) ||
+	  !read_write(server->tcpfd, &c2, 1, 1) ||
+	  !read_write(server->tcpfd, payload, (c1 << 8) | c2, 1))
+	new_status = STAT_INSECURE;
       else
-	new_status = dnssec_validate_ds(now, header, n, daemon->namebuff, daemon->keyname, class);
-      
-      if (new_status == STAT_NEED_DS || new_status == STAT_NEED_KEY)
 	{
-	  if ((new_status = tcp_key_recurse(now, new_status, class, daemon->keyname, server) == STAT_SECURE))
+	  m = (c1 << 8) | c2;
+	  
+	  if (tcp_key_recurse(now, new_status, new_header, m, class, name, keyname, server, keycount) == STAT_SECURE)
 	    {
-	      if (status ==  STAT_NEED_KEY)
-		new_status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, class);
-	      else
-		new_status = dnssec_validate_ds(now, header, n, daemon->namebuff, daemon->keyname, class);
+	      /* Reached a validated record, now try again at this level.
+		 Note that we may get ANOTHER NEED_* if an answer needs more than one key.
+		 If so, go round again. */
 	      
+	      if (status == STAT_NEED_KEY)
+		new_status = dnssec_validate_by_ds(now, header, n, name, keyname, class);
+	      else if (status == STAT_NEED_DS)
+		new_status = dnssec_validate_ds(now, header, n, name, keyname, class);
+	      else
+		new_status = dnssec_validate_reply(now, header, n, name, keyname, &class);
+
 	      if (new_status == STAT_NEED_DS || new_status == STAT_NEED_KEY)
-		{
-		  my_syslog(LOG_ERR, _("Unexpected missing data for DNSSEC validation"));
-		  status = STAT_INSECURE;
-		}
+		goto another_tcp_key;
 	    }
 	}
+
+      free(packet);
     }
-
-  free(packet);
-
+  
   return new_status;
 }
 #endif
@@ -1454,22 +1478,20 @@
 #ifdef HAVE_DNSSEC
 		      if (option_bool(OPT_DNSSEC_VALID) && !checking_disabled)
 			{
-			  int class, status;
-			  
-			  status = dnssec_validate_reply(now, header, m, daemon->namebuff, daemon->keyname, &class);
-			  
-			  if (status == STAT_NEED_DS || status == STAT_NEED_KEY)
-			    {
-			      if ((status = tcp_key_recurse(now, status, class, daemon->keyname, last_server)) == STAT_SECURE)
-				status = dnssec_validate_reply(now, header, m, daemon->namebuff, daemon->keyname, &class);
-			    }
+			  int keycount = DNSSEC_WORK; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
+			  int status = tcp_key_recurse(now, STAT_TRUNCATED, header, m, 0, daemon->namebuff, daemon->keyname, last_server, &keycount);
+			  char *result;
 
-			  log_query(F_KEYTAG | F_SECSTAT, "result", NULL, 
-				    status == STAT_SECURE ? "SECURE" : (status == STAT_INSECURE ? "INSECURE" : "BOGUS"));
-
+			  if (keycount == 0)
+			    result = "ABANDONED";
+			  else
+			    result = (status == STAT_SECURE ? "SECURE" : (status == STAT_INSECURE ? "INSECURE" : "BOGUS"));
+			  
+			  log_query(F_KEYTAG | F_SECSTAT, "result", NULL, result);
+			  
 			  if (status == STAT_BOGUS)
 			    no_cache_dnssec = 1;
-
+			  
 			  if (status == STAT_SECURE)
 			    cache_secure = 1;
 			}