diff --git a/main.go b/main.go new file mode 100644 index 0000000..f1245e1 --- /dev/null +++ b/main.go @@ -0,0 +1,336 @@ +// Copyright 2018 Mikhail Klementev. All rights reserved. +// Use of this source code is governed by a GPLv3 license +// (or later) that can be found in the LICENSE file. + +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "regexp" + "runtime" + "time" + + "github.com/logrusorgru/aurora" + "github.com/naoina/toml" + "github.com/otiai10/copy" + "github.com/remeh/sizedwaitgroup" + + qemu "github.com/jollheef/out-of-tree/qemu" +) + +type kernelMask struct { + DistroType distroType + ReleaseMask string +} + +type artifactType int + +const ( + KernelModule artifactType = iota + KernelExploit +) + +func (at artifactType) String() string { + return [...]string{"module", "exploit"}[at] +} + +type artifact struct { + Name string + Type artifactType + SourcePath string + SupportedKernels []kernelMask +} + +func (ka artifact) checkSupport(ki kernelInfo, km kernelMask) ( + supported bool, err error) { + + if ki.DistroType != km.DistroType { + supported = false + return + } + + supported, err = regexp.MatchString(km.ReleaseMask, ki.KernelRelease) + return +} + +func (ka artifact) Supported(ki kernelInfo) (supported bool, err error) { + for _, km := range ka.SupportedKernels { + supported, err = ka.checkSupport(ki, km) + } + return +} + +type distroType int + +const ( + Ubuntu distroType = iota + CentOS + Debian +) + +func (dt distroType) String() string { + return [...]string{"Ubuntu", "CentOS", "Debian"}[dt] +} + +type kernelInfo struct { + DistroType distroType + DistroRelease string // 18.04/7.4.1708/9.1 + + // Must be *exactly* same as in `uname -r` + KernelRelease string + + // Build-time information + ContainerName string + + // Runtime information + KernelPath string + InitrdPath string + RootFS string +} + +func dockerCommand(container, workdir, timeout, command string) *exec.Cmd { + return exec.Command("timeout", "-k", timeout, timeout, "docker", "run", + "-v", workdir+":/work", container, + "bash", "-c", "cd /work && "+command) +} + +func build(tmp string, ka artifact, ki kernelInfo) (outPath string, err error) { + target := fmt.Sprintf("%s_%s-%s-%s", ka.Name, ki.DistroType, + ki.DistroRelease, ki.KernelRelease) + + tmpSourcePath := tmp + "/source" + + err = copy.Copy(ka.SourcePath, tmpSourcePath) + if err != nil { + return + } + + outPath = tmpSourcePath + "/" + target + ".ko" + + kernel := "/lib/modules/" + ki.KernelRelease + "/build" + + cmd := dockerCommand(ki.ContainerName, tmpSourcePath, "1m", // TODO CFG + "make KERNEL="+kernel+" TARGET="+target) + output, err := cmd.CombinedOutput() + if err != nil { + err = errors.New(string(output)) + return + } + + return +} + +func run(q *qemu.QemuSystem, ka artifact, ki kernelInfo, file string) (output string, err error) { + start := time.Now() + for { + output, err = q.Command("root", "dmesg -c") + if err == nil { + break + } + time.Sleep(time.Second) + + if time.Now().After(start.Add(time.Minute)) { + err = errors.New("Can't connect to qemu") + return + } + } + + switch ka.Type { + case KernelModule: + output, err = q.CopyAndInsmod(file) + case KernelExploit: + output, err = q.CopyAndRun("user", file) + default: + err = errors.New("Unsupported artifact type") + } + + return +} + +func test(q *qemu.QemuSystem, ka artifact) (output string, err error) { + // TODO + return +} + +func genOkFail(name string, ok bool) aurora.Value { + if ok { + s := " " + name + " SUCCESS " + return aurora.BgGreen(aurora.Black(s)) + } else { + s := " " + name + " FAILURE " + return aurora.BgRed(aurora.Gray(aurora.Bold(s))) + } +} + +func dumpResult(ka artifact, ki kernelInfo, build_ok, run_ok, test_ok *bool) { + var stest aurora.Value + if ka.Type == KernelExploit { + stest = genOkFail("LPE", *test_ok) + } else { + stest = genOkFail("TEST", *test_ok) + } + + var srun aurora.Value + if ka.Type == KernelExploit { + srun = genOkFail("RUN", *run_ok) + } else { + srun = genOkFail("INSMOD", *run_ok) + } + + colored := aurora.Sprintf("[*] %40s: %s %s %s", + fmt.Sprintf("%s-%s {%s}", ki.DistroType, ki.DistroRelease, + ki.KernelRelease), + genOkFail("BUILD", *build_ok), + srun, stest) + + fmt.Println(colored) +} + +func whatever(swg *sizedwaitgroup.SizedWaitGroup, ka artifact, ki kernelInfo) { + defer swg.Done() + + kernel := qemu.Kernel{KernelPath: ki.KernelPath, InitrdPath: ki.InitrdPath} + q, err := qemu.NewQemuSystem(qemu.X86_64, kernel, ki.RootFS) + if err != nil { + return + } + q.Timeout = time.Minute + + err = q.Start() + if err != nil { + return + } + defer q.Stop() + + tmp, err := ioutil.TempDir("/tmp/", "out-of-tree_") + if err != nil { + return + } + defer os.RemoveAll(tmp) + + build_ok := false + run_ok := false + test_ok := false + defer dumpResult(ka, ki, &build_ok, &run_ok, &test_ok) + + // TODO Write build log to file or database + outFile, err := build(tmp, ka, ki) + if err != nil { + return + } + build_ok = true + + // TODO Write run log to file or database + _, err = run(q, ka, ki, outFile) + if err != nil { + return + } + run_ok = true + + // TODO Write test results to file or database + _, err = test(q, ka) + if err != nil { + return + } + test_ok = true +} + +type kernelConfig struct { + Kernels []kernelInfo +} + +func readFileAll(path string) (buf []byte, err error) { + f, err := os.Open(path) + if err != nil { + return + } + defer f.Close() + + buf, err = ioutil.ReadAll(f) + return +} + +func readKernelConfig(path string) (kernelCfg kernelConfig, err error) { + buf, err := readFileAll(path) + if err != nil { + return + } + + err = toml.Unmarshal(buf, &kernelCfg) + if err != nil { + return + } + + return +} + +func readArtifactConfig(path string) (artifactCfg artifact, err error) { + buf, err := readFileAll(path) + if err != nil { + return + } + + err = toml.Unmarshal(buf, &artifactCfg) + if err != nil { + return + } + + return +} + +func performCI(ka artifact, kcfg kernelConfig) (err error) { + swg := sizedwaitgroup.New(runtime.NumCPU()) + for _, kernel := range kcfg.Kernels { + var supported bool + supported, err = ka.Supported(kernel) + if err != nil { + return + } + + if supported { + swg.Add() + go whatever(&swg, ka, kernel) + } + } + swg.Wait() + return +} + +func exists(path string) bool { + if _, err := os.Stat(path); err != nil { + return false + } + return true +} + +func main() { + ka, err := readArtifactConfig(".out-of-tree.toml") + if err != nil { + log.Fatalln(err) + } + + kcfgEnv := "OUT_OF_TREE_KERNELS_CONFIG" + kcfgPath := os.Getenv(kcfgEnv) + if !exists(kcfgPath) { + kcfgPath = os.Getenv("GOPATH") + "/src/github.com/jollheef/" + + "out-of-tree/tools/kernel-factory/output/kernels.toml" + } + if !exists(kcfgPath) { + log.Fatalln("Please specify kernels config path in " + kcfgEnv) + } + + kcfg, err := readKernelConfig(kcfgPath) + if err != nil { + log.Fatalln(err) + } + + err = performCI(ka, kcfg) + if err != nil { + log.Fatalln(err) + } +}