diff --git a/dirmngr/domaininfo.c b/dirmngr/domaininfo.c index f6263b06d..b41aef366 100644 --- a/dirmngr/domaininfo.c +++ b/dirmngr/domaininfo.c @@ -1,291 +1,378 @@ /* domaininfo.c - Gather statistics about accessed domains * Copyright (C) 2017 Werner Koch * * This file is part of GnuPG. * * GnuPG is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * GnuPG is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * SPDX-License-Identifier: GPL-3.0+ */ #include #include #include #include "dirmngr.h" /* Number of bucket for the hash array and limit for the length of a * bucket chain. For debugging values of 13 and 10 are more suitable * and a command like * for j in a b c d e f g h i j k l m n o p q r s t u v w z y z; do \ * for i in a b c d e f g h i j k l m n o p q r s t u v w z y z; do \ * gpg-connect-agent --dirmngr "wkd_get foo@$i.$j.gnupg.net" /bye \ * >/dev/null ; done; done * will quickly add a couple of domains. */ #define NO_OF_DOMAINBUCKETS 103 #define MAX_DOMAINBUCKET_LEN 20 /* Object to keep track of a domain name. */ struct domaininfo_s { struct domaininfo_s *next; unsigned int no_name:1; /* Domain name not found. */ unsigned int wkd_not_found:1; /* A WKD query failed. */ unsigned int wkd_supported:1; /* One WKD entry was found. */ unsigned int wkd_not_supported:1; /* Definitely does not support WKD. */ + unsigned int keepmark:1; /* Private to insert_or_update(). */ char name[1]; }; typedef struct domaininfo_s *domaininfo_t; /* And the hashed array. */ static domaininfo_t domainbuckets[NO_OF_DOMAINBUCKETS]; /* The hash function we use. Must not call a system function. */ static inline u32 hash_domain (const char *domain) { const unsigned char *s = (const unsigned char*)domain; u32 hashval = 0; u32 carry; for (; *s; s++) { if (*s == '.') continue; hashval = (hashval << 4) + *s; if ((carry = (hashval & 0xf0000000))) { hashval ^= (carry >> 24); hashval ^= carry; } } return hashval % NO_OF_DOMAINBUCKETS; } void domaininfo_print_stats (void) { int bidx; domaininfo_t di; int count, no_name, wkd_not_found, wkd_supported, wkd_not_supported; int len, minlen, maxlen; count = no_name = wkd_not_found = wkd_supported = wkd_not_supported = 0; maxlen = 0; minlen = -1; for (bidx = 0; bidx < NO_OF_DOMAINBUCKETS; bidx++) { len = 0; for (di = domainbuckets[bidx]; di; di = di->next) { count++; len++; if (di->no_name) no_name++; if (di->wkd_not_found) wkd_not_found++; if (di->wkd_supported) wkd_supported++; if (di->wkd_not_supported) wkd_not_supported++; } if (len > maxlen) maxlen = len; if (minlen == -1 || len < minlen) minlen = len; } log_info ("domaininfo: items=%d chainlen=%d..%d nn=%d nf=%d ns=%d s=%d\n", count, minlen > 0? minlen : 0, maxlen, no_name, wkd_not_found, wkd_not_supported, wkd_supported); } /* Return true if DOMAIN definitely does not support WKD. Note that * DOMAIN is expected to be lowercase. */ int domaininfo_is_wkd_not_supported (const char *domain) { domaininfo_t di; for (di = domainbuckets[hash_domain (domain)]; di; di = di->next) if (!strcmp (di->name, domain)) return !!di->wkd_not_supported; return 0; /* We don't know. */ } /* Core update function. DOMAIN is expected to be lowercase. * CALLBACK is called to update the existing or the newly inserted * item. */ static void insert_or_update (const char *domain, void (*callback)(domaininfo_t di, int insert_mode)) { domaininfo_t di; domaininfo_t di_new; - domaininfo_t di_cut; + domaininfo_t drop = NULL; + domaininfo_t drop_extra = NULL; + int nkept = 0; + int ndropped = 0; u32 hash; int count; hash = hash_domain (domain); for (di = domainbuckets[hash]; di; di = di->next) if (!strcmp (di->name, domain)) { callback (di, 0); /* Update */ return; } di_new = xtrycalloc (1, sizeof *di + strlen (domain)); if (!di_new) return; /* Out of core - we ignore this. */ strcpy (di_new->name, domain); /* Need to do another lookup because the malloc is a system call and * thus the hash array may have been changed by another thread. */ - di_cut = NULL; for (count=0, di = domainbuckets[hash]; di; di = di->next, count++) if (!strcmp (di->name, domain)) { callback (di, 0); /* Update */ xfree (di_new); return; } /* Before we insert we need to check whether the chain gets too long. */ - di_cut = NULL; if (count >= MAX_DOMAINBUCKET_LEN) { - for (count=0, di = domainbuckets[hash]; di; di = di->next, count++) - if (count >= MAX_DOMAINBUCKET_LEN/2) - { - di_cut = di->next; - di->next = NULL; - break; - } + domaininfo_t bucket; + domaininfo_t *array; + int narray, idx; + domaininfo_t keep = NULL; + + /* Unlink from the global list before doing a syscall. */ + bucket = domainbuckets[hash]; + domainbuckets[hash] = NULL; + + array = xtrycalloc (count, sizeof *array); + if (!array) + { + /* That's bad; give up the entire bucket. */ + log_error ("domaininfo: error allocating helper array: %s\n", + gpg_strerror (gpg_err_code_from_syserror ())); + drop_extra = bucket; + goto leave; + } + narray = 0; + + /* Move all items into an array for easier processing. */ + for (di = bucket; di; di = di->next) + array[narray++] = di; + log_assert (narray == count); + + /* Mark all item in the array which are flagged to support wkd + * but not more than half of the maximum. This way we will at + * the end drop half of the items. */ + count = 0; + for (idx=0; idx < narray; idx++) + { + di = array[idx]; + di->keepmark = 0; /* Clear flag here on the first pass. */ + if (di->wkd_supported && count < MAX_DOMAINBUCKET_LEN/2) + { + di->keepmark = 1; + count++; + } + } + /* Now mark those which are marked as not found. */ + /* FIXME: we should use an LRU algorithm here. */ + for (idx=0; idx < narray; idx++) + { + di = array[idx]; + if (!di->keepmark + && di->wkd_not_supported && count < MAX_DOMAINBUCKET_LEN/2) + { + di->keepmark = 1; + count++; + } + } + + /* Build a bucket list and a second list for later freeing the + * items (we can't do it directly because a free is a system + * call and we want to avoid locks in this module. Note that + * the kept items will be reversed order which does not matter. */ + for (idx=0; idx < narray; idx++) + { + di = array[idx]; + if (di->keepmark) + { + di->next = keep; + keep = di; + nkept++; + } + else + { + di->next = drop; + drop = di; + ndropped++; + } + } + + /* In case another thread added new stuff to the domain list we + * simply drop them instead all. It would also be possible to + * append them to our list but then we can't guarantee that a + * bucket list is almost all of the time limited to + * MAX_DOMAINBUCKET_LEN. Not sure whether this is really a + * sensible strategy. */ + drop_extra = domainbuckets[hash]; + domainbuckets[hash] = keep; } /* Insert */ callback (di_new, 1); di = di_new; di->next = domainbuckets[hash]; domainbuckets[hash] = di; - /* Remove the rest of the cutted chain. */ - while (di_cut) + if (opt.verbose && (nkept || ndropped)) + log_info ("domaininfo: bucket=%lu kept=%d purged=%d\n", + (unsigned long)hash, nkept, ndropped); + + leave: + /* Remove the dropped items. */ + while (drop) + { + di = drop->next; + xfree (drop); + drop = di; + } + while (drop_extra) { - di = di_cut->next; - xfree (di_cut); - di_cut = di; + di = drop_extra->next; + xfree (drop_extra); + drop_extra = di; } } -/* Helper for domaininfo_set_no_name. */ +/* Helper for domaininfo_set_no_name. May not do any syscalls. */ static void set_no_name_cb (domaininfo_t di, int insert_mode) { (void)insert_mode; di->no_name = 1; /* Obviously the domain is in this case also not supported. */ di->wkd_not_supported = 1; /* The next should already be 0 but we clear it anyway in the case * of a temporary DNS failure. */ di->wkd_supported = 0; } /* Mark DOMAIN as not existent. */ void domaininfo_set_no_name (const char *domain) { insert_or_update (domain, set_no_name_cb); } -/* Helper for domaininfo_set_wkd_supported. */ +/* Helper for domaininfo_set_wkd_supported. May not do any syscalls. */ static void set_wkd_supported_cb (domaininfo_t di, int insert_mode) { (void)insert_mode; di->wkd_supported = 1; /* The next will already be set unless the domain enabled WKD in the * meantime. Thus we need to clear it. */ di->wkd_not_supported = 0; } /* Mark DOMAIN as supporting WKD. */ void domaininfo_set_wkd_supported (const char *domain) { insert_or_update (domain, set_wkd_supported_cb); } -/* Helper for domaininfo_set_wkd_not_supported. */ +/* Helper for domaininfo_set_wkd_not_supported. May not do any syscalls. */ static void set_wkd_not_supported_cb (domaininfo_t di, int insert_mode) { (void)insert_mode; di->wkd_not_supported = 1; di->wkd_supported = 0; } /* Mark DOMAIN as not supporting WKD queries (e.g. no policy file). */ void domaininfo_set_wkd_not_supported (const char *domain) { insert_or_update (domain, set_wkd_not_supported_cb); } -/* Helper for domaininfo_set_wkd_not_found. */ +/* Helper for domaininfo_set_wkd_not_found. May not do any syscalls. */ static void set_wkd_not_found_cb (domaininfo_t di, int insert_mode) { /* Set the not found flag but there is no need to do this if we * already know that the domain either does not support WKD or we * know that it supports WKD. */ if (insert_mode) di->wkd_not_found = 1; else if (!di->wkd_not_supported && !di->wkd_supported) di->wkd_not_found = 1; /* Better clear this flag in case we had a DNS failure in the * past. */ di->no_name = 0; } /* Update a counter for DOMAIN to keep track of failed WKD queries. */ void domaininfo_set_wkd_not_found (const char *domain) { insert_or_update (domain, set_wkd_not_found_cb); }