]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
hwdb: bounds-check trie offsets against the mmap before dereferencing
authorjeffhuang <jeff@docker.xydrsucermoubd24xgo33yhsgd.bx.internal.cloudapp.net>
Wed, 27 May 2026 18:41:54 +0000 (18:41 +0000)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 28 May 2026 06:05:24 +0000 (15:05 +0900)
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 <noreply@anthropic.com>
12 files changed:
src/libsystemd/meson.build
src/libsystemd/sd-hwdb/fuzz-hwdb.c [new file with mode: 0644]
src/libsystemd/sd-hwdb/sd-hwdb.c
src/libsystemd/sd-hwdb/test-sd-hwdb.c
src/systemd/sd-hwdb.h
test/fuzz/fuzz-hwdb/empty [new file with mode: 0644]
test/fuzz/fuzz-hwdb/magic-only [new file with mode: 0644]
test/fuzz/fuzz-hwdb/regression-42340-trie-fnmatch-node-oob [new file with mode: 0644]
test/fuzz/fuzz-hwdb/regression-42341-trie-search-prefix-oob [new file with mode: 0644]
test/fuzz/fuzz-hwdb/regression-42342-node-lookup-bsearch [new file with mode: 0644]
test/fuzz/fuzz-hwdb/regression-42343-hwdb-add-property-key [new file with mode: 0644]
test/fuzz/fuzz-hwdb/regression-trie-fnmatch-stack-overflow [new file with mode: 0644]

index 0cecea3b0d194e5cd9329eb0b9757abcf01ee91a..24979e8882e6f7456a2afe99a5e4366d8efb8d0c 100644 (file)
@@ -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 (file)
index 0000000..711d471
--- /dev/null
@@ -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 <unistd.h>
+
+#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;
+}
index 996f253fd908aefc010b5950e8b69348ce72d72c..2a265aae874e0743e0bd966d05bb730f71bcd7d9 100644 (file)
@@ -3,6 +3,7 @@
   Copyright © 2008 Alan Jenkins <alan.christopher.jenkins@googlemail.com>
 ***/
 
+#include <errno.h>
 #include <fnmatch.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -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;
 }
index dff29c01e8fe059137f9bb780b5a588e206895dd..1c4223d7d78c593e79af27d2131d9797c9b79d2f 100644 (file)
@@ -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) {
index 4504c5b5b1dcabc00eb3e37f188954eed3ec2705..35aa1b291e096b6a9294ab8c74212321e34c0ebc 100644 (file)
@@ -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 (file)
index 0000000..e69de29
diff --git a/test/fuzz/fuzz-hwdb/magic-only b/test/fuzz/fuzz-hwdb/magic-only
new file mode 100644 (file)
index 0000000..4257e5d
Binary files /dev/null and b/test/fuzz/fuzz-hwdb/magic-only differ
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 (file)
index 0000000..26e82c7
Binary files /dev/null and b/test/fuzz/fuzz-hwdb/regression-42340-trie-fnmatch-node-oob differ
diff --git a/test/fuzz/fuzz-hwdb/regression-42341-trie-search-prefix-oob b/test/fuzz/fuzz-hwdb/regression-42341-trie-search-prefix-oob
new file mode 100644 (file)
index 0000000..cb08783
Binary files /dev/null and b/test/fuzz/fuzz-hwdb/regression-42341-trie-search-prefix-oob differ
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 (file)
index 0000000..5677045
Binary files /dev/null and b/test/fuzz/fuzz-hwdb/regression-42342-node-lookup-bsearch differ
diff --git a/test/fuzz/fuzz-hwdb/regression-42343-hwdb-add-property-key b/test/fuzz/fuzz-hwdb/regression-42343-hwdb-add-property-key
new file mode 100644 (file)
index 0000000..8ee12b1
Binary files /dev/null and b/test/fuzz/fuzz-hwdb/regression-42343-hwdb-add-property-key differ
diff --git a/test/fuzz/fuzz-hwdb/regression-trie-fnmatch-stack-overflow b/test/fuzz/fuzz-hwdb/regression-trie-fnmatch-stack-overflow
new file mode 100644 (file)
index 0000000..8f3cd0d
Binary files /dev/null and b/test/fuzz/fuzz-hwdb/regression-trie-fnmatch-stack-overflow differ