From 16a6bc5a7a5da2482d96f7dc43da360ceab1c320 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Wed, 7 Dec 2022 23:39:56 +0900 Subject: [PATCH] resolve: dedup entries in /etc/hosts This improves the performance of parsing the file and reduces memory pressure. Running 'fuzz-etc-hosts timeout-strv' with valgrind, Before: total heap usage: 321,020 allocs, 321,020 frees, 15,820,387,193 bytes allocated real 0m23.531s user 0m21.458s sys 0m1.961s After: total heap usage: 112,408 allocs, 112,408 frees, 7,297,480 bytes allocated real 0m8.664s user 0m8.545s sys 0m0.065s Hopefully fixes oss-fuzz#47708 (https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=47708). --- src/resolve/resolved-etc-hosts.c | 69 +++++++++++++++--------- src/resolve/resolved-etc-hosts.h | 8 +-- src/resolve/test-resolved-etc-hosts.c | 45 +++++++--------- test/fuzz/fuzz-etc-hosts/oss-fuzz-47708 | Bin 0 -> 314200 bytes 4 files changed, 64 insertions(+), 58 deletions(-) create mode 100644 test/fuzz/fuzz-etc-hosts/oss-fuzz-47708 diff --git a/src/resolve/resolved-etc-hosts.c b/src/resolve/resolved-etc-hosts.c index 2c002a3be4..334778706a 100644 --- a/src/resolve/resolved-etc-hosts.c +++ b/src/resolve/resolved-etc-hosts.c @@ -22,7 +22,7 @@ static EtcHostsItemByAddress *etc_hosts_item_by_address_free(EtcHostsItemByAddre if (!item) return NULL; - strv_free(item->names); + set_free(item->names); return mfree(item); } @@ -41,7 +41,7 @@ static EtcHostsItemByName *etc_hosts_item_by_name_free(EtcHostsItemByName *item) return NULL; free(item->name); - free(item->addresses); + set_free(item->addresses); return mfree(item); } @@ -153,20 +153,21 @@ static int parse_line(EtcHosts *hosts, unsigned nr, const char *line) { continue; } - r = strv_extend_with_size(&item->names, &item->n_names, name); - if (r < 0) - return log_oom(); - bn = hashmap_get(hosts->by_name, name); if (!bn) { _cleanup_(etc_hosts_item_by_name_freep) EtcHostsItemByName *new_item = NULL; + _cleanup_free_ char *name_copy = NULL; + + name_copy = strdup(name); + if (!name_copy) + return log_oom(); new_item = new(EtcHostsItemByName, 1); if (!new_item) return log_oom(); *new_item = (EtcHostsItemByName) { - .name = TAKE_PTR(name), + .name = TAKE_PTR(name_copy), }; r = hashmap_ensure_put(&hosts->by_name, &by_name_hash_ops, new_item->name, new_item); @@ -176,10 +177,21 @@ static int parse_line(EtcHosts *hosts, unsigned nr, const char *line) { bn = TAKE_PTR(new_item); } - if (!GREEDY_REALLOC(bn->addresses, bn->n_addresses + 1)) - return log_oom(); + if (!set_contains(bn->addresses, &address)) { + _cleanup_free_ struct in_addr_data *address_copy = NULL; - bn->addresses[bn->n_addresses++] = &item->address; + address_copy = newdup(struct in_addr_data, &address, 1); + if (!address_copy) + return log_oom(); + + r = set_ensure_consume(&bn->addresses, &in_addr_data_hash_ops_free, TAKE_PTR(address_copy)); + if (r < 0) + return log_oom(); + } + + r = set_ensure_consume(&item->names, &dns_name_hash_ops_free, TAKE_PTR(name)); + if (r < 0) + return log_oom(); } if (!found) @@ -217,6 +229,7 @@ static void strip_localhost(EtcHosts *hosts) { for (size_t j = 0; j < ELEMENTSOF(local_in_addrs); j++) { bool all_localhost, all_local_address; EtcHostsItemByAddress *item; + const char *name; item = hashmap_get(hosts->by_address, local_in_addrs + j); if (!item) @@ -224,8 +237,8 @@ static void strip_localhost(EtcHosts *hosts) { /* Check whether all hostnames the loopback address points to are localhost ones */ all_localhost = true; - STRV_FOREACH(i, item->names) - if (!is_localhost(*i)) { + SET_FOREACH(name, item->names) + if (!is_localhost(name)) { all_localhost = false; break; } @@ -236,17 +249,18 @@ static void strip_localhost(EtcHosts *hosts) { /* Now check if the names listed for this address actually all point back just to this * address (or the other loopback address). If not, let's stay away from this too. */ all_local_address = true; - STRV_FOREACH(i, item->names) { + SET_FOREACH(name, item->names) { EtcHostsItemByName *n; + struct in_addr_data *a; - n = hashmap_get(hosts->by_name, *i); + n = hashmap_get(hosts->by_name, name); if (!n) /* No reverse entry? Then almost certainly the entry already got deleted from * the previous iteration of this loop, i.e. via the other protocol */ break; /* Now check if the addresses of this item are all localhost addresses */ - for (size_t m = 0; m < n->n_addresses; m++) - if (!in_addr_is_localhost(n->addresses[m]->family, &n->addresses[m]->address)) { + SET_FOREACH(a, n->addresses) + if (!in_addr_is_localhost(a->family, &a->address)) { all_local_address = false; break; } @@ -258,8 +272,8 @@ static void strip_localhost(EtcHosts *hosts) { if (!all_local_address) continue; - STRV_FOREACH(i, item->names) - etc_hosts_item_by_name_free(hashmap_remove(hosts->by_name, *i)); + SET_FOREACH(name, item->names) + etc_hosts_item_by_name_free(hashmap_remove(hosts->by_name, name)); assert_se(hashmap_remove(hosts->by_address, local_in_addrs + j) == item); etc_hosts_item_by_address_free(item); @@ -397,18 +411,20 @@ static int etc_hosts_lookup_by_address( } if (found_ptr) { - r = dns_answer_reserve(answer, item->n_names); + const char *n; + + r = dns_answer_reserve(answer, set_size(item->names)); if (r < 0) return r; - STRV_FOREACH(n, item->names) { + SET_FOREACH(n, item->names) { _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; rr = dns_resource_record_new(found_ptr); if (!rr) return -ENOMEM; - rr->ptr.name = strdup(*n); + rr->ptr.name = strdup(n); if (!rr->ptr.name) return -ENOMEM; @@ -428,6 +444,7 @@ static int etc_hosts_lookup_by_name( DnsAnswer **answer) { bool found_a = false, found_aaaa = false; + const struct in_addr_data *a; EtcHostsItemByName *item; DnsResourceKey *t; int r; @@ -439,7 +456,7 @@ static int etc_hosts_lookup_by_name( item = hashmap_get(hosts->by_name, name); if (item) { - r = dns_answer_reserve(answer, item->n_addresses); + r = dns_answer_reserve(answer, set_size(item->addresses)); if (r < 0) return r; } else { @@ -469,14 +486,14 @@ static int etc_hosts_lookup_by_name( break; } - for (unsigned i = 0; item && i < item->n_addresses; i++) { + SET_FOREACH(a, item->addresses) { _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; - if ((!found_a && item->addresses[i]->family == AF_INET) || - (!found_aaaa && item->addresses[i]->family == AF_INET6)) + if ((!found_a && a->family == AF_INET) || + (!found_aaaa && a->family == AF_INET6)) continue; - r = dns_resource_record_new_address(&rr, item->addresses[i]->family, &item->addresses[i]->address, item->name); + r = dns_resource_record_new_address(&rr, a->family, &a->address, item->name); if (r < 0) return r; diff --git a/src/resolve/resolved-etc-hosts.h b/src/resolve/resolved-etc-hosts.h index 55f884746f..e1a7249f29 100644 --- a/src/resolve/resolved-etc-hosts.h +++ b/src/resolve/resolved-etc-hosts.h @@ -7,16 +7,12 @@ typedef struct EtcHostsItemByAddress { struct in_addr_data address; - - char **names; - size_t n_names; + Set *names; } EtcHostsItemByAddress; typedef struct EtcHostsItemByName { char *name; - - struct in_addr_data **addresses; - size_t n_addresses; + Set *addresses; } EtcHostsItemByName; int etc_hosts_parse(EtcHosts *hosts, FILE *f); diff --git a/src/resolve/test-resolved-etc-hosts.c b/src/resolve/test-resolved-etc-hosts.c index a0f712cbb6..19b2991a35 100644 --- a/src/resolve/test-resolved-etc-hosts.c +++ b/src/resolve/test-resolved-etc-hosts.c @@ -27,13 +27,11 @@ TEST(parse_etc_hosts_system) { assert_se(etc_hosts_parse(&hosts, f) == 0); } -#define address_equal_4(_addr, _address) \ - ((_addr)->family == AF_INET && \ - !memcmp(&(_addr)->address.in, &(struct in_addr) { .s_addr = (_address) }, 4)) +#define has_4(_set, _address_str) \ + set_contains(_set, &(struct in_addr_data) { .family = AF_INET, .address.in = { .s_addr = inet_addr(_address_str) } }) -#define address_equal_6(_addr, ...) \ - ((_addr)->family == AF_INET6 && \ - !memcmp(&(_addr)->address.in6, &(struct in6_addr) { .s6_addr = __VA_ARGS__}, 16) ) +#define has_6(_set, ...) \ + set_contains(_set, &(struct in_addr_data) { .family = AF_INET6, .address.in6 = { .s6_addr = __VA_ARGS__ } }) TEST(parse_etc_hosts) { _cleanup_(unlink_tempfilep) char @@ -72,33 +70,29 @@ TEST(parse_etc_hosts) { EtcHostsItemByName *bn; assert_se(bn = hashmap_get(hosts.by_name, "some.where")); - assert_se(bn->n_addresses == 3); - assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 3); - assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.4"))); - assert_se(address_equal_4(bn->addresses[1], inet_addr("1.2.3.5"))); - assert_se(address_equal_6(bn->addresses[2], {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5})); + assert_se(set_size(bn->addresses) == 3); + assert_se(has_4(bn->addresses, "1.2.3.4")); + assert_se(has_4(bn->addresses, "1.2.3.5")); + assert_se(has_6(bn->addresses, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5})); assert_se(bn = hashmap_get(hosts.by_name, "dash")); - assert_se(bn->n_addresses == 1); - assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 1); - assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.6"))); + assert_se(set_size(bn->addresses) == 1); + assert_se(has_4(bn->addresses, "1.2.3.6")); assert_se(bn = hashmap_get(hosts.by_name, "dash-dash.where-dash")); - assert_se(bn->n_addresses == 1); - assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 1); - assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.6"))); + assert_se(set_size(bn->addresses) == 1); + assert_se(has_4(bn->addresses, "1.2.3.6")); /* See https://tools.ietf.org/html/rfc1035#section-2.3.1 */ FOREACH_STRING(s, "bad-dash-", "-bad-dash", "-bad-dash.bad-") assert_se(!hashmap_get(hosts.by_name, s)); assert_se(bn = hashmap_get(hosts.by_name, "before.comment")); - assert_se(bn->n_addresses == 4); - assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 4); - assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.9"))); - assert_se(address_equal_4(bn->addresses[1], inet_addr("1.2.3.10"))); - assert_se(address_equal_4(bn->addresses[2], inet_addr("1.2.3.11"))); - assert_se(address_equal_4(bn->addresses[3], inet_addr("1.2.3.12"))); + assert_se(set_size(bn->addresses) == 4); + assert_se(has_4(bn->addresses, "1.2.3.9")); + assert_se(has_4(bn->addresses, "1.2.3.10")); + assert_se(has_4(bn->addresses, "1.2.3.11")); + assert_se(has_4(bn->addresses, "1.2.3.12")); assert_se(!hashmap_get(hosts.by_name, "within.comment")); assert_se(!hashmap_get(hosts.by_name, "within.comment2")); @@ -113,9 +107,8 @@ TEST(parse_etc_hosts) { assert_se(!set_contains(hosts.no_address, "multi.colon")); assert_se(bn = hashmap_get(hosts.by_name, "some.other")); - assert_se(bn->n_addresses == 1); - assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 1); - assert_se(address_equal_6(bn->addresses[0], {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5})); + assert_se(set_size(bn->addresses) == 1); + assert_se(has_6(bn->addresses, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5})); assert_se( set_contains(hosts.no_address, "some.where")); assert_se( set_contains(hosts.no_address, "some.other")); diff --git a/test/fuzz/fuzz-etc-hosts/oss-fuzz-47708 b/test/fuzz/fuzz-etc-hosts/oss-fuzz-47708 new file mode 100644 index 0000000000000000000000000000000000000000..c0f59de6c9481fb527dca38fb0b89c594b04882c GIT binary patch literal 314200 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