From a05aa2ae622fd113b4dd0d6568865c0dba82085f Mon Sep 17 00:00:00 2001 From: Zvonimir Pavlinovic Date: Wed, 18 Oct 2023 12:32:04 -0700 Subject: [PATCH] internal/symbols: compute patched symbols for a given commit This is part of a series of CLs implementing automatic symbol extraction. Change-Id: I3aa0485a597005622d4be40ac2aea5fe5b55467f Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/536297 TryBot-Result: Gopher Robot Run-TryBot: Zvonimir Pavlinovic LUCI-TryBot-Result: Go LUCI Reviewed-by: Tatiana Bradley --- internal/gitrepo/gitrepo.go | 18 ++++++- internal/symbols/patched_functions.go | 76 +++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/internal/gitrepo/gitrepo.go b/internal/gitrepo/gitrepo.go index 2d356588..9e9ab335 100644 --- a/internal/gitrepo/gitrepo.go +++ b/internal/gitrepo/gitrepo.go @@ -23,7 +23,7 @@ import ( "golang.org/x/vulndb/internal/worker/log" ) -// Clone returns a repo by cloning the repo at repoURL. +// Clone returns a bare repo by cloning the repo at repoURL. func Clone(ctx context.Context, repoURL string) (repo *git.Repository, err error) { defer derrors.Wrap(&err, "gitrepo.Clone(%q)", repoURL) ctx = event.Start(ctx, "gitrepo.Clone") @@ -39,6 +39,22 @@ func Clone(ctx context.Context, repoURL string) (repo *git.Repository, err error }) } +// PlainClone returns a (non-bare) repo with its history by cloning the repo at repoURL. +func PlainClone(ctx context.Context, dir, repoURL string) (repo *git.Repository, err error) { + defer derrors.Wrap(&err, "gitrepo.PlainClone(%q)", repoURL) + ctx = event.Start(ctx, "gitrepo.PlainClone") + defer event.End(ctx) + + log.Infof(ctx, "Plain cloning repo %q at HEAD", repoURL) + return git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ + URL: repoURL, + ReferenceName: plumbing.HEAD, + SingleBranch: true, // allow branches other than master + Depth: 0, // pull in history + Tags: git.NoTags, + }) +} + // Open returns a repo by opening the repo at the local path dirpath. func Open(ctx context.Context, dirpath string) (repo *git.Repository, err error) { defer derrors.Wrap(&err, "gitrepo.Open(%q)", dirpath) diff --git a/internal/symbols/patched_functions.go b/internal/symbols/patched_functions.go index ba92248f..1e3ee2f1 100644 --- a/internal/symbols/patched_functions.go +++ b/internal/symbols/patched_functions.go @@ -6,6 +6,7 @@ package symbols import ( "bytes" + "context" "errors" "fmt" "go/ast" @@ -19,9 +20,84 @@ import ( "reflect" "strings" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" "golang.org/x/mod/modfile" + "golang.org/x/vulndb/internal/derrors" + "golang.org/x/vulndb/internal/gitrepo" ) +// Patched returns symbols of module patched in commit identified +// by commitHash. repoURL is URL of the git repository containing +// the module. +// +// Patched returns a map from package import paths to symbols +// patched in the package. Test packages and symbols are omitted. +// +// If the commit has more than one parent, an error is returned. +func Patched(module, repoURL, commitHash string) (_ map[string][]string, err error) { + defer derrors.Wrap(&err, "Patched(%s ,%s, %s)", module, repoURL, commitHash) + + repoRoot, err := os.MkdirTemp("", commitHash) + if err != nil { + return nil, err + } + defer func() { + _ = os.RemoveAll(repoRoot) + }() + + ctx := context.Background() + repo, err := gitrepo.PlainClone(ctx, repoRoot, repoURL) + if err != nil { + return nil, err + } + + w, err := repo.Worktree() + if err != nil { + return nil, err + } + + hash := plumbing.NewHash(commitHash) + commit, err := repo.CommitObject(hash) + if err != nil { + return nil, err + } + + if commit.NumParents() != 1 { + return nil, fmt.Errorf("more than 1 parent: %d", commit.NumParents()) + } + + parent, err := commit.Parent(0) + if err != nil { + return nil, err + } + + if err := w.Checkout(&git.CheckoutOptions{Hash: hash, Force: true}); err != nil { + return nil, err + } + + newSymbols, err := moduleSymbols(repoRoot, module) + if err != nil { + return nil, err + } + + if err := w.Checkout(&git.CheckoutOptions{Hash: parent.Hash, Force: true}); err != nil { + return nil, err + } + + oldSymbols, err := moduleSymbols(repoRoot, module) + if err != nil { + return nil, err + } + + patched := patchedSymbols(oldSymbols, newSymbols) + pkgSyms := make(map[string][]string) + for _, sym := range patched { + pkgSyms[sym.pkg] = append(pkgSyms[sym.pkg], sym.symbol) + } + return pkgSyms, nil +} + // patchedSymbols returns symbol indices in oldSymbols that either 1) cannot // be identified in newSymbols or 2) the corresponding functions have their // source code changed.