July 15, 2017·4 min read·engineeringperlsecuritymodules

Perl 5.24.2 narrows optional loading in base.pm

The maintenance release handles the security and compatibility tension in base.pm's optional loads. It also fixes a disagreement between Perl's PATH taint checking and Unix path interpretation.

An inheritance helper can need a package that already exists without needing a corresponding file. Perl's base module accommodates that case by trying a load and tolerating the absence of the file when the package has symbols. That convenience complicates the security rule for optional loading from the current directory. Perl 5.24.2 improves its handling of . in @INC, according to the release notes. The distribution history dates the update to July 15, 2017.

The interesting part is the scope of the restriction. The shipped base.pm implementation hides the default trailing dot for its own optional load while avoiding that change for nested loads. Simply removing the directory around an entire operation would affect code that the loaded module runs. This implementation distinguishes the immediate lookup from calls deeper in the stack.

I value predictable loading behavior even in a language I often use for short text programs. A helper that infers whether a file is needed has more compatibility obligations than an explicit load. This maintenance change is useful because it addresses that particular inference without claiming that the whole interpreter has adopted a new global search policy.

A package can exist without its matching file

The module's own documentation illustrates a file containing both Foo and Bar packages. Foo defines a subroutine, and Bar declares inheritance with use base "Foo". There need not be a separate Foo.pm. The failed attempt to locate that file is acceptable because the package already contains symbols.

Other errors are different. The documentation says that base ignores failure to find the module file, but does not ignore other loading errors. An empty base package is also an error. That contract explains why the implementation cannot simply treat every failed require as permission to continue.

The optional lookup creates a security concern when a matching file happens to exist in the working directory. The package may already be usable without it, yet an ordinary search could still execute the file. Its presence is then sufficient to change what an inheritance declaration loads. Preventing that unnecessary discovery is the purpose of hiding the default dot entry in this case.

Hooks restrict one level of lookup

The source checks two conditions before arranging the restriction. The last entry in @INC must be . and the base package must already have symbols. Together, they identify the default directory entry and a package for which the load is optional. The restriction is therefore conditional, rather than a removal of every current-directory entry from every search.

Two hooks perform the work. One at the front of @INC records the call-stack depth. Another immediately before the trailing dot checks that depth and removes itself when the relevant lookup reaches it. Removing that hook shifts the dot into the position the search loop has already reached, so the loop skips the directory.

The call-depth check keeps the mechanism inactive for nested require calls. The source comments explain why depth is recorded through a hook, because a CORE::GLOBAL::require override prevents assuming the exact depth in advance. A scope guard removes the temporary hooks at the end of the operation.

This is a precise solution to a compatibility problem with global state. The code needs to alter one lookup while preserving the list used by other loads. Its comments also acknowledge a boundary, because pre-existing hooks that modify @INC during that initial lookup could disturb the arrangement. The implementation makes its assumptions visible instead of implying an unconditional guarantee.

A local collision gets an explicit error

After hiding dot, the module checks whether the corresponding file exists in the working directory. If it does, the error explains that the package is not empty but the file is present locally. It says that base.pm now refuses to load optional modules from the current working directory when it is the last search entry.

The diagnostic recommends detecting the intended path with FindBin and adding that path properly. It mentions use lib '.' as a last resort. Those suggestions distinguish locating code deliberately from restoring an ambient directory dependency. The error also explains why software that previously worked can now stop despite the base package already existing.

I prefer that explicit failure to silently executing a local file that the package does not require. It gives an author enough information to decide where the base class is meant to live. The module's documentation separately recommends the lighter parent module when fields behavior is unnecessary.

Taint checking must agree with Unix PATH

The release also corrects handling of backslash before colon in PATH. Perl had treated the backslash as escaping the separator, although Unix path interpretation does not. A value such as /\:. could consequently make the relative . entry appear safe to Perl's check.

The corrected check treats that dot as tainted when starting a new process. This is independent of @INC, but it addresses a related source-selection error. Perl's safety decision must use the same separator rules as the mechanism that selects the program. Otherwise, a check can approve a path interpretation that the operating system never uses.