From: Peter Rabbitson Date: Tue, 2 Feb 2016 10:23:04 +0000 (+0100) Subject: Detect and very loudly warn about Return::Multilevel in exception_action() X-Git-Tag: v0.082840~15 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=2bbdb85a47bab6c27ddf44e0a3a77224ba5242d8;p=dbsrgits%2FDBIx-Class.git Detect and very loudly warn about Return::Multilevel in exception_action() ( cherry-pick of 7cb3585200 ) --- diff --git a/Changes b/Changes index 5f78107..49461a7 100644 --- a/Changes +++ b/Changes @@ -6,6 +6,8 @@ Revision history for DBIx::Class to be a mostly useless overprotection) * Fixes + - Ensure leaving an exception stack via Return::MultiLevel or something + similar produces a large warning - Another relatively invasive set of ::FilterColumn changes, covering potential data loss (RT#111567). Please run your regression tests! - Ensure failing on_connect* / on_disconnect* are dealt with properly, diff --git a/lib/DBIx/Class/Schema.pm b/lib/DBIx/Class/Schema.pm index ed219b0..84e4e37 100644 --- a/lib/DBIx/Class/Schema.pm +++ b/lib/DBIx/Class/Schema.pm @@ -8,7 +8,7 @@ use base 'DBIx::Class'; use DBIx::Class::Carp; use Try::Tiny; use Scalar::Util qw/weaken blessed/; -use DBIx::Class::_Util qw(refcount quote_sub); +use DBIx::Class::_Util qw(refcount quote_sub is_exception scope_guard); use Devel::GlobalDestruction; use namespace::clean; @@ -1055,26 +1055,67 @@ default behavior will provide a detailed stack trace. =cut sub throw_exception { - my $self = shift; + my ($self, @args) = @_; if (my $act = $self->exception_action) { - if ($act->(@_)) { - DBIx::Class::Exception->throw( + + my $guard_disarmed; + + my $guard = scope_guard { + return if $guard_disarmed; + local $SIG{__WARN__}; + Carp::cluck(" + !!! DBIx::Class INTERNAL PANIC !!! + +The exception_action() handler installed on '$self' +aborted the stacktrace below via a longjmp (either via Return::Multilevel or +plain goto, or Scope::Upper or something equally nefarious). There currently +is nothing safe DBIx::Class can do, aside from displaying this error. A future +version ( 0.082900, when available ) will reduce the cases in which the +handler is invoked, but this is neither a complete solution, nor can it do +anything for other software that might be affected by a similar problem. + + !!! FIX YOUR ERROR HANDLING !!! + +This guard was activated beginning" + ); + }; + + eval { + # if it throws - good, we'll go down to the do{} below + # if it doesn't - do different things depending on RV truthiness + if( $act->(@args) ) { + $args[0] = ( "Invocation of the exception_action handler installed on $self did *not*" .' result in an exception. DBIx::Class is unable to function without a reliable' .' exception mechanism, ensure that exception_action does not hide exceptions' - ." (original error: $_[0])" - ); + ." (original error: $args[0])" + ); + } + else { + carp_unique ( + "The exception_action handler installed on $self returned false instead" + .' of throwing an exception. This behavior has been deprecated, adjust your' + .' handler to always rethrow the supplied error' + ); + } + + $guard_disarmed = 1; } - carp_unique ( - "The exception_action handler installed on $self returned false instead" - .' of throwing an exception. This behavior has been deprecated, adjust your' - .' handler to always rethrow the supplied error.' - ); + or + + do { + # We call this to get the necessary warnings emitted and disregard the RV + # as it's definitely an exception if we got as far as this do{} block + is_exception($@); + + $guard_disarmed = 1; + $args[0] = $@; + }; } - DBIx::Class::Exception->throw($_[0], $self->stacktrace); + DBIx::Class::Exception->throw($args[0], $self->stacktrace); } =head2 deploy diff --git a/lib/DBIx/Class/_Util.pm b/lib/DBIx/Class/_Util.pm index 6ee81a8..091f976 100644 --- a/lib/DBIx/Class/_Util.pm +++ b/lib/DBIx/Class/_Util.pm @@ -25,6 +25,8 @@ BEGIN { HAS_ITHREADS => $Config{useithreads} ? 1 : 0, + UNSTABLE_DOLLARAT => ( "$]" < 5.013002 ) ? 1 : 0, + DBICTEST => $INC{"DBICTest/Util.pm"} ? 1 : 0, # During 5.13 dev cycle HELEMs started to leak on copy @@ -69,7 +71,8 @@ use base 'Exporter'; our @EXPORT_OK = qw( sigwarn_silencer modver_gt_or_eq modver_gt_or_eq_and_lt fail_on_internal_wantarray fail_on_internal_call - refdesc refcount hrefaddr is_exception detected_reinvoked_destructor + refdesc refcount hrefaddr + scope_guard is_exception detected_reinvoked_destructor quote_sub qsub perlstring serialize UNRESOLVABLE_CONDITION ); @@ -115,6 +118,31 @@ sub serialize ($) { nfreeze($_[0]); } +sub scope_guard (&) { + croak 'Calling scope_guard() in void context makes no sense' + if ! defined wantarray; + + # no direct blessing of coderefs - DESTROY is buggy on those + bless [ $_[0] ], 'DBIx::Class::_Util::ScopeGuard'; +} +{ + package # + DBIx::Class::_Util::ScopeGuard; + + sub DESTROY { + &DBIx::Class::_Util::detected_reinvoked_destructor; + + local $@ if DBIx::Class::_ENV_::UNSTABLE_DOLLARAT; + + eval { + $_[0]->[0]->(); + 1; + } or do { + Carp::cluck "Execution of scope guard $_[0] resulted in the non-trappable exception:\n\n$@"; + }; + } +} + sub is_exception ($) { my $e = $_[0]; diff --git a/t/35exception_inaction.t b/t/35exception_inaction.t new file mode 100644 index 0000000..0d8597f --- /dev/null +++ b/t/35exception_inaction.t @@ -0,0 +1,102 @@ +use strict; +use warnings; + +use lib 't/lib'; +use DBICTest::RunMode; +BEGIN { + if( DBICTest::RunMode->is_plain ) { + print "1..0 # SKIP not running dangerous segfault-prone test on plain install\n"; + exit 0; + } +} + +use File::Temp (); +use DBIx::Class::_Util 'scope_guard'; +use DBIx::Class::Schema; + +# Do not use T::B - the test is hard enough not to segfault as it is +my $test_count = 0; + +# start with one failure, and decrement it at the end +my $failed = 1; + +sub ok { + printf STDOUT ("%s %u - %s\n", + ( $_[0] ? 'ok' : 'not ok' ), + ++$test_count, + $_[1] || '', + ); + + unless( $_[0] ) { + $failed++; + printf STDERR ("# Failed test #%d at %s line %d\n", + $test_count, + (caller(0))[1,2] + ); + } + + return !!$_[0]; +} + +# yes, make it even dirtier +my $schema = 'DBIx::Class::Schema'; + +$schema->connection('dbi:SQLite::memory:'); + +# this is incredibly horrible... +# demonstrate utter breakage of the reconnection/retry logic +# +open(my $stderr_copy, '>&', *STDERR) or die "Unable to dup STDERR: $!"; +my $tf = File::Temp->new( UNLINK => 1 ); + +my $output; + +ESCAPE: +{ + my $guard = scope_guard { + close STDERR; + open(STDERR, '>&', $stderr_copy); + $output = do { local (@ARGV, $/) = $tf; <> }; + close $tf; + unlink $tf; + undef $tf; + close $stderr_copy; + }; + + close STDERR; + open(STDERR, '>&', $tf) or die "Unable to reopen STDERR: $!"; + + $schema->storage->ensure_connected; + $schema->storage->_dbh->disconnect; + + local $SIG{__WARN__} = sub {}; + + $schema->exception_action(sub { + ok(1, 'exception_action invoked'); + # essentially what Dancer2's redirect() does after https://github.com/PerlDancer/Dancer2/pull/485 + # which "nicely" combines with: https://metacpan.org/source/MARKOV/Log-Report-1.12/lib/Dancer2/Plugin/LogReport.pm#L143 + # as encouraged by: https://metacpan.org/pod/release/MARKOV/Log-Report-1.12/lib/Dancer2/Plugin/LogReport.pod#Logging-DBIC-database-queries-and-errors + last ESCAPE; + }); + + # this *DOES* throw, but the exception will *NEVER SHOW UP* + $schema->storage->dbh_do(sub { $_[1]->selectall_arrayref("SELECT * FROM wfwqfdqefqef") } ); + + # NEITHER will this + ok(0, "Nope"); +} + +ok(1, "Post-escape reached"); + +ok( + !!( $output =~ /DBIx::Class INTERNAL PANIC.+FIX YOUR ERROR HANDLING/s ), + 'Proper warning emitted on STDERR' +) or print STDERR "Instead found:\n\n$output\n"; + +print "1..$test_count\n"; + +# this is our "done_testing" +$failed--; + +# avoid tasty segfaults on 5.8.x +exit( $failed );