blob: b3d694b91202b5d3c37ccd1eab26b8442d14e020 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package security
import (
"compress/gzip"
"context"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"chromiumos/tast/common/testexec"
"chromiumos/tast/local/sysutil"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: ModuleLocking,
Desc: "Checks that kernel modules can't be loaded from outside the root filesystem",
Contacts: []string{
"jorgelo@chromium.org", // Security team
"chromeos-security@google.com",
},
Attr: []string{"group:mainline"},
})
}
func ModuleLocking(ctx context.Context, s *testing.State) {
const (
sysctl = "/proc/sys/kernel/chromiumos/module_locking"
module = "test_module" // installed in test images
moduleFile = "kernel/lib/test_module.ko" // standard upstream location
altModuleFile = "kernel/kernel/test_module.ko" // TODO(crbug.com/908226): remove
)
s.Log("Checking ", sysctl)
if b, err := ioutil.ReadFile(sysctl); err != nil && !os.IsNotExist(err) {
s.Fatalf("Failed to read %s: %v", sysctl, err)
} else if err == nil && string(b) != "1\n" {
s.Fatalf("%v contains %q; want 1", sysctl, string(b))
}
u, err := sysutil.Uname()
if err != nil {
s.Fatal("Failed to get kernel release: ", err)
}
moduleDir := filepath.Join("/lib/modules", u.Release)
var modulePath string
for _, fn := range []string{moduleFile, altModuleFile} {
p := filepath.Join(moduleDir, fn)
if _, err := os.Stat(p); err == nil {
modulePath = p
break
}
}
if modulePath == "" {
s.Fatalf("Failed to find %q module in %s: %v", module, moduleDir, err)
}
s.Log("Using ", modulePath)
// Runs the supplied command. An test error is reported if the result doesn't match wantSuccess.
run := func(wantSuccess bool, name string, args ...string) {
cmd := testexec.CommandContext(ctx, name, args...)
if err := cmd.Run(); err != nil && wantSuccess {
s.Errorf("%q failed: %v", strings.Join(cmd.Args, " "), err)
cmd.DumpLog(ctx)
} else if err == nil && !wantSuccess {
s.Errorf("%q unexpectedly succeeded", strings.Join(cmd.Args, " "))
}
}
unloadModule(ctx, s, module)
s.Log("Attempting to modprobe ", module)
run(true, "modprobe", module)
unloadModule(ctx, s, module)
s.Log("Attempting to insmod ", modulePath)
run(true, "insmod", modulePath)
unloadModule(ctx, s, module)
td, err := ioutil.TempDir("", "security.ModuleLocking.")
if err != nil {
s.Fatal("Failed to create temp dir: ", err)
}
defer os.RemoveAll(td)
tmpPath := filepath.Join(td, module+".ko")
copyModule(s, modulePath, tmpPath, false /* gzip */)
s.Log("Attempting to insmod ", tmpPath)
run(false, "insmod", tmpPath)
unloadModule(ctx, s, module)
tmpGzPath := filepath.Join(td, module+".ko.gz")
copyModule(s, modulePath, tmpGzPath, true /* gzip */)
s.Logf("Attempting to insmod %s to trigger old blob-style kernel syscall", tmpGzPath)
run(false, "insmod", tmpGzPath)
unloadModule(ctx, s, module)
// Guard against a regression of http://b/21762937, where a bind unmount would
// incorrectly trigger protections against unmounts of pinned filesystems.
s.Log("Bind-mounting/unmounting and attempting to modprobe ", module)
mountPoint := filepath.Join(td, "mount")
if err := os.Mkdir(mountPoint, 0755); err != nil {
s.Fatalf("Failed to create %v: %v", mountPoint, err)
}
run(true, "mount", "-o", "bind", "/", mountPoint)
run(true, "umount", mountPoint)
run(true, "modprobe", module)
unloadModule(ctx, s, module)
}
// moduleLoaded returns true if the named kernel module (e.g. "test_module") is currently loaded.
// Errors cause a fatal test error to be reported via s.
func moduleLoaded(s *testing.State, module string) bool {
const modulesPath = "/proc/modules"
b, err := ioutil.ReadFile(modulesPath)
if err != nil {
s.Fatalf("Failed to read %s: %v", modulesPath, err)
}
for _, line := range strings.Split(string(b), "\n") {
if parts := strings.Fields(line); len(parts) > 0 && parts[0] == module {
return true
}
}
return false
}
// unloadModule unloads the named kernel module if it's currently loaded.
// This function is a no-op if the module isn't already loaded.
// Errors cause a fatal test error to be reported via s.
func unloadModule(ctx context.Context, s *testing.State, module string) {
if !moduleLoaded(s, module) {
return
}
cmd := testexec.CommandContext(ctx, "rmmod", module)
if err := cmd.Run(); err != nil {
defer cmd.DumpLog(ctx)
s.Fatalf("Failed to run %q: %v", strings.Join(cmd.Args, " "), err)
}
}
// copyModule copies the kernel module file at srcPath to dstPath.
// If useGzip is true, the file is gzip-compressed as it is copied.
// Errors cause a fatal test error to be reported via s.
func copyModule(s *testing.State, srcPath, dstPath string, useGzip bool) {
src, err := os.Open(srcPath)
if err != nil {
s.Fatalf("Failed to open %v: %v", srcPath, err)
}
defer src.Close()
var dst io.WriteCloser
if dst, err = os.Create(dstPath); err != nil {
s.Fatalf("Failed to create %v: %v", dstPath, err)
}
if useGzip {
// Wrap the dest file and check that the gzip.Writer closes successfully instead.
defer dst.Close()
dst = gzip.NewWriter(dst)
}
if _, err := io.Copy(dst, src); err != nil {
dst.Close()
s.Fatalf("Failed to copy from %v to %v: %v", srcPath, dstPath, err)
}
if err := dst.Close(); err != nil {
s.Fatalf("Failed to close %v: %v", dstPath, err)
}
}