diff --git a/pkg/sentry/fsimpl/sys/sys.go b/pkg/sentry/fsimpl/sys/sys.go index 7fcb2d26b..3c6b41137 100644 --- a/pkg/sentry/fsimpl/sys/sys.go +++ b/pkg/sentry/fsimpl/sys/sys.go @@ -34,6 +34,7 @@ import ( const ( // Name is the default filesystem name. Name = "sysfs" + defaultSysMode = linux.FileMode(0444) defaultSysDirMode = linux.FileMode(0755) defaultMaxCachedDentries = uint64(1000) ) @@ -43,6 +44,15 @@ const ( // +stateify savable type FilesystemType struct{} +// InternalData contains internal data passed in via +// vfs.GetFilesystemOptions.InternalData. +// +// +stateify savable +type InternalData struct { + // ProductName is the value to be set to devices/virtual/dmi/id/product_name. + ProductName string +} + // filesystem implements vfs.FilesystemImpl. // // +stateify savable @@ -96,18 +106,38 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt fsDirChildren["cgroup"] = fs.newDir(ctx, creds, defaultSysDirMode, nil) } - root := fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ - "block": fs.newDir(ctx, creds, defaultSysDirMode, nil), - "bus": fs.newDir(ctx, creds, defaultSysDirMode, nil), - "class": fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ - "power_supply": fs.newDir(ctx, creds, defaultSysDirMode, nil), + classSub := map[string]kernfs.Inode{ + "power_supply": fs.newDir(ctx, creds, defaultSysDirMode, nil), + } + devicesSub := map[string]kernfs.Inode{ + "system": fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ + "cpu": cpuDir(ctx, fs, creds), }), - "dev": fs.newDir(ctx, creds, defaultSysDirMode, nil), - "devices": fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ - "system": fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ - "cpu": cpuDir(ctx, fs, creds), + } + productName := "" + if opts.InternalData != nil { + data := opts.InternalData.(*InternalData) + productName = data.ProductName + } + if len(productName) > 0 { + log.Debugf("Setting product_name: %q", productName) + classSub["dmi"] = fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ + "id": kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), "../../devices/virtual/dmi/id"), + }) + devicesSub["virtual"] = fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ + "dmi": fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ + "id": fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ + "product_name": fs.newStaticFile(ctx, creds, defaultSysMode, productName+"\n"), + }), }), - }), + }) + } + root := fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{ + "block": fs.newDir(ctx, creds, defaultSysDirMode, nil), + "bus": fs.newDir(ctx, creds, defaultSysDirMode, nil), + "class": fs.newDir(ctx, creds, defaultSysDirMode, classSub), + "dev": fs.newDir(ctx, creds, defaultSysDirMode, nil), + "devices": fs.newDir(ctx, creds, defaultSysDirMode, devicesSub), "firmware": fs.newDir(ctx, creds, defaultSysDirMode, nil), "fs": fs.newDir(ctx, creds, defaultSysDirMode, fsDirChildren), "kernel": kernelDir(ctx, fs, creds), @@ -239,3 +269,15 @@ type implStatFS struct{} func (*implStatFS) StatFS(context.Context, *vfs.Filesystem) (linux.Statfs, error) { return vfs.GenericStatFS(linux.SYSFS_MAGIC), nil } + +// +stateify savable +type staticFile struct { + kernfs.DynamicBytesFile + vfs.StaticData +} + +func (fs *filesystem) newStaticFile(ctx context.Context, creds *auth.Credentials, mode linux.FileMode, data string) kernfs.Inode { + s := &staticFile{StaticData: vfs.StaticData{Data: data}} + s.Init(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), s, mode) + return s +} diff --git a/runsc/boot/controller.go b/runsc/boot/controller.go index 76e1f596b..22686f711 100644 --- a/runsc/boot/controller.go +++ b/runsc/boot/controller.go @@ -432,7 +432,7 @@ func (cm *containerManager) Restore(o *RestoreOpts, _ *struct{}) error { // Set up the restore environment. ctx := k.SupervisorContext() - mntr := newContainerMounter(&cm.l.root, cm.l.k, cm.l.mountHints, kernel.VFS2Enabled) + mntr := newContainerMounter(&cm.l.root, cm.l.k, cm.l.mountHints, kernel.VFS2Enabled, cm.l.productName) if kernel.VFS2Enabled { ctx, err = mntr.configureRestore(ctx) if err != nil { diff --git a/runsc/boot/fs.go b/runsc/boot/fs.go index 3ef746b54..c8bc07085 100644 --- a/runsc/boot/fs.go +++ b/runsc/boot/fs.go @@ -622,15 +622,20 @@ type containerMounter struct { k *kernel.Kernel hints *podMountHints + + // productName is the value to show in + // /sys/devices/virtual/dmi/id/product_name. + productName string } -func newContainerMounter(info *containerInfo, k *kernel.Kernel, hints *podMountHints, vfs2Enabled bool) *containerMounter { +func newContainerMounter(info *containerInfo, k *kernel.Kernel, hints *podMountHints, vfs2Enabled bool, productName string) *containerMounter { return &containerMounter{ - root: info.spec.Root, - mounts: compileMounts(info.spec, info.conf, vfs2Enabled), - fds: fdDispenser{fds: info.goferFDs}, - k: k, - hints: hints, + root: info.spec.Root, + mounts: compileMounts(info.spec, info.conf, vfs2Enabled), + fds: fdDispenser{fds: info.goferFDs}, + k: k, + hints: hints, + productName: productName, } } diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index d229e290b..eddc01a6f 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -144,6 +144,10 @@ type Loader struct { // mountHints provides extra information about mounts for containers that // apply to the entire pod. mountHints *podMountHints + + // productName is the value to show in + // /sys/devices/virtual/dmi/id/product_name. + productName string } // execID uniquely identifies a sentry process that is executed in a container. @@ -219,6 +223,9 @@ type Args struct { // TraceFD is the file descriptor to write a Go execution trace to. // Valid if >=0. TraceFD int + // ProductName is the value to show in + // /sys/devices/virtual/dmi/id/product_name. + ProductName string } // make sure stdioFDs are always the same on initial start and on restore @@ -424,6 +431,7 @@ func New(args Args) (*Loader, error) { mountHints: mountHints, root: info, stopProfiling: stopProfiling, + productName: args.ProductName, } // We don't care about child signals; some platforms can generate a @@ -769,7 +777,7 @@ func (l *Loader) createContainerProcess(root bool, cid string, info *containerIn } l.startGoferMonitor(cid, int32(info.goferFDs[0].FD())) - mntr := newContainerMounter(info, l.k, l.mountHints, kernel.VFS2Enabled) + mntr := newContainerMounter(info, l.k, l.mountHints, kernel.VFS2Enabled, l.productName) if root { if err := mntr.processHints(info.conf, info.procArgs.Credentials); err != nil { return nil, nil, nil, err diff --git a/runsc/boot/loader_test.go b/runsc/boot/loader_test.go index 86334dbba..0f68f0795 100644 --- a/runsc/boot/loader_test.go +++ b/runsc/boot/loader_test.go @@ -447,7 +447,7 @@ func TestCreateMountNamespace(t *testing.T) { goferFDs: []*fd.FD{fd.New(sandEnd)}, } - mntr := newContainerMounter(&info, nil, &podMountHints{}, false /* vfs2Enabled */) + mntr := newContainerMounter(&info, nil, &podMountHints{}, false /* vfs2Enabled */, "") mns, err := mntr.createMountNamespace(ctx, conf) if err != nil { t.Fatalf("failed to create mount namespace: %v", err) @@ -487,7 +487,7 @@ func TestCreateMountNamespaceVFS2(t *testing.T) { defer l.Destroy() defer loaderCleanup() - mntr := newContainerMounter(&l.root, l.k, l.mountHints, true /* vfs2Enabled */) + mntr := newContainerMounter(&l.root, l.k, l.mountHints, true /* vfs2Enabled */, "") if err := mntr.processHints(l.root.conf, l.root.procArgs.Credentials); err != nil { t.Fatalf("failed process hints: %v", err) } @@ -716,7 +716,7 @@ func TestRestoreEnvironment(t *testing.T) { spec: tc.spec, goferFDs: ioFDs, } - mntr := newContainerMounter(&info, nil, &podMountHints{}, conf.VFS2) + mntr := newContainerMounter(&info, nil, &podMountHints{}, conf.VFS2, "") actualRenv, err := mntr.createRestoreEnvironment(conf) if !tc.errorExpected && err != nil { t.Fatalf("could not create restore environment for test:%s", tc.name) diff --git a/runsc/boot/vfs.go b/runsc/boot/vfs.go index 0029f54e1..fe7304826 100644 --- a/runsc/boot/vfs.go +++ b/runsc/boot/vfs.go @@ -500,12 +500,17 @@ func (c *containerMounter) getMountNameAndOptionsVFS2(conf *config.Config, m *mo // Find filesystem name and FS specific data field. switch m.mount.Type { - case devpts.Name, devtmpfs.Name, proc.Name, sys.Name: + case devpts.Name, devtmpfs.Name, proc.Name: // Nothing to do. case nonefs: fsName = sys.Name + case sys.Name: + if len(c.productName) > 0 { + internalData = &sys.InternalData{ProductName: c.productName} + } + case tmpfs.Name: var err error data, err = parseAndFilterOptions(m.mount.Options, tmpfsAllowedData...) diff --git a/runsc/cmd/boot.go b/runsc/cmd/boot.go index 9ded710d3..b1b90b127 100644 --- a/runsc/cmd/boot.go +++ b/runsc/cmd/boot.go @@ -16,6 +16,7 @@ package cmd import ( "context" + "io/ioutil" "os" "runtime/debug" "strings" @@ -107,6 +108,10 @@ type Boot struct { // terminates. This flag is set when the command execve's itself because // parent death signal doesn't propagate through execve when uid/gid changes. attached bool + + // productName is the value to show in + // /sys/devices/virtual/dmi/id/product_name. + productName string } // Name implements subcommands.Command.Name. @@ -146,6 +151,7 @@ func (b *Boot) SetFlags(f *flag.FlagSet) { f.IntVar(&b.profileMutexFD, "profile-mutex-fd", -1, "file descriptor to write mutex profile to. -1 disables profiling.") f.IntVar(&b.traceFD, "trace-fd", -1, "file descriptor to write Go execution trace to. -1 disables tracing.") f.BoolVar(&b.attached, "attached", false, "if attached is true, kills the sandbox process when the parent process terminates") + f.StringVar(&b.productName, "product-name", "", "value to show in /sys/devices/virtual/dmi/id/product_name") } // Execute implements subcommands.Command.Execute. It starts a sandbox in a @@ -161,6 +167,16 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) // Set traceback level debug.SetTraceback(conf.Traceback) + if len(b.productName) == 0 { + // Do this before chroot takes effect, otherwise we can't read /sys. + if product, err := ioutil.ReadFile("/sys/devices/virtual/dmi/id/product_name"); err != nil { + log.Warningf("Not setting product_name: %v", err) + } else { + b.productName = strings.TrimSpace(string(product)) + log.Infof("Setting product_name: %q", b.productName) + } + } + if b.attached { // Ensure this process is killed after parent process terminates when // attached mode is enabled. In the unfortunate event that the parent @@ -177,7 +193,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) if !b.applyCaps && !conf.Rootless { // Remove --apply-caps arg to call myself. It has already been done. - args := prepareArgs(b.attached, "setup-root") + args := b.prepareArgs("setup-root") // Note that we've already read the spec from the spec FD, and // we will read it again after the exec call. This works @@ -217,7 +233,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) // Remove --apply-caps and --setup-root arg to call myself. Both have // already been done. - args := prepareArgs(b.attached, "setup-root", "apply-caps") + args := b.prepareArgs("setup-root", "apply-caps") // Note that we've already read the spec from the spec FD, and // we will read it again after the exec call. This works @@ -271,6 +287,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) ProfileHeapFD: b.profileHeapFD, ProfileMutexFD: b.profileMutexFD, TraceFD: b.traceFD, + ProductName: b.productName, } l, err := boot.New(bootArgs) if err != nil { @@ -308,7 +325,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) return subcommands.ExitSuccess } -func prepareArgs(attached bool, exclude ...string) []string { +func (b *Boot) prepareArgs(exclude ...string) []string { var args []string for _, arg := range os.Args { for _, excl := range exclude { @@ -317,10 +334,17 @@ func prepareArgs(attached bool, exclude ...string) []string { } } args = append(args, arg) - if attached && arg == "boot" { - // Strategicaly place "--attached" after the command. This is needed - // to ensure the new process is killed when the parent process terminates. - args = append(args, "--attached") + // Strategically add parameters after the command and before the container + // ID at the end. + if arg == "boot" { + if b.attached { + // This is needed to ensure the new process is killed when the parent + // process terminates. + args = append(args, "--attached") + } + if len(b.productName) > 0 { + args = append(args, "--product-name", b.productName) + } } skip: } diff --git a/test/e2e/integration_test.go b/test/e2e/integration_test.go index 1003e0209..206398856 100644 --- a/test/e2e/integration_test.go +++ b/test/e2e/integration_test.go @@ -798,3 +798,23 @@ func TestDeleteInterface(t *testing.T) { t.Fatalf("loopback interface is removed") } } + +func TestProductName(t *testing.T) { + want, err := ioutil.ReadFile("/sys/devices/virtual/dmi/id/product_name") + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + d := dockerutil.MakeContainer(ctx, t) + defer d.CleanUp(ctx) + + opts := dockerutil.RunOpts{Image: "basic/alpine"} + got, err := d.Run(ctx, opts, "cat", "/sys/devices/virtual/dmi/id/product_name") + if err != nil { + t.Fatalf("docker run failed: %v", err) + } + if string(want) != got { + t.Errorf("invalid product name, want: %q, got: %q", want, got) + } +}