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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