EV-Telegram-TDLib

 view release on metacpan or  search on metacpan

xt/argument_poison.t  view on Meta::CPAN

# The positive control, run before the real sweep every time: the unguarded
# helpers the round-20 conversion replaced are swapped into every symbol
# table that aliases them, and the same sweep, on a slice, must report leaks
# of each kind it claims to catch. plain_text here also ignores an overloaded
# "", so the overload rerun has something to catch too.
my %LEAKY = (
    plain_text => sub { !defined $_[1] ? '' : ref $_[1] ? overload::StrVal($_[1]) : $_[1] },
    num        => sub { no warnings; 0 + $_[1] },
    real       => sub { no warnings; 0 + $_[1] },
    unix_time  => sub { no warnings; 0 + $_[1] },
    num_list   => sub { no warnings; [ map { 0 + $_ } ref $_[1] eq 'ARRAY' ? @{ $_[1] } : $_[1] ] },
    text_list  => sub { [ map { "$_" } ref $_[1] eq 'ARRAY' ? @{ $_[1] } : $_[1] ] },
    tl_bytes   => sub { MIME::Base64::encode_base64("$_[1]", '') },
    input_file => sub { ref $_[0] ? $_[0] : { '@type' => 'inputFileLocal', path => "$_[0]" } },
    # only definedness, as need() checked before it refused a ref in an id
    # slot: history() then carries the address into the pages it fetches
    # after a reply, which is what proves follow-up requests are checked
    need       => sub { my ($what, @v) = @_; defined or die "required\n" for @v; 1 },
);
{
    my (%saved, $control, $classes, $cdiverge, $ctor, %ccount, %cocount);
    {
        no strict 'refs';
        no warnings 'redefine';
        for my $h (sort keys %LEAKY) {
            for my $pkg ($sw->helper_globs($h)) {
                $saved{"${pkg}::$h"} = \&{"${pkg}::$h"};
                *{"${pkg}::$h"} = $LEAKY{$h};
            }
        }
    }
    my $ok = eval {
        ($control, $classes) = sweep(bare => ['HASH'], wrapped => ['HASH'], first_only => 1,
                                     count => \%ccount,
                                     sites => sub { !$_[0]{added} });
        $cdiverge = overload_sweep(first_only => 1, count => \%cocount);
        $ctor = ctor_sweep(bare => ['HASH'], wrapped => []);
        1;
    };
    my $err = $@;
    {
        no strict 'refs';
        no warnings 'redefine';
        *{$_} = $saved{$_} for keys %saved;
    }
    ok $ok, 'the positive control ran' or diag $err;
    ok !(grep { no strict 'refs'; \&{$_} != $saved{$_} } keys %saved),
        'and every helper it swapped is restored';
    cmp_ok scalar(keys %saved), '>', 50, 'the control swapped the helpers in every mixin';
    my %n = map { $_ => scalar keys %{ $classes->{$_} || {} } }
            qw(stringified address base64 passed-through followup execute);
    cmp_ok $n{stringified}, '>=', 100, "leaky plain_text is caught as a stringified ref at $n{stringified} sites";
    cmp_ok $n{address}, '>=', 150, "leaky num is caught as an address at $n{address} sites";
    cmp_ok $n{base64}, '>=', 2, "leaky tl_bytes is caught as base64 at $n{base64} sites";
    cmp_ok $n{'passed-through'}, '>=', 20, "leaky input_file is caught passing the ref through at $n{'passed-through'} sites";
    cmp_ok $n{execute}, '>=', 1, "a leak in a synchronous execute() request is caught at $n{execute} sites";
    cmp_ok $n{followup}, '>=', 1, "a leak in a request sent only after a reply is caught at $n{followup} sites";
    cmp_ok scalar(keys %$cdiverge), '>=', 50, 'leaky plain_text is caught by the overload rerun at '
        . scalar(keys %$cdiverge) . ' sites';
    my %nc = map { $_ => scalar keys %{ $ctor->{classes}{$_} || {} } } qw(stringified base64);
    cmp_ok $nc{stringified}, '>=', 5, "and in the constructor options and credentials at $nc{stringified} sites";
    cmp_ok $nc{base64}, '>=', 1, "including the base64 encryption key at $nc{base64} sites";
    cmp_ok scalar(keys %{ $ctor->{diverge} }), '>=', 5, 'and the constructor overload rerun at '
        . scalar(keys %{ $ctor->{diverge} }) . ' sites';
    diag sprintf 'positive control: %d poisoned and %d overload calls; %d distinct leaks (%s); %d divergences',
        $ccount{calls} // 0, $cocount{calls} // 0, scalar(keys %$control),
        join(', ', map { "$_ $n{$_}" } sort keys %n), scalar keys %$cdiverge;
}

phase('control');

# the real sweep
my %count;
my $followups_before = $sw->{stat}{followups} // 0;
my ($fail) = sweep(bare => \@BARE, wrapped => \@WRAPPED, count => \%count);
$count{followups} = ($sw->{stat}{followups} // 0) - $followups_before;
phase('sweep');
is scalar(keys %$fail), 0, 'no poisoned argument reaches tdjson as an address, a string form or itself'
    or report($fail);

# the overload rerun: every plain string leaf of a baseline replaced by an
# object that stringifies to it, and the whole capture compared
sub overload_sweep {
    my (%o) = @_;
    my %diverge;
    for my $name (sort keys %base) {
        for my $b (@{ $base{$name} }[0 .. ($o{first_only} ? 0 : $#{ $base{$name} })]) {
            my ($m, $args) = @$b{qw(m args)};
            my $want = ArgumentPoison::canon_wire($b->{res});
            my %optname = map { $_ => 1 } @{ $m->{keys} }, qw(timeout retry);
            my @leaf;
            my $walk;
            $walk = sub {
                my ($v, $path) = @_;
                if (ref $v eq 'ARRAY') {
                    $walk->($v->[$_], [ @$path, $_ ]) for 0 .. $#$v;
                } elsif (ArgumentPoison::is_spec_hash($v)) {
                    $walk->($v->{$_}, [ @$path, "{$_}" ]) for sort keys %$v;
                } elsif (defined $v && !ref $v && $v !~ /\A\s*[+-]?[0-9.]+(?:[eE][+-]?[0-9]+)?\s*\z/) {
                    return if @$path == 1 && $optname{$v} && $path->[0] < $#$args;
                    push @leaf, $path;
                }
            };
            $walk->($args, []);
            undef $walk;
            for my $path (@leaf) {
                my $a = ArgumentPoison::dclone($args);
                ArgumentPoison::set_at($a, $path, ArgumentPoison::Stringy->new(ArgumentPoison::get_at($args, $path)));
                my $res = $sw->invoke($m, $a);
                $o{count}{calls}++;
                next if $res->{err} && $res->{err} =~ /option name must be a string/;
                my $got = ArgumentPoison::canon_wire($res);
                next if !$res->{err} && $got eq $want;
                # a request that embeds the clock can differ from its own rerun
                next if ArgumentPoison::canon_wire($sw->invoke($m, ArgumentPoison::dclone($args))) ne $want;
                my $label = ArgumentPoison::label($path);
                next if $OVERLOAD_OK{$name} && $OVERLOAD_OK{$name}{$label};
                $diverge{"$name $label"} = sprintf "%s: %s", ArgumentPoison::render_call($m, $a),
                    $res->{err} ? 'croaks: ' . (split /\n/, $res->{err})[0]
                                : 'sends ' . ArgumentPoison::trim_wire($got, undef, 200)
                                . ' instead of ' . ArgumentPoison::trim_wire($want, undef, 200);



( run in 0.738 second using v1.01-cache-2.11-cpan-3f962061493 )