From 70a64c23c9d77b114dcc71e70e69aa8868693f3f Mon Sep 17 00:00:00 2001 From: Nathan Chandrasekar Date: Sat, 17 Oct 2026 20:14:17 -0400 Subject: test: Add binary plist parser behavior tests Build small binary plists in memory and check that arrays and dicts parse correctly with 1, 2 and 3 byte object references, and that strings, extended size markers and array/dict references extending beyond the object data area into the offset table are rejected. Co-Authored-By: Claude Opus 5.5 --- test/Makefile.am | 7 +- test/bplist_behavior.test | 2 + test/bplist_behavior_test.c | 199 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100755 test/bplist_behavior.test create mode 100644 test/bplist_behavior_test.c (limited to 'test') diff --git a/test/Makefile.am b/test/Makefile.am index 76d2373..1d98349 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -14,7 +14,8 @@ noinst_PROGRAMS = \ plist_btest \ plist_jtest \ plist_otest \ - xml_behavior_test + xml_behavior_test \ + bplist_behavior_test plist_cmp_SOURCES = plist_cmp.c plist_cmp_LDADD = \ @@ -42,6 +43,9 @@ plist_otest_LDADD = $(top_builddir)/src/libplist-2.0.la xml_behavior_test_SOURCES = xml_behavior_test.c xml_behavior_test_LDADD = $(top_builddir)/src/libplist-2.0.la +bplist_behavior_test_SOURCES = bplist_behavior_test.c +bplist_behavior_test_LDADD = $(top_builddir)/src/libplist-2.0.la + TESTS = \ empty.test \ small.test \ @@ -85,6 +89,7 @@ TESTS = \ ostep-comments.test \ ostep-invalid-types.test \ xml_behavior.test \ + bplist_behavior.test \ plistutil-streams.test EXTRA_DIST = \ diff --git a/test/bplist_behavior.test b/test/bplist_behavior.test new file mode 100755 index 0000000..130a6a0 --- /dev/null +++ b/test/bplist_behavior.test @@ -0,0 +1,2 @@ +## -*- sh -*- +$top_builddir/test/bplist_behavior_test diff --git a/test/bplist_behavior_test.c b/test/bplist_behavior_test.c new file mode 100644 index 0000000..f7365f1 --- /dev/null +++ b/test/bplist_behavior_test.c @@ -0,0 +1,199 @@ +/* + * bplist_behavior_test.c + * + * Tests binary plist parser behavior around the object data area: + * + * 1) Arrays and dictionaries are parsed correctly for different + * object reference sizes, including sizes that are not a power of 2. + * + * 2) Objects whose data extends beyond the object data area, i.e. + * into the offset table, must cause parsing to fail. This covers + * strings, extended size markers, array references and dictionary + * key/value references. + * + * The binary plists are assembled in memory with a minimal writer so + * each test case only has to specify the object bytes. + */ + +#include +#include +#include +#include + +#include "plist/plist.h" + +#define MAX_BPLIST_SIZE 256 + +static void put_be64(uint8_t *p, uint64_t v) +{ + int i; + for (i = 7; i >= 0; i--) { + p[i] = (uint8_t)(v & 0xFF); + v >>= 8; + } +} + +/* + * Assemble a binary plist from the given object bytes. offsets[] holds the + * start of each object relative to the beginning of objs. A 1 byte offset + * table is placed directly after the objects, followed by the trailer. + */ +static uint32_t build_bplist(uint8_t *out, const uint8_t *objs, size_t objs_len, const uint8_t *offsets, size_t num_objects, uint8_t ref_size, uint64_t root) +{ + size_t len = 0; + size_t i; + memcpy(out, "bplist00", 8); + len = 8; + memcpy(out + len, objs, objs_len); + len += objs_len; + size_t offset_table_offset = len; + for (i = 0; i < num_objects; i++) { + out[len++] = (uint8_t)(8 + offsets[i]); + } + memset(out + len, 0, 6); + out[len + 6] = 1; /* offset size */ + out[len + 7] = ref_size; + put_be64(out + len + 8, num_objects); + put_be64(out + len + 16, root); + put_be64(out + len + 24, offset_table_offset); + len += 32; + return (uint32_t)len; +} + +static int check_int_item(plist_t item, uint64_t expected, const char *name) +{ + uint64_t val = 0; + if (plist_get_node_type(item) != PLIST_INT) { + fprintf(stderr, "%s: expected PLIST_INT item\n", name); + return 0; + } + plist_get_uint_val(item, &val); + if (val != expected) { + fprintf(stderr, "%s: expected %" PRIu64 ", got %" PRIu64 "\n", name, expected, val); + return 0; + } + return 1; +} + +static int test_array_ref_sizes(void) +{ + /* [ 1, 2 ] with 1 byte object references */ + const uint8_t objs1[] = { 0xA2, 0x01, 0x02, 0x10, 0x01, 0x10, 0x02 }; + const uint8_t offs1[] = { 0, 3, 5 }; + /* [ 1, 2 ] with 3 byte object references */ + const uint8_t objs3[] = { 0xA2, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x10, 0x01, 0x10, 0x02 }; + const uint8_t offs3[] = { 0, 7, 9 }; + struct { + const uint8_t *objs; + size_t objs_len; + const uint8_t *offs; + uint8_t ref_size; + } cases[] = { + { objs1, sizeof(objs1), offs1, 1 }, + { objs3, sizeof(objs3), offs3, 3 }, + }; + size_t i; + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + uint8_t buf[MAX_BPLIST_SIZE]; + uint32_t len = build_bplist(buf, cases[i].objs, cases[i].objs_len, cases[i].offs, 3, cases[i].ref_size, 0); + plist_t root = NULL; + plist_err_t err = plist_from_bin((const char*)buf, len, &root); + if (err != PLIST_ERR_SUCCESS || !root) { + fprintf(stderr, "array ref_size %u: plist_from_bin failed (err=%d)\n", cases[i].ref_size, err); + plist_free(root); + return 0; + } + if (plist_get_node_type(root) != PLIST_ARRAY || plist_array_get_size(root) != 2) { + fprintf(stderr, "array ref_size %u: expected array with 2 items\n", cases[i].ref_size); + plist_free(root); + return 0; + } + if (!check_int_item(plist_array_get_item(root, 0), 1, "array item 0") || + !check_int_item(plist_array_get_item(root, 1), 2, "array item 1")) { + plist_free(root); + return 0; + } + plist_free(root); + } + return 1; +} + +static int test_dict_ref_size(void) +{ + /* { "a": 5 } with 2 byte object references */ + const uint8_t objs[] = { 0xD1, 0x00, 0x01, 0x00, 0x02, 0x51, 'a', 0x10, 0x05 }; + const uint8_t offs[] = { 0, 5, 7 }; + uint8_t buf[MAX_BPLIST_SIZE]; + uint32_t len = build_bplist(buf, objs, sizeof(objs), offs, 3, 2, 0); + plist_t root = NULL; + plist_err_t err = plist_from_bin((const char*)buf, len, &root); + if (err != PLIST_ERR_SUCCESS || !root) { + fprintf(stderr, "dict ref_size 2: plist_from_bin failed (err=%d)\n", err); + plist_free(root); + return 0; + } + if (plist_get_node_type(root) != PLIST_DICT || plist_dict_get_size(root) != 1) { + fprintf(stderr, "dict ref_size 2: expected dict with 1 entry\n"); + plist_free(root); + return 0; + } + if (!check_int_item(plist_dict_get_item(root, "a"), 5, "dict value")) { + plist_free(root); + return 0; + } + plist_free(root); + return 1; +} + +static int expect_rejected(const uint8_t *objs, size_t objs_len, const uint8_t *offs, size_t num_objects, uint64_t root_index, const char *name) +{ + uint8_t buf[MAX_BPLIST_SIZE]; + uint32_t len = build_bplist(buf, objs, objs_len, offs, num_objects, 1, root_index); + plist_t root = NULL; + plist_err_t err = plist_from_bin((const char*)buf, len, &root); + if (err == PLIST_ERR_SUCCESS || root != NULL) { + fprintf(stderr, "%s: expected failure, got err=%d root=%p\n", name, err, (void*)root); + plist_free(root); + return 0; + } + return 1; +} + +static int test_data_beyond_object_area_is_rejected(void) +{ + int ok = 1; + + /* string claims 5 bytes, only 2 are present before the offset table */ + const uint8_t str_objs[] = { 0x55, 'a', 'b' }; + const uint8_t str_offs[] = { 0 }; + ok &= expect_rejected(str_objs, sizeof(str_objs), str_offs, 1, 0, "truncated string"); + + /* extended size marker, but no size integer before the offset table */ + const uint8_t ext_objs[] = { 0x5F }; + const uint8_t ext_offs[] = { 0 }; + ok &= expect_rejected(ext_objs, sizeof(ext_objs), ext_offs, 1, 0, "missing size integer"); + + /* array claims 3 references, only 2 are present before the offset table */ + const uint8_t arr_objs[] = { 0x10, 0x01, 0x10, 0x02, 0xA3, 0x00, 0x01 }; + const uint8_t arr_offs[] = { 0, 2, 4 }; + ok &= expect_rejected(arr_objs, sizeof(arr_objs), arr_offs, 3, 2, "truncated array references"); + + /* dict has its key reference, but the value reference is missing */ + const uint8_t dict_objs[] = { 0x51, 'a', 0x10, 0x05, 0xD1, 0x00 }; + const uint8_t dict_offs[] = { 0, 2, 4 }; + ok &= expect_rejected(dict_objs, sizeof(dict_objs), dict_offs, 3, 2, "truncated dict references"); + + return ok; +} + +int main(void) +{ + int ok = 1; + + ok &= test_array_ref_sizes(); + ok &= test_dict_ref_size(); + ok &= test_data_beyond_object_area_is_rejected(); + + return ok ? 0 : 1; +} -- cgit v1.1-32-gdbae