Data-SortedSet-Shared

 view release on metacpan or  search on metacpan

xt/corrupt_shm.t  view on Meta::CPAN

use strict;
use warnings;
use Test::More;
use Config;
use POSIX ':sys_wait_h';
use File::Temp qw(tempdir);
use Data::SortedSet::Shared;

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

# Defense-in-depth for a hostile segment: node indices and index-slot state
# bytes live in peer-writable shared memory.  These tests corrupt the mapping
# through the backing file (MAP_SHARED: immediately visible) and then call the
# write paths in a child process:
#   * F3: a poisoned B+tree child pointer must stop the write paths (insert,
#     delete, merge, underflow) instead of a wild read/write under the lock.
#   * F4: an all-occupied member index must bound the open-addressing probe
#     (and the backward-shift delete scan) by the table size instead of
#     spinning forever under the lock.
#   * F6: add_many must keep the rows array alive across element magic.
# The children run under a parent-side watchdog: the unguarded code spins in
# pure C, where Perl signal handlers never fire, so alarm() cannot be used.

# Shared-memory layout constants (must match sortedset.h; the geometry is
# cross-checked against stats() at runtime).
use constant {
    HEADER_SIZE   => 256,
    READER_SLOTS  => 1024,
    SLOT_SIZE     => 16,
    OCC_BYTES     => 128,
    IDX_SLOT_SIZE => 24,   # int64 member + double score + uint8 state, padded
    IDX_STATE_OFF => 16,
    NODE_SIZE     => 424,
    NODE_CHILDREN => 288,  # offsetof(SsNode, children)
    HDR_ROOT_OFF  => 56,
};

sub index_off { HEADER_SIZE + READER_SLOTS * SLOT_SIZE + OCC_BYTES }   # 16768
sub nodes_off { my ($slots) = @_; (index_off() + $slots * IDX_SLOT_SIZE + 7) & ~7 }

# run a vulnerable snippet in a child; return (reaped?, status)
sub run_child {
    my ($code) = @_;
    my $pid = fork();
    die "fork: $!" unless defined $pid;
    unless ($pid) { $code->(); POSIX::_exit(0) }
    my $done; my $deadline = time + 10;
    while (time < $deadline) {
        my $r = waitpid($pid, WNOHANG);
        if ($r == $pid) { $done = 1; last }
        select undef, undef, undef, 0.05;
    }
    if (!$done) { kill 9, $pid; waitpid($pid, 0); }
    return ($done, $?);
}

# --- F4: corrupt all-occupied member index -----------------------------------
{
    my $dir  = tempdir(CLEANUP => 1);
    my $path = "$dir/idx.ss";
    my $z = Data::SortedSet::Shared->new($path, 8);   # index_slots == 16
    $z->add($_, $_ + 0.5) for 1 .. 4;
    my $slots = $z->stats->{index_slots};
    die "test setup: expected 16 index slots, got $slots" unless $slots == 16;

    # Flip every index slot's state byte to "occupied".  Members left as-is
    # (real members 1..4, zeros elsewhere); none equals the probe member 999.
    open my $fh, '+<', $path or die $!;
    for my $i (0 .. $slots - 1) {
        sysseek $fh, index_off() + $i * IDX_SLOT_SIZE + IDX_STATE_OFF, 0 or die $!;
        syswrite $fh, "\x01" or die $!;
    }
    close $fh;

    my ($done, $st) = run_child(sub {
        # Each of these hits an unbounded loop without the fix, in order:
        # ss_idx_find (add's absent-member probe), ss_idx_del's backward-shift
        # scan (remove), ss_idx_find again (exists).
        my $a = $z->add(999, 9.5);        # must fail cleanly (undef), not spin/clobber
        my $r = $z->remove(2);            # present member: bounded shift scan



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