summaryrefslogtreecommitdiffstats
path: root/src
AgeCommit message (Collapse)AuthorFilesLines
3 hoursplist: treat NaN date values as 0 in plist_get_date_valGravatar Nikias Bassen1-0/+2
Converting NaN to int32_t is undefined behavior, and NaN can come from a parsed date node.
4 hoursplist: fix the (sec, usec) date API round tripGravatar Flavio Toffalini1-5/+15
plist_set_date_val stores val = sec + usec / 1000000, but plist_get_date_val splits it with a truncating cast and fabs(): *sec = (int32_t)val; val = fabs((val - (int64_t)val) * 1000000); *usec = (int32_t)val; Two results are wrong. The product is truncated, so set(5, 6) reads back as (5, 5) - 29.3% of round trips over sec in [0, 3] come back a microsecond short. And for a negative value, truncation toward zero plus fabs() give a pair that does not denote the same instant: set(-3, 250000) stores -2.75 and reads back as (-2, 750000), which the setter would store as -1.25. Every round trip with sec < 0 and usec != 0 is wrong. (int32_t)val is also undefined when the stored value is outside the int32_t range. Round the seconds down, round the fraction to microseconds carrying 1000000 into the seconds, and clamp the seconds before converting, so sec + usec / 1000000 == val to the microsecond with 0 <= usec < 1000000 - the convention the setter uses. Add test/date_val.c covering fixed cases and a sweep over sec in [-2, 2]. It fails on an unmodified tree - set(5, 6), set(-3, 250000) and set(-1, 1) all come back as different instants - and passes with this change, with make check at 44/44. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Arn4YaWtcQBDsQuJtyBmFP
4 hoursbplist: Guard uint64_mul_overflow fallback against division by zeroGravatar Nikias Bassen1-1/+1
The portable fallback used when __builtin_umulll_overflow is not available computed a > UINT64_MAX / b, which divides by zero if b is 0. No current caller passes 0, but return no overflow in that case so the helper is safe for any input.
14 hoursbplist: Use length-based range checks for object dataGravatar Nathan Chandrasekar1-44/+68
The binary plist parser validated object data by computing pointers such as `object + size` or `refs + j * ref_size` and comparing them against the offset table. Each of these checks had to be written with its own wrap-around handling, and the pointer was formed before it was known to be inside the input buffer. Add bplist_object_range_valid(), which checks a length against the bytes remaining before the offset table, and bplist_read_object_ref(), which reads an array/dict object reference with an overflow-checked offset. Use them for all object data checks in parse_bin_node() and for the reference reads in parse_array_node() and parse_dict_node(). Arrays and dicts now validate the full reference list (count * ref_size, or twice that for dicts) up front instead of only the item count. The node offset checks in parse_bin_node_at_index() and the offset table check in plist_from_bin() compare offsets instead of pointers as well. Valid binary plists are parsed exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
26 hoursoplist: detect errors when parsing 'strings' dataGravatar Nikias Bassen1-2/+2
plist_from_openstep() tested ctx.err > 0 after parsing the plist as 'strings' data, but error codes are negative, so a failure was never detected and the function returned success with a NULL plist (leaking the temporary dict). Check for any error and return it.
26 hoursFix leaks and crashes on allocation failureGravatar Nikias Bassen4-82/+86
Found by failing each allocation in turn under ASan/UBSan/LSan: - plist_free() leaked the whole tree if its work stack could not be allocated; free the children iteratively without extra memory - plist_copy(): when a shallow copy of a dict/array failed, the original's lookup table was freed along with the copy's data (use-after-free) - bplist parser: fix a NULL dereference in parse_date_node(), check the node_attach() results, and don't leak on failure; plist_to_bin() no longer leaks on an allocation failure - free the plist data when node creation fails (plist constructors, XML, JSON and binary parsers)
26 hoursHarden allocation failure handling and size arithmeticGravatar Nikias Bassen12-46/+291
- check malloc/calloc/realloc results in constructors, the JSON and OpenStep writers, the XML text-part helpers and base64 decoding, and return PLIST_ERR_NO_MEM instead of asserting or dereferencing NULL - guard size calculations against overflow in the UTF-8/UTF-16BE conversions, ptrarray, and bytearray - make hash_table_insert() report failure instead of silently dropping the entry; writers propagate PLIST_ERR_NO_MEM, and dict lookup caches are discarded so lookups fall back to a linear search - don't overwrite an earlier parse error code in the XML parser, and report PLIST_ERR_NO_MEM when reading text content fails for lack of memory - OpenStep parser: check node/data/string allocations and dict/array insertion results Based on #323 by bg0d-droid. Co-authored-by: bg0d-droid <81825610+bg0d-droid@users.noreply.github.com>
26 hoursxplist: fix double-free in node_from_xml() error pathsGravatar Nikias Bassen1-5/+9
node_from_xml() hands a freshly created node over to the tree (as root, or by adding it to a dict/array parent), but only reset `subnode` at the very end of the block. If an error occurred in between, e.g. the allocation of the node path item failing right after the root node was assigned to *plist, err_out freed the node while the tree still referenced it, and plist_free(*plist) then freed it a second time. Clear `subnode` right after the node has been successfully attached so that err_out can never free a node owned by the tree. Also report PLIST_ERR_NO_MEM instead of PLIST_ERR_PARSE when allocating the node path item fails. Co-authored-by: bg0d-droid <81825610+bg0d-droid@users.noreply.github.com>
27 hoursbytearray: grow buffer geometrically to avoid quadratic appendsGravatar Nikias Bassen1-1/+20
byte_array_grow() increased the capacity by a single page at a time, so building large outputs (e.g. the plutil writer on a plist with heavily shared references) caused a realloc and memcpy per page, i.e. quadratic behavior that ran into fuzzer timeouts. Double the capacity when growing. If the larger allocation fails, fall back to the minimum size required before putting the array into its failed state, so low-memory systems are not penalized by the extra headroom. Also guard against size_t overflow in the capacity arithmetic.
28 hoursbplist: limit total parsed nodes to prevent memory exhaustionGravatar Nikias Bassen1-0/+12
Binary plist objects may be referenced multiple times, and each reference is parsed into a separate node. A tiny crafted input where arrays reference the same objects repeatedly (without forming a cycle, so the recursion check does not trigger) expands into millions of nodes and exhausts memory. Track the number of nodes parsed and fail with PLIST_ERR_PARSE once it exceeds num_objects * 16 + 65536. Legitimate files create roughly as many nodes as they have objects, so they stay well below this limit. Credit to OSSFuzz
28 hourstime64: fix signed overflow for extreme years in gmtime64_r()Gravatar Nikias Bassen1-2/+4
IS_LEAP() computed `tm_year + 1900` in int arithmetic, which overflows for dates around year 2^31 (hit via check_tm() when writing dates). Cast to Year (64-bit) in IS_LEAP(). Also make gmtime64_r() return NULL with EOVERFLOW when the calendar year (tm_year + 1900) does not fit in an int; the previous check compared tm_year with itself and could never fail. This avoids strftime() wrapping the year in the output, and callers fall back to their existing default date string. Credit to OSSFuzz
28 hoursxplist: fix memory leak when adding a node to its parent failsGravatar Nikias Bassen1-2/+10
node_from_xml() ignored the return values of plist_dict_set_item() and plist_array_append_item(). When the insertion failed (e.g. when the nesting depth limit is hit in node_attach()), the newly created node was never attached, yet subnode was reset to NULL, so the node, its data and its string value were leaked. Check the return values and bail out with PLIST_ERR_PARSE so that the unattached node is freed in the error path. Credit to OSSFuzz
28 hoursplist: fix use-after-free in plist_copy_node() on stack reallocGravatar Nikias Bassen1-1/+3
plist_copy_node() kept a pointer `f` to the top frame of its traversal stack and dereferenced it after the stack was grown with realloc(), which may move the buffer. Plists nested deeper than 64 levels hit this path, causing a heap-use-after-free when reading f->depth. Read the child depth before the realloc and use it when building the new frame. Thanks to @sdnuhfiu for reporting! Fixes #328
2026-09-29plist: fix inverted strcmp in string to boolean conversionGravatar Arpit Jain1-2/+2
plist_dict_get_bool() compared the string value with strcmp() but treated a non-zero return as a match. strcmp() returns 0 on equality, so the conditions were inverted: "true" produced 0, "false" produced 1, and any other string also produced 1. The error branch could never be reached, since it required both comparisons to return 0 at once. The result is that the API returns the opposite of the stored value for both valid boolean strings, and returns true for strings that are not booleans at all, instead of reporting the conversion error. Compare == 0 in both conditions: input before after true 0 1 false 1 0 not-a-bool 1 error TRUE 1 error (empty) 1 error Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
2026-09-29bytearray: Handle realloc() failure in byte_array_grow()Gravatar Comtea048-2/+72
byte_array_grow() assigned the result of realloc() directly to ba->data and increased the capacity even if realloc() failed, so the following memcpy() in byte_array_append() wrote to NULL + len. On failure, free the old buffer and leave the byte array in a failed state (data == NULL), which byte_array_append() already ignores. The callers check for that state: - the writers (bin, xml, json, openstep and the text output formats) return PLIST_ERR_NO_MEM instead of handing out a NULL/truncated buffer - node_to_xml() base64-encodes <data> directly into the grown buffer, so it must bail out there (only guarded by assert() before) - the OpenStep parser returns PLIST_ERR_NO_MEM for <hex data> instead of silently returning truncated data Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27ptrarray: Handle realloc() failure in ptr_array_insert()Gravatar Comtea044-12/+32
ptr_array_insert() assigned the result of realloc() directly to pa->pdata and bumped the capacity without checking for failure, so on out-of-memory the old buffer was leaked and the following store or memmove() wrote through a NULL pointer. Keep the old buffer on failure and return -1 from ptr_array_insert() and ptr_array_add(). The callers now handle the error: - the array lookup cache (plist.c) is dropped instead of silently going out of sync with the node list; lookups then fall back to walking the children, as they do before the cache exists - bplist parsing (used_indexes) and serialization (objects) return PLIST_ERR_NO_MEM; ignoring the error in parse_bin_node_at_index() would otherwise loop forever since the array never grows Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-05-22Print debug error message when encoutering invalid PLIST_DATE valuesGravatar Nikias Bassen6-0/+12
2026-05-22bplist: Use format macro to use correct format in error messageGravatar Nikias Bassen1-1/+1
2026-05-22common: validate PLIST_DATE values before Time64_T conversionGravatar Nikias Bassen9-6/+48
Avoid undefined behavior when serializing malformed PLIST_DATE values containing NaN, infinity, or values outside the Time64_T range. Add a shared helper for checked date conversion and use it across writer paths.
2026-05-22refactor: centralize formatting helpers and harden out-plutilGravatar Nikias Bassen10-435/+204
2026-05-22out-default: harden node serialization pathsGravatar Nikias Bassen1-23/+63
Improve robustness and memory safety in node_to_string() and dtostr(): - add missing NULL and allocation checks - fix snprintf() error handling and signed/unsigned conversions - replace sprintf() with snprintf() - fix base64 buffer sizing - switch string offset tracking to size_t - improve malformed data handling for strings and data blobs
2026-05-22out-limd: Properly handle snprintf and some smaller improvementsGravatar Nikias Bassen1-27/+84
2026-05-22out-limd: Fix memory buffer allocation size (#313)Gravatar Nikias Bassen1-3/+9
Credit to @Bri1987
2026-04-27Add error handling to all modification functionsGravatar Nikias Bassen1-80/+94
Convert all array/dict modification functions from void to plist_err_t return type: - plist_array_set_item: replace at index n - plist_array_append_item: append to end - plist_array_insert_item: insert at position n - plist_array_remove_item: remove item at index n - plist_array_item_remove: remove item from its array parent - plist_dict_set_item: replace or insert key/value - plist_dict_remove_item: remove key/value pair - plist_dict_merge: merge source dict into target Returns: - PLIST_ERR_SUCCESS on success - PLIST_ERR_INVALID_ARG for invalid arguments (NULL, wrong type, out of range, etc.) - PLIST_ERR_NO_MEM on memory allocation failure - PLIST_ERR_UNKNOWN on unexpected internal errors Header documentation updated with full error code semantics for each function.
2026-03-22Add OpenStep coercion support for non-OpenStep plist typesGravatar Nikias Bassen2-30/+117
- Use PLIST_OPT_COERCE option to coerce PLIST_BOOLEAN, PLIST_DATE, PLIST_UID, and PLIST_NULL to OpenStep-compatible types (1 or 0, ISO 8601 strings, integers, and 'NULL' string) - Add plist_to_openstep_with_options() function to allow passing coercion option (and others) - Update plist_write_to_string() and plist_write_to_stream() accordingly
2026-03-20Add JSON coercion support for non-JSON plist typesGravatar Calil Khalil2-29/+104
- Add PLIST_OPT_COERCE option to coerce PLIST_DATE, PLIST_DATA, and PLIST_UID to JSON-compatible types (ISO 8601 strings, Base64 strings, and integers) - Add plist_to_json_with_options() function to allow passing coercion options (and others) - Update plist_write_to_string() and plist_write_to_stream() to support coercion option - Add --coerce flag to plistutil for JSON output - Create plist2json symlink that automatically enables coercion when invoked
2026-02-25xplist: Enforce single root value inside <plist>Gravatar Sami Kortelainen1-15/+17
Ensure that XML property lists contain exactly one root value inside the <plist> element and reject any additional value nodes before </plist>. Add tests covering root value handling and nested CF$UID conversion behavior. Co-authored-by: Sami Kortelainen <sami.kortelainen@piceasoft.com> Co-authored-by: Nikias Bassen <nikias@gmx.li>
2026-02-22xplist: Convert nested {CF$UID:<int>} dicts to PLIST_UID safelyGravatar Sami Kortelainen2-32/+100
Convert single-entry { "CF$UID" : <integer> } dictionaries to PLIST_UID nodes when closing a dict in the XML parser. Refactor node cleanup logic: - Split plist_free_data() into internal _plist_free_data() - Introduce plist_free_children() to release child nodes separately - Update plist_set_element_val() to free children before changing container node types - Ensure PLIST_DICT hashtables do not free values (assert + force free_func = NULL) This avoids in-place container mutation issues and ensures child nodes and container metadata are released correctly before changing node type. Co-authored-by: Sami Kortelainen <sami.kortelainen@piceasoft.com> Co-authored-by: Nikias Bassen <nikias@gmx.li>
2026-02-13json: Fix a few memory leaksGravatar Nikias Bassen1-0/+25
2026-02-12bplist: Add overflow check to node offset pointer arithmeticGravatar Nikias Bassen1-1/+7
Credit to OSSFuzz
2026-02-12jplist: Add another NULL check to prevent NULL pointer dereferenceGravatar Nikias Bassen1-0/+1
2026-02-12plist: make array and dict iterators opaqueGravatar Nikias Bassen1-41/+68
Introduce private iterator structs for plist_array_iter and plist_dict_iter, and fix *_next_item() to properly advance iterator state and handle malformed containers safely.
2026-02-12Add NULL checks across codebaseGravatar Nikias Bassen5-18/+197
2026-02-10plist: Make plist copy and free implementations iterativeGravatar Nikias Bassen2-48/+211
Convert plist_free_node() and plist_copy_node() to iterative implementations. This avoids unbounded recursion and stack overflow when handling deeply nested plist data, while preserving existing semantics and caches.
2026-02-08plist: Handle node_attach/node_insert failuresGravatar Nikias Bassen1-43/+154
Update plist array and dict mutation helpers to check return values from node_attach() and node_insert(). This prevents cache corruption and allows new depth and cycle checks to be enforced correctly.
2026-01-29xplist: Improve robustness of XML text parsing and value conversionGravatar Nikias Bassen1-69/+152
This change adds stricter validation for numeric and date nodes, including full-input consumption, overflow/range checks, and rejection of invalid floating-point values. Whitespace handling is clarified by explicitly trimming trailing XML whitespace for value nodes.
2026-01-26xplist: Use small stack buffer instead of dynamic allocationsGravatar Nikias Bassen1-31/+16
This removes the necessity for malloc failures and reduces overhead
2026-01-23plist: Improve plist_dict_get_item() to safely iterate key/value pairsGravatar Nikias Bassen1-24/+32
Use explicit key/value stepping, zero-initialize hash lookup key, and perform length-checked comparisons on NUL-terminated key strings.
2026-01-22bplist: Fix compiler warning with explicit castGravatar Nikias Bassen1-1/+1
Credit to @ylwango613
2026-01-22time64: Add time_s support for WIN32Gravatar Rosen Penev2-2/+6
Signed-off-by: Rosen Penev <rosenp@gmail.com>
2026-01-22plist: Fix plist_is_binary() not checking for NULL inputGravatar Nikias Bassen1-1/+1
Fixes issue #300 Credit to @jasonmli8
2026-01-22xplist: Use memcpy instead of strncpy since we know the exact sizeGravatar Nikias Bassen1-3/+3
2026-01-22xplist: Harden entity unescaping against malformed inputGravatar Nikias Bassen1-18/+32
- Fix numeric character reference parsing - Enforce exact entity name matching - Guard against size_t underflow and oversized entities - Reject invalid Unicode code points
2026-01-21bplist: Fix is_ascii_string by using sufficiently large data typeGravatar Nikias Bassen1-11/+10
Fixes #285 Credit to @ylwango613 for reporting.
2026-01-21bplist: Fix UTF-8 to UTF-16 decoding and enforce strict validationGravatar Nikias Bassen1-54/+69
- Treat input as unsigned bytes - Correct UTF-8 bit decoding for 2/3/4-byte sequences - Add overlong, surrogate, and range checks - Enforce lead/continuation byte constraints This addresses issue #283. Credit to @hgarrereyn for reporting.
2026-01-21plist: Fix incorrect size storage in plist_copy() for PLIST_STRING nodesGravatar Nikias Bassen1-3/+3
2026-01-21jsmn: use size_t for token offsets and harden against overflowGravatar Nikias Bassen3-25/+59
Use size_t for token start/end offsets instead of int, replace the -1 sentinel with SIZE_MAX, and add a defensive guard against offset wraparound. This prevents overflow when parsing very large JSON inputs. This addresses issue #282. Credit to @ylwango613 for repporting.
2026-01-20plist: Fix heap overflow caused by incorrect PLIST_STRING length during copyGravatar Nikias Bassen1-3/+18
Credit to @LkkkLxy. Addresses #277.
2026-01-20plist: Reject insertion of plist nodes that already have a parentGravatar Nikias Bassen1-57/+74
Credit to @LkkkLxy for reporting (#276). libplist nodes are owned by exactly one container. Inserting the same plist_t into multiple dicts or arrays corrupts the tree structure and leads to use-after-free crashes during traversal or plist_free(). Add explicit parent checks to dict and array insertion APIs to reject nodes that already belong to another container. In debug builds, this fails loudly via assert() and optional diagnostics; in release builds, the operation safely no-ops. Callers that need to reuse values must create a copy using plist_copy() or explicitly detach the node before reinserting it.
2026-01-17plistutil: Use proper error description for new error codesGravatar Nikias Bassen1-1/+1