RLIMIT_CPU on OpenBSD doesn't work reliably with few syscalls or
on mostly idle systems.  Even at its most accurate, it takes an
extra second to fire compared to FreeBSD or Linux due to
internal accounting differences, but worst case even the SIGKILL
can be 50s delayed.

So rewrite the CPU burner script in Perl where we can unblock
SIGXCPU and reliably use more syscalls.

Link: https://marc.info/?i=20230829010110.M269767@dcvr
---
 t/spawn.t | 33 +++++++++++++++++++++++++++++----
 1 file changed, 29 insertions(+), 4 deletions(-)

diff --git a/t/spawn.t b/t/spawn.t
index ff95ae8e..87800dd6 100644
--- a/t/spawn.t
+++ b/t/spawn.t
@@ -185,17 +185,42 @@ SKIP: {
                require BSD::Resource;
                defined(BSD::Resource::RLIMIT_CPU())
        } or skip 'BSD::Resource::RLIMIT_CPU missing', 3;
-       my ($r, $w);
-       pipe($r, $w) or die "pipe: $!";
-       my $cmd = ['sh', '-c', 'while true; do :; done'];
+       my $cmd = [$^X, '-w', '-e', <<'EOM' ];
+use POSIX qw(:signal_h);
+use BSD::Resource qw(times);
+my $set = POSIX::SigSet->new;
+$set->emptyset; # spawn() defaults to blocking all signals
+sigprocmask(SIG_SETMASK, $set) or die "SIG_SETMASK: $!";
+my $tot = 0;
+$SIG{XCPU} = sub { print "SIGXCPU $tot\n"; exit(1) };
+my $next = time + 1;
+while (1) {
+       # OpenBSD needs both `times' and `write' (via Perl warn) syscalls
+       # here are both required to trigger RLIMIT_CPU; not sure why.
+       # Even the hard limit seems ignored unless we make those syscalls.
+       # Staying entirely in userspace had no effect, and neither did
+       # some other syscalls tried.  Neither fstat, nor sigprocmask were
+       # able to cause either SIGXCPU or SIGKILL.
+       # to fire on respective soft and hard rlimits being exceeded.
+       my @t = times;
+       $tot += $_ for ($t[0], $t[1]);
+       if (time > $next) {
+               warn "# T: @t (utime, ctime, cutime, cstime)\n";
+               $next = time + 1;
+       }
+}
+EOM
+       pipe(my($r, $w)) or die "pipe: $!";
        my $fd = fileno($w);
-       my $opt = { RLIMIT_CPU => [ 1, 1 ], RLIMIT_CORE => [ 0, 0 ], 1 => $fd };
+       my $opt = { RLIMIT_CPU => [ 1, 9 ], RLIMIT_CORE => [ 0, 0 ], 1 => $fd };
        my $pid = spawn($cmd, undef, $opt);
        close $w or die "close(w): $!";
        my $rset = '';
        vec($rset, fileno($r), 1) = 1;
        ok(select($rset, undef, undef, 5), 'child died before timeout');
        is(waitpid($pid, 0), $pid, 'XCPU child process reaped');
+       like(my $line = readline($r), qr/SIGXCPU/, 'SIGXCPU handled');
+       diag "line=$line";
        isnt($?, 0, 'non-zero exit status');
 }
 

Reply via email to