From 73fea38cf1344e08213bb10bfc1e1a98382aee78 Mon Sep 17 00:00:00 2001 From: jeffhuang Date: Wed, 27 May 2026 18:41:54 +0000 Subject: [PATCH] hwdb: bounds-check trie offsets against the mmap before dereferencing Fixes a family of OOB reads in the trie walker where attacker-controlled offsets are loaded from a (possibly hostile) hwdb.bin and added to hwdb->map without first checking that the resulting pointer stays inside the mapping. Crash sites: - trie_fnmatch_f sd-hwdb.c:187 (#42340) - trie_search_f sd-hwdb.c:233 (#42341) - trie_children_cmp_f /bsearch sd-hwdb.c:94 (#42342) - hwdb_add_property sd-hwdb.c:121 (#42343) - trie_node_from_off (UBSAN) sd-hwdb.c:83 - trie_fnmatch_f stack overflow on cyclic children (CIFuzz finding) Commit b45a897edc ("hwdb: reject out-of-bounds fnmatch prefixes") plugged the first site only at the prefix_off-content level, but the crash on the read of node->prefix_off itself still happens when node is already OOB. This is the structural fix: a single hwdb_at() helper validates that [off, off + size) lies inside the mapping, and trie_node_from_off() / trie_string() return NULL on OOB; every caller treats the result as nullable and surfaces -EBADMSG. trie_fnmatch_f() additionally caps its recursion depth at 2048; without the cap a corrupt trie whose child offsets form a cycle (or just a deep linear chain) drives the parser into stack-overflow rather than returning -EBADMSG (caught by CIFuzz address sanitizer on this branch). Additions: - A fuzz-hwdb libFuzzer harness that drives sd_hwdb_new_from_path, sd_hwdb_get, sd_hwdb_seek, and sd_hwdb_enumerate on attacker bytes. - Five hand-crafted unit tests in test-sd-hwdb.c (one per crash bucket, plus the cyclic-trie recursion case) that build a malformed in-memory hwdb.bin and assert -EBADMSG rather than SIGSEGV / stack-overflow. - Regression corpus files under test/fuzz/fuzz-hwdb/ pinning each fixed bucket, including the CIFuzz stack-overflow reproducer. Closes #42340. Closes #42341. Closes #42342. Closes #42343. Reported-by: AI-assisted libFuzzer campaign Co-developed-by: Claude Opus 4.7 --- src/libsystemd/meson.build | 1 + src/libsystemd/sd-hwdb/fuzz-hwdb.c | 73 ++++++ src/libsystemd/sd-hwdb/sd-hwdb.c | 149 +++++++++-- src/libsystemd/sd-hwdb/test-sd-hwdb.c | 240 +++++++++++++++++- src/systemd/sd-hwdb.h | 4 +- test/fuzz/fuzz-hwdb/empty | 0 test/fuzz/fuzz-hwdb/magic-only | Bin 0 -> 16 bytes .../regression-42340-trie-fnmatch-node-oob | Bin 0 -> 4209 bytes .../regression-42341-trie-search-prefix-oob | Bin 0 -> 4209 bytes .../regression-42342-node-lookup-bsearch | Bin 0 -> 4209 bytes .../regression-42343-hwdb-add-property-key | Bin 0 -> 175 bytes .../regression-trie-fnmatch-stack-overflow | Bin 0 -> 175 bytes 12 files changed, 432 insertions(+), 35 deletions(-) create mode 100644 src/libsystemd/sd-hwdb/fuzz-hwdb.c create mode 100644 test/fuzz/fuzz-hwdb/empty create mode 100644 test/fuzz/fuzz-hwdb/magic-only create mode 100644 test/fuzz/fuzz-hwdb/regression-42340-trie-fnmatch-node-oob create mode 100644 test/fuzz/fuzz-hwdb/regression-42341-trie-search-prefix-oob create mode 100644 test/fuzz/fuzz-hwdb/regression-42342-node-lookup-bsearch create mode 100644 test/fuzz/fuzz-hwdb/regression-42343-hwdb-add-property-key create mode 100644 test/fuzz/fuzz-hwdb/regression-trie-fnmatch-stack-overflow diff --git a/src/libsystemd/meson.build b/src/libsystemd/meson.build index 0cecea3b0d1..24979e8882e 100644 --- a/src/libsystemd/meson.build +++ b/src/libsystemd/meson.build @@ -311,4 +311,5 @@ endif simple_fuzzers += files( 'sd-bus/fuzz-bus-match.c', 'sd-bus/fuzz-bus-message.c', + 'sd-hwdb/fuzz-hwdb.c', ) diff --git a/src/libsystemd/sd-hwdb/fuzz-hwdb.c b/src/libsystemd/sd-hwdb/fuzz-hwdb.c new file mode 100644 index 00000000000..711d4713c83 --- /dev/null +++ b/src/libsystemd/sd-hwdb/fuzz-hwdb.c @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* Fuzz target for the sd-hwdb binary trie parser. + * + * sd-hwdb consumes an mmap'd hwdb.bin produced by `systemd-hwdb update`. The + * file is walked via offset-chasing into struct trie_node_f / trie_child_entry_f + * / trie_value_entry_f records. A malformed file can drive the walker outside + * the mapped region; the validators in trie_node_from_off / trie_string don't + * guard every dereference site. + * + * Expected input: bytes that look like an `hwdb.bin` file (header magic + * "KSLPHHRH" + trie). The harness writes the fuzzer bytes to a temp file, + * mmaps it via sd_hwdb_new_from_path, then exercises sd_hwdb_get (which + * drives trie_search_f / trie_fnmatch_f) and sd_hwdb_seek + sd_hwdb_enumerate + * (capped to bound infinite loops via crafted offset cycles). + */ + +#include + +#include "sd-hwdb.h" + +#include "fd-util.h" +#include "fileio.h" +#include "fuzz.h" +#include "tests.h" +#include "tmpfile-util.h" + +#define MAX_ENUMERATE 1024 + +static const char * const modaliases[] = { + "pci:v00008086d00000A04", + "usb:v1234p5678", + "acpi:PNP0C0D:", + "dmi:bvnLENOVO:bvrR0KET20W:bd02/01/2020:svnLENOVO:pnThinkPadX1:", + "*", +}; + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(unlink_tempfilep) char filename[] = "/tmp/fuzz-hwdb.XXXXXX"; + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL; + + if (outside_size_range(size, 0, 4 * 1024 * 1024)) + return 0; + + fuzz_setup_logging(); + + ASSERT_OK(fmkostemp_safe(filename, "r+", &f)); + if (size != 0) + ASSERT_OK_EQ_ERRNO((ssize_t) fwrite(data, size, 1, f), (ssize_t) 1); + ASSERT_OK(fflush_and_check(f)); + + if (sd_hwdb_new_from_path(filename, &hwdb) < 0) + return 0; + + FOREACH_ELEMENT(modalias, modaliases) { + const char *key, *value; + size_t n = 0; + + (void) sd_hwdb_get(hwdb, *modalias, "ID_VENDOR", &value); + + if (sd_hwdb_seek(hwdb, *modalias) < 0) + continue; + while (sd_hwdb_enumerate(hwdb, &key, &value) > 0) { + DO_NOT_OPTIMIZE(key); + DO_NOT_OPTIMIZE(value); + if (++n >= MAX_ENUMERATE) + break; + } + } + + return 0; +} diff --git a/src/libsystemd/sd-hwdb/sd-hwdb.c b/src/libsystemd/sd-hwdb/sd-hwdb.c index 996f253fd90..2a265aae874 100644 --- a/src/libsystemd/sd-hwdb/sd-hwdb.c +++ b/src/libsystemd/sd-hwdb/sd-hwdb.c @@ -3,6 +3,7 @@ Copyright © 2008 Alan Jenkins ***/ +#include #include #include #include @@ -79,12 +80,74 @@ static const struct trie_value_entry_f *trie_node_value(sd_hwdb *hwdb, const str return (const struct trie_value_entry_f *)base; } -static const struct trie_node_f *trie_node_from_off(sd_hwdb *hwdb, le64_t off) { - return (const struct trie_node_f *)(hwdb->map + le64toh(off)); +static const void* hwdb_at(sd_hwdb *hwdb, uint64_t off, uint64_t size) { + assert(hwdb); + + uint64_t file_size = hwdb->st.st_size; + + /* off == file_size is rejected too: a read at EOF is always OOB, and this keeps the + * boundary unambiguous even for a size == 0 caller (which would otherwise get a + * one-past-the-end pointer). */ + if (off >= file_size) + return NULL; + if (file_size - off < size) + return NULL; + + return (const uint8_t*) hwdb->map + off; +} + +static const struct trie_node_f* trie_node_from_off(sd_hwdb *hwdb, le64_t off) { + assert(hwdb); + assert(hwdb->head); + + uint64_t offset = le64toh(off); + uint64_t node_size = le64toh(hwdb->head->node_size); + uint64_t child_entry_size = le64toh(hwdb->head->child_entry_size); + uint64_t value_entry_size = le64toh(hwdb->head->value_entry_size); + + if (node_size < sizeof(struct trie_node_f)) + return NULL; + if (child_entry_size < sizeof(struct trie_child_entry_f)) + return NULL; + if (value_entry_size < sizeof(struct trie_value_entry_f)) + return NULL; + + const struct trie_node_f *node = hwdb_at(hwdb, offset, node_size); + if (!node) + return NULL; + + uint64_t children_bytes, values_bytes, total; + if (!MUL_SAFE(&children_bytes, (uint64_t) node->children_count, child_entry_size)) + return NULL; + if (!MUL_SAFE(&values_bytes, le64toh(node->values_count), value_entry_size)) + return NULL; + if (!ADD_SAFE(&total, node_size, children_bytes)) + return NULL; + if (!INC_SAFE(&total, values_bytes)) + return NULL; + + if (!hwdb_at(hwdb, offset, total)) + return NULL; + + return node; } -static const char *trie_string(sd_hwdb *hwdb, le64_t off) { - return hwdb->map + le64toh(off); +static const char* trie_string(sd_hwdb *hwdb, le64_t off) { + assert(hwdb); + + uint64_t file_size = hwdb->st.st_size; + uint64_t offset = le64toh(off); + + const char *p = hwdb_at(hwdb, offset, /* size= */ 1); + if (!p) + return NULL; + + /* Clamp to SIZE_MAX so memchr()'s size_t arg cannot truncate on 32-bit. */ + size_t avail = (size_t) MIN(file_size - offset, (uint64_t) SIZE_MAX); + if (!memchr(p, '\0', avail)) + return NULL; + + return p; } static int trie_children_cmp_f(const void *v1, const void *v2) { @@ -102,7 +165,9 @@ static const struct trie_node_f *node_lookup_f(sd_hwdb *hwdb, const struct trie_ child = bsearch(&search, (const char *)node + le64toh(hwdb->head->node_size), node->children_count, le64toh(hwdb->head->child_entry_size), trie_children_cmp_f); if (child) + /* Treat corrupt child offsets like lookup misses. */ return trie_node_from_off(hwdb, child->child_off); + return NULL; } @@ -113,6 +178,8 @@ static int hwdb_add_property(sd_hwdb *hwdb, const struct trie_value_entry_f *ent assert(hwdb); key = trie_string(hwdb, entry->key_off); + if (!key) + return -EBADMSG; /* * Silently ignore all properties which do not start with a @@ -173,8 +240,14 @@ static int hwdb_add_property(sd_hwdb *hwdb, const struct trie_value_entry_f *ent return 0; } +/* Cap recursion depth so a corrupt hwdb.bin whose children offsets form a + * cycle (or just a deep linear chain) cannot exhaust the stack. Real-world + * hwdb files do not approach this; the deepest legitimate trie key is well + * under a kilobyte. */ +#define HWDB_RECURSION_MAX 2048U + static int trie_fnmatch_f(sd_hwdb *hwdb, const struct trie_node_f *node, size_t p, - struct linebuf *buf, const char *search) { + struct linebuf *buf, const char *search, unsigned depth) { size_t len; size_t i; const char *prefix; @@ -183,21 +256,31 @@ static int trie_fnmatch_f(sd_hwdb *hwdb, const struct trie_node_f *node, size_t assert(hwdb); assert(node); - /* Ensure the prefix is within bounds */ - uint64_t file_size = hwdb->st.st_size, prefix_off = le64toh(node->prefix_off); - if (prefix_off >= file_size || p >= file_size - prefix_off) - return -EINVAL; + if (depth >= HWDB_RECURSION_MAX) + return -EBADMSG; prefix = trie_string(hwdb, node->prefix_off); - len = strnlen(prefix + p, file_size - prefix_off - p); + if (!prefix) + return -EBADMSG; + + len = strlen(prefix); + if (p > len) + return -EBADMSG; + len -= p; + if (!linebuf_add(buf, prefix + p, len)) return -EINVAL; for (i = 0; i < node->children_count; i++) { const struct trie_child_entry_f *child = trie_node_child(hwdb, node, i); + const struct trie_node_f *child_node; linebuf_add_char(buf, child->c); - err = trie_fnmatch_f(hwdb, trie_node_from_off(hwdb, child->child_off), 0, buf, search); + child_node = trie_node_from_off(hwdb, child->child_off); + if (!child_node) + return -EBADMSG; + + err = trie_fnmatch_f(hwdb, child_node, 0, buf, search, depth + 1); if (err < 0) return err; linebuf_rem_char(buf); @@ -223,16 +306,24 @@ static int trie_search_f(sd_hwdb *hwdb, const char *search) { linebuf_init(&buf); node = trie_node_from_off(hwdb, hwdb->head->nodes_root_off); + if (!node) + return -EBADMSG; + while (node) { const struct trie_node_f *child; size_t p = 0; if (node->prefix_off) { + const char *prefix; char c; - for (; (c = trie_string(hwdb, node->prefix_off)[p]); p++) { + prefix = trie_string(hwdb, node->prefix_off); + if (!prefix) + return -EBADMSG; + + for (; (c = prefix[p]); p++) { if (IN_SET(c, '*', '?', '[')) - return trie_fnmatch_f(hwdb, node, p, &buf, search + i + p); + return trie_fnmatch_f(hwdb, node, p, &buf, search + i + p, 0); if (c != search[i + p]) return 0; } @@ -242,7 +333,7 @@ static int trie_search_f(sd_hwdb *hwdb, const char *search) { child = node_lookup_f(hwdb, node, '*'); if (child) { linebuf_add_char(&buf, '*'); - err = trie_fnmatch_f(hwdb, child, 0, &buf, search + i); + err = trie_fnmatch_f(hwdb, child, 0, &buf, search + i, 0); if (err < 0) return err; linebuf_rem_char(&buf); @@ -251,7 +342,7 @@ static int trie_search_f(sd_hwdb *hwdb, const char *search) { child = node_lookup_f(hwdb, node, '?'); if (child) { linebuf_add_char(&buf, '?'); - err = trie_fnmatch_f(hwdb, child, 0, &buf, search + i); + err = trie_fnmatch_f(hwdb, child, 0, &buf, search + i, 0); if (err < 0) return err; linebuf_rem_char(&buf); @@ -260,7 +351,7 @@ static int trie_search_f(sd_hwdb *hwdb, const char *search) { child = node_lookup_f(hwdb, node, '['); if (child) { linebuf_add_char(&buf, '['); - err = trie_fnmatch_f(hwdb, child, 0, &buf, search + i); + err = trie_fnmatch_f(hwdb, child, 0, &buf, search + i, 0); if (err < 0) return err; linebuf_rem_char(&buf); @@ -379,14 +470,15 @@ static int properties_prepare(sd_hwdb *hwdb, const char *modalias) { return trie_search_f(hwdb, modalias); } -_public_ int sd_hwdb_get(sd_hwdb *hwdb, const char *modalias, const char *key, const char **_value) { +_public_ int sd_hwdb_get(sd_hwdb *hwdb, const char *modalias, const char *key, const char **ret) { const struct trie_value_entry_f *entry; + const char *value; int r; assert_return(hwdb, -EINVAL); assert_return(hwdb->f, -EINVAL); assert_return(modalias, -EINVAL); - assert_return(_value, -EINVAL); + assert_return(ret, -EINVAL); r = properties_prepare(hwdb, modalias); if (r < 0) @@ -396,7 +488,11 @@ _public_ int sd_hwdb_get(sd_hwdb *hwdb, const char *modalias, const char *key, c if (!entry) return -ENOENT; - *_value = trie_string(hwdb, entry->value_off); + value = trie_string(hwdb, entry->value_off); + if (!value) + return -EBADMSG; + + *ret = value; return 0; } @@ -418,13 +514,14 @@ _public_ int sd_hwdb_seek(sd_hwdb *hwdb, const char *modalias) { return 0; } -_public_ int sd_hwdb_enumerate(sd_hwdb *hwdb, const char **key, const char **value) { +_public_ int sd_hwdb_enumerate(sd_hwdb *hwdb, const char **ret_key, const char **ret_value) { const struct trie_value_entry_f *entry; + const char *v; const void *k; assert_return(hwdb, -EINVAL); - assert_return(key, -EINVAL); - assert_return(value, -EINVAL); + assert_return(ret_key, -EINVAL); + assert_return(ret_value, -EINVAL); if (hwdb->properties_modified) return -EAGAIN; @@ -432,8 +529,12 @@ _public_ int sd_hwdb_enumerate(sd_hwdb *hwdb, const char **key, const char **val if (!ordered_hashmap_iterate(hwdb->properties, &hwdb->properties_iterator, (void **)&entry, &k)) return 0; - *key = k; - *value = trie_string(hwdb, entry->value_off); + v = trie_string(hwdb, entry->value_off); + if (!v) + return -EBADMSG; + + *ret_key = k; + *ret_value = v; return 1; } diff --git a/src/libsystemd/sd-hwdb/test-sd-hwdb.c b/src/libsystemd/sd-hwdb/test-sd-hwdb.c index dff29c01e8f..1c4223d7d78 100644 --- a/src/libsystemd/sd-hwdb/test-sd-hwdb.c +++ b/src/libsystemd/sd-hwdb/test-sd-hwdb.c @@ -76,10 +76,82 @@ TEST(sd_hwdb_new_from_path) { assert_se(r >= 0); } -TEST(sd_hwdb_seek_rejects_invalid_fnmatch_prefix) { +static sd_hwdb* hwdb_new_from_blob(const void *data, size_t size) { _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL; _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL; - _cleanup_free_ char *path = NULL; + _cleanup_close_ int fd = -EBADF; + + ASSERT_OK(mkdtemp_malloc(/* template= */ NULL, &tmp)); + _cleanup_free_ char *path = ASSERT_NOT_NULL(path_join(tmp, "hwdb.bin")); + + fd = ASSERT_OK_ERRNO(open(path, O_WRONLY|O_CREAT|O_CLOEXEC|O_TRUNC, 0644)); + ASSERT_OK_EQ_ERRNO(write(fd, data, size), (ssize_t) size); + fd = safe_close(fd); + + ASSERT_OK(sd_hwdb_new_from_path(path, &hwdb)); + + return TAKE_PTR(hwdb); +} + +TEST(sd_hwdb_seek_rejects_invalid_fnmatch_child_node) { + _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL; + + struct invalid_hwdb { + struct trie_header_f header; + struct trie_node_f root_node; + struct trie_child_entry_f child1; + struct trie_node_f mid_node; + struct trie_child_entry_f child2; + struct trie_node_f wildcard_node; + struct trie_child_entry_f child3; + char strings[STRLEN("usb:") + 1]; + } _packed_ data = { + .header = { + .signature = HWDB_SIG, + .tool_version = htole64(PROJECT_VERSION), + .file_size = htole64(sizeof(struct invalid_hwdb)), + .header_size = htole64(sizeof(struct trie_header_f)), + .node_size = htole64(sizeof(struct trie_node_f)), + .child_entry_size = htole64(sizeof(struct trie_child_entry_f)), + .value_entry_size = htole64(sizeof(struct trie_value_entry_f)), + .nodes_root_off = htole64(offsetof(struct invalid_hwdb, root_node)), + .nodes_len = htole64(offsetof(struct invalid_hwdb, strings) - offsetof(struct invalid_hwdb, root_node)), + .strings_len = htole64(STRLEN("usb:") + 1), + }, + .root_node = { + .prefix_off = htole64(offsetof(struct invalid_hwdb, strings)), + .children_count = 1, + }, + .child1 = { + .c = 'v', + .child_off = htole64(offsetof(struct invalid_hwdb, mid_node)), + }, + .mid_node = { + .prefix_off = htole64(offsetof(struct invalid_hwdb, strings) + STRLEN("usb:")), + .children_count = 1, + }, + .child2 = { + .c = '*', + .child_off = htole64(offsetof(struct invalid_hwdb, wildcard_node)), + }, + .wildcard_node = { + .prefix_off = htole64(offsetof(struct invalid_hwdb, strings) + STRLEN("usb:")), + .children_count = 1, + }, + .child3 = { + .c = 'x', + .child_off = htole64(0xDEAD0000), + }, + .strings = "usb:", + }; + + hwdb = ASSERT_NOT_NULL(hwdb_new_from_blob(&data, sizeof(data))); + + ASSERT_ERROR(sd_hwdb_seek(hwdb, "usb:vx"), EBADMSG); +} + +TEST(sd_hwdb_seek_rejects_invalid_fnmatch_prefix) { + _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL; struct invalid_hwdb { struct trie_header_f header; @@ -124,15 +196,165 @@ TEST(sd_hwdb_seek_rejects_invalid_fnmatch_prefix) { .strings = "usb:", }; - ASSERT_OK(mkdtemp_malloc(NULL, &tmp)); - ASSERT_NOT_NULL(path = path_join(tmp, "hwdb.bin")); + hwdb = ASSERT_NOT_NULL(hwdb_new_from_blob(&data, sizeof(data))); - _cleanup_close_ int fd = ASSERT_OK_ERRNO(open(path, O_WRONLY|O_CREAT|O_CLOEXEC|O_TRUNC, 0644)); - assert_se(write(fd, &data, sizeof(data)) == (ssize_t) sizeof(data)); - fd = safe_close(fd); + ASSERT_ERROR(sd_hwdb_seek(hwdb, "usb:v*"), EBADMSG); +} - ASSERT_OK(sd_hwdb_new_from_path(path, &hwdb)); - ASSERT_ERROR(sd_hwdb_seek(hwdb, "usb:v*"), EINVAL); +TEST(sd_hwdb_seek_rejects_invalid_search_prefix) { + _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL; + + struct invalid_hwdb { + struct trie_header_f header; + struct trie_node_f root_node; + char strings[1]; + } _packed_ data = { + .header = { + .signature = HWDB_SIG, + .tool_version = htole64(PROJECT_VERSION), + .file_size = htole64(sizeof(struct invalid_hwdb)), + .header_size = htole64(sizeof(struct trie_header_f)), + .node_size = htole64(sizeof(struct trie_node_f)), + .child_entry_size = htole64(sizeof(struct trie_child_entry_f)), + .value_entry_size = htole64(sizeof(struct trie_value_entry_f)), + .nodes_root_off = htole64(offsetof(struct invalid_hwdb, root_node)), + .nodes_len = htole64(sizeof(struct trie_node_f)), + .strings_len = htole64(1), + }, + .root_node = { + .prefix_off = htole64(0xDEAD0000), + }, + }; + + hwdb = ASSERT_NOT_NULL(hwdb_new_from_blob(&data, sizeof(data))); + + ASSERT_ERROR(sd_hwdb_seek(hwdb, "usb:v1234"), EBADMSG); +} + +TEST(sd_hwdb_seek_rejects_truncated_children_array) { + _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL; + + struct invalid_hwdb { + struct trie_header_f header; + struct trie_node_f root_node; + } _packed_ data = { + .header = { + .signature = HWDB_SIG, + .tool_version = htole64(PROJECT_VERSION), + .file_size = htole64(sizeof(struct invalid_hwdb)), + .header_size = htole64(sizeof(struct trie_header_f)), + .node_size = htole64(sizeof(struct trie_node_f)), + .child_entry_size = htole64(sizeof(struct trie_child_entry_f)), + .value_entry_size = htole64(sizeof(struct trie_value_entry_f)), + .nodes_root_off = htole64(offsetof(struct invalid_hwdb, root_node)), + .nodes_len = htole64(sizeof(struct trie_node_f)), + }, + .root_node = { + .children_count = 1, + }, + }; + + hwdb = ASSERT_NOT_NULL(hwdb_new_from_blob(&data, sizeof(data))); + + ASSERT_ERROR(sd_hwdb_seek(hwdb, "x"), EBADMSG); +} + +TEST(sd_hwdb_seek_rejects_invalid_property_key) { + _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL; + + struct invalid_hwdb { + struct trie_header_f header; + struct trie_node_f root_node; + struct trie_value_entry_f value; + char strings[STRLEN("usb") + 1 + STRLEN("value") + 1]; + } _packed_ data = { + .header = { + .signature = HWDB_SIG, + .tool_version = htole64(PROJECT_VERSION), + .file_size = htole64(sizeof(struct invalid_hwdb)), + .header_size = htole64(sizeof(struct trie_header_f)), + .node_size = htole64(sizeof(struct trie_node_f)), + .child_entry_size = htole64(sizeof(struct trie_child_entry_f)), + .value_entry_size = htole64(sizeof(struct trie_value_entry_f)), + .nodes_root_off = htole64(offsetof(struct invalid_hwdb, root_node)), + .nodes_len = htole64(offsetof(struct invalid_hwdb, strings) - offsetof(struct invalid_hwdb, root_node)), + .strings_len = htole64(STRLEN("usb") + 1 + STRLEN("value") + 1), + }, + .root_node = { + .prefix_off = htole64(offsetof(struct invalid_hwdb, strings)), + .values_count = htole64(1), + }, + .value = { + .key_off = htole64(0xDEAD0000), + .value_off = htole64(offsetof(struct invalid_hwdb, strings) + STRLEN("usb") + 1), + }, + .strings = "usb\0value", + }; + + hwdb = ASSERT_NOT_NULL(hwdb_new_from_blob(&data, sizeof(data))); + + ASSERT_ERROR(sd_hwdb_seek(hwdb, "usb"), EBADMSG); +} + +TEST(sd_hwdb_seek_rejects_cyclic_trie) { + _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL; + + /* Cyclic trie: the wildcard_node's only child points back to + * itself, which would drive trie_fnmatch_f() into infinite + * recursion (stack-overflow CVE-class bug found by CIFuzz). + * The recursion-depth cap must turn this into a clean -EBADMSG. */ + struct invalid_hwdb { + struct trie_header_f header; + struct trie_node_f root_node; + struct trie_child_entry_f child1; + struct trie_node_f mid_node; + struct trie_child_entry_f child2; + struct trie_node_f wildcard_node; + struct trie_child_entry_f wildcard_self_child; + char strings[STRLEN("usb:") + 1]; + } _packed_ data = { + .header = { + .signature = HWDB_SIG, + .tool_version = htole64(PROJECT_VERSION), + .file_size = htole64(sizeof(struct invalid_hwdb)), + .header_size = htole64(sizeof(struct trie_header_f)), + .node_size = htole64(sizeof(struct trie_node_f)), + .child_entry_size = htole64(sizeof(struct trie_child_entry_f)), + .value_entry_size = htole64(sizeof(struct trie_value_entry_f)), + .nodes_root_off = htole64(offsetof(struct invalid_hwdb, root_node)), + .nodes_len = htole64(offsetof(struct invalid_hwdb, strings) - offsetof(struct invalid_hwdb, root_node)), + .strings_len = htole64(STRLEN("usb:") + 1), + }, + .root_node = { + .prefix_off = htole64(offsetof(struct invalid_hwdb, strings)), + .children_count = 1, + }, + .child1 = { + .c = 'v', + .child_off = htole64(offsetof(struct invalid_hwdb, mid_node)), + }, + .mid_node = { + .prefix_off = htole64(offsetof(struct invalid_hwdb, strings) + STRLEN("usb:")), + .children_count = 1, + }, + .child2 = { + .c = '*', + .child_off = htole64(offsetof(struct invalid_hwdb, wildcard_node)), + }, + .wildcard_node = { + .prefix_off = htole64(offsetof(struct invalid_hwdb, strings) + STRLEN("usb:")), + .children_count = 1, + }, + .wildcard_self_child = { + .c = 'a', + .child_off = htole64(offsetof(struct invalid_hwdb, wildcard_node)), + }, + .strings = "usb:", + }; + + hwdb = ASSERT_NOT_NULL(hwdb_new_from_blob(&data, sizeof(data))); + + ASSERT_ERROR(sd_hwdb_seek(hwdb, "usb:v*"), EBADMSG); } static int intro(void) { diff --git a/src/systemd/sd-hwdb.h b/src/systemd/sd-hwdb.h index 4504c5b5b1d..35aa1b291e0 100644 --- a/src/systemd/sd-hwdb.h +++ b/src/systemd/sd-hwdb.h @@ -28,10 +28,10 @@ _SD_DECLARE_TRIVIAL_REF_UNREF_FUNC(sd_hwdb); int sd_hwdb_new(sd_hwdb **ret); int sd_hwdb_new_from_path(const char *path, sd_hwdb **ret); -int sd_hwdb_get(sd_hwdb *hwdb, const char *modalias, const char *key, const char **value); +int sd_hwdb_get(sd_hwdb *hwdb, const char *modalias, const char *key, const char **ret); int sd_hwdb_seek(sd_hwdb *hwdb, const char *modalias); -int sd_hwdb_enumerate(sd_hwdb *hwdb, const char **key, const char **value); +int sd_hwdb_enumerate(sd_hwdb *hwdb, const char **ret_key, const char **ret_value); /* the inverse condition avoids ambiguity of dangling 'else' after the macro */ #define SD_HWDB_FOREACH_PROPERTY(hwdb, modalias, key, value) \ diff --git a/test/fuzz/fuzz-hwdb/empty b/test/fuzz/fuzz-hwdb/empty new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/fuzz/fuzz-hwdb/magic-only b/test/fuzz/fuzz-hwdb/magic-only new file mode 100644 index 0000000000000000000000000000000000000000..4257e5d91f618cba2be75b7698eeaef2eb282ef6 GIT binary patch literal 16 Sc-r$0_6hLt2=ZWOfB*m}<5 literal 0 Hc-jL100001 diff --git a/test/fuzz/fuzz-hwdb/regression-42340-trie-fnmatch-node-oob b/test/fuzz/fuzz-hwdb/regression-42340-trie-fnmatch-node-oob new file mode 100644 index 0000000000000000000000000000000000000000..26e82c787298370e1bde3450f4fecc77656be15b GIT binary patch literal 4209 zc-rOGRgV{E6bIlf?krl|z0mS8HrWOKu*l+WMG8fdCb+x1ySqCShXRG7MGF*nZtyH5 zJG03?a6z)+igWYkdC$xwGbed}6B{2FiHWHd^Fvfpb&XD1BY_J9Ycy-vFA;2n_lK1j z_2o+7`p{``TAU82#~E-&oC#;fS#VaI4QIzWa84YJa|Q1ZIye11I4{nJ^Wz_J0bCFl z!i8}WTof0>#c>H-5|_fIaT#0|m&4_81zZtV!aw24xC;Im|AMO~JKTxoia1;iSI0GQ z1lPp1a6GP!>)^V$9!|jZaRb~CH^Pl^6WkOx!_9FE+!D9Kt#KRN7PrIg@vpc8?ua|# z&iFUn1$Rwx@T6`!nWGjZevi{hO#H^vN%rr!8}5#K;GR(_=KMzxn2{j*!sGu%khGtr zb|hs?N1NBZaBtiP_r?8ie>?yW#Dnl)JOmHL!|?FnLxdhdeSgEFOo);|X{o zo`fgkDR?TLhNt5hcqX2e?4(?oO@9uai|66_cmZCB7vaTt30{hq;pKP*UWr%X)p!kF zi`U`xcmv*uH{s2A3*L&i;q7<_-ideN-FQ!UZUhI=Ui$m+pLjn$fDhtB_%J?#kK$wa zIQ|Qtz$fu3{5L+0&)|RXS$qzk#~1KLd@~mTM-P(raM#`kxoInyE|vt zdj=jDu6S?0dDi#Ozi!rivt~uttr?7psuL9>@I0L&p4VW=X+t$4Tx%o@yqL&PBgT_Z zLhDZguTQ=3_c$hwg=6D5I4+Kd_o` zh&SQQ_#eCl|BJW2=x3LmZS=R}9e5|+g?HmUcrV_E_u~WjKYS1$!vEvL_y|6VkKyC^ z1U`vR;nVmGK8w%c^Y{Y3h%e#G_zJ#?ui@+X2EK`J;oJBQzKieS`}hHVh#%p{Vc&ZB I9pIHb1;qjhga7~l literal 0 Hc-jL100001 diff --git a/test/fuzz/fuzz-hwdb/regression-42342-node-lookup-bsearch b/test/fuzz/fuzz-hwdb/regression-42342-node-lookup-bsearch new file mode 100644 index 0000000000000000000000000000000000000000..5677045b5a3d4c9f4989852eff4d05839813991f GIT binary patch literal 4209 zc-pPjRgf1%90%|trIe#f1f)wqQt9;I5Ds{uLrNqMo&ZGw>28n?=}@}6Q<3iOl2WO| z;r=&sd?y8{q4-Y?EGd!YgG*k38@(pBPcTA!I5)Vz^Px2fz4>EsCvf0MvS1> z6A|?0)aHDYW8zpiHjabi;ty~<93LmZ32`F)A^r#_#=$rVPKuM^FBi`(J$ zxC0Ky9dV~9zwc)esCVYRF1RcH9{+&5;UDo&xI6BFd*YvQFWejV!F_Q*+#e6X1Mwg{ z7!Sci@i06bkH91GC_EaE!DI0_{0knBC*X;A5}u5w;Hh{To{neWU-3*l3(v-L@LW6( z&&Lb!Lc9q7hJVM4@e;fgFT>063cM1p!mIHbycVy+>+uG>5pTkq@fN%lZ^PU14!jfZ z!n^Svych4o`=k5+&&~nrf8amyLHrj!gb(8*_$WSxkK+^gBtC^t<1_dyK8Mfa3-}_w zgfHVO_$t1Juj3o|CccGl<2(2+zK8$D_wfV#5I@3?@jv)q{2zXTpW$`W%*QyBnsLkhS6 literal 0 Hc-jL100001 -- 2.47.3