EV-MariaDB
view release on metacpan or search on metacpan
t/16_review_fixes.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use lib 't/lib';
use TestMariaDB;
plan skip_all => 'No MariaDB/MySQL server' unless TestMariaDB::server_available();
use EV;
use EV::MariaDB;
# Regression tests for the 0.07 review fixes:
# Finding 1 - statement handles are validated ids, not raw pointers
# (was: UAF after close_stmt, segfault on fabricated handle)
# Finding 2 - the EV::Loop object is kept alive by the connection
# (was: dangling loop pointer -> watcher ops on freed memory)
# Finding 3 - change_user(undef db) keeps the current database (POD contract)
my %A = TestMariaDB::connect_args();
sub run_connected {
my ($body) = @_;
my $m = EV::MariaDB->new(on_error => sub { diag("on_error: $_[0]"); EV::break });
$m->on_connect(sub { $body->($m) });
$m->connect(@A{qw(host user password database port)}, $A{unix_socket});
my $timeout = EV::timer(10, 0, sub { diag("safety timeout"); EV::break });
EV::run;
$m->finish if $m->is_connected;
}
# --- Finding 1a: reusing a handle after close_stmt croaks instead of UAF ---
run_connected(sub {
my ($m) = @_;
$m->prepare("select 1", sub {
my ($stmt, $err) = @_;
BAIL_OUT("prepare failed: $err") if $err;
$m->close_stmt($stmt, sub {
my ($ok, $cerr) = @_;
ok($ok && !$cerr, 'close_stmt succeeds');
eval { $m->execute($stmt, [], sub { }) };
like($@, qr/invalid statement handle/,
'Finding 1a: execute on a closed handle croaks (no use-after-free)');
eval { $m->close_stmt($stmt, sub { }) };
like($@, qr/invalid statement handle/,
'Finding 1a: second close_stmt on the same handle croaks (no double-free)');
EV::break;
});
});
});
# --- Finding 1b: fabricated handles croak instead of dereferencing garbage ---
run_connected(sub {
my ($m) = @_;
eval { $m->execute(999_999_999, [], sub { }) };
like($@, qr/invalid statement handle/,
'Finding 1b: execute on a fabricated handle croaks (no wild dereference)');
eval { $m->bind_params(888_888_888, []) };
like($@, qr/invalid statement handle/,
'Finding 1b: bind_params on a fabricated handle croaks');
eval { $m->stmt_reset(777_777_777, sub { }) };
like($@, qr/invalid statement handle/,
'Finding 1b: stmt_reset on a fabricated handle croaks');
EV::break;
});
# --- Finding 3: change_user(undef db) preserves the current database ---
run_connected(sub {
my ($m) = @_;
$m->change_user($A{user}, $A{password}, undef, sub {
my ($ok, $err) = @_;
ok($ok && !$err, 'change_user(undef db) succeeds') or diag("err: " . ($err // ''));
$m->query("SELECT DATABASE()", sub {
my ($rows, $qerr) = @_;
is($rows->[0][0], $A{database},
'Finding 3: change_user(undef db) keeps the current database');
EV::break;
});
});
});
# --- Finding 2: a temporary custom loop must be kept alive by the object ---
# Run in a child: build with loop => EV::Loop->new, arm a watcher, drop the
# loop reference, then destroy the object. Pre-fix this dereferenced a freed
# loop (SIGSEGV); post-fix the object holds a reference so it stays valid.
{
my $child = <<'CHILD';
use lib 't/lib'; use TestMariaDB; use EV; use EV::MariaDB;
my %a = TestMariaDB::connect_args();
my $loop = EV::Loop->new;
my $m = EV::MariaDB->new(loop => $loop, on_error => sub { });
$m->on_connect(sub { $loop->break });
$m->connect(@a{qw(host user password database port)}, $a{unix_socket});
$loop->run; # until connected
$m->query("select sleep(1)", sub { });
$loop->run(EV::RUN_NOWAIT); # arm the read watcher (result not yet ready)
undef $loop; # drop our ref; loop survives iff object holds one
undef $m; # DESTROY must not touch a freed loop
exit 0;
CHILD
# Propagate the parent's @INC so the child loads the same EV::MariaDB
# (blib under `make test`) rather than a possibly-installed copy.
my @inc = map { ('-I', $_) } @INC;
my $rc = system($^X, @inc, '-e', $child);
my $sig = $rc & 127;
is($sig, 0,
'Finding 2: object keeps the EV::Loop alive - no crash on destroy')
or diag(sprintf("child terminated by signal %d (11/139 = dangling loop pointer)", $sig));
}
done_testing();
( run in 0.831 second using v1.01-cache-2.11-cpan-14f38c9f855 )