Merge "Revert "Revert "Add path interposer"""

This commit is contained in:
Dan Willemsen 2018-05-25 22:57:59 +00:00 committed by Gerrit Code Review
commit bfb2b7e877
12 changed files with 1165 additions and 2 deletions

View File

@ -0,0 +1,20 @@
// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
blueprint_go_binary {
name: "path_interposer",
deps: ["soong-ui-build-paths"],
srcs: ["main.go"],
testSrcs: ["main_test.go"],
}

247
cmd/path_interposer/main.go Normal file
View File

@ -0,0 +1,247 @@
// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"syscall"
"android/soong/ui/build/paths"
)
func main() {
interposer, err := os.Executable()
if err != nil {
fmt.Fprintln(os.Stderr, "Unable to locate interposer executable:", err)
os.Exit(1)
}
if fi, err := os.Lstat(interposer); err == nil {
if fi.Mode()&os.ModeSymlink != 0 {
link, err := os.Readlink(interposer)
if err != nil {
fmt.Fprintln(os.Stderr, "Unable to read link to interposer executable:", err)
os.Exit(1)
}
if filepath.IsAbs(link) {
interposer = link
} else {
interposer = filepath.Join(filepath.Dir(interposer), link)
}
}
} else {
fmt.Fprintln(os.Stderr, "Unable to stat interposer executable:", err)
os.Exit(1)
}
disableError := false
if e, ok := os.LookupEnv("TEMPORARY_DISABLE_PATH_RESTRICTIONS"); ok {
disableError = e == "1" || e == "y" || e == "yes" || e == "on" || e == "true"
}
exitCode, err := Main(os.Stdout, os.Stderr, interposer, os.Args, mainOpts{
disableError: disableError,
sendLog: paths.SendLog,
config: paths.GetConfig,
lookupParents: lookupParents,
})
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
os.Exit(exitCode)
}
var usage = fmt.Errorf(`To use the PATH interposer:
* Write the original PATH variable to <interposer>_origpath
* Set up a directory of symlinks to the PATH interposer, and use that in PATH
If a tool isn't in the allowed list, a log will be posted to the unix domain
socket at <interposer>_log.`)
type mainOpts struct {
disableError bool
sendLog func(logSocket string, entry *paths.LogEntry, done chan interface{})
config func(name string) paths.PathConfig
lookupParents func() []paths.LogProcess
}
func Main(stdout, stderr io.Writer, interposer string, args []string, opts mainOpts) (int, error) {
base := filepath.Base(args[0])
origPathFile := interposer + "_origpath"
if base == filepath.Base(interposer) {
return 1, usage
}
origPath, err := ioutil.ReadFile(origPathFile)
if err != nil {
if os.IsNotExist(err) {
return 1, usage
} else {
return 1, fmt.Errorf("Failed to read original PATH: %v", err)
}
}
cmd := &exec.Cmd{
Args: args,
Env: os.Environ(),
Stdin: os.Stdin,
Stdout: stdout,
Stderr: stderr,
}
if err := os.Setenv("PATH", string(origPath)); err != nil {
return 1, fmt.Errorf("Failed to set PATH env: %v", err)
}
if config := opts.config(base); config.Log || config.Error {
var procs []paths.LogProcess
if opts.lookupParents != nil {
procs = opts.lookupParents()
}
if opts.sendLog != nil {
waitForLog := make(chan interface{})
opts.sendLog(interposer+"_log", &paths.LogEntry{
Basename: base,
Args: args,
Parents: procs,
}, waitForLog)
defer func() { <-waitForLog }()
}
if config.Error && !opts.disableError {
return 1, fmt.Errorf("%q is not allowed to be used. See https://android.googlesource.com/platform/build/+/master/Changes.md#PATH_Tools for more information.", base)
}
}
cmd.Path, err = exec.LookPath(base)
if err != nil {
return 1, err
}
if err = cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
if status.Exited() {
return status.ExitStatus(), nil
} else if status.Signaled() {
exitCode := 128 + int(status.Signal())
return exitCode, nil
} else {
return 1, exitErr
}
} else {
return 1, nil
}
}
}
return 0, nil
}
type procEntry struct {
Pid int
Ppid int
Command string
}
func readProcs() map[int]procEntry {
cmd := exec.Command("ps", "-o", "pid,ppid,command")
data, err := cmd.Output()
if err != nil {
return nil
}
return parseProcs(data)
}
func parseProcs(data []byte) map[int]procEntry {
lines := bytes.Split(data, []byte("\n"))
if len(lines) < 2 {
return nil
}
// Remove the header
lines = lines[1:]
ret := make(map[int]procEntry, len(lines))
for _, line := range lines {
fields := bytes.SplitN(line, []byte(" "), 2)
if len(fields) != 2 {
continue
}
pid, err := strconv.Atoi(string(fields[0]))
if err != nil {
continue
}
line = bytes.TrimLeft(fields[1], " ")
fields = bytes.SplitN(line, []byte(" "), 2)
if len(fields) != 2 {
continue
}
ppid, err := strconv.Atoi(string(fields[0]))
if err != nil {
continue
}
ret[pid] = procEntry{
Pid: pid,
Ppid: ppid,
Command: string(bytes.TrimLeft(fields[1], " ")),
}
}
return ret
}
func lookupParents() []paths.LogProcess {
procs := readProcs()
if procs == nil {
return nil
}
list := []paths.LogProcess{}
pid := os.Getpid()
for {
entry, ok := procs[pid]
if !ok {
break
}
list = append([]paths.LogProcess{
{
Pid: pid,
Command: entry.Command,
},
}, list...)
pid = entry.Ppid
}
return list
}

View File

@ -0,0 +1,196 @@
// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"android/soong/ui/build/paths"
)
var tmpDir string
var origPATH string
func TestMain(m *testing.M) {
os.Exit(func() int {
var err error
tmpDir, err = ioutil.TempDir("", "interposer_test")
if err != nil {
panic(err)
}
defer os.RemoveAll(tmpDir)
origPATH = os.Getenv("PATH")
err = os.Setenv("PATH", "")
if err != nil {
panic(err)
}
return m.Run()
}())
}
func setup(t *testing.T) string {
f, err := ioutil.TempFile(tmpDir, "interposer")
if err != nil {
t.Fatal(err)
}
defer f.Close()
err = ioutil.WriteFile(f.Name()+"_origpath", []byte(origPATH), 0666)
if err != nil {
t.Fatal(err)
}
return f.Name()
}
func TestInterposer(t *testing.T) {
interposer := setup(t)
logConfig := func(name string) paths.PathConfig {
if name == "true" {
return paths.PathConfig{
Log: false,
Error: false,
}
} else if name == "path_interposer_test_not_allowed" {
return paths.PathConfig{
Log: false,
Error: true,
}
}
return paths.PathConfig{
Log: true,
Error: false,
}
}
testCases := []struct {
name string
args []string
exitCode int
err error
logEntry string
}{
{
name: "direct call",
args: []string{interposer},
exitCode: 1,
err: usage,
},
{
name: "relative call",
args: []string{filepath.Base(interposer)},
exitCode: 1,
err: usage,
},
{
name: "true",
args: []string{"/my/path/true"},
},
{
name: "relative true",
args: []string{"true"},
},
{
name: "exit code",
args: []string{"bash", "-c", "exit 42"},
exitCode: 42,
logEntry: "bash",
},
{
name: "signal",
args: []string{"bash", "-c", "kill -9 $$"},
exitCode: 137,
logEntry: "bash",
},
{
name: "does not exist",
args: []string{"path_interposer_test_does_not_exist"},
exitCode: 1,
err: fmt.Errorf(`exec: "path_interposer_test_does_not_exist": executable file not found in $PATH`),
logEntry: "path_interposer_test_does_not_exist",
},
{
name: "not allowed",
args: []string{"path_interposer_test_not_allowed"},
exitCode: 1,
err: fmt.Errorf(`"path_interposer_test_not_allowed" is not allowed to be used. See https://android.googlesource.com/platform/build/+/master/Changes.md#PATH_Tools for more information.`),
logEntry: "path_interposer_test_not_allowed",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
logged := false
logFunc := func(logSocket string, entry *paths.LogEntry, done chan interface{}) {
defer close(done)
logged = true
if entry.Basename != testCase.logEntry {
t.Errorf("unexpected log entry:\nwant: %q\n got: %q", testCase.logEntry, entry.Basename)
}
}
exitCode, err := Main(ioutil.Discard, ioutil.Discard, interposer, testCase.args, mainOpts{
sendLog: logFunc,
config: logConfig,
})
errstr := func(err error) string {
if err == nil {
return ""
}
return err.Error()
}
if errstr(testCase.err) != errstr(err) {
t.Errorf("unexpected error:\nwant: %v\n got: %v", testCase.err, err)
}
if testCase.exitCode != exitCode {
t.Errorf("expected exit code %d, got %d", testCase.exitCode, exitCode)
}
if !logged && testCase.logEntry != "" {
t.Errorf("no log entry, but expected %q", testCase.logEntry)
}
})
}
}
func TestMissingPath(t *testing.T) {
interposer := setup(t)
err := os.Remove(interposer + "_origpath")
if err != nil {
t.Fatalf("Failed to remove:", err)
}
exitCode, err := Main(ioutil.Discard, ioutil.Discard, interposer, []string{"true"}, mainOpts{})
if err != usage {
t.Errorf("Unexpected error:\n got: %v\nwant: %v", err, usage)
}
if exitCode != 1 {
t.Errorf("expected exit code %d, got %d", 1, exitCode)
}
}

View File

@ -18,8 +18,6 @@ def SearchPathEnv(name):
for directory in search_path:
if directory == '': continue
path = os.path.join(directory, name)
if os.path.islink(path):
path = os.path.realpath(path)
# Check if path is actual executable file.
if os.path.isfile(path) and os.access(path, os.X_OK):
return path

View File

@ -12,10 +12,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
bootstrap_go_package {
name: "soong-ui-build-paths",
pkgPath: "android/soong/ui/build/paths",
srcs: [
"paths/config.go",
"paths/logs.go",
],
testSrcs: [
"paths/logs_test.go",
],
}
bootstrap_go_package {
name: "soong-ui-build",
pkgPath: "android/soong/ui/build",
deps: [
"soong-ui-build-paths",
"soong-ui-logger",
"soong-ui-tracer",
"soong-shared",
@ -33,6 +46,7 @@ bootstrap_go_package {
"finder.go",
"kati.go",
"ninja.go",
"path.go",
"proc_sync.go",
"signal.go",
"soong.go",

View File

@ -140,6 +140,8 @@ func Build(ctx Context, config Config, what int) {
ensureEmptyDirectoriesExist(ctx, config.TempDir())
SetupPath(ctx, config)
if what&BuildProductConfig != 0 {
// Run make for product config
runMakeProductConfig(ctx, config)

View File

@ -51,6 +51,8 @@ type configImpl struct {
targetDeviceDir string
brokenDupRules bool
pathReplaced bool
}
const srcDirFileCheck = "build/soong/root.bp"

149
ui/build/path.go Normal file
View File

@ -0,0 +1,149 @@
// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package build
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/google/blueprint/microfactory"
"android/soong/ui/build/paths"
)
func parsePathDir(dir string) []string {
f, err := os.Open(dir)
if err != nil {
return nil
}
defer f.Close()
if s, err := f.Stat(); err != nil || !s.IsDir() {
return nil
}
infos, err := f.Readdir(-1)
if err != nil {
return nil
}
ret := make([]string, 0, len(infos))
for _, info := range infos {
if m := info.Mode(); !m.IsDir() && m&0111 != 0 {
ret = append(ret, info.Name())
}
}
return ret
}
func SetupPath(ctx Context, config Config) {
if config.pathReplaced {
return
}
ctx.BeginTrace("path")
defer ctx.EndTrace()
origPath, _ := config.Environment().Get("PATH")
myPath := filepath.Join(config.OutDir(), ".path")
interposer := myPath + "_interposer"
var cfg microfactory.Config
cfg.Map("android/soong", "build/soong")
cfg.TrimPath, _ = filepath.Abs(".")
if _, err := microfactory.Build(&cfg, interposer, "android/soong/cmd/path_interposer"); err != nil {
ctx.Fatalln("Failed to build path interposer:", err)
}
if err := ioutil.WriteFile(interposer+"_origpath", []byte(origPath), 0777); err != nil {
ctx.Fatalln("Failed to write original path:", err)
}
entries, err := paths.LogListener(ctx.Context, interposer+"_log")
if err != nil {
ctx.Fatalln("Failed to listen for path logs:", err)
}
go func() {
for log := range entries {
curPid := os.Getpid()
for i, proc := range log.Parents {
if proc.Pid == curPid {
log.Parents = log.Parents[i:]
break
}
}
procPrints := []string{
"See https://android.googlesource.com/platform/build/+/master/Changes.md#PATH_Tools for more information.",
}
if len(log.Parents) > 0 {
procPrints = append(procPrints, "Process tree:")
for i, proc := range log.Parents {
procPrints = append(procPrints, fmt.Sprintf("%s→ %s", strings.Repeat(" ", i), proc.Command))
}
}
config := paths.GetConfig(log.Basename)
if config.Error {
ctx.Printf("Disallowed PATH tool %q used: %#v", log.Basename, log.Args)
for _, line := range procPrints {
ctx.Println(line)
}
} else {
ctx.Verbosef("Unknown PATH tool %q used: %#v", log.Basename, log.Args)
for _, line := range procPrints {
ctx.Verboseln(line)
}
}
}
}()
ensureEmptyDirectoriesExist(ctx, myPath)
var execs []string
for _, pathEntry := range filepath.SplitList(origPath) {
if pathEntry == "" {
// Ignore the current directory
continue
}
// TODO(dwillemsen): remove path entries under TOP? or anything
// that looks like an android source dir? They won't exist on
// the build servers, since they're added by envsetup.sh.
// (Except for the JDK, which is configured in ui/build/config.go)
execs = append(execs, parsePathDir(pathEntry)...)
}
allowAllSymlinks := config.Environment().IsEnvTrue("TEMPORARY_DISABLE_PATH_RESTRICTIONS")
for _, name := range execs {
if !paths.GetConfig(name).Symlink && !allowAllSymlinks {
continue
}
err := os.Symlink("../.path_interposer", filepath.Join(myPath, name))
// Intentionally ignore existing files -- that means that we
// just created it, and the first one should win.
if err != nil && !os.IsExist(err) {
ctx.Fatalln("Failed to create symlink:", err)
}
}
myPath, _ = filepath.Abs(myPath)
config.Environment().Set("PATH", myPath)
config.pathReplaced = true
}

160
ui/build/paths/config.go Normal file
View File

@ -0,0 +1,160 @@
// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package paths
import "runtime"
type PathConfig struct {
// Whether to create the symlink in the new PATH for this tool.
Symlink bool
// Whether to log about usages of this tool to the soong.log
Log bool
// Whether to exit with an error instead of invoking the underlying tool.
Error bool
}
var Allowed = PathConfig{
Symlink: true,
Log: false,
Error: false,
}
var Forbidden = PathConfig{
Symlink: false,
Log: true,
Error: true,
}
// The configuration used if the tool is not listed in the config below.
// Currently this will create the symlink, but log a warning. In the future,
// I expect this to move closer to Forbidden.
var Missing = PathConfig{
Symlink: true,
Log: true,
Error: false,
}
func GetConfig(name string) PathConfig {
if config, ok := Configuration[name]; ok {
return config
}
return Missing
}
var Configuration = map[string]PathConfig{
"awk": Allowed,
"basename": Allowed,
"bash": Allowed,
"bzip2": Allowed,
"cat": Allowed,
"chmod": Allowed,
"cmp": Allowed,
"comm": Allowed,
"cp": Allowed,
"cut": Allowed,
"date": Allowed,
"dd": Allowed,
"diff": Allowed,
"dirname": Allowed,
"echo": Allowed,
"egrep": Allowed,
"env": Allowed,
"expr": Allowed,
"find": Allowed,
"getconf": Allowed,
"getopt": Allowed,
"git": Allowed,
"grep": Allowed,
"gzip": Allowed,
"head": Allowed,
"hexdump": Allowed,
"hostname": Allowed,
"jar": Allowed,
"java": Allowed,
"javap": Allowed,
"ln": Allowed,
"ls": Allowed,
"m4": Allowed,
"make": Allowed,
"md5sum": Allowed,
"mkdir": Allowed,
"mktemp": Allowed,
"mv": Allowed,
"openssl": Allowed,
"patch": Allowed,
"perl": Allowed,
"pstree": Allowed,
"python": Allowed,
"python2.7": Allowed,
"python3": Allowed,
"readlink": Allowed,
"realpath": Allowed,
"rm": Allowed,
"rsync": Allowed,
"runalarm": Allowed,
"sed": Allowed,
"setsid": Allowed,
"sh": Allowed,
"sha256sum": Allowed,
"sha512sum": Allowed,
"sort": Allowed,
"stat": Allowed,
"sum": Allowed,
"tar": Allowed,
"tail": Allowed,
"touch": Allowed,
"tr": Allowed,
"true": Allowed,
"uname": Allowed,
"uniq": Allowed,
"unzip": Allowed,
"wc": Allowed,
"which": Allowed,
"whoami": Allowed,
"xargs": Allowed,
"xmllint": Allowed,
"xz": Allowed,
"zip": Allowed,
"zipinfo": Allowed,
// Host toolchain is removed. In-tree toolchain should be used instead.
// GCC also can't find cc1 with this implementation.
"ar": Forbidden,
"as": Forbidden,
"cc": Forbidden,
"clang": Forbidden,
"clang++": Forbidden,
"gcc": Forbidden,
"g++": Forbidden,
"ld": Forbidden,
"ld.bfd": Forbidden,
"ld.gold": Forbidden,
"pkg-config": Forbidden,
// We've got prebuilts of these
//"dtc": Forbidden,
//"lz4": Forbidden,
//"lz4c": Forbidden,
}
func init() {
if runtime.GOOS == "darwin" {
Configuration["md5"] = Allowed
Configuration["sw_vers"] = Allowed
Configuration["xcrun"] = Allowed
}
}

202
ui/build/paths/logs.go Normal file
View File

@ -0,0 +1,202 @@
// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package paths
import (
"context"
"encoding/gob"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"runtime"
"syscall"
"time"
)
type LogProcess struct {
Pid int
Command string
}
type LogEntry struct {
Basename string
Args []string
Parents []LogProcess
}
const timeoutDuration = time.Duration(100) * time.Millisecond
type socketAddrFunc func(string) (string, func(), error)
func procFallback(name string) (string, func(), error) {
d, err := os.Open(filepath.Dir(name))
if err != nil {
return "", nil, err
}
return fmt.Sprintf("/proc/self/fd/%d/%s", d.Fd(), filepath.Base(name)), func() {
d.Close()
}, nil
}
func tmpFallback(name string) (addr string, cleanup func(), err error) {
d, err := ioutil.TempDir("/tmp", "log_sock")
if err != nil {
cleanup = func() {}
return
}
cleanup = func() {
os.RemoveAll(d)
}
dir := filepath.Dir(name)
absDir, err := filepath.Abs(dir)
if err != nil {
return
}
err = os.Symlink(absDir, filepath.Join(d, "d"))
if err != nil {
return
}
addr = filepath.Join(d, "d", filepath.Base(name))
return
}
func getSocketAddr(name string) (string, func(), error) {
maxNameLen := len(syscall.RawSockaddrUnix{}.Path)
if len(name) < maxNameLen {
return name, func() {}, nil
}
if runtime.GOOS == "linux" {
addr, cleanup, err := procFallback(name)
if err == nil {
if len(addr) < maxNameLen {
return addr, cleanup, nil
}
}
cleanup()
}
addr, cleanup, err := tmpFallback(name)
if err == nil {
if len(addr) < maxNameLen {
return addr, cleanup, nil
}
}
cleanup()
return name, func() {}, fmt.Errorf("Path to socket is still over size limit, fallbacks failed.")
}
func dial(name string, lookup socketAddrFunc) (net.Conn, error) {
socket, cleanup, err := lookup(name)
defer cleanup()
if err != nil {
return nil, err
}
dialer := &net.Dialer{
Timeout: timeoutDuration,
}
return dialer.Dial("unix", socket)
}
func listen(name string, lookup socketAddrFunc) (net.Listener, error) {
socket, cleanup, err := lookup(name)
defer cleanup()
if err != nil {
return nil, err
}
return net.Listen("unix", socket)
}
func SendLog(logSocket string, entry *LogEntry, done chan interface{}) {
sendLog(logSocket, getSocketAddr, entry, done)
}
func sendLog(logSocket string, lookup socketAddrFunc, entry *LogEntry, done chan interface{}) {
defer close(done)
conn, err := dial(logSocket, lookup)
if err != nil {
return
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(timeoutDuration))
enc := gob.NewEncoder(conn)
enc.Encode(entry)
}
func LogListener(ctx context.Context, logSocket string) (chan *LogEntry, error) {
return logListener(ctx, logSocket, getSocketAddr)
}
func logListener(ctx context.Context, logSocket string, lookup socketAddrFunc) (chan *LogEntry, error) {
ret := make(chan *LogEntry, 5)
if err := os.Remove(logSocket); err != nil && !os.IsNotExist(err) {
return nil, err
}
ln, err := listen(logSocket, lookup)
if err != nil {
return nil, err
}
go func() {
for {
select {
case <-ctx.Done():
ln.Close()
}
}
}()
go func() {
defer close(ret)
for {
conn, err := ln.Accept()
if err != nil {
ln.Close()
break
}
conn.SetDeadline(time.Now().Add(timeoutDuration))
go func() {
defer conn.Close()
dec := gob.NewDecoder(conn)
entry := &LogEntry{}
if err := dec.Decode(entry); err != nil {
return
}
ret <- entry
}()
}
}()
return ret, nil
}

167
ui/build/paths/logs_test.go Normal file
View File

@ -0,0 +1,167 @@
// Copyright 2018 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package paths
import (
"context"
"io/ioutil"
"net"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
)
func TestSendLog(t *testing.T) {
t.Run("Short name", func(t *testing.T) {
d, err := ioutil.TempDir("", "s")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(d)
f := filepath.Join(d, "s")
testSendLog(t, f, getSocketAddr)
})
testLongName := func(t *testing.T, lookup socketAddrFunc) {
d, err := ioutil.TempDir("", strings.Repeat("s", 150))
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(d)
f := filepath.Join(d, strings.Repeat("s", 10))
testSendLog(t, f, lookup)
}
// Using a name longer than the ~100 limit of the underlying calls to bind, etc
t.Run("Long name", func(t *testing.T) {
testLongName(t, getSocketAddr)
})
if runtime.GOOS == "linux" {
t.Run("Long name proc fallback", func(t *testing.T) {
testLongName(t, procFallback)
})
}
t.Run("Long name tmp fallback", func(t *testing.T) {
testLongName(t, tmpFallback)
})
}
func testSendLog(t *testing.T, socket string, lookup socketAddrFunc) {
ctx, _ := context.WithTimeout(context.Background(), 2*timeoutDuration)
recv, err := logListener(ctx, socket, lookup)
if err != nil {
t.Fatal(err)
}
go func() {
for i := 0; i < 10; i++ {
sendLog(socket, lookup, &LogEntry{
Basename: "test",
Args: []string{"foo", "bar"},
}, make(chan interface{}))
}
}()
count := 0
for {
select {
case entry := <-recv:
ref := LogEntry{
Basename: "test",
Args: []string{"foo", "bar"},
}
if !reflect.DeepEqual(ref, *entry) {
t.Fatalf("Bad log entry: %v", entry)
}
count++
if count == 10 {
return
}
case <-ctx.Done():
t.Error("Hit timeout before receiving all logs")
}
}
}
func TestSendLogError(t *testing.T) {
d, err := ioutil.TempDir("", "log_socket")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(d)
t.Run("Missing file", func(t *testing.T) {
start := time.Now()
SendLog(filepath.Join(d, "missing"), &LogEntry{}, make(chan interface{}))
elapsed := time.Since(start)
if elapsed > timeoutDuration {
t.Errorf("Should have been << timeout (%s), but was %s", timeoutDuration, elapsed)
}
})
t.Run("Regular file", func(t *testing.T) {
f := filepath.Join(d, "file")
if fp, err := os.Create(f); err == nil {
fp.Close()
} else {
t.Fatal(err)
}
start := time.Now()
SendLog(f, &LogEntry{}, make(chan interface{}))
elapsed := time.Since(start)
if elapsed > timeoutDuration {
t.Errorf("Should have been << timeout (%s), but was %s", timeoutDuration, elapsed)
}
})
t.Run("Reader not reading", func(t *testing.T) {
f := filepath.Join(d, "sock1")
ln, err := net.Listen("unix", f)
if err != nil {
t.Fatal(err)
}
defer ln.Close()
done := make(chan bool, 1)
go func() {
for i := 0; i < 10; i++ {
SendLog(f, &LogEntry{
// Ensure a relatively large payload
Basename: strings.Repeat(" ", 100000),
}, make(chan interface{}))
}
done <- true
}()
select {
case <-done:
break
case <-time.After(12 * timeoutDuration):
t.Error("Should have finished")
}
})
}

View File

@ -35,6 +35,12 @@
(global-name-regex #"^com\.apple\.distributed_notifications") ; xcodebuild in Soong
)
; Allow suid /bin/ps to function
(allow process-exec (literal "/bin/ps") (with no-sandbox))
; Allow path_interposer unix domain socket without logging
(allow network-outbound (literal (string-append (param "OUT_DIR") "/.path_interposer_log")))
; Allow executing any file
(allow process-exec*)
(allow process-fork)