// Copyright 2017 Google Inc. // // 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 integration import ( "bufio" "encoding/json" "fmt" "io" "io/ioutil" "os" "path/filepath" "reflect" "strings" "sync" "syscall" "testing" "github.com/bazelbuild/sandboxfs/integration/utils" ) // mapping represents a mapping entry in the reconfiguration protocol. type mapping struct { Path string `json:"path"` PathPrefix int `json:"path_prefix"` UnderlyingPath string `json:"underlying_path"` UnderlyingPathPrefix int `json:"underlying_path_prefix"` Writable bool `json:"writable"` } // mapStep represents a map operation in the reconfiguration protocol. type createSandboxRequest struct { ID string `json:"id"` Mappings []mapping `json:"mappings"` Prefixes map[string]string `json:"prefixes"` } // request represents a single reconfiguration request. type request struct { CreateSanbox *createSandboxRequest `json:"CreateSandbox,omitempty"` DestroySandbox *string `json:"DestroySandbox,omitempty"` } // getID returns the sandbox identifier in a request message. func (req request) getID() string { if req.CreateSanbox != nil && req.DestroySandbox != nil { panic("Bad request: contains both create and destroy requests") } else if req.CreateSanbox != nil { return req.CreateSanbox.ID } else { return *req.DestroySandbox } } // response represents the result of a reconfiguration request. type response struct { ID *string `json:"id,omitempty"` Error *string `json:"error,omitempty"` } // makeCreateSandboxRequest is a convenience function to instantiate a single map step. func makeCreateSandboxRequest(id string, mapping1 mapping, mappingN ...mapping) request { return request{ CreateSanbox: &createSandboxRequest{ ID: id, Mappings: append([]mapping{mapping1}, mappingN...), Prefixes: make(map[string]string), }, } } // makeDestroySandboxRequest is a convenience function to instantiate a single unmap step. func makeDestroySandboxRequest(id string) request { return request{ DestroySandbox: &id, } } // tryRawReconfigure pushes a new configuration to the sandboxfs process and waits for // acknowledgement. The reconfiguration request is provided as a string, which may be invalid (to // verify error cases). Returns the error message from the server, which might be nil. func tryRawReconfigure(input io.Writer, output io.Reader, root string, config string) (response, error) { config = strings.Replace(string(config), "%ROOT%", root, -1) + "\n" n, err := io.WriteString(input, config) if err != nil { return response{}, fmt.Errorf("failed to send new configuration to sandboxfs: %v", err) } if n != len(config) { return response{}, fmt.Errorf("failed to send full configuration to sandboxfs: got %d bytes, want %d bytes", n, len(config)) } decoder := json.NewDecoder(output) resp := response{} if err := decoder.Decode(&resp); err != nil { return response{}, fmt.Errorf("failed to read from sandboxfs's output: %v", err) } return resp, nil } // tryReconfigure pushes a new configuration to the sandboxfs process and waits for // acknowledgement. The reconfiguration request is provided as a sequence of objects, which is // assumed to be valid (except for semantical errors). Returns the error message from the server, // which might be nil. func tryReconfigure(input io.Writer, output io.Reader, root string, config request) (response, error) { configBytes, err := json.Marshal(config) if err != nil { panic(fmt.Sprintf("Bad configuration request in test: %v", err)) } return tryRawReconfigure(input, output, root, string(configBytes)) } // reconfigure pushes a new configuration to the sandboxfs process and waits for acknowledgement. func reconfigure(input io.Writer, output io.Reader, root string, requests ...request) error { for _, req := range requests { resp, err := tryReconfigure(input, output, root, req) if err != nil { return err } if resp.ID == nil || *resp.ID != req.getID() { return fmt.Errorf("sandboxfs replied with a bad id: got %v, want %s", resp.ID, req.getID()) } if resp.Error != nil { return fmt.Errorf("sandboxfs did not ack configuration: %s", *resp.Error) } } return nil } // existsViaReaddir checks if a directory entry exists within a directory by scanning the contents // of the directory itself (i.e. via readdir), not by attempting a direct lookup on the entry. func existsViaReaddir(dir string, name string) (bool, error) { dirents, err := ioutil.ReadDir(dir) if err != nil { return false, err } for _, dirent := range dirents { if dirent.Name() == name { return true, nil } } return false, nil } // errorIfNotUnmapped fails the calling test case if the given directory entry, which is expected to // not be mapped any longer, still exists. func errorIfNotUnmapped(t *testing.T, dir string, name string) { t.Helper() // The FUSE library we use from Rust currently lacks support for kernel cache // invalidations. As a result, sandboxfs cannot make unmapped entries disappear // right away (and setting a lower TTL for the file system does not help because // OSXFUSE does nothing with the entries' TTL). We know that this is suboptimal, // but instead of making the tests fail, make sure sandboxfs mostly works by // checking that it really did unmap the entry (which we can confirm by looking // at readdir output). exists, err := existsViaReaddir(dir, name) if err != nil { t.Errorf("Failed to read contents of %s: %v", dir, err) } else if exists { t.Errorf("Unmapped %s is not gone from %s", name, dir) } } func TestReconfiguration_Streams(t *testing.T) { reconfigureAndCheck := func(t *testing.T, state *utils.MountState, input io.Writer, output io.Reader) { utils.MustMkdirAll(t, state.RootPath("a/b"), 0755) config := makeCreateSandboxRequest("sb", mapping{Path: "/ro", UnderlyingPath: "%ROOT%/a", Writable: false}) if err := reconfigure(input, output, state.RootPath(), config); err != nil { t.Fatal(err) } if _, err := os.Lstat(state.MountPath("sb/ro/b")); err != nil { t.Errorf("Cannot stat a/b in mount point; reconfiguration failed? Got %v", err) } } // TODO(jmmv): Consider dropping stdin/stdout support as defaults. This is quite an // artificial construct and makes our testing quite complex. t.Run("Default", func(t *testing.T) { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr) defer stdoutReader.Close() // Just in case the test fails half-way through. defer state.TearDown(t) defer stdoutWriter.Close() // Just in case the test fails half-way through. reconfigureAndCheck(t, state, state.Stdin, stdoutReader) }) t.Run("Explicit", func(t *testing.T) { tempDir, err := ioutil.TempDir("", "test") if err != nil { t.Fatalf("Failed to create temporary directory: %v", err) } defer os.RemoveAll(tempDir) inFifo := filepath.Join(tempDir, "input") if err := syscall.Mkfifo(inFifo, 0600); err != nil { t.Fatalf("Failed to create %s fifo: %v", inFifo, err) } outFifo := filepath.Join(tempDir, "output") if err := syscall.Mkfifo(outFifo, 0600); err != nil { t.Fatalf("Failed to create %s fifo: %v", outFifo, err) } state := utils.MountSetupWithOutputs(t, nil, os.Stderr, "--input="+inFifo, "--output="+outFifo) defer state.TearDown(t) input, err := os.OpenFile(inFifo, os.O_WRONLY, 0) if err != nil { t.Fatalf("Failed to open input fifo for writing: %v", err) } defer input.Close() output, err := os.OpenFile(outFifo, os.O_RDONLY, 0) if err != nil { t.Fatalf("Failed to open output fifo for reading: %v", err) } defer output.Close() reconfigureAndCheck(t, state, input, output) }) } func TestReconfiguration_Steps(t *testing.T) { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr, "--mapping=ro:/:%ROOT%", "--mapping=rw:/initial:%ROOT%/initial") defer stdoutReader.Close() // Just in case the test fails half-way through. defer state.TearDown(t) defer stdoutWriter.Close() // Just in case the test fails half-way through. utils.MustMkdirAll(t, state.RootPath("some/read-only-dir"), 0755) utils.MustMkdirAll(t, state.RootPath("some/read-write-dir"), 0755) config := makeCreateSandboxRequest( "sb", mapping{Path: "/ro", UnderlyingPath: "%ROOT%/some/read-only-dir", Writable: false}, mapping{Path: "/ro/rw", UnderlyingPath: "%ROOT%/some/read-write-dir", Writable: true}, mapping{Path: "/nested/dup", UnderlyingPath: "%ROOT%/some/read-only-dir", Writable: false}, ) if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), config); err != nil { t.Fatal(err) } if _, err := os.Lstat(state.MountPath("initial")); err != nil { t.Errorf("Initial mapping seems to be gone; got %v", err) } if err := os.MkdirAll(state.MountPath("sb/ro/hello"), 0755); err == nil { t.Errorf("Mkdir succeeded in read-only mapping") } if _, err := os.Lstat(state.RootPath("some/read-only-dir/hello")); err == nil { t.Errorf("Mkdir through read-only mapping propagated to root") } if err := os.MkdirAll(state.MountPath("sb/ro/rw/hello2"), 0755); err != nil { t.Errorf("Mkdir failed in nested read-write mapping: %v", err) } if _, err := os.Lstat(state.RootPath("some/read-write-dir/hello2")); err != nil { t.Errorf("Mkdir through read-write didn't propagate to root; got %v", err) } if err := os.MkdirAll(state.MountPath("sb/a"), 0755); err == nil { t.Errorf("Mkdir succeeded in read-only root mapping") } config = makeCreateSandboxRequest("sb2", mapping{Path: "/rw/dir", UnderlyingPath: "%ROOT%/some/read-write-dir", Writable: true}) if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), config); err != nil { t.Fatal(err) } if _, err := os.Lstat(state.MountPath("initial")); err != nil { t.Errorf("Initial mapping seems to be gone; got %v", err) } if _, err := os.Lstat(state.MountPath("sb/nested/dup")); err != nil { t.Errorf("Previously-mapped /nested/dup seems to be gone; got %v", err) } if err := os.MkdirAll(state.MountPath("sb2/rw/dir/hello"), 0755); err != nil { t.Errorf("Mkdir failed in read-write mapping: %v", err) } config = makeDestroySandboxRequest("sb") if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), config); err != nil { t.Fatal(err) } errorIfNotUnmapped(t, state.MountPath(), "sb/ro") errorIfNotUnmapped(t, state.MountPath(), "sb") if _, err := os.Lstat(state.MountPath("sb2")); err != nil { t.Errorf("Non-deleted sandbox sb2 seems to be gone; got %v", err) } } func TestReconfiguration_EmptySubroot(t *testing.T) { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr, "--mapping=ro:/:%ROOT%") defer stdoutReader.Close() // Just in case the test fails half-way through. defer state.TearDown(t) defer stdoutWriter.Close() // Just in case the test fails half-way through. config := request{ CreateSanbox: &createSandboxRequest{ ID: "empty", Mappings: []mapping{}, Prefixes: make(map[string]string), }, } if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), config); err != nil { t.Fatal(err) } if _, err := os.Lstat(state.MountPath("empty")); err != nil { t.Errorf("Failed to stat empty root: %v", err) } config = makeDestroySandboxRequest("empty") if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), config); err != nil { t.Fatal(err) } errorIfNotUnmapped(t, state.MountPath(), "empty") } func TestReconfiguration_Subroots(t *testing.T) { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr) defer stdoutReader.Close() // Just in case the test fails half-way through. defer state.TearDown(t) defer stdoutWriter.Close() // Just in case the test fails half-way through. utils.MustMkdirAll(t, state.RootPath("sandbox1"), 0755) utils.MustMkdirAll(t, state.RootPath("sandbox1subdir"), 0755) utils.MustMkdirAll(t, state.RootPath("sandbox2"), 0755) config := []request{ makeCreateSandboxRequest( "sandbox1", mapping{Path: "/", UnderlyingPath: "%ROOT%/sandbox1", Writable: true}, mapping{Path: "/subdir", UnderlyingPath: "%ROOT%/sandbox1subdir", Writable: true}, ), makeCreateSandboxRequest( "sandbox2", mapping{Path: "/", UnderlyingPath: "%ROOT%/sandbox2", Writable: true}, ), } if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), config...); err != nil { t.Fatal(err) } for _, path := range []string{"sandbox1/a", "sandbox1/subdir/b", "sandbox2/c"} { if err := os.Mkdir(state.MountPath(path), 0755); err != nil { t.Errorf("Failed to create mount path %s: %v", path, err) } } for _, path := range []string{"sandbox1/a", "sandbox1subdir/b", "sandbox2/c"} { if _, err := os.Lstat(state.RootPath(path)); err != nil { t.Errorf("Failed to stat underlying path %s: %v", path, err) } } for _, subroot := range []string{"sandbox1", "sandbox2"} { config := makeDestroySandboxRequest(subroot) if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), config); err != nil { t.Fatal(err) } errorIfNotUnmapped(t, state.MountPath(), subroot) } } func TestReconfiguration_Prefixes(t *testing.T) { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr) defer stdoutReader.Close() // Just in case the test fails half-way through. defer state.TearDown(t) defer stdoutWriter.Close() // Just in case the test fails half-way through. utils.MustMkdirAll(t, state.RootPath("x"), 0755) utils.MustMkdirAll(t, state.RootPath("y"), 0755) config1 := request{ CreateSanbox: &createSandboxRequest{ ID: "sb1", Mappings: []mapping{ {Path: "a", PathPrefix: 1, UnderlyingPath: "x", UnderlyingPathPrefix: 2, Writable: true}, }, Prefixes: map[string]string{ "1": "/foo/bar", "2": "%ROOT%", }, }, } // This second request is intended to define new prefixes and also use previously-defined // prefixes. config2 := request{ CreateSanbox: &createSandboxRequest{ ID: "sb2", Mappings: []mapping{ {Path: "", PathPrefix: 3, UnderlyingPath: "y", UnderlyingPathPrefix: 2, Writable: true}, }, Prefixes: map[string]string{ "3": "/", }, }, } if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), config1, config2); err != nil { t.Fatal(err) } for _, path := range []string{"sb1/foo/bar/a/test", "sb2/test"} { if err := os.Mkdir(state.MountPath(path), 0755); err != nil { t.Errorf("Failed to create mount path %s: %v", path, err) } } for _, path := range []string{"x/test", "y/test"} { if _, err := os.Lstat(state.RootPath(path)); err != nil { t.Errorf("Failed to stat underlying path %s: %v", path, err) } } } func TestReconfiguration_Minimized(t *testing.T) { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr) defer stdoutReader.Close() // Just in case the test fails half-way through. defer state.TearDown(t) defer stdoutWriter.Close() // Just in case the test fails half-way through. utils.MustMkdirAll(t, state.RootPath("dir"), 0755) doOne := func(config string) { t.Helper() resp, err := tryRawReconfigure(state.Stdin, stdoutReader, state.RootPath(), config) if err != nil { t.Fatal(err) } else if resp.Error != nil { t.Fatal(*resp.Error) } } doOne(`{"C":{"i":"empty","q":{"1":"%ROOT%"}}}`) if _, err := os.Lstat(state.MountPath("empty")); err != nil { t.Errorf("Failed to stat mount path %s: %v", "empty", err) } doOne(`{"C":{"i":"sb1","m":[{"p":"/a","u":"%ROOT%"}]}}`) if _, err := os.Lstat(state.MountPath("sb1/a")); err != nil { t.Errorf("Failed to stat mount path %s: %v", "empty", err) } doOne(`{"C":{"i":"sb2","m":[{"p":"a","x":2,"u":"dir","y":1,"w":true}],"q":{"2":"/x"}}}`) if err := os.Mkdir(state.MountPath("sb2/x/a/test"), 0755); err != nil { t.Errorf("Failed to create mount path %s: %v", "sb2/x/a/test", err) } if _, err := os.Lstat(state.MountPath("sb2/x/a")); err != nil { t.Errorf("Failed to stat mount path %s: %v", "sb2/x/a", err) } if _, err := os.Lstat(state.RootPath("dir/test")); err != nil { t.Errorf("Failed to stat underlying path %s: %v", "dir/test", err) } doOne(`{"D":"empty"}`) errorIfNotUnmapped(t, state.MountPath(), "empty") } func TestReconfiguration_RecoverableErrors(t *testing.T) { // checkBadConfig applies the set of reconfiguration requests in configs and checks that the // last one fails with the error provided in wantError. All requests but the last one are // expected to succeed, as they are intended to prepare the sandboxfs state before issuing // the failing request. checkBadConfig := func(t *testing.T, state *utils.MountState, stdoutReader io.Reader, configs []request, wantError string) { t.Helper() if len(configs) > 1 { for _, req := range configs[:len(configs)-1] { if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), req); err != nil { t.Fatalf("prerequisite reconfiguration failed: %v", err) } } } req := configs[len(configs)-1] resp, err := tryReconfigure(state.Stdin, stdoutReader, state.RootPath(), req) if err != nil { t.Fatalf("want reconfiguration of / to fail; got success") } if resp.ID == nil || *resp.ID != req.getID() { t.Errorf("sandboxfs replied with a bad id: got %v, want %s", resp.ID, req.getID()) } if resp.Error == nil { t.Errorf("want reconfiguration to respond with %s; got OK", wantError) } else if !utils.MatchesRegexp(wantError, *resp.Error) { t.Errorf("want reconfiguration to respond with %s; got %s", wantError, *resp.Error) } if _, err := os.Lstat(state.MountPath("file")); err != nil { t.Errorf("want file to still exist after failed reconfiguration; got %v", err) } } testData := []struct { name string config []request wantError string }{ { "InvalidMapping", []request{ makeCreateSandboxRequest("sb", mapping{Path: "foo/../.", UnderlyingPath: "%ROOT%/subdir", Writable: false}), }, "path.*not absolute", }, { "MapRootLate", []request{ makeCreateSandboxRequest("sb", mapping{Path: "/too-late", UnderlyingPath: "%ROOT%/subdir", Writable: false}, mapping{Path: "/", UnderlyingPath: "%ROOT%/subdir", Writable: false}), }, "Root can be mapped at most once", }, { "MapTwice", []request{ makeCreateSandboxRequest("sb", mapping{Path: "/foo", UnderlyingPath: "%ROOT%/subdir", Writable: false}), makeCreateSandboxRequest("sb", mapping{Path: "/foo", UnderlyingPath: "%ROOT%/file", Writable: false}), }, "Already mapped", }, { "MapSubrootLate", []request{ makeCreateSandboxRequest("sb", mapping{Path: "/too-late", UnderlyingPath: "%ROOT%/file", Writable: false}, mapping{Path: "/", UnderlyingPath: "%ROOT%/subdir", Writable: false}), }, "Root can be mapped at most once", }, { "UnmapInvalidID", []request{ makeDestroySandboxRequest(""), }, "Identifier cannot be empty", }, { "BadPrefixes", []request{ makeCreateSandboxRequest("sb", mapping{Path: "foo", PathPrefix: 5, UnderlyingPath: "%ROOT%/file", Writable: false}, mapping{Path: "/", UnderlyingPath: "%ROOT%/subdir", Writable: false}), }, "Prefix 5 does not exist", }, } for _, d := range testData { t.Run(d.name, func(t *testing.T) { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr, "--mapping=rw:/:%ROOT%") defer stdoutReader.Close() // Just in case the test fails half-way through. defer state.TearDown(t) defer stdoutWriter.Close() // Just in case the test fails half-way through. utils.MustMkdirAll(t, state.RootPath("subdir"), 0755) utils.MustWriteFile(t, state.RootPath("file"), 0644, "") checkBadConfig(t, state, stdoutReader, d.config, d.wantError) }) } } func TestReconfiguration_RaceSystemComponents(t *testing.T) { // This test verifies that a dynamic sandboxfs instance can be unmounted immediately after // reconfiguration. // // When this test was originally conceived, it did not actually test for a sandboxfs problem // despite it being in the "reconfiguration" category. This test was added as a check to // ensure that our own testing infrastructure can cope with system components (e.g. macOS's // Finder) interfering with the mount point while we are cleaning up the test state. The // reason this exists under the "reconfiguration" category is because a // dynamically-configured sandboxfs instance is more subject to this problem than a // statically-configured one: during test setup in the former case, we cannot wait for // sandboxfs to be ready for serving, which means that the time window between the // reconfiguration operation and the unmount is smaller. This shorter window makes the race // between us and the system easier to trigger. oneShot := func() error { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr, "--mapping=ro:/:%ROOT%") defer stdoutReader.Close() // state.TearDown and stdoutWriter.Close not deferred here because we want to // explicitly control for any possible error they may report and abort the whole // test early in that case. utils.MustWriteFile(t, state.RootPath("first"), 0644, "First") firstConfig := makeCreateSandboxRequest("first", mapping{Path: "/", UnderlyingPath: "%ROOT%/first", Writable: false}) if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), firstConfig); err != nil { stdoutWriter.Close() state.TearDown(t) return err } stdoutWriter.Close() return state.TearDown(t) } for i := 0; i < 200; i++ { if err := oneShot(); err != nil { t.Fatalf("Failed after %d mount+reconfigure sequences: %v", i, err) } } } // TODO(jmmv): These tests probably make little sense after reconfigurations protocol changed to // deal with sandboxes, not arbitrary paths. func TestReconfiguration_DirectoryListings(t *testing.T) { testData := []struct { name string dir string firstConfigTarget string secondConfigTarget string keepDirOpen string }{ {"MappedDir", "/mapped", "dir1", "dir2", ""}, {"MappedDirAndKeepRootOpen", "/mapped", "dir1", "dir2", "/"}, {"MappedDirAndKeepSelfOpen", "/mapped", "dir1", "dir2", "/mapped"}, {"Root", "/", "dir1/first", "dir2/second", ""}, {"RootAndKeepSelfOpen", "/", "dir1/first", "dir2/second", "/"}, {"ScaffoldDir", "/scaffold", "dir1/first", "dir2/second", ""}, {"ScaffoldDirAndKeepRootOpen", "/scaffold", "dir1/first", "dir2/second", "/"}, {"ScaffoldDirAndKeepSelfOpen", "/scaffold", "dir1/first", "dir2/second", "/scaffold"}, } for _, d := range testData { t.Run(d.name, func(t *testing.T) { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr) defer stdoutReader.Close() defer state.TearDown(t) defer stdoutWriter.Close() utils.MustMkdirAll(t, state.RootPath("dir1"), 0755) utils.MustWriteFile(t, state.RootPath("dir1/first"), 0644, "First") utils.MustMkdirAll(t, state.RootPath("dir2"), 0755) utils.MustWriteFile(t, state.RootPath("dir2/second"), 0644, "Second") firstConfig := []request{ makeCreateSandboxRequest("sb", mapping{Path: filepath.Join(d.dir, "first"), UnderlyingPath: state.RootPath(d.firstConfigTarget), Writable: false}), } if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), firstConfig...); err != nil { t.Fatalf("First configuration failed: %v", err) } if err := utils.DirEntryNamesEqual(state.MountPath("sb", d.dir), []string{"first"}); err != nil { t.Error(err) } if d.keepDirOpen != "" { // Keep a handle open to the directory for the duration of the test, which makes everything // more difficult to handle. This ensures that no handles made stale during reconfiguration // are used. handle, err := os.OpenFile(state.MountPath("sb", d.keepDirOpen), os.O_RDONLY, 0) if err != nil { t.Fatalf("Cannot open %s to keep directory busy: %v", d.keepDirOpen, err) } defer handle.Close() } secondConfig := []request{ makeDestroySandboxRequest("sb"), makeCreateSandboxRequest("sb2", mapping{Path: filepath.Join(d.dir, "second"), UnderlyingPath: state.RootPath(d.secondConfigTarget), Writable: false}), } if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), secondConfig...); err != nil { t.Fatalf("Second configuration failed: %v", err) } if err := utils.DirEntryNamesEqual(state.MountPath("sb2", d.dir), []string{"second"}); err != nil { t.Error(err) } }) } } func TestReconfiguration_FileSystemStillWorksAfterInputEOF(t *testing.T) { // grepStderr reads from a pipe connected to stderr looking for the given pattern and writes // to the found channel when the pattern is found. Any contents read from the pipe are // dumped to the process' stderr so that they are visible to the user, and so that the child // process connected to the pipe does not stall due to a full pipe. grepStderr := func(stderr io.Reader, pattern string, found chan<- bool) { scanner := bufio.NewScanner(stderr) match := false for { if !scanner.Scan() { if err := scanner.Err(); err != io.EOF && err != io.ErrClosedPipe { t.Errorf("Got error while reading from stderr: %v", err) } break } fmt.Fprintln(os.Stderr, scanner.Text()) if utils.MatchesRegexp(pattern, scanner.Text()) { match = true found <- true } } if !match { found <- false } } stderrReader, stderrWriter := io.Pipe() defer stderrReader.Close() defer stderrWriter.Close() stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, stderrWriter) defer stdoutReader.Close() defer state.TearDown(t) defer stdoutWriter.Close() gotEOF := make(chan bool) go grepStderr(stderrReader, `Reached end of reconfiguration input`, gotEOF) utils.MustMkdirAll(t, state.RootPath("dir"), 0755) config := makeCreateSandboxRequest("sb", mapping{Path: "/dir", UnderlyingPath: "%ROOT%/dir", Writable: true}) if err := reconfigure(state.Stdin, stdoutReader, state.RootPath(), config); err != nil { t.Fatal(err) } if err := state.Stdin.Close(); err != nil { t.Fatalf("Failed to close stdin: %v", err) } state.Stdin = nil // Tell state.TearDown that we cleaned up ourselves. match := <-gotEOF if !match { t.Errorf("EOF not detected by sandboxfs") } // sandboxfs stopped listening for reconfiguration requests but the file system should // continue to be functional. Make sure that's the case. if err := os.MkdirAll(state.MountPath("sb/dir/still-alive"), 0755); err != nil { t.Errorf("Mkdir failed: %v", err) } } func TestReconfiguration_StreamFileDoesNotExist(t *testing.T) { tempDir, err := ioutil.TempDir("", "test") if err != nil { t.Fatalf("Failed to create temporary directory: %v", err) } defer os.RemoveAll(tempDir) nonExistentFile := filepath.Join(tempDir, "non-existent/file") testData := []struct { name string flag string wantStderr string }{ { "Input", "--input=" + nonExistentFile, fmt.Sprintf("Failed to open reconfiguration input '%s': No such file or directory", nonExistentFile), }, { "Output", "--output=" + nonExistentFile, fmt.Sprintf("Failed to open reconfiguration output '%s': No such file or directory", nonExistentFile), }, } for _, d := range testData { t.Run(d.name, func(t *testing.T) { stdout, stderr, err := utils.RunAndWait(1, d.flag, filepath.Join(tempDir, "mnt")) if err != nil { t.Fatal(err) } if len(stdout) > 0 { t.Errorf("Got %s; want stdout to be empty", stdout) } if !utils.MatchesRegexp(d.wantStderr, stderr) { t.Errorf("Got %s; want stderr to match %s", stderr, d.wantStderr) } }) } } // parallelTest is a helper function to verify that sandboxfs processes requests in parallel. // // To do so, sandboxfs runs with multiple reconfiguration threads and we send many more // reconfiguration requests in parallel. Because of the non-determinism introduced by the // multiple threads, we expect the responses to come in a different order. // // Returns true if the responses we received were in the same order as the requests, in which case // we suspect sandboxfs processed all of them sequentially. Because of non-determinism, the caller // has to wrap this helper function and run it several times in a row to get a good answer. func requestsMatchResponsesWithNThreads(t *testing.T, threads int) bool { stdoutReader, stdoutWriter := io.Pipe() state := utils.MountSetupWithOutputs(t, stdoutWriter, os.Stderr, fmt.Sprintf("--reconfig_threads=%d", threads)) defer stdoutReader.Close() // Just in case the test fails half-way through. defer state.TearDown(t) defer stdoutWriter.Close() // Just in case the test fails half-way through. utils.MustMkdirAll(t, state.RootPath("dir"), 0755) requests := []string{} requestsMu := sync.Mutex{} wg := sync.WaitGroup{} for i := 0; i < 500; i++ { id := fmt.Sprintf("sandbox-%d", i) req := makeCreateSandboxRequest(id, mapping{Path: "/", UnderlyingPath: state.RootPath("dir"), Writable: false}) wg.Add(1) go func() { bytes, err := json.Marshal(req) if err != nil { panic(fmt.Sprintf("Bad configuration request in test: %v", err)) } requestsMu.Lock() n, err := io.WriteString(state.Stdin, string(bytes)) if n != len(bytes) || err != nil { requestsMu.Unlock() panic(fmt.Sprintf("Reconfiguration failed unexpectedly: %d bytes written of %d, error %v", n, len(bytes), err)) } requests = append(requests, id) requestsMu.Unlock() wg.Done() }() } decoder := json.NewDecoder(stdoutReader) responses := []string{} for i := 0; i < 500; i++ { resp := response{} if err := decoder.Decode(&resp); err != nil { t.Errorf("Failed to decode %v", err) } if resp.Error != nil { t.Errorf("Unexpected error in response %v: %s", resp.ID, *resp.Error) } responses = append(responses, *resp.ID) } wg.Wait() return reflect.DeepEqual(requests, responses) } func TestReconfiguration_ParallelOneThread(t *testing.T) { ok := true for i := 0; i < 50; i++ { if !requestsMatchResponsesWithNThreads(t, 1) { ok = false break } } if !ok { t.Fatalf("Requests were expected to be processed in order but weren't") } } func TestReconfiguration_ParallelMultipleThreads(t *testing.T) { ok := false for i := 0; i < 50; i++ { if !requestsMatchResponsesWithNThreads(t, 4) { ok = true break } } if !ok { t.Fatalf("Requests were expected to be processed out of order but weren't") } }