WIP: fix Cwd taint, typeglob slot semantics, and caller line metadata (#1125, #1119, #1135) - #1186
Draft
fglock wants to merge 7 commits into
Draft
WIP: fix Cwd taint, typeglob slot semantics, and caller line metadata (#1125, #1119, #1135)#1186fglock wants to merge 7 commits into
fglock wants to merge 7 commits into
Conversation
Mark the operating-system paths returned by Internals::getcwd and Internals::abs_path as tainted while taint mode is active, so every Cwd entry point Cwd.pm aliases to them (getcwd, cwd, fastcwd, fastgetcwd, abs_path, realpath, fast_abs_path, fast_realpath) matches standard Perl. Data::Compare's "unless tainted getcwd()" guard now disables plugin discovery instead of walking plugin directories with File::Find. Fixes #1125 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Document the parallel-worktree + single draft PR flow in the debug-perlonjava skill: open the draft PR as soon as the first commit lands so no fix exists only in a worktree, merge each issue branch with --no-ff as it arrives, base dependent work on the integration branch, verify every agent claim against system perl before merging, flip to ready once make is green, and hand off for UAT while CI runs. Also correct the build table: make dev has been disabled on purpose, so the skill no longer advertises it as the quick-iteration path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Perl attaches one COP (source line) per statement, taken from the
statement's first token, so every call inside a multi-line statement
reports that line. PerlOnJava instead derived a method call's caller
line from `node.left.getIndex()`, and arrow/paren AST nodes carry the
token index they finished parsing at, so a chained call such as
Local::Thing->new(
value => 1,
)->report;
reported the closing `)->report` line instead of the statement's line.
Both backends shared the defect because both read the same AST indices.
ParseBlock now records each statement's first token in a
`statementStartIndex` annotation, the JVM (EmitBlock, EmitForeach) and
interpreter (BytecodeCompiler) statement loops publish it while emitting
the statement, and the sub/method/coderef call emitters use it as the
caller line. Deliberate exceptions are preserved: an explicit
`callerLineTokenOverride`, short-circuit expression propagation, and
literal anon sub / `&`-prototype block arguments still report the block
line.
Perl's own line for these shapes shifts by one or two lines when the
preceding statement is block-terminated (`if (...) { }`, a bare block,
`{ package ... }`); that copline artifact is not reproduced.
Refs #1135
Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cwd.pm aliased all four current-directory functions to the single
Internals::getcwd builtin, which reports the physical (symlink-resolved)
path. Standard Perl splits them on Unix-like platforms: cwd() and
fastgetcwd() run `pwd`, which is logical, while getcwd() and fastcwd()
ask the operating system. With the shell in /tmp on darwin, real perl
returns /tmp from cwd()/fastgetcwd() and /private/tmp from
getcwd()/fastcwd(); PerlOnJava returned /private/tmp from all four.
Add Internals::logical_cwd, which returns $ENV{PWD} only when it can be
trusted and the physical path otherwise. $ENV{PWD} is accepted only when
it is a defined, non-empty, absolute path that names the very same
directory as the current one, decided by Files.isSameFile (a device+inode
comparison on Unix, the same test pwd performs). A stale value left behind
by a plain chdir, a relative or nonexistent path, or a hostile value all
fall back to the physical path, so cwd() cannot be made to lie. A
different symlinked alias of the current directory is accepted, matching
pwd.
Cwd.pm now aliases getcwd/fastcwd to Internals::getcwd and cwd/fastgetcwd
to Internals::logical_cwd, following standard Perl's %METHOD_MAP for the
platforms where the split does not exist: MSWin32/NT/dos/os2/VMS/qnx alias
all four to one platform function, and cygwin/amigaos route all four
through `pwd`. Both builtins stay tainted under -T.
Callers that need a canonical path already use getcwd or abs_path
(File::Find, File::Spec::Unix::rel2abs, FindBin), so none needed changing;
the cwd() callers in CPAN, Archive::Tar, ExtUtils::Install and
Pod::Simple::Search only save-and-restore or join paths, exactly as they
do under standard Perl.
Add unit/cwd_logical_physical.t, which builds its own symlinked temp
directory so it does not depend on the runner's cwd, skips where symlinks
are unavailable, and covers the valid, stale, nonexistent, relative,
empty, absent, and aliased $ENV{PWD} cases. Validated against system perl
v5.42 on darwin; three assertions fail with the pre-fix Cwd.pm.
Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Java-backed File::Spec canonpath, catdir and catfile built their answer
from fresh scalars, so every path component's taint was dropped. Because the
bundled File/Spec/Unix.pm keeps those three Java methods and implements the
rest in Perl on top of them, that one gap silently laundered the whole module:
rel2abs('x') and abs2rel('x') came back clean even though both resolve their
base through Cwd::getcwd(), which is operating-system data and tainted in
standard Perl.
Carry the taint of the supplied path components onto the result of those three
methods. The Perl originals build their answers with join, s/// and string
concatenation, all of which propagate taint, so this restores standard Perl
behavior for the whole module without introducing taint of its own:
- rel2abs/abs2rel taint only when Cwd::getcwd() actually contributes to the
answer, and stay clean when an absolute base makes it unnecessary or when
the cwd components cancel out of an abs2rel difference.
- tmpdir stays clean, since File::Spec::Unix::_tmpdir already discards tainted
%ENV candidates under -T.
- splitpath keeps laundering through its regexp captures, and path() keeps
returning tainted $ENV{PATH} entries.
Add taint_filespec.t covering the matrix that was recorded from system perl
5.42 under -T: rel2abs and abs2rel with relative and absolute paths and bases,
the constant accessors, canonpath, catfile, catdir, catpath, splitpath,
splitdir, file_name_is_absolute, no_upwards, tmpdir, path, and taint
propagation from tainted arguments. Sixteen of its assertions fail without
this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Undefining a typeglob now detaches every slot from the symbol instead of
leaving empty ARRAY/HASH/IO containers behind, so `*Pkg::name{ARRAY}` and
friends read back as undef. Containers that Perl code still references keep
their contents, which lets Symbol::Util::delete_glob back a slot up and
re-install it. `undef $Pkg::{name}` reaches the same code path through a new
scalar-lvalue undef helper, while `$Pkg::{name} = undef` keeps Perl's
no-op-with-warning behavior.
Also stop the parser's take-reference mode from leaking into a nested block,
so `&name` inside `defined eval { &name }` is a call rather than a code
reference, and only create a DATA placeholder filehandle when a compilation
unit actually has a `__DATA__` section (or a top-level `__END__`), attributing
it to the package that encloses the marker. `require` no longer adds a phantom
`DATA` entry to the requiring package's stash.
The unchanged Symbol::Util 0.0203 suite now passes 307/307 on the JVM backend
(was 262/307). Fixes #1119.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
caller(EXPR)[5] indexed callContextStack by raw stack depth. The interpreter enters one Perl subroutine through both the compiler-supplied wrapper and the interpreted body, so that stack holds two entries per Perl frame and caller(N) read an adjacent wrapper's context. Index it by logical Perl frame instead, using the same wrapper collapse the active-code lookups already use, so the interpreter reports the call-site context the JVM backend reports. caller(EXPR)[3] reported '(eval)' instead of the enclosing named subroutine when a subroutine defined inside eval STRING ran under an interpreted eval BLOCK. An interpreter virtual-eval entry is inserted without consuming a Java execution frame, so the frame before it already owns its formatted entry and must not be skipped; that rule was gated on the inspected frame itself being interpreted, which is false for eval-STRING-defined subroutines because their bodies are JVM-compiled. Fixes caller_wantarray_context.t assertion 1 and zz_eval_defined_named_caller.t assertion 3 on the interpreter backend. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fglock
force-pushed
the
fix/issues-1119-1125-1135
branch
from
August 29, 2026 16:32
c3dc517 to
6cc538e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Work in progress — fixes compatibility defects found by the CPAN random tester,
plus adjacent divergences discovered while investigating them.
Cwd::*results not tainted under-Tcallerline metadata for multiline chained callscwd/fastgetcwdlogical vs physical path (found via #1125)File::Specargument taint (found via #1125)Every fix was reproduced against system
perlfirst, verified on both the JVMand interpreter backends, and carries a regression test. Each claim in this
description was re-verified against system perl independently of the change
author.
#1125 — taint
Cwdresults under-TInternals::getcwdandInternals::abs_pathreturned untainted scalars, soevery entry point
Cwd.pmaliases to them produced a clean value under-T.They now use the existing
taintFromExternalInput()helper (the same one%ENV,readline, andreaddiruse), which is a no-op outside taint mode.This covers
getcwd,cwd,fastcwd,fastgetcwd,abs_path,realpath,fast_abs_path, andfast_realpath.The
jar:PERL5LIBbranch ofabs_pathechoes its argument back rather thanconsulting the file system, so it propagates the caller's taint instead of
asserting OS taint — this keeps Inline's
-Iflag derivation working.Verified correct against system perl: Data::Compare's
unless tainted(getcwd())guard now disables plugin discovery, and File::Find's
Insecure dependency in chdirerror under-Tmatches standard Perl exactly. That error was notsuppressed.
#1135 — report the statement source line for multiline call expressions
->and(AST nodes were constructed with the post-parseparser.tokenIndex(the token the parser finished on), and the call emitters derived the caller
line from
node.left.getIndex(). For a chained call the invocant is the innerFoo->new(...)node, whose index is the closing)— socallerreported theclosing line. Nothing in the AST carried the enclosing statement's first token.
The statement's first token index is now stamped on each statement node in
ParseBlockand threaded through both backends, so sub and method call sitesreport the statement line. Existing override precedence is preserved
(
callerLineTokenOverride> short-circuit/argument context > statement line >expression start).
Perl's rule was derived empirically over 96 generated cases (16 expression
shapes × 6 preceding-statement contexts) against perl 5.42.2: a statement's
reported line is the line of its first token, for every call inside it. In
non-quirk contexts mismatches went 36 → 0; overall agreement 30 → 63. Both
backends agree everywhere.
Caveat — #1119's stated acceptance criterion for #1135 is not met, deliberately.
The issue's reproducer prints
10here, not perl's11. Perl's11is anartifact of the preceding block-terminated statement (
{ package ... }with no;), not the general rule — adding a;after that block, or any plainstatement before the chain, makes perl report
10too. The shift is triggeredby a preceding
if {},for {},while {}, or bare block, but not bysub name {}, a plain statement, or};. Reproducing it would mean emulatingperl's
PL_parser->coplinebookkeeping and injecting an off-by-one after everysuch block. Left out pending a maintainer decision.
cwdlogical vs physical (discovered while fixing #1125)Cwd.pmaliased all four current-directory functions to one physical(symlink-resolved) builtin. Real Perl splits them:
cwd/fastgetcwdarelogical (
_backtick_pwd, i.e./bin/pwdin its default logical mode) andgetcwd/fastcwdare physical.A new
Internals::logical_cwdreturns a validated$ENV{PWD}, accepted onlywhen it is defined, non-empty, NUL-free, absolute, and
Files.isSameFileto thephysical path — the device+inode check
pwdperforms — falling back to thephysical path otherwise. A stale or hostile
PWDtherefore cannot makecwd()lie.
Cwd.pmfollows real Perl's%METHOD_MAPfor platforms with no split.Regression risk is low because the physical form kept the name canonical-path
consumers already use: File::Find, File::Spec, and FindBin all call
getcwd/fastcwd.File::Specargument taint (discovered while fixing #1125)The original premise — that
FileSpec.java'srel2abs/abs2relneeded OStaint — was wrong, and worth recording: those Java methods are dead code.
File/Spec/Unix.pmdefines those subs unconditionally and shadows the Javaregistrations; only
canonpath,catdir, andcatfilesurvive, because the.pminstalls those withunless defined &name.The real defect was that those three live methods dropped argument taint.
Every downstream divergence followed, since the Perl paths feed
Cwd::getcwd()through
catdir/canonpath. They now propagate taint from their arguments.Deliberately left clean, matching real perl:
tmpdir(_tmpdirfilters taintedcandidates under
-Tand falls back to literal/tmp),splitpath(regexpcaptures launder),
abs2relwhen the cwd components cancel, and theconstant/boolean accessors. Over-tainting causes spurious
Insecure dependencyfailures and is worse than under-tainting.
Test plan
cwdmatrix byte-identical to system perl on both backends, incl. stale/invalidPWDFile::Spectaint matrix byte-identical to system perl on both backendsmakegreen on the integration branch (all fixes combined)🤖 Generated with Claude Code