Skip to content

fix: close the handles to an object pdf_annot_remove_object() removes (ADR-032) - #73

Merged
billdenney merged 4 commits into
mainfrom
claude/annot-object-remove-invalidate
Sep 27, 2026
Merged

billdenney merged 4 commits into
mainfrom
claude/annot-object-remove-invalidate

Conversation

@billdenney

Copy link
Copy Markdown
Member

Summary

pdf_annot_remove_object() destroys an object of an annotation's appearance stream, but handles made to it earlier through pdf_annot_objects() still pinned an open annotation. They passed every validity check and read the freed object. Now those handles, and any clip paths or nested form objects read through them, are refused with a clear error. The design is recorded as ADR-032, which extends ADR-025 and ADR-029.

Changes

  • The handles pdf_annot_objects() returns are registered in the per-document handle registry under a new kind whose release only clears the handle. PDFium owns the object.
  • cpp_annot_remove_object() releases the handles whose address is the removed object's, but only after PDFium has actually removed it. PDFium refuses removal for subtypes other than ink and stamp, and clearing first would close handles to objects that still exist. A test pins the refused case.
  • Clip paths and nested objects read through a removed object are refused through the ADR-029 prot chain, with a message naming pdf_annot_remove_object().
  • cpp_annot_get_object() and cpp_annot_remove_object() take the document handle.
  • Docs: ADR-032 and its index row, NEWS, the CLAUDE.md memory-model section, dev/architecture.md, the architecture vignette, and the Rd for pdf_annot_objects() and pdf_annot_remove_object().

User-visible change

a <- pdf_annot_new(page, "stamp", bounds = c(0, 0, 100, 100))
pdf_annot_append_object(a, pdf_rect_new(page, 0, 0, 50, 50))
obj <- pdf_annot_objects(a)[[1L]]
pdf_annot_remove_object(a, 1L)
pdf_obj_bounds(obj)
#> Error: The object was removed from its annotation by pdf_annot_remove_object(). The object handle is no longer valid.

Objects read through a second handle to the same annotation are separate copies and stay valid.

Verification

Linux, R 4.6.1, valgrind 3.22, PDFium chromium/8066. Invalid reads under valgrind, before and after:

Case Before After
The removed object's handle, and a second handle to it 4 (R aborts under valgrind) refused, 0
Clip path of a removed object 1 refused, 0
Child of a form removed from its annotation 5 refused, 0
Objects read through a second annotation handle 0 0 (separate copies)
  • Weekly valgrind invocation: definitely, indirectly and possibly lost 0 bytes; 0 invalid reads, writes or frees; [ FAIL 0 | SKIP 1 | PASS 3611 ].
  • Full suite under MALLOC_CHECK_=3 with libc_malloc_debug: 3611 pass.
  • Crash-isolated object sweep (65 functions × 8 targets × 2 teardown orders): 1040 of 1040 exit 0, with outcomes unchanged.
  • covr: 100% of R code, and 100% of handle_registry.cpp and api_completion.cpp.

Merging with the R-level finalizers PR

This PR and #72 both change src/handle_registry.{cpp,h}. Whichever lands second resolves the conflict:

  • keep this PR's annotation-object kind and its clear-only release;
  • use that PR's R-level finalizers in place of this PR's C finalizer entry;
  • keep both sides of the prose in CLAUDE.md and dev/architecture.md;
  • keep the ADR index rows in number order.

🤖 Generated with Claude Code

billdenney and others added 4 commits September 27, 2026 16:51
pdf_annot_remove_object() destroys an object of an annotation's
appearance stream, but the handles made to it earlier still pinned an
open annotation, so they passed every check. Under valgrind, the
removed object's handles read the freed object (4 invalid reads), a
clip path read from it 1, and an object nested in a removed form 5.

The handles pdf_annot_objects() returns are now registered under the
annotation's document as a kind whose release only clears the handle,
and cpp_annot_remove_object() releases those whose address is the
removed object's once PDFium has removed it. The clip paths and nested
objects read through them are then refused by the ADR-029 chain, with
a message naming pdf_annot_remove_object(). A removal PDFium refuses
leaves the handles open, and objects read through another handle to
the same annotation are separate copies that stay valid. See ADR-032.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md's memory-model section, dev/architecture.md and the
architecture vignette now mention that the handles to an annotation's
page-objects are registered under their document and that
pdf_annot_remove_object() closes them (ADR-032).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR 71 (claude/handle-lifetime-leftovers, this branch's base) has
merged, so the branch now targets main. The merge also brings in
PR 70, which runs the pdf_image_extract() example only when png is
installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in the R-level handle finalizers (ADR-031) along with the
verified downloads and rhub checks. The handle registry conflicted
where both branches changed it, and the resolution keeps both
designs: the annotation page-object kind, its close_nothing release
and release_annot_object_handles() from this branch; the single
R-level finalizer from main in place of this branch's table of C
finalizers, and no memory-buffer kind, which ADR-031 removed. The
registry comments, CLAUDE.md and dev/architecture.md combine both
descriptions, and the ADR index lists 027 to 032 in order.

Rcpp exports and roxygen output are unchanged by the merge. The full
suite passes under MALLOC_CHECK_=3 (3681 passed); the weekly valgrind
invocation reports 0 bytes lost and no invalid access; the removed-
object reproducer is still refused with 0 invalid reads, and the
unload scenarios still exit cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@billdenney
billdenney merged commit 879ca6d into main Sep 27, 2026
14 checks passed
@billdenney
billdenney deleted the claude/annot-object-remove-invalidate branch September 27, 2026 21:21
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