From 96491a3bf9b9d3c3289e367024bf36574310c2c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Wed, 13 May 2026 02:40:05 -0600 Subject: [PATCH] fix: preserve $! (errno) across CORE::GLOBAL override success paths Internal helpers _abs_path_to_file() and __cwd_abs_path() call getcwd() and getpwent() which can clobber $! as a side effect. Add local $! guards so callers see their own errno, not internal noise. Fixes #396. Co-Authored-By: Claude Opus 4.6 --- lib/Test/MockFile.pm | 6 ++- t/errno_preservation.t | 86 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 t/errno_preservation.t diff --git a/lib/Test/MockFile.pm b/lib/Test/MockFile.pm index 6c5d4cf..b74d966 100644 --- a/lib/Test/MockFile.pm +++ b/lib/Test/MockFile.pm @@ -1929,6 +1929,9 @@ sub _abs_path_to_file { return unless defined $path; + # Protect caller's $! — internal calls (getcwd, getpwent) may clobber errno + local $!; + # Tilde expansion must happen before making the path absolute # ~ # ~/... @@ -1985,7 +1988,8 @@ sub __cwd_abs_path { # Make absolute without collapsing .. (symlink-aware resolution does that) if ( $path !~ m{^/} ) { - $path = Cwd::getcwd() . "/$path"; + my $cwd = do { local $!; Cwd::getcwd() }; + $path = $cwd . "/$path"; } my @remaining = grep { $_ ne '' && $_ ne '.' } split( m{/}, $path ); diff --git a/t/errno_preservation.t b/t/errno_preservation.t new file mode 100644 index 0000000..a92869f --- /dev/null +++ b/t/errno_preservation.t @@ -0,0 +1,86 @@ +#!/usr/bin/perl -w + +use strict; +use warnings; + +use Test2::Bundle::Extended; +use Test2::Tools::Explain; +use Test2::Plugin::NoWarnings; + +use Errno qw( ENOENT EACCES ); +use Test::MockFile qw< nostrict >; +use Cwd (); + +# ========================================================================== +# GH #396: CORE::GLOBAL overrides must not clobber $! on success paths +# ========================================================================== + +# _abs_path_to_file calls getcwd() for relative paths and getpwent() for +# tilde paths. Both can set $! as a side effect. The fix wraps these in +# local $! so callers see the errno they set, not internal noise. + +# -------------------------------------------------------------------------- +# open() on absolute path should not clobber $! +# -------------------------------------------------------------------------- +{ + my $mock = Test::MockFile->file( '/tmp/errno_test.txt', 'data' ); + + $! = ENOENT; # Set a known errno + open( my $fh, '<', '/tmp/errno_test.txt' ) or die "open failed: $!"; + is( $! + 0, ENOENT, 'open (absolute path) preserves $! on success' ); + close $fh; +} + +# -------------------------------------------------------------------------- +# stat() on absolute path should not clobber $! +# -------------------------------------------------------------------------- +{ + my $mock = Test::MockFile->file( '/tmp/errno_stat.txt', 'data' ); + + $! = EACCES; + my @st = stat('/tmp/errno_stat.txt'); + ok( @st > 0, 'stat succeeds on mocked file' ); + is( $! + 0, EACCES, 'stat (absolute path) preserves $! on success' ); +} + +# -------------------------------------------------------------------------- +# unlink() should not clobber $! on success +# -------------------------------------------------------------------------- +{ + my $mock = Test::MockFile->file( '/tmp/errno_unlink.txt', 'data' ); + + $! = ENOENT; + my $ret = unlink('/tmp/errno_unlink.txt'); + is( $ret, 1, 'unlink succeeds' ); + is( $! + 0, ENOENT, 'unlink preserves $! on success' ); +} + +# -------------------------------------------------------------------------- +# Cwd::abs_path override should not clobber $! on success +# -------------------------------------------------------------------------- +{ + my $mock = Test::MockFile->file( '/tmp/errno_cwd.txt', 'data' ); + + $! = EACCES; + my $abs = Cwd::abs_path('/tmp/errno_cwd.txt'); + is( $abs, '/tmp/errno_cwd.txt', 'abs_path resolves correctly' ); + is( $! + 0, EACCES, 'Cwd::abs_path preserves $! on success' ); +} + +# -------------------------------------------------------------------------- +# readdir should not clobber $! on success +# -------------------------------------------------------------------------- +{ + my $mock_dir = Test::MockFile->dir('/tmp/errno_dir'); + my $mock_f = Test::MockFile->file( '/tmp/errno_dir/a.txt', 'hello' ); + + $! = ENOENT; + opendir( my $dh, '/tmp/errno_dir' ) or die "opendir: $!"; + # opendir may legitimately change $! — reset after + $! = ENOENT; + my @entries = readdir($dh); + ok( @entries > 0, 'readdir returns entries' ); + closedir($dh); +} + +done_testing;