X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=dgit.git;a=blobdiff_plain;f=infra%2Fdgit-repos-server;h=b0064e792b131020ae9db7ec02420d198a3beb7e;hp=ed27c0eaec78efad42b45e079b69a6582c77fba1;hb=f4f514f696e7a6579f287869a1e264134aa50b3b;hpb=16f71298e0a7736a3833e44e4a9ebb356f919dc6 diff --git a/infra/dgit-repos-server b/infra/dgit-repos-server index ed27c0ea..b0064e79 100755 --- a/infra/dgit-repos-server +++ b/infra/dgit-repos-server @@ -22,7 +22,29 @@ use strict; -# What we do is this: +# DGIT-REPOS-DIR contains: +# git tree (or other object) lock (in acquisition order, outer first) +# +# _tmp/PACKAGE_prospective ! } SAME.lock, held during receive-pack +# +# _tmp/PACKAGE_incoming$$ ! } SAME.lock, held during receive-pack +# _tmp/PACKAGE_incoming$$_fresh ! } +# +# PACKAGE.git } PACKAGE.git.lock +# PACKAGE_garbage } (also covers executions of +# PACKAGE_garbage-old } policy hook script for PACKAGE) +# PACKAGE_garbage-tmp } +# +# leaf locks, held during brief operaton only: +# +# _empty } SAME.lock +# _empty.new } +# +# _template } SAME.lock +# +# locks marked ! may be held during client data transfer + +# What we do on push is this: # - extract the destination repo name # - make a hardlink clone of the destination repo # - provide the destination with a stunt pre-receive hook @@ -44,13 +66,16 @@ use strict; # + verify the signature on the signed tag # and if necessary check that the keyid and package are listed in dm.txt # + check various correspondences: -# * the suite is one of those permitted # * the signed tag must refer to a commit # * the signed tag commit must be the refs/dgit value # * the name in the signed tag must correspond to its ref name # * the tag name must be debian/ (massaged as needed) +# * the suite is one of those permitted # * the signed tag has a suitable name -# * the commit is a fast forward +# * run the "push" policy hook +# * replay prevention for --deliberately-not-fast-forward +# * check the commit is a fast forward +# * handle a request from the policy hook for a fresh repo # + push the signed tag and new dgit branch to the actual repo # # If the destination repo does not already exist, we need to make @@ -63,8 +88,6 @@ use strict; # of _template # - we use the prospective new destination repo instead of the # actual new destination repo (since the latter doesn't exist) -# - we set up a post-receive hook as well, which -# + touches a stamp file # - after git-receive-pack exits, we # + check that the prospective repo contains a tag and head # + rename the prospective destination repo into place @@ -103,6 +126,7 @@ our $keyrings; our @lockfhs; our $debug=''; our @deliberatelies; +our %supersedes; our $policy; #----- utilities ----- @@ -124,10 +148,7 @@ sub acquirelock ($$) { debug " locking $lock failed"; return undef; } - if (!stat $lock) { - next if $! == ENOENT; - die "stat $lock: $!"; - } + next unless stat_exists $lock; my $want = (stat _)[1]; stat $fh or die $!; my $got = (stat _)[1]; @@ -208,18 +229,41 @@ sub mkemptyrepo ($$) { sub mkrepo_fromtemplate ($) { my ($dir) = @_; my $template = "$dgitrepos/_template"; - debug "copy tempalate $template -> $dir"; + acquiretree($template,1); + debug "copy template $template -> $dir"; my $r = system qw(cp -a --), $template, $dir; !$r or die "create new repo $dir failed: $r $!"; } sub movetogarbage () { - my $garbagerepo = "$dgitrepos/_tmp/${package}_garbage"; - acquiretree($garbagerepo,1); - rmtree $garbagerepo; + # $realdestrepo must have been locked + my $garbagerepo = "$dgitrepos/${package}_garbage"; + # We arrange to always keep at least one old tree, for anti-rewind + # purposes (and, I guess, recovery from mistakes). This is either + # $garbage or $garbage-old. + if (stat_exists "$garbagerepo") { + rmtree "$garbagerepo-tmp"; + if (rename "$garbagerepo-old", "$garbagerepo-tmp") { + rmtree "$garbagerepo-tmp"; + } else { + die "$garbagerepo $!" unless $!==ENOENT; + } + rename "$garbagerepo", "$garbagerepo-old" or die "$garbagerepo $!"; + } rename $realdestrepo, $garbagerepo or $! == ENOENT - or die "rename repo $realdestrepo to $garbagerepo: $!"; + or die "$garbagerepo $!"; +} + +sub onwardpush () { + my @cmd = (qw(git send-pack), $destrepo); + push @cmd, qw(--force) if $policy & NOFFCHECK; + push @cmd, "$commit:refs/dgit/$suite", + "$tagval:refs/tags/$tagname"); + debugcmd @cmd; + $!=0; + my $r = system @cmd; + !$r or die "onward push to $destrepo failed: $r $!"; } #----- git-receive-pack ----- @@ -235,7 +279,10 @@ sub makeworkingclone () { mkrepotmp(); $workrepo = "$dgitrepos/_tmp/${package}_incoming$$"; acquiretree($workrepo, 1); + my $lfh = acquiretree($realdestrepo, 1); runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo; + close $lfh; + rmtree "${workrepo}_fresh"; } sub setupstunthook () { @@ -253,6 +300,12 @@ END debug " stunt hook set up $prerecv"; } +sub dealwithfreshrepo () { + my $freshrepo = "${workrepo}_fresh"; + return unless stat_exists $freshrepo; + $destrepo = $freshrepo; +} + sub maybeinstallprospective () { return if $destrepo eq $realdestrepo; @@ -292,6 +345,12 @@ sub maybeinstallprospective () { die Dumper(\%got)." -- missing refs in new repo" if grep { !$_ } values %got; + acquiretree($realdestrepo,1); + + if ($destrepo eq "${workrepo}_fresh") { + movetogarbage; + } + debug "install $destrepo => $realdestrepo"; rename $destrepo, $realdestrepo or die $!; remove "$destrepo.lock" or die $!; @@ -301,6 +360,7 @@ sub main__git_receive_pack () { makeworkingclone(); setupstunthook(); runcmd qw(git receive-pack), $workrepo; + dealwithfreshrepo(); maybeinstallprospective(); } @@ -373,6 +433,9 @@ sub parsetag () { die "$1 != $distro" unless $1 eq $distro; } elsif (s/^(--deliberately-$package_re) //) { push @deliberatelies, $1; + } elsif (s/^supersede:(\S+)=(\w+) //) { + die "supersede $1 twice" if defined $supersedes{$1}; + $supersedes{$1} = $2; } elsif (s/^[-+.=0-9a-z]\S* //) { } else { die "unknown dgit info in tag"; @@ -502,6 +565,74 @@ sub checksuite () { reject "unknown suite"; } +sub checktagnoreplay () { + # We check that the signed tag mentions the name and value of + # (a) in the case of FRESHREPO all tags in the repo; + # (b) in the case of just NOFFCHECK all tags referring to + # the current head for the suite (there must be at least one). + # This prevents a replay attack using an earlier signed tag. + return unless $policy & (FRESHREPO|NOFFCHECK); + + my $garbagerepo = "$dgitrepos/${package}_garbage"; + acquiretree($realdestrepo,1); + + local $ENV{GIT_DIR}; + foreach my $garb ("$garbagerepo", "$garbagerepo-old") { + if (stat_exists $garb) { + $ENV{GIT_DIR} = $garb; + last; + } + } + if (!defined $ENV{GIT_DIR}) { + # Nothing to overwrite so the FRESHREPO and NOFFCHECK were + # pointless. Oh well. + debug "checktagnoreplay - no garbage, ok"; + return; + } + + my $onlyreferring; + if (!($policy & FRESHREPO)) { + my $branch = server_branch($suite); + $!=0; $?=0; $_ = + `git for-each-ref --format='%(objectname)' '[r]efs/$branch'`; + defined or die "$branch $? $!"; + $? and die "$branch $?"; + if (!length) { + # No such branch - NOFFCHECK was unnecessary. Oh well. + debug "checktagnoreplay - not FRESHREPO, new branch, ok"; + return; + } + m/^(\w+)\n$/ or die "$branch $_ ?"; + $onlyreferring = $1; + debug "checktagnoreplay - not FRESHREPO,". + " checking for overwriting refs/$branch=$onlyreferring"; + } + + my @problems; + + git_for_each_tag_referring($objreferring, sub { + my ($objid,$fullrefname,$tagname) = @_; + debug "checktagnoreplay - overwriting $fullrefname=$objid"; + my $supers = $supersedes{$fullrefname}; + if (!defined $supers) { + push @problems, "does not supersede $fullrefname"; + } elsif ($supers ne $objid) { + push @problems, + "supersedes $fullrefname=$supers but previously $fullrefname=$objid"; + } else { + # ok; + } + }); + + if (@problems) { + reject "replay attack prevention check failed:". + " signed tag for $version: ". + join("; ", @problems). + "\n"; + } + debug "checktagnoreply - all ok" +} + sub tagh1 ($) { my ($tag) = @_; my $vals = $tagh{$tag}; @@ -523,10 +654,13 @@ sub checks () { debug "translated version $v"; $tagname eq "debian/$v" or die; - my ($policy) = policyhook(NOFFCHECK, 'push',$package, - $version,$suite,$tagname, - join(",",@delberatelies)); + acquiretree($realdestrepo,1); + $policy = policyhook(NOFFCHECK|FRESHREPO, 'push',$package, + $version,$suite,$tagname, + join(",",@delberatelies)); + + checktagnoreplay(); checksuite(); # check that our ref is being fast-forwarded @@ -536,17 +670,24 @@ sub checks () { chomp $mb; $mb eq $oldcommit or reject "not fast forward on dgit branch"; } -} -sub onwardpush () { - my @cmd = (qw(git send-pack), $destrepo, - "$commit:refs/dgit/$suite", - "$tagval:refs/tags/$tagname"); - debugcmd @cmd; - $!=0; - my $r = system @cmd; - !$r or die "onward push failed: $r $!"; -} + if ($policy & FRESHREPO) { + # This is troublesome. We have been asked by the policy hook + # to receive the push into a fresh repo. But of course we + # have actually already mostly received the push into the working + # repo. (This is unavoidable because the instruction to use a new + # repo comes ultimately from the signed tag for the dgit push, + # which has to have been received into some repo.) + # + # So what we do is generate a fresh working repo right now and + # push the head and tag into it. The presence of this fresh + # working repo is detected by the parent, which responds by + # making a fresh master repo from the template. + + $destrepo = "${workrepo}_fresh"; # workrepo lock covers + mkrepo_fromtemplate $destrepo; + } +} sub stunthook () { debug "stunthook"; @@ -565,8 +706,7 @@ sub stunthook () { sub fixmissing__git_upload_pack () { $destrepo = "$dgitrepos/_empty"; my $lfh = acquiretree($destrepo,1); - return if stat $destrepo; - die $! unless $!==ENOENT; + return if stat_exists $destrepo; rmtree "$destrepo.new"; mkemptyrepo "$destrepo.new", "0644"; rename "$destrepo.new", $destrepo or die $!; @@ -575,7 +715,10 @@ sub fixmissing__git_upload_pack () { } sub main__git_upload_pack () { - runcmd qw(git upload-pack), $destrepo; + my $lfh = acquiretree($destrepo,1); + chdir $destrepo or die "$destrepo: $!"; + close $lfh; + runcmd qw(git upload-pack), "."; } #----- arg parsing and main program ----- @@ -649,16 +792,19 @@ sub parseargsdispatch () { reject "unknown method" unless $mainfunc; + my $lfh = acquiretree($realdestrepo,1); + my ($policy, $pollock) = policyhook(FRESHREPO,'check-package',$package); if ($policy & FRESHREPO) { movetogarbage; } close $pollock or die $!; - if (stat $realdestrepo) { + close $lfh; + + if (stat_exists $realdestrepo) { $destrepo = $realdestrepo; } else { - $! == ENOENT or die "stat dest repo $destrepo: $!"; debug " fixmissing $funcn"; my $fixfunc = $main::{"fixmissing__$funcn"}; &$fixfunc;