#!/usr/bin/perl
# hardening-info-helper -- lintian collection script helper

# Copyright (C) 2012 Niels Thykier
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, you can find it on the World Wide
# Web at http://www.gnu.org/copyleft/gpl.html, or write to the Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
# MA 02110-1301, USA.

use strict;
use warnings;
use autodie;

use FileHandle;

use lib "$ENV{'LINTIAN_ROOT'}/lib";
use Lintian::Command qw(spawn reap);

# To reduce the number of false-positives in hardening-check for
# fortify-functions, we have to "double-check" its output in some
# cases (like we do with file-info).
#
# Basic idea - fork and pipe to child in up to two passes.
# - The parent will filter "hardening-check --lintian" input in first
#   pass.
#   - Filter out (and collect) all no-fortify-function tags
#   - Work around bug #677530
# - The parent will (in second pass) pipe the verbose hardening-check
#   output to the child.
#   - Only binaries with a no-fortify-function tag in the first pass
#     will be re-checked with --verbose.
#
# - In the first pass, the child will behave like cat.
# - In the second pass, the child will parse hardening-check --verbose
#   output.
#
# Implied by the above - the second pass is only done if needed.

my ($in, $out);
my ($cread, $cwrite);
my ($cpid, @recheck);
my %whitelisted_funcs = (
    'memcpy' => 1,
    'memset' => 1,
    'memmove' => 1,
);

pipe($cread, $cwrite);
$cpid = fork();
if ($cpid) {
    # parent
    close($cread); # read end not needed
    $in = \*STDIN;
    $out = $cwrite;
} else {
    # child
    close($cwrite); # write end not needed.
    $in = $cread;
    $out = \*STDOUT;
}

while (my $line = <$in>) {
    chomp $line;
    if ($cpid) {
        if ($line =~ m/^no-fortify-functions:(.*)$/o) {
            my $bin = $1;
            push @recheck, $bin;
            next;
        }
    } else {
        # End of "first pass" marker (for the child).
        last if $line eq '__VERBOSE__';
    }
    print {$out} "$line\n";
}

if (not $cpid) {
    # child's second pass
    my $bin;
    my $infsf = 0;
    my $emit = 0;
    while (my $line = <$in>) {
        chomp $line;
        # At this point we are reading "verbose" hardening-check output
        if ($line =~ m,^(\S.+):$,) {
            if ($emit) {
                print {$out} "no-fortify-functions:$bin\n";
            }
            $bin = $1;
            $infsf = 0;
            $emit = 0;
        } elsif ($line =~ m/^\s+Fortify Source functions:/) {
            $infsf = 1;
        } elsif ($infsf and $line =~ m/^\s+(un)?protected:\s*(\S+)/) {
            next unless ($1//'') eq 'un';
            next if exists $whitelisted_funcs{$2};
            $emit = 1;
        } else {
            $infsf = 0;
        }
    }
    if ($emit) {
        print {$out} "no-fortify-functions:$bin\n";
    }
    # ensure $out is flushed before exiting.
    close($out);
    require POSIX;
    POSIX::_exit(0);
} elsif (@recheck) {
    # (optionally) parent's second pass.
    my %opts = (
        pipe_in => FileHandle->new,
        out => $out,
        fail => 'never'
    );
    # End the first pass for the child
    print {$out} "__VERBOSE__\n";
    spawn(\%opts, ['xargs', '-0r', 'hardening-check', '--verbose', '--']);
    $opts{pipe_in}->blocking(1);
    foreach my $file (@recheck) {
        printf {$opts{pipe_in}} "%s\0", $file;
    }
    close($opts{pipe_in});
    reap(\%opts);
}

# Close the out handle, else the child process will wait for
# ever.
close($out);
# wait for the child process.
wait();
exit $?;

