// Copyright 2019 The gVisor Authors. // // 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 container import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "testing" "gvisor.dev/gvisor/pkg/sentry/control" "gvisor.dev/gvisor/pkg/sentry/kernel/auth" "gvisor.dev/gvisor/pkg/test/testutil" "gvisor.dev/gvisor/runsc/config" ) // TestSharedVolume checks that modifications to a volume mount are propagated // into and out of the sandbox. func TestSharedVolume(t *testing.T) { conf := testutil.TestConfig(t) conf.FileAccess = config.FileAccessShared // Main process just sleeps. We will use "exec" to probe the state of // the filesystem. spec := testutil.NewSpecWithArgs("sleep", "1000") dir, err := ioutil.TempDir(testutil.TmpDir(), "shared-volume-test") if err != nil { t.Fatalf("TempDir failed: %v", err) } _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) if err != nil { t.Fatalf("error setting up container: %v", err) } defer cleanup() // Create and start the container. args := Args{ ID: testutil.RandomContainerID(), Spec: spec, BundleDir: bundleDir, } c, err := New(conf, args) if err != nil { t.Fatalf("error creating container: %v", err) } defer c.Destroy() if err := c.Start(conf); err != nil { t.Fatalf("error starting container: %v", err) } // File that will be used to check consistency inside/outside sandbox. filename := filepath.Join(dir, "file") // File does not exist yet. Reading from the sandbox should fail. argsTestFile := &control.ExecArgs{ Filename: "/usr/bin/test", Argv: []string{"test", "-f", filename}, } if ws, err := c.executeSync(conf, argsTestFile); err != nil { t.Fatalf("unexpected error testing file %q: %v", filename, err) } else if ws.ExitStatus() == 0 { t.Errorf("test %q exited with code %v, wanted not zero", ws.ExitStatus(), err) } // Create the file from outside of the sandbox. if err := ioutil.WriteFile(filename, []byte("foobar"), 0777); err != nil { t.Fatalf("error writing to file %q: %v", filename, err) } // Now we should be able to test the file from within the sandbox. if ws, err := c.executeSync(conf, argsTestFile); err != nil { t.Fatalf("unexpected error testing file %q: %v", filename, err) } else if ws.ExitStatus() != 0 { t.Errorf("test %q exited with code %v, wanted zero", filename, ws.ExitStatus()) } // Rename the file from outside of the sandbox. newFilename := filepath.Join(dir, "newfile") if err := os.Rename(filename, newFilename); err != nil { t.Fatalf("os.Rename(%q, %q) failed: %v", filename, newFilename, err) } // File should no longer exist at the old path within the sandbox. if ws, err := c.executeSync(conf, argsTestFile); err != nil { t.Fatalf("unexpected error testing file %q: %v", filename, err) } else if ws.ExitStatus() == 0 { t.Errorf("test %q exited with code %v, wanted not zero", filename, ws.ExitStatus()) } // We should be able to test the new filename from within the sandbox. argsTestNewFile := &control.ExecArgs{ Filename: "/usr/bin/test", Argv: []string{"test", "-f", newFilename}, } if ws, err := c.executeSync(conf, argsTestNewFile); err != nil { t.Fatalf("unexpected error testing file %q: %v", newFilename, err) } else if ws.ExitStatus() != 0 { t.Errorf("test %q exited with code %v, wanted zero", newFilename, ws.ExitStatus()) } // Delete the renamed file from outside of the sandbox. if err := os.Remove(newFilename); err != nil { t.Fatalf("error removing file %q: %v", filename, err) } // Renamed file should no longer exist at the old path within the sandbox. if ws, err := c.executeSync(conf, argsTestNewFile); err != nil { t.Fatalf("unexpected error testing file %q: %v", newFilename, err) } else if ws.ExitStatus() == 0 { t.Errorf("test %q exited with code %v, wanted not zero", newFilename, ws.ExitStatus()) } // Now create the file from WITHIN the sandbox. argsTouch := &control.ExecArgs{ Filename: "/usr/bin/touch", Argv: []string{"touch", filename}, KUID: auth.KUID(os.Getuid()), KGID: auth.KGID(os.Getgid()), } if ws, err := c.executeSync(conf, argsTouch); err != nil { t.Fatalf("unexpected error touching file %q: %v", filename, err) } else if ws.ExitStatus() != 0 { t.Errorf("touch %q exited with code %v, wanted zero", filename, ws.ExitStatus()) } // File should exist outside the sandbox. if _, err := os.Stat(filename); err != nil { t.Errorf("stat %q got error %v, wanted nil", filename, err) } // File should exist outside the sandbox. if _, err := os.Stat(filename); err != nil { t.Errorf("stat %q got error %v, wanted nil", filename, err) } // Delete the file from within the sandbox. argsRemove := &control.ExecArgs{ Filename: "/bin/rm", Argv: []string{"rm", filename}, } if ws, err := c.executeSync(conf, argsRemove); err != nil { t.Fatalf("unexpected error removing file %q: %v", filename, err) } else if ws.ExitStatus() != 0 { t.Errorf("remove %q exited with code %v, wanted zero", filename, ws.ExitStatus()) } // File should not exist outside the sandbox. if _, err := os.Stat(filename); !os.IsNotExist(err) { t.Errorf("stat %q got error %v, wanted ErrNotExist", filename, err) } } func checkFile(conf *config.Config, c *Container, filename string, want []byte) error { cpy := filename + ".copy" if _, err := execute(conf, c, "/bin/cp", "-f", filename, cpy); err != nil { return fmt.Errorf("unexpected error copying file %q to %q: %v", filename, cpy, err) } got, err := ioutil.ReadFile(cpy) if err != nil { return fmt.Errorf("error reading file %q: %v", filename, err) } if !bytes.Equal(got, want) { return fmt.Errorf("file content inside the sandbox is wrong, got: %q, want: %q", got, want) } return nil } // TestSharedVolumeFile tests that changes to file content outside the sandbox // is reflected inside. func TestSharedVolumeFile(t *testing.T) { conf := testutil.TestConfig(t) conf.FileAccess = config.FileAccessShared // Main process just sleeps. We will use "exec" to probe the state of // the filesystem. spec := testutil.NewSpecWithArgs("sleep", "1000") dir, err := ioutil.TempDir(testutil.TmpDir(), "shared-volume-test") if err != nil { t.Fatalf("TempDir failed: %v", err) } _, bundleDir, cleanup, err := testutil.SetupContainer(spec, conf) if err != nil { t.Fatalf("error setting up container: %v", err) } defer cleanup() // Create and start the container. args := Args{ ID: testutil.RandomContainerID(), Spec: spec, BundleDir: bundleDir, } c, err := New(conf, args) if err != nil { t.Fatalf("error creating container: %v", err) } defer c.Destroy() if err := c.Start(conf); err != nil { t.Fatalf("error starting container: %v", err) } // File that will be used to check consistency inside/outside sandbox. filename := filepath.Join(dir, "file") // Write file from outside the container and check that the same content is // read inside. want := []byte("host-") if err := ioutil.WriteFile(filename, []byte(want), 0666); err != nil { t.Fatalf("Error writing to %q: %v", filename, err) } if err := checkFile(conf, c, filename, want); err != nil { t.Fatal(err.Error()) } // Append to file inside the container and check that content is not lost. if _, err := execute(conf, c, "/bin/bash", "-c", "echo -n sandbox- >> "+filename); err != nil { t.Fatalf("unexpected error appending file %q: %v", filename, err) } want = []byte("host-sandbox-") if err := checkFile(conf, c, filename, want); err != nil { t.Fatal(err.Error()) } // Write again from outside the container and check that the same content is // read inside. f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0) if err != nil { t.Fatalf("Error openning file %q: %v", filename, err) } defer f.Close() if _, err := f.Write([]byte("host")); err != nil { t.Fatalf("Error writing to file %q: %v", filename, err) } want = []byte("host-sandbox-host") if err := checkFile(conf, c, filename, want); err != nil { t.Fatal(err.Error()) } // Shrink file outside and check that the same content is read inside. if err := f.Truncate(5); err != nil { t.Fatalf("Error truncating file %q: %v", filename, err) } want = want[:5] if err := checkFile(conf, c, filename, want); err != nil { t.Fatal(err.Error()) } }