Skip to content

Fix bad-free heap corruption in ppathstatus and psyncer#392

Merged
lneely merged 3 commits intomainfrom
fix/pcl-26j-bad-free-heap-corruption
Mar 24, 2026
Merged

Fix bad-free heap corruption in ppathstatus and psyncer#392
lneely merged 3 commits intomainfrom
fix/pcl-26j-bad-free-heap-corruption

Conversation

@lneely
Copy link
Copy Markdown
Owner

@lneely lneely commented Mar 19, 2026

Summary

  • ppathstatus.c (×2) and psyncer.c (×1) called bare free() via ptree_for_each_element_call_safe on nodes allocated with pmem_malloc
  • pmem_malloc prepends a pmem_header_t, making the returned pointer an interior pointer to the glibc chunk; free() reads garbage as the chunk-size field and aborts with free(): invalid size
  • Crash reproduces reliably with larger files because more sync-queue / path-status churn raises the probability these bulk-free paths fire on non-empty trees
  • Same bug class as the pintervaltree.c / pfscrypto.c fixes in 9c10304

Fix

Added free_folder_tasks_node() (ppathstatus.c) and free_synced_down_folder() (psyncer.c) — thin wrappers calling pmem_free(PMEM_SUBSYS_OTHER, ...) — and substituted them for bare free in the three affected ptree_for_each_element_call_safe calls. Pattern matches the existing free_interval_tree_node fix.

Test plan

  • Build succeeds without warnings
  • Open and read a file larger than a few MB through the FUSE mount; confirm no free(): invalid size / PANIC: Abort crash
  • Trigger a sync cycle (add/remove a folder) to exercise ppathstatus_init() and psyncer_dl_queue_clear() paths

🤖 Generated with Claude Code

Levi Neely and others added 3 commits March 19, 2026 23:08
Both ppathstatus.c and psyncer.c used bare free() via
ptree_for_each_element_call_safe to bulk-free tree nodes that were
allocated with pmem_malloc.  pmem_malloc prepends a pmem_header_t to
every allocation, so the returned pointer is an interior pointer to the
underlying glibc chunk.  Passing it to free() makes glibc read a garbage
size field from the pmem header and abort with "free(): invalid size".

This is the same class of bug fixed earlier in pintervaltree.c and
pfscrypto.c.  The crash manifests reliably with larger files because
more sync-queue and path-status churn occurs, increasing the likelihood
that one of these bulk-free paths is hit while a non-empty tree exists.

Fix: add free_folder_tasks_node() (ppathstatus.c) and
free_synced_down_folder() (psyncer.c) helpers that call pmem_free, and
use them as the ptree_for_each_element_call_safe callback in all three
affected call sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two more call sites passed bare free() as a callback to tree/list
traversal macros, but the nodes were allocated via pmem_malloc which
prepends a pmem_header_t. Freeing the data pointer directly skips the
header and corrupts the heap.

- pfs.c: ptree_for_each_element_call_safe on sectorsinlog used free()
  on psync_sector_inlog_t; replaced with free_sector_inlog_node() that
  calls pmem_free(PMEM_SUBSYS_OTHER, e).
- ppagecache.c: psync_list_for_each_element_call on request->ranges
  used free() on psync_request_range_t; replaced with
  free_request_range() that calls pmem_free(PMEM_SUBSYS_CACHE, range).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unit test (test_pcl26j_free): exercises all four fixed call sites —
psync_request_range_t, synced_down_folder, folder_tasks_t, and
psync_sector_inlog_t — using --wrap=malloc/free to verify that
pmem_free() passes the header pointer to free(), not the data pointer.
Includes a harness self-check that confirms bare free(data_ptr) is
detected. Would have failed against pre-fix code.

Smoke test (smoke-test-large-read.sh): builds pcloudcc with ASAN,
starts the daemon, streams a large file (>=50 MB) from the FUSE mount
to /dev/null to force multi-range psync_request_range_t allocation and
teardown via psync_pagecache_free_request, then scans the ASAN log for
any bad-free reports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lneely lneely merged commit 7595c48 into main Mar 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant