blob: fb95118f92a38579caae3c2b17bf431c1d28d830 [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 (
"bufio"
"context"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"chromiumos/tast/local/upstart"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: ASLR,
Desc: "Verifies that address space is randomized between runs",
Contacts: []string{
"jorgelo@chromium.org", // Security team
"ejcaruso@chromium.org", // Tast port author
"chromeos-security@google.com",
},
SoftwareDeps: []string{"aslr"},
Attr: []string{"group:mainline"},
})
}
func ASLR(ctx context.Context, s *testing.State) {
type deviceNumber struct {
major uint64
minor uint64
}
type addressMapping struct {
start uintptr
end uintptr
prot int
shared bool
offset uint64
device deviceNumber
inode uint64
name string
}
type addressMap []addressMapping
parseNum := func(str string, base int) uint64 {
parsed, err := strconv.ParseUint(str, base, 64)
if err != nil {
// Fataling here should be fine since we already do validation
// when we match on the regex below.
s.Fatalf("Failed to parse %v as base %v: %v", str, base, err)
}
return parsed
}
parseAddressMap := func(reader io.Reader) addressMap {
mapping := `([0-9a-f]+)-([0-9a-f]+) +` + // start (1) and end (2)
`(r|-)(w|-)(x|-)(s|p) +` + // protections (3-5) and sharing (6)
`([0-9a-f]+) +` + // offset (7)
`([0-9a-f]+):([0-9a-f]+) +` + // device major (8) and minor (9)
`(\d+) *` + // inode number (10)
`(.*)` // name (11)
mappingMatcher := regexp.MustCompile(mapping)
var am addressMap
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
group := mappingMatcher.FindStringSubmatch(line)
if group == nil {
s.Fatalf("Address map file line failed to parse: %q", line)
}
start := uintptr(parseNum(group[1], 16))
end := uintptr(parseNum(group[2], 16))
prot := 0
if group[3][0] == 'r' {
prot |= syscall.PROT_READ
}
if group[4][0] == 'w' {
prot |= syscall.PROT_WRITE
}
if group[5][0] == 'x' {
prot |= syscall.PROT_EXEC
}
shared := (group[6][0] == 's')
offset := parseNum(group[7], 16)
major := parseNum(group[8], 16)
minor := parseNum(group[9], 16)
inode := parseNum(group[10], 10)
am = append(am, addressMapping{
start, end, prot, shared, offset,
deviceNumber{major, minor}, inode, group[11],
})
}
if err := scanner.Err(); err != nil {
s.Fatal("Failed to read map file: ", err)
}
return am
}
dumpMap := func(am addressMap, filename, header string) error {
dumpFile, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer dumpFile.Close()
if _, err = dumpFile.WriteString(header); err != nil {
return err
}
for _, mapping := range am {
prot := ""
add := func(v bool, t, f string) {
if v {
prot += t
} else {
prot += f
}
}
add(mapping.prot&syscall.PROT_READ != 0, "r", "-")
add(mapping.prot&syscall.PROT_WRITE != 0, "w", "-")
add(mapping.prot&syscall.PROT_EXEC != 0, "x", "-")
add(mapping.shared, "s", "p")
line := fmt.Sprintf("%016x-%016x %v %8x %02x:%02x %-10d %v\n",
mapping.start, mapping.end, prot, mapping.offset,
mapping.device.major, mapping.device.minor, mapping.inode,
mapping.name)
if _, err = dumpFile.WriteString(line); err != nil {
return err
}
}
return nil
}
// Restarts job and returns memory mapping names to start addresses.
getNewJobMap := func(job string) map[string]uintptr {
if err := upstart.RestartJob(ctx, job); err != nil {
s.Fatalf("Job %v did not restart: %v", job, err)
}
_, _, pid, err := upstart.JobStatus(ctx, job)
if err != nil {
s.Fatalf("Could not get status for job %v: %v", job, err)
}
mapFile, err := os.Open(fmt.Sprintf("/proc/%v/maps", pid))
if err != nil {
s.Fatalf("Could not open address map for job %v: %v", job, err)
}
defer mapFile.Close()
am := parseAddressMap(mapFile)
// dump maps to text file for future inspection if necessary
newMapPath := filepath.Join(s.OutDir(), fmt.Sprintf("%v.txt", job))
header := fmt.Sprintf("\n=== pid %v ===\n\n", pid)
dumpMap(am, newMapPath, header)
starts := make(map[string]uintptr)
for _, mapping := range am {
// There will probably be multiple mappings for a lot of the files mapped into
// memory. To deal with this, we only check the mappings with offset 0.
if (mapping.name != "[heap]" && mapping.name != "[stack]" &&
mapping.inode == 0) || mapping.offset != 0 {
// This isn't a mapped file or a private mapping we care about. Skip it.
continue
}
starts[mapping.name] = mapping.start
}
return starts
}
const iterations = 5
testRandomization := func(job string) {
s.Log("Testing job ", job)
// allStarts is a map of vmarea name to start addresses and the number of times each
// start address has been seen across all iterations.
type addrCounts map[uintptr]int
allStarts := make(map[string]addrCounts)
for name, start := range getNewJobMap(job) {
startSet := make(map[uintptr]int)
startSet[start] = 1
allStarts[name] = startSet
}
// Collect start addresses for vm areas over several job spawns.
for i := 0; i < iterations; i++ {
newStarts := getNewJobMap(job)
for name := range allStarts {
if otherStart, present := newStarts[name]; present {
allStarts[name][otherStart]++
}
}
}
// Check that at least one address was different for each vm area.
for name, starts := range allStarts {
if len(starts) == 1 {
for start, occurrences := range starts {
if occurrences == 1 {
// This isn't actually a duplicate address; it only showed up once.
continue
}
s.Errorf("Mapping for %v always occurred at %#x", name, start)
}
}
}
}
for _, job := range []string{"ui", "debugd", "update-engine"} {
testRandomization(job)
}
}