Move pew-related stuff
This commit is contained in:
		
							
								
								
									
										484
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										484
									
								
								main.go
									
									
									
									
									
								
							| @@ -5,498 +5,14 @@ | |||||||
| package main | package main | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"log" | 	"log" | ||||||
| 	"math/rand" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" |  | ||||||
| 	"os/user" | 	"os/user" | ||||||
| 	"regexp" |  | ||||||
| 	"runtime" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/logrusorgru/aurora" |  | ||||||
| 	"github.com/naoina/toml" |  | ||||||
| 	"github.com/otiai10/copy" |  | ||||||
| 	"github.com/remeh/sizedwaitgroup" |  | ||||||
| 	kingpin "gopkg.in/alecthomas/kingpin.v2" | 	kingpin "gopkg.in/alecthomas/kingpin.v2" | ||||||
|  |  | ||||||
| 	qemu "github.com/jollheef/out-of-tree/qemu" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type kernelMask struct { |  | ||||||
| 	DistroType    distroType |  | ||||||
| 	DistroRelease string // 18.04/7.4.1708/9.1 |  | ||||||
| 	ReleaseMask   string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type artifactType int |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	KernelModule artifactType = iota |  | ||||||
| 	KernelExploit |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func (at artifactType) String() string { |  | ||||||
| 	return [...]string{"module", "exploit"}[at] |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (at *artifactType) UnmarshalTOML(data []byte) (err error) { |  | ||||||
| 	stype := strings.Trim(string(data), `"`) |  | ||||||
| 	stypelower := strings.ToLower(stype) |  | ||||||
| 	if strings.Contains(stypelower, "module") { |  | ||||||
| 		*at = KernelModule |  | ||||||
| 	} else if strings.Contains(stypelower, "exploit") { |  | ||||||
| 		*at = KernelExploit |  | ||||||
| 	} else { |  | ||||||
| 		err = errors.New(fmt.Sprintf("Type %s is unsupported", stype)) |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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 |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// DistroRelease is optional |  | ||||||
| 	if km.DistroRelease != "" && ki.DistroRelease != km.DistroRelease { |  | ||||||
| 		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) |  | ||||||
| 		if supported { |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type distroType int |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	Ubuntu distroType = iota |  | ||||||
| 	CentOS |  | ||||||
| 	Debian |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var distroTypeStrings = [...]string{"Ubuntu", "CentOS", "Debian"} |  | ||||||
|  |  | ||||||
| func newDistroType(dType string) (dt distroType, err error) { |  | ||||||
| 	err = dt.UnmarshalTOML([]byte(dType)) |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (dt distroType) String() string { |  | ||||||
| 	return distroTypeStrings[dt] |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (dt *distroType) UnmarshalTOML(data []byte) (err error) { |  | ||||||
| 	sDistro := strings.Trim(string(data), `"`) |  | ||||||
| 	if strings.EqualFold(sDistro, "Ubuntu") { |  | ||||||
| 		*dt = Ubuntu |  | ||||||
| 	} else if strings.EqualFold(sDistro, "CentOS") { |  | ||||||
| 		*dt = CentOS |  | ||||||
| 	} else if strings.EqualFold(sDistro, "Debian") { |  | ||||||
| 		*dt = Debian |  | ||||||
| 	} else { |  | ||||||
| 		err = errors.New(fmt.Sprintf("Distro %s is unsupported", sDistro)) |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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, |  | ||||||
| 	dockerTimeout time.Duration) (outPath, output string, err error) { |  | ||||||
|  |  | ||||||
| 	target := fmt.Sprintf("%d_%s", rand.Int(), ki.KernelRelease) |  | ||||||
|  |  | ||||||
| 	tmpSourcePath := tmp + "/source" |  | ||||||
|  |  | ||||||
| 	err = copy.Copy(ka.SourcePath, tmpSourcePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	outPath = tmpSourcePath + "/" + target |  | ||||||
| 	if ka.Type == KernelModule { |  | ||||||
| 		outPath += ".ko" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	kernel := "/lib/modules/" + ki.KernelRelease + "/build" |  | ||||||
|  |  | ||||||
| 	seconds := fmt.Sprintf("%ds", dockerTimeout/time.Second) |  | ||||||
| 	cmd := dockerCommand(ki.ContainerName, tmpSourcePath, seconds, |  | ||||||
| 		"make KERNEL="+kernel+" TARGET="+target) |  | ||||||
| 	rawOutput, err := cmd.CombinedOutput() |  | ||||||
| 	output = string(rawOutput) |  | ||||||
| 	if err != nil { |  | ||||||
| 		err = errors.New("make execution error") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func cleanDmesg(q *qemu.QemuSystem) (err error) { |  | ||||||
| 	start := time.Now() |  | ||||||
| 	for { |  | ||||||
| 		_, 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") |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func testKernelModule(q *qemu.QemuSystem, ka artifact, test string) (output string, err error) { |  | ||||||
| 	output, err = q.Command("root", test) |  | ||||||
| 	// TODO generic checks for WARNING's and so on |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func testKernelExploit(q *qemu.QemuSystem, ka artifact, test, exploit string) (output string, err error) { |  | ||||||
| 	output, err = q.Command("user", "chmod +x "+exploit) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	randFilePath := fmt.Sprintf("/root/%d", rand.Int()) |  | ||||||
|  |  | ||||||
| 	cmd := fmt.Sprintf("%s %s %s", test, exploit, randFilePath) |  | ||||||
| 	output, err = q.Command("user", cmd) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	_, err = q.Command("root", "stat "+randFilePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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(q *qemu.QemuSystem, ka artifact, ki kernelInfo, build_ok, run_ok, test_ok *bool) { |  | ||||||
| 	distroInfo := fmt.Sprintf("%s-%s {%s}", ki.DistroType, |  | ||||||
| 		ki.DistroRelease, ki.KernelRelease) |  | ||||||
|  |  | ||||||
| 	colored := "" |  | ||||||
| 	if ka.Type == KernelExploit { |  | ||||||
| 		colored = aurora.Sprintf("[*] %40s: %s %s", distroInfo, |  | ||||||
| 			genOkFail("BUILD", *build_ok), |  | ||||||
| 			genOkFail("LPE", *test_ok)) |  | ||||||
| 	} else { |  | ||||||
| 		colored = aurora.Sprintf("[*] %40s: %s %s %s", distroInfo, |  | ||||||
| 			genOkFail("BUILD", *build_ok), |  | ||||||
| 			genOkFail("INSMOD", *run_ok), |  | ||||||
| 			genOkFail("TEST", *test_ok)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	additional := "" |  | ||||||
| 	if q.KernelPanic { |  | ||||||
| 		additional = "(panic)" |  | ||||||
| 	} else if q.KilledByTimeout { |  | ||||||
| 		additional = "(timeout)" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if additional != "" { |  | ||||||
| 		fmt.Println(colored, additional) |  | ||||||
| 	} else { |  | ||||||
| 		fmt.Println(colored) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func whatever(swg *sizedwaitgroup.SizedWaitGroup, ka artifact, ki kernelInfo, |  | ||||||
| 	binaryPath, testPath string, |  | ||||||
| 	qemuTimeout, dockerTimeout time.Duration) { |  | ||||||
|  |  | ||||||
| 	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 = qemuTimeout |  | ||||||
|  |  | ||||||
| 	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(q, ka, ki, &build_ok, &run_ok, &test_ok) |  | ||||||
|  |  | ||||||
| 	var outFile, output string |  | ||||||
| 	if binaryPath == "" { |  | ||||||
| 		// TODO Write build log to file or database |  | ||||||
| 		outFile, output, err = build(tmp, ka, ki, dockerTimeout) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Println(output) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		build_ok = true |  | ||||||
| 	} else { |  | ||||||
| 		outFile = binaryPath |  | ||||||
| 		build_ok = true |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	err = cleanDmesg(q) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if testPath == "" { |  | ||||||
| 		testPath = outFile + "_test" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	remoteTest := fmt.Sprintf("/tmp/test_%d", rand.Int()) |  | ||||||
| 	err = q.CopyFile("user", testPath, remoteTest) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Println("copy file err", err) |  | ||||||
| 		// we should not exit because of testing 'insmod' part |  | ||||||
| 		// for kernel module |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	_, err = q.Command("root", "chmod +x "+remoteTest) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ka.Type == KernelModule { |  | ||||||
| 		// TODO Write insmod log to file or database |  | ||||||
| 		output, err := q.CopyAndInsmod(outFile) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Println(output, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		run_ok = true |  | ||||||
|  |  | ||||||
| 		// TODO Write test results to file or database |  | ||||||
| 		output, err = testKernelModule(q, ka, remoteTest) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Println(output, err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		test_ok = true |  | ||||||
| 	} else if ka.Type == KernelExploit { |  | ||||||
| 		remoteExploit := fmt.Sprintf("/tmp/exploit_%d", rand.Int()) |  | ||||||
| 		err = q.CopyFile("user", outFile, remoteExploit) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// TODO Write test results to file or database |  | ||||||
| 		output, err = testKernelExploit(q, ka, remoteTest, remoteExploit) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Println(output) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 		run_ok = true // does not really used |  | ||||||
| 		test_ok = true |  | ||||||
| 	} else { |  | ||||||
| 		err = errors.New("Unsupported artifact type") |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| 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, binaryPath, testPath string, |  | ||||||
| 	qemuTimeout, dockerTimeout time.Duration) (err error) { |  | ||||||
|  |  | ||||||
| 	found := false |  | ||||||
|  |  | ||||||
| 	swg := sizedwaitgroup.New(runtime.NumCPU()) |  | ||||||
| 	for _, kernel := range kcfg.Kernels { |  | ||||||
| 		var supported bool |  | ||||||
| 		supported, err = ka.Supported(kernel) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if supported { |  | ||||||
| 			found = true |  | ||||||
| 			swg.Add() |  | ||||||
| 			go whatever(&swg, ka, kernel, binaryPath, testPath, |  | ||||||
| 				qemuTimeout, dockerTimeout) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	swg.Wait() |  | ||||||
|  |  | ||||||
| 	if !found { |  | ||||||
| 		err = errors.New("No supported kernels found") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func exists(path string) bool { |  | ||||||
| 	if _, err := os.Stat(path); err != nil { |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	return true |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func pewHandler(kcfg kernelConfig, workPath, ovrrdKrnl, binary, test string, |  | ||||||
| 	guess bool, qemuTimeout, dockerTimeout time.Duration) (err error) { |  | ||||||
|  |  | ||||||
| 	ka, err := readArtifactConfig(workPath + "/.out-of-tree.toml") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ka.SourcePath == "" { |  | ||||||
| 		ka.SourcePath = workPath |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if ovrrdKrnl != "" { |  | ||||||
| 		parts := strings.Split(ovrrdKrnl, ":") |  | ||||||
| 		if len(parts) != 2 { |  | ||||||
| 			return errors.New("Kernel is not 'distroType:regex'") |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		var dt distroType |  | ||||||
| 		dt, err = newDistroType(parts[0]) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		km := kernelMask{DistroType: dt, ReleaseMask: parts[1]} |  | ||||||
| 		ka.SupportedKernels = []kernelMask{km} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if guess { |  | ||||||
| 		ka.SupportedKernels = []kernelMask{} |  | ||||||
| 		for _, dType := range distroTypeStrings { |  | ||||||
| 			var dt distroType |  | ||||||
| 			dt, err = newDistroType(dType) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			km := kernelMask{DistroType: dt, ReleaseMask: ".*"} |  | ||||||
| 			ka.SupportedKernels = append(ka.SupportedKernels, km) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	err = performCI(ka, kcfg, binary, test, qemuTimeout, dockerTimeout) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func kernelListHandler(kcfg kernelConfig) (err error) { | func kernelListHandler(kcfg kernelConfig) (err error) { | ||||||
| 	for _, kernel := range kcfg.Kernels { | 	for _, kernel := range kcfg.Kernels { | ||||||
| 		fmt.Println(kernel.DistroType, kernel.DistroRelease, | 		fmt.Println(kernel.DistroType, kernel.DistroRelease, | ||||||
|   | |||||||
							
								
								
									
										496
									
								
								pew.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										496
									
								
								pew.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,496 @@ | |||||||
|  | // Copyright 2018 Mikhail Klementev. All rights reserved. | ||||||
|  | // Use of this source code is governed by a AGPLv3 license | ||||||
|  | // (or later) that can be found in the LICENSE file. | ||||||
|  |  | ||||||
|  | package main | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	"math/rand" | ||||||
|  | 	"os" | ||||||
|  | 	"os/exec" | ||||||
|  | 	"regexp" | ||||||
|  | 	"runtime" | ||||||
|  | 	"strings" | ||||||
|  | 	"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 | ||||||
|  | 	DistroRelease string // 18.04/7.4.1708/9.1 | ||||||
|  | 	ReleaseMask   string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type artifactType int | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	KernelModule artifactType = iota | ||||||
|  | 	KernelExploit | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (at artifactType) String() string { | ||||||
|  | 	return [...]string{"module", "exploit"}[at] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (at *artifactType) UnmarshalTOML(data []byte) (err error) { | ||||||
|  | 	stype := strings.Trim(string(data), `"`) | ||||||
|  | 	stypelower := strings.ToLower(stype) | ||||||
|  | 	if strings.Contains(stypelower, "module") { | ||||||
|  | 		*at = KernelModule | ||||||
|  | 	} else if strings.Contains(stypelower, "exploit") { | ||||||
|  | 		*at = KernelExploit | ||||||
|  | 	} else { | ||||||
|  | 		err = errors.New(fmt.Sprintf("Type %s is unsupported", stype)) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// DistroRelease is optional | ||||||
|  | 	if km.DistroRelease != "" && ki.DistroRelease != km.DistroRelease { | ||||||
|  | 		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) | ||||||
|  | 		if supported { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type distroType int | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	Ubuntu distroType = iota | ||||||
|  | 	CentOS | ||||||
|  | 	Debian | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var distroTypeStrings = [...]string{"Ubuntu", "CentOS", "Debian"} | ||||||
|  |  | ||||||
|  | func newDistroType(dType string) (dt distroType, err error) { | ||||||
|  | 	err = dt.UnmarshalTOML([]byte(dType)) | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dt distroType) String() string { | ||||||
|  | 	return distroTypeStrings[dt] | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (dt *distroType) UnmarshalTOML(data []byte) (err error) { | ||||||
|  | 	sDistro := strings.Trim(string(data), `"`) | ||||||
|  | 	if strings.EqualFold(sDistro, "Ubuntu") { | ||||||
|  | 		*dt = Ubuntu | ||||||
|  | 	} else if strings.EqualFold(sDistro, "CentOS") { | ||||||
|  | 		*dt = CentOS | ||||||
|  | 	} else if strings.EqualFold(sDistro, "Debian") { | ||||||
|  | 		*dt = Debian | ||||||
|  | 	} else { | ||||||
|  | 		err = errors.New(fmt.Sprintf("Distro %s is unsupported", sDistro)) | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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, | ||||||
|  | 	dockerTimeout time.Duration) (outPath, output string, err error) { | ||||||
|  |  | ||||||
|  | 	target := fmt.Sprintf("%d_%s", rand.Int(), ki.KernelRelease) | ||||||
|  |  | ||||||
|  | 	tmpSourcePath := tmp + "/source" | ||||||
|  |  | ||||||
|  | 	err = copy.Copy(ka.SourcePath, tmpSourcePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	outPath = tmpSourcePath + "/" + target | ||||||
|  | 	if ka.Type == KernelModule { | ||||||
|  | 		outPath += ".ko" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	kernel := "/lib/modules/" + ki.KernelRelease + "/build" | ||||||
|  |  | ||||||
|  | 	seconds := fmt.Sprintf("%ds", dockerTimeout/time.Second) | ||||||
|  | 	cmd := dockerCommand(ki.ContainerName, tmpSourcePath, seconds, | ||||||
|  | 		"make KERNEL="+kernel+" TARGET="+target) | ||||||
|  | 	rawOutput, err := cmd.CombinedOutput() | ||||||
|  | 	output = string(rawOutput) | ||||||
|  | 	if err != nil { | ||||||
|  | 		err = errors.New("make execution error") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func cleanDmesg(q *qemu.QemuSystem) (err error) { | ||||||
|  | 	start := time.Now() | ||||||
|  | 	for { | ||||||
|  | 		_, 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") | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testKernelModule(q *qemu.QemuSystem, ka artifact, test string) (output string, err error) { | ||||||
|  | 	output, err = q.Command("root", test) | ||||||
|  | 	// TODO generic checks for WARNING's and so on | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testKernelExploit(q *qemu.QemuSystem, ka artifact, test, exploit string) (output string, err error) { | ||||||
|  | 	output, err = q.Command("user", "chmod +x "+exploit) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	randFilePath := fmt.Sprintf("/root/%d", rand.Int()) | ||||||
|  |  | ||||||
|  | 	cmd := fmt.Sprintf("%s %s %s", test, exploit, randFilePath) | ||||||
|  | 	output, err = q.Command("user", cmd) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = q.Command("root", "stat "+randFilePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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(q *qemu.QemuSystem, ka artifact, ki kernelInfo, build_ok, run_ok, test_ok *bool) { | ||||||
|  | 	distroInfo := fmt.Sprintf("%s-%s {%s}", ki.DistroType, | ||||||
|  | 		ki.DistroRelease, ki.KernelRelease) | ||||||
|  |  | ||||||
|  | 	colored := "" | ||||||
|  | 	if ka.Type == KernelExploit { | ||||||
|  | 		colored = aurora.Sprintf("[*] %40s: %s %s", distroInfo, | ||||||
|  | 			genOkFail("BUILD", *build_ok), | ||||||
|  | 			genOkFail("LPE", *test_ok)) | ||||||
|  | 	} else { | ||||||
|  | 		colored = aurora.Sprintf("[*] %40s: %s %s %s", distroInfo, | ||||||
|  | 			genOkFail("BUILD", *build_ok), | ||||||
|  | 			genOkFail("INSMOD", *run_ok), | ||||||
|  | 			genOkFail("TEST", *test_ok)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	additional := "" | ||||||
|  | 	if q.KernelPanic { | ||||||
|  | 		additional = "(panic)" | ||||||
|  | 	} else if q.KilledByTimeout { | ||||||
|  | 		additional = "(timeout)" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if additional != "" { | ||||||
|  | 		fmt.Println(colored, additional) | ||||||
|  | 	} else { | ||||||
|  | 		fmt.Println(colored) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func whatever(swg *sizedwaitgroup.SizedWaitGroup, ka artifact, ki kernelInfo, | ||||||
|  | 	binaryPath, testPath string, | ||||||
|  | 	qemuTimeout, dockerTimeout time.Duration) { | ||||||
|  |  | ||||||
|  | 	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 = qemuTimeout | ||||||
|  |  | ||||||
|  | 	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(q, ka, ki, &build_ok, &run_ok, &test_ok) | ||||||
|  |  | ||||||
|  | 	var outFile, output string | ||||||
|  | 	if binaryPath == "" { | ||||||
|  | 		// TODO Write build log to file or database | ||||||
|  | 		outFile, output, err = build(tmp, ka, ki, dockerTimeout) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(output) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		build_ok = true | ||||||
|  | 	} else { | ||||||
|  | 		outFile = binaryPath | ||||||
|  | 		build_ok = true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = cleanDmesg(q) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if testPath == "" { | ||||||
|  | 		testPath = outFile + "_test" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	remoteTest := fmt.Sprintf("/tmp/test_%d", rand.Int()) | ||||||
|  | 	err = q.CopyFile("user", testPath, remoteTest) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Println("copy file err", err) | ||||||
|  | 		// we should not exit because of testing 'insmod' part | ||||||
|  | 		// for kernel module | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = q.Command("root", "chmod +x "+remoteTest) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ka.Type == KernelModule { | ||||||
|  | 		// TODO Write insmod log to file or database | ||||||
|  | 		output, err := q.CopyAndInsmod(outFile) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(output, err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		run_ok = true | ||||||
|  |  | ||||||
|  | 		// TODO Write test results to file or database | ||||||
|  | 		output, err = testKernelModule(q, ka, remoteTest) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(output, err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		test_ok = true | ||||||
|  | 	} else if ka.Type == KernelExploit { | ||||||
|  | 		remoteExploit := fmt.Sprintf("/tmp/exploit_%d", rand.Int()) | ||||||
|  | 		err = q.CopyFile("user", outFile, remoteExploit) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// TODO Write test results to file or database | ||||||
|  | 		output, err = testKernelExploit(q, ka, remoteTest, remoteExploit) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Println(output) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		run_ok = true // does not really used | ||||||
|  | 		test_ok = true | ||||||
|  | 	} else { | ||||||
|  | 		err = errors.New("Unsupported artifact type") | ||||||
|  | 	} | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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, binaryPath, testPath string, | ||||||
|  | 	qemuTimeout, dockerTimeout time.Duration) (err error) { | ||||||
|  |  | ||||||
|  | 	found := false | ||||||
|  |  | ||||||
|  | 	swg := sizedwaitgroup.New(runtime.NumCPU()) | ||||||
|  | 	for _, kernel := range kcfg.Kernels { | ||||||
|  | 		var supported bool | ||||||
|  | 		supported, err = ka.Supported(kernel) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if supported { | ||||||
|  | 			found = true | ||||||
|  | 			swg.Add() | ||||||
|  | 			go whatever(&swg, ka, kernel, binaryPath, testPath, | ||||||
|  | 				qemuTimeout, dockerTimeout) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	swg.Wait() | ||||||
|  |  | ||||||
|  | 	if !found { | ||||||
|  | 		err = errors.New("No supported kernels found") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func exists(path string) bool { | ||||||
|  | 	if _, err := os.Stat(path); err != nil { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func pewHandler(kcfg kernelConfig, workPath, ovrrdKrnl, binary, test string, | ||||||
|  | 	guess bool, qemuTimeout, dockerTimeout time.Duration) (err error) { | ||||||
|  |  | ||||||
|  | 	ka, err := readArtifactConfig(workPath + "/.out-of-tree.toml") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ka.SourcePath == "" { | ||||||
|  | 		ka.SourcePath = workPath | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if ovrrdKrnl != "" { | ||||||
|  | 		parts := strings.Split(ovrrdKrnl, ":") | ||||||
|  | 		if len(parts) != 2 { | ||||||
|  | 			return errors.New("Kernel is not 'distroType:regex'") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		var dt distroType | ||||||
|  | 		dt, err = newDistroType(parts[0]) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		km := kernelMask{DistroType: dt, ReleaseMask: parts[1]} | ||||||
|  | 		ka.SupportedKernels = []kernelMask{km} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if guess { | ||||||
|  | 		ka.SupportedKernels = []kernelMask{} | ||||||
|  | 		for _, dType := range distroTypeStrings { | ||||||
|  | 			var dt distroType | ||||||
|  | 			dt, err = newDistroType(dType) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			km := kernelMask{DistroType: dt, ReleaseMask: ".*"} | ||||||
|  | 			ka.SupportedKernels = append(ka.SupportedKernels, km) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = performCI(ka, kcfg, binary, test, qemuTimeout, dockerTimeout) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user