[SV 67046] Don't modify the hash table when inside hash_map*()

It's illegal for the user-supplied function to modify the hash table
while executing hash_map*() methods.  snap_file() may add elements
due to EXTRA_PREREQS, so we can't call it via hash_map_args().

Diagnosis and test by Shim Manning <shimmanning@gmail.com>
Possible fix by Dmitry Goncharov <dgoncharov@users.sf.net>

* src/hash.h (hash_table): Remember when we're in hash_map*().
* src/hash.c (hash_init): Initialize the in-map value to false.
(hash_map): Set the in-map value to true when walking the map.
(hash_map_arg): Ditto.
(hash_insert_at): Assert we're not in hash_map*()
(hash_free_items): Ditto.
(hash_delete_items): Ditto.
(hash_free): Ditto.
* src/file.c (snap_deps): Use hash_dump() to create a copy of the
hash and walk that to call snap_file(), so we can add more files.
(snap_file): Since we're calling it directly, don't use void* args.
diff --git a/src/file.c b/src/file.c
index 65517d0..447662a 100644
--- a/src/file.c
+++ b/src/file.c
@@ -723,9 +723,8 @@
 /* Perform per-file snap operations. */
 
 static void
-snap_file (const void *item, void *arg)
+snap_file (struct file *f, const struct dep *deps)
 {
-  struct file *f = (struct file*)item;
   struct dep *prereqs = NULL;
   struct dep *d;
 
@@ -759,7 +758,7 @@
           }
     }
   else if (f->is_target)
-    prereqs = copy_dep_chain (arg);
+    prereqs = copy_dep_chain (deps);
 
   if (prereqs)
     {
@@ -914,9 +913,16 @@
   {
     struct dep *prereqs = expand_extra_prereqs (lookup_variable (STRING_SIZE_TUPLE(".EXTRA_PREREQS")));
 
-    /* Perform per-file snap operations.  */
-    hash_map_arg(&files, snap_file, prereqs);
+    /* Perform per-file snap operations.
+       We can't use hash_map*() here because snap_file may add new elements
+       into the files hash, which is not allowed in the map.  Instead make a
+       dump of the files and walk through that.  */
+    void** filedump = hash_dump (&files, NULL, 0);
 
+    for (void** filep = filedump; *filep; ++filep)
+      snap_file (*filep, prereqs);
+
+    free (filedump);
     free_dep_chain (prereqs);
   }
 
diff --git a/src/hash.c b/src/hash.c
index a118552..a0b370f 100644
--- a/src/hash.c
+++ b/src/hash.c
@@ -59,6 +59,7 @@
   ht->ht_collisions = 0;
   ht->ht_lookups = 0;
   ht->ht_rehashes = 0;
+  ht->ht_in_map = 0;
   ht->ht_hash_1 = hash_1;
   ht->ht_hash_2 = hash_2;
   ht->ht_compare = hash_cmp;
@@ -138,6 +139,10 @@
 hash_insert_at (struct hash_table *ht, const void *item, const void *slot)
 {
   const void *old_item = *(void **) slot;
+
+  /* It's illegal to insert while in hash_map*().  */
+  assert (! ht->ht_in_map);
+
   if (HASH_VACANT (old_item))
     {
       ht->ht_fill++;
@@ -181,6 +186,10 @@
 {
   void **vec = ht->ht_vec;
   void **end = &vec[ht->ht_size];
+
+  /* It's illegal to free items while in hash_map*().  */
+  assert (! ht->ht_in_map);
+
   for (; vec < end; vec++)
     {
       void *item = *vec;
@@ -197,6 +206,10 @@
 {
   void **vec = ht->ht_vec;
   void **end = &vec[ht->ht_size];
+
+  /* It's illegal to delete all items while in hash_map*().  */
+  assert (! ht->ht_in_map);
+
   for (; vec < end; vec++)
     *vec = 0;
   ht->ht_fill = 0;
@@ -209,6 +222,9 @@
 void
 hash_free (struct hash_table *ht, int free_items)
 {
+  /* It's illegal to free while in hash_map*().  */
+  assert (! ht->ht_in_map);
+
   if (free_items)
     hash_free_items (ht);
   else
@@ -227,11 +243,15 @@
   void **slot;
   void **end = &ht->ht_vec[ht->ht_size];
 
+  ht->ht_in_map = 1;
+
   for (slot = ht->ht_vec; slot < end; slot++)
     {
       if (!HASH_VACANT (*slot))
         (*map) (*slot);
     }
+
+  ht->ht_in_map = 0;
 }
 
 void
@@ -240,11 +260,15 @@
   void **slot;
   void **end = &ht->ht_vec[ht->ht_size];
 
+  ht->ht_in_map = 1;
+
   for (slot = ht->ht_vec; slot < end; slot++)
     {
       if (!HASH_VACANT (*slot))
         (*map) (*slot, arg);
     }
+
+  ht->ht_in_map = 0;
 }
 
 /* Double the size of the hash table in the event of overflow... */
diff --git a/src/hash.h b/src/hash.h
index 1bbda0f..27b202a 100644
--- a/src/hash.h
+++ b/src/hash.h
@@ -51,6 +51,7 @@
   unsigned long ht_collisions;	/* # of failed calls to comparison function */
   unsigned long ht_lookups;	/* # of queries */
   unsigned int ht_rehashes;	/* # of times we've expanded table */
+  unsigned int ht_in_map:1;     /* 1 if we're inside a hash_map*() function */
 };
 
 typedef int (*qsort_cmp_t) __P((void const *, void const *));
diff --git a/tests/scripts/variables/EXTRA_PREREQS b/tests/scripts/variables/EXTRA_PREREQS
index 2f72be3..a34abd9 100644
--- a/tests/scripts/variables/EXTRA_PREREQS
+++ b/tests/scripts/variables/EXTRA_PREREQS
@@ -178,7 +178,16 @@
 ',
               '', "hello.x.d\nhello.x\nworld.x.d\nworld.x\n");
 
+# SV 67046. Rehashing of hash table files during iteration.
+my $files = "";
+for my $k (1 .. 1000) {
+    $files = "$files file$k";
+}
+run_make_test(qq!
+all: .EXTRA_PREREQS := $files
+all:;
+file%:;
+!,
+              '', "#MAKE#: 'all' is up to date.");
 
-
-# This tells the test driver that the perl test script executed properly.
 1;