Skip to content

WIP: fix Cwd taint, typeglob slot semantics, and caller line metadata (#1125, #1119, #1135) - #1186

Draft
fglock wants to merge 7 commits into
masterfrom
fix/issues-1119-1125-1135
Draft

WIP: fix Cwd taint, typeglob slot semantics, and caller line metadata (#1125, #1119, #1135)#1186
fglock wants to merge 7 commits into
masterfrom
fix/issues-1119-1125-1135

Conversation

@fglock

@fglock fglock commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Summary

Work in progress — fixes compatibility defects found by the CPAN random tester,
plus adjacent divergences discovered while investigating them.

Issue Area Status
#1125 Cwd::* results not tainted under -T landed
#1135 caller line metadata for multiline chained calls landed (see caveat)
cwd/fastgetcwd logical vs physical path (found via #1125) landed
File::Spec argument taint (found via #1125) landed
#1119 Typeglob slot / package-stash semantics (Symbol::Util) in progress

Every fix was reproduced against system perl first, verified on both the JVM
and 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 Cwd results under -T

Internals::getcwd and Internals::abs_path returned untainted scalars, so
every entry point Cwd.pm aliases to them produced a clean value under -T.
They now use the existing taintFromExternalInput() helper (the same one
%ENV, readline, and readdir use), which is a no-op outside taint mode.
This covers getcwd, cwd, fastcwd, fastgetcwd, abs_path, realpath,
fast_abs_path, and fast_realpath.

The jar:PERL5LIB branch of abs_path echoes its argument back rather than
consulting the file system, so it propagates the caller's taint instead of
asserting OS taint — this keeps Inline's -I flag 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 chdir error under -T matches standard Perl exactly. That error was not
suppressed.

#1135 — report the statement source line for multiline call expressions

-> and ( AST nodes were constructed with the post-parse parser.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 inner
Foo->new(...) node, whose index is the closing ) — so caller reported the
closing 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
ParseBlock and threaded through both backends, so sub and method call sites
report 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 10 here, not perl's 11. Perl's 11 is an
artifact of the preceding block-terminated statement ({ package ... } with no
;), not the general rule — adding a ; after that block, or any plain
statement before the chain, makes perl report 10 too. The shift is triggered
by a preceding if {}, for {}, while {}, or bare block, but not by
sub name {}, a plain statement, or };. Reproducing it would mean emulating
perl's PL_parser->copline bookkeeping and injecting an off-by-one after every
such block. Left out pending a maintainer decision.

cwd logical vs physical (discovered while fixing #1125)

Cwd.pm aliased all four current-directory functions to one physical
(symlink-resolved) builtin. Real Perl splits them: cwd/fastgetcwd are
logical (_backtick_pwd, i.e. /bin/pwd in its default logical mode) and
getcwd/fastcwd are physical.

A new Internals::logical_cwd returns a validated $ENV{PWD}, accepted only
when it is defined, non-empty, NUL-free, absolute, and Files.isSameFile to the
physical path — the device+inode check pwd performs — falling back to the
physical path otherwise. A stale or hostile PWD therefore cannot make cwd()
lie. Cwd.pm follows real Perl's %METHOD_MAP for 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::Spec argument taint (discovered while fixing #1125)

The original premise — that FileSpec.java's rel2abs/abs2rel needed OS
taint — was wrong, and worth recording: those Java methods are dead code.
File/Spec/Unix.pm defines those subs unconditionally and shadows the Java
registrations; only canonpath, catdir, and catfile survive, because the
.pm installs those with unless 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 (_tmpdir filters tainted
candidates under -T and falls back to literal /tmp), splitpath (regexp
captures launder), abs2rel when the cwd components cancel, and the
constant/boolean accessors. Over-tainting causes spurious Insecure dependency
failures and is worse than under-tainting.

Test plan

🤖 Generated with Claude Code

fglock and others added 7 commits August 29, 2026 18:31
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
fglock force-pushed the fix/issues-1119-1125-1135 branch from c3dc517 to 6cc538e Compare August 29, 2026 16:32
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