1// Copyright 2018 syzkaller project authors. All rights reserved. 2// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4// Package vcs provides helper functions for working with various repositories (e.g. git). 5package vcs 6 7import ( 8 "bytes" 9 "fmt" 10 "io" 11 "regexp" 12 "strings" 13 "time" 14 15 "github.com/google/syzkaller/pkg/osutil" 16) 17 18type Repo interface { 19 // Poll checkouts the specified repository/branch. 20 // This involves fetching/resetting/cloning as necessary to recover from all possible problems. 21 // Returns hash of the HEAD commit in the specified branch. 22 Poll(repo, branch string) (*Commit, error) 23 24 // CheckoutBranch checkouts the specified repository/branch. 25 CheckoutBranch(repo, branch string) (*Commit, error) 26 27 // CheckoutCommit checkouts the specified repository on the specified commit. 28 CheckoutCommit(repo, commit string) (*Commit, error) 29 30 // SwitchCommit checkouts the specified commit without fetching. 31 SwitchCommit(commit string) (*Commit, error) 32 33 // HeadCommit returns info about the HEAD commit of the current branch of git repository. 34 HeadCommit() (*Commit, error) 35 36 // ListRecentCommits returns list of recent commit titles starting from baseCommit. 37 ListRecentCommits(baseCommit string) ([]string, error) 38 39 // ExtractFixTagsFromCommits extracts fixing tags for bugs from git log. 40 // Given email = "user@domain.com", it searches for tags of the form "user+tag@domain.com" 41 // and return pairs {tag, commit title}. 42 ExtractFixTagsFromCommits(baseCommit, email string) ([]FixCommit, error) 43 44 // PreviousReleaseTags returns list of preceding release tags that are reachable from the given commit. 45 PreviousReleaseTags(commit string) ([]string, error) 46 47 // Bisect bisects good..bad commit range against the provided predicate (wrapper around git bisect). 48 // The predicate should return an error only if there is no way to proceed 49 // (it will abort the process), if possible it should prefer to return BisectSkip. 50 // Progress of the process is streamed to the provided trace. 51 // Returns the first commit on which the predicate returns BisectBad. 52 Bisect(bad, good string, trace io.Writer, pred func() (BisectResult, error)) (*Commit, error) 53} 54 55type Commit struct { 56 Hash string 57 Title string 58 Author string 59 CC []string 60 Date time.Time 61} 62 63type FixCommit struct { 64 Tag string 65 Title string 66} 67 68type BisectResult int 69 70const ( 71 BisectBad BisectResult = iota 72 BisectGood 73 BisectSkip 74) 75 76func NewRepo(os, vm, dir string) (Repo, error) { 77 switch os { 78 case "linux": 79 return newGit(os, vm, dir), nil 80 case "akaros": 81 return newAkaros(vm, dir), nil 82 case "fuchsia": 83 return newFuchsia(vm, dir), nil 84 } 85 return nil, fmt.Errorf("vcs is unsupported for %v", os) 86} 87 88func NewSyzkallerRepo(dir string) Repo { 89 return newGit("syzkaller", "", dir) 90} 91 92func Patch(dir string, patch []byte) error { 93 // Do --dry-run first to not mess with partially consistent state. 94 cmd := osutil.Command("patch", "-p1", "--force", "--ignore-whitespace", "--dry-run") 95 if err := osutil.Sandbox(cmd, true, true); err != nil { 96 return err 97 } 98 cmd.Stdin = bytes.NewReader(patch) 99 cmd.Dir = dir 100 if output, err := cmd.CombinedOutput(); err != nil { 101 // If it reverses clean, then it's already applied 102 // (seems to be the easiest way to detect it). 103 cmd = osutil.Command("patch", "-p1", "--force", "--ignore-whitespace", "--reverse", "--dry-run") 104 if err := osutil.Sandbox(cmd, true, true); err != nil { 105 return err 106 } 107 cmd.Stdin = bytes.NewReader(patch) 108 cmd.Dir = dir 109 if _, err := cmd.CombinedOutput(); err == nil { 110 return fmt.Errorf("patch is already applied") 111 } 112 return fmt.Errorf("failed to apply patch:\n%s", output) 113 } 114 // Now apply for real. 115 cmd = osutil.Command("patch", "-p1", "--force", "--ignore-whitespace") 116 if err := osutil.Sandbox(cmd, true, true); err != nil { 117 return err 118 } 119 cmd.Stdin = bytes.NewReader(patch) 120 cmd.Dir = dir 121 if output, err := cmd.CombinedOutput(); err != nil { 122 return fmt.Errorf("failed to apply patch after dry run:\n%s", output) 123 } 124 return nil 125} 126 127// CheckRepoAddress does a best-effort approximate check of a git repo address. 128func CheckRepoAddress(repo string) bool { 129 return gitRepoRe.MatchString(repo) 130} 131 132// CheckBranch does a best-effort approximate check of a git branch name. 133func CheckBranch(branch string) bool { 134 return gitBranchRe.MatchString(branch) 135} 136 137func CheckCommitHash(hash string) bool { 138 if !gitHashRe.MatchString(hash) { 139 return false 140 } 141 ln := len(hash) 142 return ln == 8 || ln == 10 || ln == 12 || ln == 16 || ln == 20 || ln == 40 143} 144 145func runSandboxed(dir, command string, args ...string) ([]byte, error) { 146 cmd := osutil.Command(command, args...) 147 cmd.Dir = dir 148 if err := osutil.Sandbox(cmd, true, false); err != nil { 149 return nil, err 150 } 151 return osutil.Run(time.Hour, cmd) 152} 153 154var ( 155 // nolint: lll 156 gitRepoRe = regexp.MustCompile(`^(git|ssh|http|https|ftp|ftps)://[a-zA-Z0-9-_]+(\.[a-zA-Z0-9-_]+)+(:[0-9]+)?/[a-zA-Z0-9-_./]+\.git(/)?$`) 157 gitBranchRe = regexp.MustCompile("^[a-zA-Z0-9-_/.]{2,200}$") 158 gitHashRe = regexp.MustCompile("^[a-f0-9]+$") 159 releaseTagRe = regexp.MustCompile(`^v([0-9]+).([0-9]+)(?:\.([0-9]+))?$`) 160 ccRes = []*regexp.Regexp{ 161 regexp.MustCompile(`^Reviewed\-.*: (.*)$`), 162 regexp.MustCompile(`^[A-Za-z-]+\-and\-[Rr]eviewed\-.*: (.*)$`), 163 regexp.MustCompile(`^Acked\-.*: (.*)$`), 164 regexp.MustCompile(`^[A-Za-z-]+\-and\-[Aa]cked\-.*: (.*)$`), 165 regexp.MustCompile(`^Tested\-.*: (.*)$`), 166 regexp.MustCompile(`^[A-Za-z-]+\-and\-[Tt]ested\-.*: (.*)$`), 167 } 168) 169 170// CanonicalizeCommit returns commit title that can be used when checking 171// if a particular commit is present in a git tree. 172// Some trees add prefixes to commit titles during backporting, 173// so we want e.g. commit "foo bar" match "BACKPORT: foo bar". 174func CanonicalizeCommit(title string) string { 175 for _, prefix := range commitPrefixes { 176 if strings.HasPrefix(title, prefix) { 177 title = title[len(prefix):] 178 break 179 } 180 } 181 return strings.TrimSpace(title) 182} 183 184var commitPrefixes = []string{ 185 "UPSTREAM:", 186 "CHROMIUM:", 187 "FROMLIST:", 188 "BACKPORT:", 189 "FROMGIT:", 190 "net-backports:", 191} 192