Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pm_to_blib
Test-MockFile-*
Test-MockFile-*.tar.gz
.DS_Store
.claude/

# VIM - https://github.com/github/gitignore/blob/main/Global/Vim.gitignore
# Swap
Expand Down
2 changes: 1 addition & 1 deletion Makefile.PL
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ WriteMakefile(
'Test::MockModule' => 0,
},
PREREQ_PM => {
'Overload::FileCheck' => '0.013',
'Overload::FileCheck' => '0.014',
'Text::Glob' => 0,
},
dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
Expand Down
2 changes: 1 addition & 1 deletion cpanfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on 'test' => sub {
requires 'Test2::Tools::Explain' => 0;
requires 'Test2::Plugin::NoWarnings' => 0;
requires 'File::Slurper' => 0;
requires 'Overload::FileCheck' => '0.013';
requires 'Overload::FileCheck' => '0.014';
requires 'Test::Pod::Coverage' => 0;
requires 'Test::Pod' => 0;
requires 'Test2::API' => 0;
Expand Down
90 changes: 90 additions & 0 deletions t/fh-ref-leak.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/perl -w

# Test for GitHub issue #179: "Spooky action-at-a-distance"
#
# File check operators (-S, -f, etc.) on real (unmocked) filehandles should
# not retain references that prevent garbage collection. A leaked reference
# to a socket filehandle can keep the fd open, causing reads on the other
# end of a socketpair to hang waiting for EOF.
#
# Root cause: $_last_call_for in Overload::FileCheck stored filehandle refs.
# Fix: Only cache string filenames, not refs (Overload::FileCheck PR #25).

use strict;
use warnings;

use Test2::Bundle::Extended;
use Test2::Tools::Explain;

use Scalar::Util qw(weaken);
use Socket;

use Test::MockFile qw< nostrict >;

# Test 1: Filehandle passed to -f is not retained
{
my $weak_ref;

{
open my $fh, '<', '/dev/null' or die "Cannot open /dev/null: $!";
$weak_ref = $fh;
weaken($weak_ref);

ok( defined $weak_ref, "weak ref is defined before scope exit" );

no warnings;
-f $fh;
}

ok( !defined $weak_ref, "filehandle is garbage collected after -f (GH #179)" );
}

# Test 2: Socket filehandle passed to -S is not retained
{
my $weak_ref;

{
open my $fh, '<', '/dev/null' or die "Cannot open /dev/null: $!";
$weak_ref = $fh;
weaken($weak_ref);

no warnings;
-S $fh;
}

ok( !defined $weak_ref, "filehandle is garbage collected after -S (GH #179)" );
}

# Test 3: The exact scenario from GH #179 — socketpair with dup'd fd
# This would hang without the fix because the dup'd write handle stays open.
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to bump the minimal version for Overload::FileCheck to be 0.014 to make sure every users got the correct fix.

{
socketpair my $r, my $w, AF_UNIX, SOCK_STREAM, 0
or die "socketpair: $!";

my $pid = fork();
die "fork: $!" unless defined $pid;

if ( $pid == 0 ) {
# Child: reproduce the bug scenario with a timeout
$SIG{ALRM} = sub { exit 1 }; # exit 1 = hung (bug present)
alarm(5);

my $fd = fileno $w;
do {
open my $w2, "<&=", $fd;
-S $w2;
};

close $w;
my $line = <$r>; # Should get EOF immediately if $w2 was freed
exit 0; # exit 0 = success (no hang)
}

close $w;
waitpid $pid, 0;
my $exit = $? >> 8;

is( $exit, 0, "socketpair read does not hang after -S on dup'd filehandle (GH #179)" );
}

done_testing;