Skip to content

stop using Safe.pm - #599

Merged
rjbs merged 7 commits into
andk:masterfrom
rjbs:safe-room
Oct 26, 2026
Merged

rjbs merged 7 commits into
andk:masterfrom
rjbs:safe-room

Conversation

@rjbs

@rjbs rjbs commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Rather than falling back to evaluating VERSION assignments inside a forked Safe compartment, just don't. Instead, refuse to index, giving the authors instructions on how to assign a version that can be statically indexed.

rjbs and others added 5 commits October 25, 2026 10:00
Nothing has called this for years.  It evaluated a user-supplied
$VERSION right-hand side in a Safe compartment, so deleting it removes
one more place where uploaded code gets run.

Co-Authored-By: Claude <noreply@anthropic.com>
parse_version calls Safe->new, but this file only ever loaded Safe from
inside the dead _module_version, and so really relied on
PAUSE::mldistwatch having loaded it first.

Co-Authored-By: Claude <noreply@anthropic.com>
To find a module's version, the indexer used to evaluate the uploaded
file's $VERSION line in a Safe compartment, in a forked child running as
an unprivileged user.  That amounted to running uploaded code, with Safe
as the main barrier.  The thing is, Safe isn't really meant to be this
kind of safety mechanism.  We should not rely on it preventing real
problems.

With this commit, the line is only ever parsed, and the parse results
turned into the indexed version.

The line is chosen exactly as before, but it's only accepted if it
assigns a literal to the package's $VERSION, either bare (for no strict
or "use vars") or with an "our" declaration.  The right hand side of
the assignment must be either:

* '...' or "..." or q/qq operator using one of [] () {} <> and
  not using interpolation or escaping
* a decimal number

It may, optionally, be passed to version->declare, ->new, ->parse, or
the qv() function.  Either lhs or rhs may be parenthesized.

Anything else is refused, and the uploader's report shows the offending
line and explains how to fix it: use a literal, use "package NAME
VERSION", or use META provides.

For every line that's still accepted, the version found is the same as
before (checked against the old code), except that "package Foo; our
$VERSION = '1.0';" on one line now gives 1.0 where it used to give
undef.  (Who would do such a thing?  Well, they'll get a better result
now, if they do it.)

The forked child, the Safe compartment, and the setuid to ML_CHOWN_USER
are all gone.

Co-Authored-By: Claude <noreply@anthropic.com>
Nothing here uses it.  PAUSE::pmfile used to rely on it having been
loaded, but no longer uses it either.

Co-Authored-By: Claude <noreply@anthropic.com>
parse_version used to return a JSON string in place of a version when it
couldn't find one.  That made sense when the result came back over a
pipe from a forked child, but the child is gone.  The string still
travelled as though it were a version: through $self->{VERSION}, into
each package's version in packages_per_pmfile, and finally to
PAUSE::package, which spotted the leading brace and decoded it.

That caused a few problems:

* the JSON appeared in the uploader's report as the package's version
* in packages_per_pmfile, the JSON string compared "gt" any real
  version, so it beat a "package NAME VERSION" version
* normalize_version passed through anything shaped like a JSON object,
  including a META provides version, which would then die in decoding

Now parse_version returns a hashref holding either the version or the
package error to report.  examine_fio passes that to packages_per_pmfile
instead of setting $self->{VERSION}, and each package that would take
the file's version gets the error in its package data, which
PAUSE::package uses directly.

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

@rspier rspier left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

Is there user documentation that needs to be updated somewhere? (Possibly in a follow-up PR.)

@rspier

rspier commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Nice work on this — replacing the Safe compartment and process forks with static extraction is a great cleanup and security improvement.

A few review notes:

1. Boolean check on version object rejects version 0

In lib/PAUSE/pmfile.pm (_static_version_from_line):

    my ($method) = $match{call} =~ /(\w+)\z/;
    my $object = eval {
        $method eq 'qv' ? version::qv($value) : version->$method($value);
    };

    return { refused => $line } unless $object;
    return { version => $object->numify };

Because version.pm objects overload boolean/numeric context, a version object representing 0 (e.g., version->declare("0"), version->new(0), or qv("0.0.0")) evaluates to false in boolean context ($object ? 1 : 0 is false).

With unless $object;, any version constructor evaluating to 0 is treated as a refusal. Changing this to check definedness resolves it:

    return { refused => $line } unless defined $object;

(Suggest also adding a test case like version_is(q{our $VERSION = version->new(0);}, '0.000'); in t/static-version.t.)

2. Immediate refusal on lines containing inline $VERSION =

In static_version_from_file:

    next unless /([\$*])(([\w\:\']*)\bVERSION)\b.*(?<![!><=])\=(?![=>])/;

    return _static_version_from_line($_);

If a line contains $VERSION = inside an inline comment or string literal (e.g. $flag = 1; # note: $VERSION = 1.0 below), it matches this check, fails _static_version_from_line, and immediately returns { refused => $line } without continuing to check subsequent lines for the real $VERSION assignment. Stripping trailing comments (s/\s*#.*//) prior to this check or continuing rather than immediately returning on refusal would avoid false rejections.

3. Readability: Chained s///gr

In lib/PAUSE/Indexer/Errors.pm:

    my $line = $arg->{line} =~ s/[^\x20-\x7e]/?/gr =~ s/\A\s+|\s+\z//gr;

Chaining =~ s///gr across two expressions is valid in modern Perl, but can be easy to misread. Splitting or parenthesizing makes the intent clearer:

    my $line = ($arg->{line} =~ s/[^\x20-\x7e]/?/gr) =~ s/\A\s+|\s+\z//gr;

rjbs and others added 2 commits October 25, 2026 12:27
Annoyingly, version.pm objects overload bool, so that "version 0" is
false.  This is painful, but it is the reality we live in.

Don't test a version object for truth, test for definedness.

Co-Authored-By: Claude <noreply@anthropic.com>
A line like "$flag = 1; # note: $VERSION = 1.0 below" was taken as the
version line, because the selection regex matched the commented-out
assignment.  Since the first selected line decides the result, it was
then refused, and the real assignment further down was never seen.

Now the selection looks only at the part of the line before a comment.
This wasn't a new bug in this branch, but as long as we're in here...

The comment detection is approximate, but neither of its failure cases
appears in any version line in the latest releases on CPAN.

Co-Authored-By: Claude <noreply@anthropic.com>
@rjbs

rjbs commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

@rspier I addressed #1 and #2. #3, I reject.

@rjbs
rjbs merged commit d3ececb into andk:master Oct 26, 2026
1 check passed
@rjbs
rjbs deleted the safe-room branch October 26, 2026 13:41
@Grinnz

Grinnz commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

I think this is a good idea overall. I would just make two suggestions for community outreach (I know, well volunteered):

  1. It would be nice to analyze the currently indexed releases and find which exactly will now fail to index that previously did thanks to this feature.

  2. The PAUSE operating model (https://github.com/andk/pause/blob/master/doc/operating-model.md) should be updated to reference the specific accepted formats under "Factors considered in the indexing phase".

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.

3 participants