Skip to content

Post-v0.50 review fixes: rv_clone_iterative cycle/CV leaves, blessed terminator, sandboxed-macOS test#3

Closed
atoomic wants to merge 43 commits into
masterfrom
koan.atoomic/post-v0.50-review-fixes
Closed

Post-v0.50 review fixes: rv_clone_iterative cycle/CV leaves, blessed terminator, sandboxed-macOS test#3
atoomic wants to merge 43 commits into
masterfrom
koan.atoomic/post-v0.50-review-fixes

Conversation

@atoomic

@atoomic atoomic commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Summary

Four fixes surfaced by an audit of changes since v0.50, each as its own commit, all with regression tests:

  • rv_clone_iterative cycle guard — a scalar-ref cycle reachable only past MAX_DEPTH made the chain walk loop forever (chain[] doubled via Renew until OOM). Now consults hseen for each referent before pushing. (fa78264)
  • Non-cloneable leaves in rv_clone_iterative — a deep RV chain ending in a CODE / IO / FM / PVLV leaf called newSVsv() and croaked "Bizarre copy of CODE in subroutine entry". Mirror the regular sv_clone() switch: share via SvREFCNT_inc for the non-cloneable types. (612b11b)
  • Blessing preservation on multi-element AV terminatorav_clone_iterative()'s post-loop fallback wrapped the cloned AV in newRV_noinc() without re-blessing, so a blessed multi-element array at the bottom of a [[[...]]] chain came back unblessed. (abb19f1)
  • t/23-mgptr-memleak.t sandboxed-macOS noise — the ps hardening from a5dc9e6 in t/12-memleak.t was missed in t/23. Same patch applied. (a176e33)

Reproduction (before)

# Cycle → OOM
my @s; $s[0] = \"leaf"; my $r = \$s[0];
for my $i (1..5000) { $s[$i] = $r; $r = \$s[$i] }
$s[0] = \$s[3000];   # close cycle past MAX_DEPTH
clone($r);           # → "Out of memory!"

# CODE leaf → croak
my @s; $s[0] = sub { 42 }; my $r = \$s[0];
for my $i (1..5000) { $s[$i] = $r; $r = \$s[$i] }
clone($r);           # → "Bizarre copy of CODE in subroutine entry"

# Multi-element blessed terminator → blessing lost
my $multi = bless [1,2,3], 'MyClass';
my $r = $multi;
$r = [$r] for 1..5000;
ref($r);             # MyClass
my $c = clone($r);
my $w = $c; $w = $w->[0] while ref($w) eq 'ARRAY' && @$w == 1;
ref($w);             # '' (was: MyClass)

Test plan

cc upstream review when ready.

Koan-Bot and others added 30 commits March 24, 2026 20:12
Set COPYFILE_DISABLE=1 before tar in the dist target so that macOS
extended attributes (com.apple.provenance, com.apple.lastuseddate, etc.)
are not embedded in the CPAN tarball. These attributes cause tar errors
on non-Apple systems (NetBSD, Linux) that do not understand them.

COPYFILE_DISABLE=1 is macOS-specific and is a no-op on Linux/Windows.

Fixes garu#85

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add AI_POLICY.md describing how AI tools are used in the project's
development workflow. The policy establishes the principle that AI
assists but humans decide — maintainers remain responsible for every
review, merge, and release.

Also update MANIFEST to include the new file and MANIFEST.SKIP to
exclude dist tarballs (Clone-*.gz) and the local/ directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix garu#82: Cloned DBI handles lack XS-level magic, causing DBI's
DESTROY to dump pages of SV internals to STDERR. Circular refs
inside the cloned handle tree also cause SEGVs during global
destruction. The previous test 16 ($cloned->prepare) segfaulted
on most platforms.

Fix by running DBI clone tests in forked subprocesses:
- Child redirects STDERR to /dev/null (suppresses all DBI noise)
- Child uses POSIX::_exit(0) to skip destructors (avoids SEGV)
- Child communicates results via pipe as key=value pairs
- Parent relays results to Test::More assertions

Also replace test 16's unsafe $cloned->prepare() call with a safe
Scalar::Util::reftype structural check, and add $sth->finish
before $dbh->disconnect to eliminate the "disconnect invalidates
active statement handle" warning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refs garu#82: The DBI clone tests use fork() + pipe to isolate
cloned DBI handle destruction (which causes SEGVs and STDERR
noise). Strawberry Perl on Windows emulates fork() via ithreads
which does not reliably support pipe+fork, causing "planned 18,
ran 12" failures in CI.

Add _can_fork() guard that returns false on MSWin32 and skips
the 6 DBI tests with a clear message instead of dying with a
bad plan count.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The file defines its own ok() sub (line 58) that returns undef
instead of the test result. This caused the `or warn` on line 133
to always fire, printing "(?^:abcdefg) vs (?^:abcdefg)" to STDERR
even when the test passed. Remove the dead diagnostic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update version timestamps in Changes to match the actual git tag
commit times for v0.32+. Normalize formatting to consistent
single-space separators, remove double spaces and tab indentation,
and convert old non-ISO dates (0.06, 0.01) to YYYY-MM-DD format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
COPYFILE_DISABLE=1 (added for issue garu#85) prevents macOS tar from
embedding ._AppleDouble sidecar files, but does not prevent it from
writing PAX extended headers for xattrs that already exist on the
files themselves (e.g. com.apple.provenance, which macOS sets on
every file in a git-cloned repo).

Add a PREOP step that runs 'xattr -cr $(DISTVNAME)' to strip all
extended attributes from the distribution directory before tar is
invoked. xattr(1) is macOS-specific; the '|| true' makes it a
silent no-op on Linux and Windows.

Fixes garu#91

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The documentation claimed 32,000 levels deep which hasn't been true
since MAX_DEPTH was lowered to prevent SEGV on CPAN smokers. Update
Clone.pm POD, README.md, and CLAUDE.md to reflect actual limits:
4000 rdepth units (~2000 nesting levels) on Linux/macOS, 2000 rdepth
units (~1000 nesting levels) on Windows/Cygwin. Also document the
iterative fallback for arrays and the shallow-copy warning for other
types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The switch cases for PERL_MAGIC_taint ('t'), PERL_MAGIC_backref ('<'),
and PERL_MAGIC_arylen_p ('@') each executed `continue` (resuming the
outer magic iteration for-loop) followed by a `break` that could never
be reached. Remove the dead `break` and add a comment clarifying that
`continue` resumes the outer loop, not the switch.

Fixes garu#97

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The `if (magic_ref) { ;; }` block was a confusing no-op whose empty body
read as a forgotten implementation. Replace with `if (!magic_ref) { ... }`
wrapping the HV/AV/RV branches, and add a comment explaining that tied
HV/AV skips direct element iteration because the tie magic handles the data.

Fixes garu#96

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
COPYFILE_DISABLE=1 (added for issue garu#85) prevents macOS tar from
embedding ._AppleDouble sidecar files, but does not prevent it from
writing PAX extended headers for xattrs that already exist on the
files themselves (e.g. com.apple.provenance, which macOS sets on
every file in a git-cloned repo).

Add a PREOP step that runs 'xattr -cr $(DISTVNAME)' to strip all
extended attributes from the distribution directory before tar is
invoked. xattr(1) is macOS-specific; the '|| true' makes it a
silent no-op on Linux and Windows.

Fixes garu#91

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When rdepth exceeded MAX_DEPTH, the guard in sv_clone handled SVt_PVAV
and RV->AV iteratively but fell through to SvREFCNT_inc(ref) for all
other types, silently aliasing hash nodes into the clone. This broke the
core contract of clone() for deeply nested hash structures.

Fix by adding hv_clone_iterative() (mirroring av_clone_iterative) and
routing SVt_PVHV and RV->HV through it in the rdepth > MAX_DEPTH block.
The new function creates a fresh HV, registers it in hseen for circular-
reference safety, and clones each value via sv_clone — which will
re-enter the iterative path for any further nested HVs still above
MAX_DEPTH.

Add six new tests in t/10-deep_recursion.t covering:
- deep hash structures at rdepth > MAX_DEPTH
- clone-independence verification at depth 2500 (past the guard)
- mixed array/hash structures past MAX_DEPTH

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reduced `$deep_target` on Windows/Cygwin from 4000 to 2500 in `t/10-deep_recursion.t` to fix Windows CI stack overflow. Hash structures (HV) are significantly heavier per nesting level than arrays during Perl's recursive `SvREFCNT_dec` cleanup, causing stack overflow on Windows' 1MB default thread stack. The new value (2500) still exceeds `MAX_DEPTH` (2000 on Windows), ensuring the iterative clone path is exercised. The clone-independence check depth (`$depth_target = 1500`) remains well within range.
Fix garu#90

Add a security policy based on the CPAN Security Group's guidelines
(v1.3.0) and modeled after WWW::Mechanize's SECURITY.md. Covers
how to report vulnerabilities via GitHub's advisory form, response
expectations, scope of the policy, and supported Perl versions.
Need to enable the github settings.
The "visible = 1" workaround was added 20+ years ago for Perl 5.9.x
when mg_find(ref, '<') stopped detecting backref magic reliably. The
root cause: Perl 5.10 moved HV weakref backrefs from PERL_MAGIC_backref
to the HV's AUX struct (SvOOK), so SvMAGICAL alone misses HV targets.

Replace with a precise condition that tracks SVs in hseen only when
needed: shared SVs (refcount > 1), magical SVs (tied, non-HV weakref
targets), or HVs with AUX data (SvOOK — covers HV weakref targets).
Unique leaf scalars skip the hash lookup/store entirely.

Add t/20-shared-sv.t with 31 tests covering shared refs, weak refs on
HV/AV/scalar targets, and topology preservation after cloning.

Co-Authored-By: Claude <noreply@anthropic.com>
When cloning deeply nested scalar references past MAX_DEPTH,
Clone silently returns a shared reference (SvREFCNT_inc) instead
of a deep copy. This adds a Perl_warn so callers know the clone
is incomplete. Also corrects the LIMITATIONS pod: hashes now have
an iterative fallback (added in GH garu#93), so only non-container
types fall back to shared references.

Co-Authored-By: Claude <noreply@anthropic.com>
Here's a summary of the changes:

- **Added `$Clone::WARN` package variable** (defaults to `1`) in `Clone.pm` to let callers suppress the depth-limit warning by setting `$Clone::WARN = 0`, per reviewer request for a mechanism to disable warnings once acknowledged
- **Made `Perl_warn()` conditional** in `Clone.xs`: checks `$Clone::WARN` before emitting the shallow-copy fallback warning; if the variable is false or set to 0, the warning is suppressed
- **Documented `$Clone::WARN`** in the LIMITATIONS pod section showing `$Clone::WARN = 0` to silence the warning
- **Added test 17** verifying that `local $Clone::WARN = 0` suppresses depth-limit warnings (test count bumped from 16 to 17)
)

Tests 15-17 verify that cloning a deeply nested scalar-ref chain
past MAX_DEPTH produces a genuine deep copy instead of a shared
alias.  They FAIL before the fix: the old code returns
SvREFCNT_inc(ref) which aliases back into the original object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…aru#107)

Previously, when rdepth exceeded MAX_DEPTH for an RV that did not point
to an AV or HV (e.g. a scalar-ref chain \\...\$val), sv_clone() fell back
to SvREFCNT_inc(ref), returning the original SV as the "clone".  This
silently violated isolation: writes through the cloned reference would
mutate the original object.  The warning was suppressable via
$Clone::WARN = 0, so production code could never rely on it.

Fix: add rv_clone_iterative(), which walks the RV->RV->...->leaf chain
without recursion (analogous to av_clone_iterative / hv_clone_iterative),
clones the leaf, and rebuilds the reference chain from the bottom up.
The MAX_DEPTH guard in sv_clone() now calls rv_clone_iterative() for any
RV type instead of falling back to a shared alias.

Non-RV, non-AV, non-HV SVs past MAX_DEPTH (PVGV, PVCV, etc.) are not
deep-copyable regardless of depth and still fall through to the existing
SvREFCNT_inc path with a warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#106)

The chain-walking loop in av_clone_iterative() never consulted hseen,
so a circular AV (e.g. $a = [$a]) placed at the bottom of a 2001+
level deep wrapper chain caused an infinite loop and unbounded memory
allocation when sv_clone() switched to iterative mode.

Add a CLONE_FETCH guard at the top of the loop body: if inner_sv is
already in hseen, link tail to the existing clone via newRV_inc and
break out of the loop. This mirrors the guard that exists at the
function's entry point and on the standard recursive sv_clone path.

Adds t/21-circular-iterative.t with five tests covering:
- no hang/die on deep circular arrays past MAX_DEPTH
- circular reference is preserved in the clone
- clone is independent from the original
- edge case at exactly MAX_DEPTH+1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When cloning an SV whose magic has mg_len == HEf_SVKEY, Clone.xs was
calling SvREFCNT_inc(mg->mg_ptr) manually before passing mg_ptr to
sv_magic().  sv_magic() already calls SvREFCNT_inc internally for
HEf_SVKEY, so the manual increment was redundant and produced a
permanent refcount leak (one increment was never matched by a decrement
on cleanup).

Remove the manual SvREFCNT_inc and let sv_magic() take sole ownership of
the reference, matching Perl's own convention for this flag.

Add t/21-hefsvkey-refcnt.t to exercise PVLV tied-element proxies (the
most common carrier of HEf_SVKEY magic) and verify refcount stability
across clone/destroy cycles.  Direct pure-Perl reproduction of the exact
buggy path is not possible on modern Perl (the GH garu#42 PVLV early-exit
prevents it); the fix is verified by code inspection and the test guards
the related invariants.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LIMITATIONS section in Clone.pm incorrectly stated that deeply
nested scalar references would be shared instead of deep-copied.
Since the addition of rv_clone_iterative (GH garu#107), all reference
types are handled by the iterative fallback. Update the docs to
reflect the current behavior.

Also fix the filehandle description — IO objects are shared by
reference (SvREFCNT_inc), not deep-copied with a shared fd.

Renumber t/21-hefsvkey-refcnt.t to t/22-hefsvkey-refcnt.t to fix
duplicate test numbering (t/21-circular-iterative.t already uses 21).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The clone() XSUB allocates hseen (HV) and weakrefs (AV) in PREINIT,
but only frees them at the end of PPCODE.  If sv_clone() triggers
croak() — via unknown SV type, CLONE_STORE failure, or unsupported
magic_ptr — the longjmp skips the explicit hv_clear/SvREFCNT_dec
calls, leaking both containers and all their contents.

Switch to SAVEFREESV(), which registers both SVs for automatic
cleanup on scope exit.  Perl's stack unwinding invokes these during
croak, preventing the leak.  On the normal (non-croak) path, cleanup
happens at scope exit just as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When cloning SVs with magic that has mg_len >= 0 and non-null mg_ptr
(e.g. vstring magic 'V'), the code allocated an intermediate buffer
via Newxz, copied the data, then passed it to sv_magic(). However,
sv_magic() with non-negative namlen calls savepvn() internally to
make its own copy — the intermediate buffer was never freed.

Over 200K clone() calls of a vstring, this leaked ~3 MB. The fix
passes mg->mg_ptr directly to sv_magic() and lets it handle the copy.

Note: the mg_len == -1 paths (utf8 cache, HEf_SVKEY) are unaffected —
sv_magic() stores those pointers directly without copying, so the
explicit allocation in those paths is correct and necessary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…#122)

Under MacPorts the build user has no valid shell configured, so backtick
`ps -o rss= -p $$` fails with "Can't exec 'ps': Operation not permitted"
and pollutes the test output (the SKIP still fires, but the noise is
confusing).

Replace the backtick call with a list-form open() using the absolute
path /bin/ps. This bypasses the shell entirely, so the invocation
works regardless of the user's shell setting, and any exec failure is
caught silently via the `or return undef` branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Koan-Bot and others added 13 commits April 11, 2026 15:25
Here's what I changed and why:

- **Added `macos-latest` CI job** to `.github/workflows/test.yml` per reviewer request from @atoomic to add macOS to the CI workflow. The job runs on `macos-latest`, uses system Perl (which has threads support), installs test dependencies via cpm, and runs the full build+test cycle. This ensures macOS-specific issues like the `/bin/ps` shell invocation problem (GH garu#122) are caught in CI going forward.
When cloning structures deeper than MAX_DEPTH/2, the iterative fallback
used SvREFCNT_inc for all non-container types, including simple scalars.
This aliased leaf strings and integers between original and clone,
causing mutations through a reference to the clone's leaf value to
corrupt the original.

Add a type switch before the SvREFCNT_inc fallback: scalar types
(SVt_NULL through SVt_PVMG) now get newSVsv() for safe, independent
copying. Non-clonable types (PVGV, PVCV, etc.) continue to use
SvREFCNT_inc with warnings.

Closes garu#109.
…aru#116)

The iterative clone path for deeply nested single-element arrays
([[[...]]]) created new AVs via newAV() without checking SvOBJECT
on the original. Blessed arrayrefs past MAX_DEPTH/2 nesting levels
silently lost their class. Fix: bless the clone AV via sv_bless()
when the original is an object.

Co-Authored-By: Claude <noreply@anthropic.com>
Summary of changes:

- **Renumbered test file `t/22-iterative-bless.t` → `t/23-iterative-bless.t`** to resolve numbering collision with `t/22-hefsvkey-refcnt.t` and `t/22-mgptr-memleak.t` that landed on master during the rebase. The project convention requires sequential test numbering (`t/NN-descriptive-name.t`).
- **Updated MANIFEST** to reflect the new filename and corrected sort order.

No review comments were present to address — the only change is fixing the test numbering collision introduced by the rebase.
MacPorts runs tests in a sandbox where even direct exec() of /bin/ps is
blocked (EPERM). The child process still prints "Can't exec" to STDERR
before dying, polluting test output even though the test already skips
gracefully.

Fix: redirect STDERR to /dev/null before the fork via `open(local *STDERR,
'>', '/dev/null')`. The child inherits the redirected fd 2 so any exec-
failure message is silently discarded. Perl's 'local' mechanism restores
STDERR when the function returns.

Fixes garu#122

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The rdepth > MAX_DEPTH block had three separate RV cases (RV-to-AV,
RV-to-HV, RV-to-scalar) that duplicated logic already present in
rv_clone_iterative. The first two cases also lacked SvWEAKREF handling,
silently converting weak references into strong ones when cloning past
MAX_DEPTH.

Consolidate all three into a single rv_clone_iterative call, which
already handles AV/HV referent dispatch, blessing preservation, and
weakref propagation. This removes ~12 lines of duplicated code and
fixes the weakref loss bug.

Add tests verifying weakref preservation on deeply nested structures
past MAX_DEPTH.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ubuntu 20.04 (Focal) reached end-of-life in April 2025.  Its package
mirrors are increasingly unreliable, causing transient apt-get failures
that produce noisy CI results unrelated to Clone itself.

Replace with Ubuntu 22.04 (Jammy), supported until April 2027.
The Windows CI step ran `prove -l t` before `perl Makefile.PL && gmake`,
meaning tests either used a pre-installed Clone (not the current code) or
failed silently in PowerShell. Also changed `-l` (lib/) to `-b` (blib/)
since the compiled XS module lives in blib/arch/, not lib/.

Separated the monolithic run block into distinct Build and Run Tests steps
for clarity, and removed the redundant `gmake test` since prove already
runs all tests with the GitHub annotations formatter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three test files shared the t/22- prefix, violating the convention
documented in CLAUDE.md (sequential numbering, NN-descriptive-name.t).
The prior fix in a3736cf renumbered only t/22-iterative-bless.t to
t/23, leaving three siblings still colliding.

Renumbering by creation order:
- t/22-hefsvkey-refcnt.t   stays 22 (oldest, 2026-03-29)
- t/22-mgptr-memleak.t     -> t/23-mgptr-memleak.t
- t/22-deep-leaf-isolation.t -> t/24-deep-leaf-isolation.t
- t/23-iterative-bless.t   -> t/25-iterative-bless.t

MANIFEST updated to match. All 345 tests pass.
rv_clone_iterative walked the SvRV chain with no cycle detection. A scalar-
ref cycle reached only past MAX_DEPTH (so the recursive sv_clone path never
registered it in hseen) caused the chain[] buffer to double via Renew()
forever until allocation aborted ("Out of memory!").

Check hseen for each referent before pushing onto chain[]; if cached, use
the existing clone as the leaf and stop walking. The leaf-cloning branch
runs only when no cached referent was found.

Add t/26-rv-iterative-cycle.t with a 5000-RV chain that closes a back-edge
deep in the chain — reproduces the OOM before the fix.
When the bottom of an RV chain past MAX_DEPTH was a non-scalar SV that
sv_setsv refuses to copy (PVCV, PVGV, PVFM, PVIO, PVLV, REGEXP/BM),
rv_clone_iterative called newSVsv() which croaked
"Bizarre copy of CODE in subroutine entry" or produced a stringified
ref. The regular sv_clone() switch already shares these types via
SvREFCNT_inc; mirror that behavior on the iterative path.

Add t/27-rv-iterative-noncloneable.t: deep chains terminating in CODE,
GLOB, and IO leaves now clone without dying.
av_clone_iterative()'s post-loop fallback for the chain terminator wrapped
the cloned AV in newRV_noinc() without re-blessing. A blessed multi-element
array at the bottom of a [[[...]]] chain therefore came back unblessed.

Apply the same SvOBJECT / sv_bless pattern already used inside the
chain-walk loop, on the wrapping RV produced for the terminator.

Extend t/25-iterative-bless.t with a multi-element terminator case that
fails before this change.
Commit a5dc9e6 hardened get_rss_kb() in t/12-memleak.t against sandboxed
macOS builds (e.g. MacPorts builder with no valid shell) by using the
list form of open and redirecting STDERR to /dev/null. The same helper
exists verbatim in t/23-mgptr-memleak.t and was missed.

Apply the same patch so the vstring memleak harness does not emit
"Can't exec" noise on sandboxed macOS hosts.
@atoomic

atoomic commented Apr 26, 2026

Copy link
Copy Markdown
Owner Author

Superseded by upstream PR (re-targeting at garu/Clone).

@atoomic atoomic closed this Apr 26, 2026
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.

2 participants