Data-DisjointSet-Shared

 view release on metacpan or  search on metacpan

xt/destroy_during_call.t  view on Meta::CPAN

#      (this is what REEXTRACT adds) rejects it;
#   4. drop the caller's last ref to the pairs arrayref mid-loop -- the AV would
#      be freed and the next av_fetch a use-after-free, unless union_many pins it.
#
# The hostile calls run in a child so a regression is reported, not fatal here.
use strict;
use warnings;
use Test::More;
use Config;
use POSIX ();
use Data::DisjointSet::Shared;

plan skip_all => 'fork required' unless $Config{d_fork};

our $victim;
our $arr;

{   package Evil::Destroy;
    use overload '0+' => sub { $_[0][0]->DESTROY; 1 },
                 '""' => sub { $_[0][0]->DESTROY; '1' },
                 fallback => 1;
}
{   package Evil::Replace;              # replace the invocant with a non-ref
    use overload '0+' => sub { $main::victim = 42; 1 },
                 '""' => sub { $main::victim = 42; '1' },
                 fallback => 1;
}
{   package Evil::ReplaceObj;           # replace with a DIFFERENT same-class object
    use overload '0+' => sub { $main::victim = Data::DisjointSet::Shared->new(undef, 64); 1 },
                 '""' => sub { $main::victim = Data::DisjointSet::Shared->new(undef, 64); '1' },
                 fallback => 1;
}
{   package Evil::FreeArr;              # drop the caller's last ref to the pairs arrayref
    use overload '0+' => sub { undef $main::arr; 1 },
                 '""' => sub { undef $main::arr; '1' },
                 fallback => 1;
}
# A tied array whose FETCHSIZE (reached via av_len) destroys the set and then
# reports EMPTY, so the element loop is skipped entirely.
{   package Tied::Empty;
    sub TIEARRAY  { bless { obj => $_[1] }, $_[0] }
    sub FETCHSIZE { $_[0]{obj}->DESTROY; 0 }
    sub FETCH     { 0 }
}

my $destroyed = qr/destroyed during the call/;
my $replaced  = qr/replaced during the call/;
my $repl_obj  = qr/replaced or destroyed during the call/;

# [ name, want-regex (undef = expect clean completion), call, expected-exit ]
#   exit 0 = croaked with want ; exit 7 = completed OK ; exit 8 = some OTHER death
my @cases = (
    [ 'element-magic destroys', $destroyed,
      sub { my $e = bless [$victim], 'Evil::Destroy'; $victim->union_many([$e, 1, 2, 3]) }, 0 ],
    [ 'element-magic replaces with a non-ref', $replaced,
      sub { my $e = bless [$victim], 'Evil::Replace'; $victim->union_many([$e, 1, 2, 3]) }, 0 ],
    [ 'element-magic replaces with a DIFFERENT object (identity check)', $repl_obj,
      sub { my $e = bless [$victim], 'Evil::ReplaceObj'; $victim->union_many([$e, 1, 2, 3]) }, 0 ],
    [ 'tied FETCHSIZE destroys (empty array skips the element loop)', $destroyed,
      sub { tie my @a, 'Tied::Empty', $victim; $victim->union_many(\@a) }, 0 ],
    [ 'element-magic frees the pairs arrayref (AV pin keeps it alive)', undef,
      sub { $main::arr = [ (bless [], 'Evil::FreeArr'), 1, 2, 3 ]; $victim->union_many($main::arr) }, 7 ],
);

for my $case (@cases) {
    my ($name, $want, $call, $exp) = @$case;
    $exp //= 0;
    my $pid = fork();
    unless (defined $pid) { plan skip_all => "fork failed: $!" }
    unless ($pid) {
        $victim = Data::DisjointSet::Shared->new(undef, 64);
        my $ok  = eval { $call->(); 1 };
        my $err = $@ // '';
        # exit 7 = completed OK; exit 0 = croaked with the wanted message; exit 8 =
        # any OTHER death (does not prove a guard fired: free() does not unmap, so a
        # stale read can trip an unrelated check and croak even with the fix removed).
        POSIX::_exit($ok ? 7 : (($want && $err =~ $want) ? 0 : 8));
    }
    waitpid($pid, 0);
    my $st = $?;
    ok !($st & 127), "union_many: no crash when $name"
        or diag sprintf('died with signal %d', $st & 127);
    is $st >> 8, $exp, "union_many: expected outcome ($exp) when $name";
}

done_testing;



( run in 1.616 second using v1.01-cache-2.11-cpan-14f38c9f855 )