Skip to content

Fix CFPropertyListCreateDeepCopy for mutable-copy options#27

Open
DTW-Thalion wants to merge 1 commit into
gnustep:masterfrom
DTW-Thalion:fix/cfplist-deep-copy-mutable-array
Open

Fix CFPropertyListCreateDeepCopy for mutable-copy options#27
DTW-Thalion wants to merge 1 commit into
gnustep:masterfrom
DTW-Thalion:fix/cfplist-deep-copy-mutable-array

Conversation

@DTW-Thalion
Copy link
Copy Markdown

Problem

CFPropertyListCreateDeepCopy is broken for mutable-copy options (kCFPropertyListMutableContainers, kCFPropertyListMutableContainersAndLeaves) in two independent ways that compound to make the function unusable for the mutable-copy path in most real processes.

Bug 1 — uninitialised type IDs

The function dispatches on file-scope _kCF*TypeID variables that are lazily populated by CFPropertyListInitTypeIDs. The only other caller that initialises those variables is CFPlistTypeIsValid. CFPropertyListCreateDeepCopy never calls the init routine itself, so whenever it is the first plist-API call in the process, every typeID == _kCF*TypeID comparison fails (because every such variable is still zero), the function drops into its trailing else branch, and returns NULL.

This is a latent bug that only surfaced when a regression test linked against libgnustep-corebase alone. Tests that also linked libgnustep-base had their type IDs initialised as a side effect of Foundation class initialisation, which is why it had not been noticed.

Bug 2 — apply function iterates the destination, not the source

In the mutable-array branch:

array = CFArrayCreateMutable (alloc, cnt, &kCFTypeArrayCallBacks);
...
ctx.container = (CFTypeRef) array;
range = CFRangeMake (0, cnt);
CFArrayApplyFunction (array, range, CFArrayCopyFunction, &ctx);  /* BUG */

The apply function is invoked on the freshly created empty destination container rather than on the source plist. Since the destination has zero live elements, the apply function visits nothing, and the returned mutable array comes back empty.

On Linux the same code path reads uninitialised slots in the destination container (CFArrayCreateMutable allocates capacity but not elements) and hands zero-valued pointers to CFArrayCopyFunction, which calls CFArrayAppendValueCFArrayReplaceValuesCFRetain(NULL)segfault. The observable failure mode therefore differs between Linux and macOS/Windows.

The identical bug exists in the mutable-dictionary branch at line 346 — CFDictionaryApplyFunction (dict, CFDictionaryCopyFunction, &ctx) should iterate plist. A previous partial fix only addressed the array case.

Fix

Source/CFPropertyList.c:

  1. Call CFPropertyListInitTypeIDs() at the top of CFPropertyListCreateDeepCopy so the function is self-sufficient and doesn't silently return NULL when invoked as the first plist call.
  2. In the mutable-array branch, iterate over the source plist, not the destination array. Commented inline with the rationale.
  3. In the mutable-dictionary branch, iterate over the source plist, not the destination dict. Commented inline.

Regression test

Tests/CFPropertyList/deepcopy.m — plain-C harness using PASS_CF from Tests/CFTesting.h, matching the style of the existing validate.m test in the same directory. Public CoreFoundation C API only — no Objective-C runtime assumptions, no blocks.

17 assertions across:

  • Mutable array deep copy: non-NULL result, exact element count, element-by-element PASS_CFEQ against the originals, and appending to the copy grows the count (proves the returned object is genuinely mutable).
  • Immutable array deep copy: guards the branch that was never broken against a fix that accidentally regresses it.
  • Mutable dictionary deep copy: non-NULL result, exact entry count, per-key value match via PASS_CFEQ, and adding to the copy grows the count.
  • Nested mutable deep copy: a mutable array containing another array and a string round-trips correctly. This catches a fix that only works at the top level, since CFPropertyListCreateDeepCopy recurses through itself.

Validation

Built and run against a clang 18.1.3 build on Ubuntu 24.04:

Build Result
Both fixes applied 17 / 17 pass
Only init fix; apply-function swap reverted Test process crashes in CFRetain(NULL) inside CFArrayCopyFunction on the first mutable array deep copy; gnustep-tests reports Failed file: deepcopy.m aborted without running all tests

The discrimination between the fixed and unfixed build is strict — the unfixed build doesn't fail some assertions, it dies outright on the first mutable-copy call, which is the worst-case manifestation of the bug and the one that would surface in any real application.

ChangeLog

Entry added in ChangeLog matching the repository's GNU-style format.

* Source/CFPropertyList.c: Two related bugs prevented the function
  from producing correct mutable deep copies.

  (1) The function dispatches on file-scope _kCF*TypeID variables
  that are lazily populated by CFPropertyListInitTypeIDs. The only
  other caller that initialised those variables was
  CFPlistTypeIsValid, so CFPropertyListCreateDeepCopy would drop
  into its trailing `else` branch and return NULL whenever it was
  the first plist-API call in the process. Add an explicit
  CFPropertyListInitTypeIDs() call at the top of the function.

  (2) The mutable-array and mutable-dictionary branches called
  CFArrayApplyFunction / CFDictionaryApplyFunction on the newly
  created empty destination container rather than on the source
  plist. The apply function therefore visited zero elements and
  every mutable deep copy came back empty. On Linux the iteration
  also read uninitialised slots in the destination container and
  crashed via CFRetain(NULL) inside CFArrayCopyFunction. Iterate
  on `plist` in both branches so the copy actually walks the
  source values.

* Tests/CFPropertyList/deepcopy.m: New regression test exercising
  every branch of the mutable-copy path:
    - mutable array deep copy has the same count as the source;
    - mutable array deep copy preserves element identity;
    - the result is genuinely mutable (append grows the count);
    - immutable array deep copy still works (guards the non-buggy
      branch against a fix that accidentally regresses it);
    - mutable dictionary deep copy has the same count as the
      source (the dictionary branch had the same bug as the
      array branch and had been missed by a previous partial fix);
    - mutable dictionary deep copy preserves values by key;
    - the result is genuinely mutable;
    - a nested mutable deep copy preserves both outer and inner
      contents, since the recursive descent reuses the same
      mutable-copy path.
  Plain C / public CoreFoundation API only, using the PASS_CF
  macro from Tests/CFTesting.h - no Objective-C runtime
  assumptions, no blocks.

The test was validated both ways against a clang build on Ubuntu
24.04: with both fixes in place, all 17 assertions pass; with the
apply-function swap reverted, the test process crashes in
CFRetain(NULL) on the first mutable array deep copy, which
gnustep-tests correctly reports as "Failed file: aborted without
running all tests". The discrimination between fixed and unfixed
builds is therefore strict, not incidental.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant