Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 136 additions & 119 deletions libcontainer/specconv/spec_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"time"

systemdDbus "github.com/coreos/go-systemd/v22/dbus"
Expand All @@ -24,26 +24,85 @@ import (
"golang.org/x/sys/unix"
)

var namespaceMapping = map[specs.LinuxNamespaceType]configs.NamespaceType{
specs.PIDNamespace: configs.NEWPID,
specs.NetworkNamespace: configs.NEWNET,
specs.MountNamespace: configs.NEWNS,
specs.UserNamespace: configs.NEWUSER,
specs.IPCNamespace: configs.NEWIPC,
specs.UTSNamespace: configs.NEWUTS,
specs.CgroupNamespace: configs.NEWCGROUP,
}
var (
initMapsOnce sync.Once
namespaceMapping map[specs.LinuxNamespaceType]configs.NamespaceType
mountPropagationMapping map[string]int
mountFlags, extensionFlags map[string]struct {
clear bool
flag int
}
)

var mountPropagationMapping = map[string]int{
"rprivate": unix.MS_PRIVATE | unix.MS_REC,
"private": unix.MS_PRIVATE,
"rslave": unix.MS_SLAVE | unix.MS_REC,
"slave": unix.MS_SLAVE,
"rshared": unix.MS_SHARED | unix.MS_REC,
"shared": unix.MS_SHARED,
"runbindable": unix.MS_UNBINDABLE | unix.MS_REC,
"unbindable": unix.MS_UNBINDABLE,
"": 0,
func initMaps() {
initMapsOnce.Do(func() {
namespaceMapping = map[specs.LinuxNamespaceType]configs.NamespaceType{
specs.PIDNamespace: configs.NEWPID,
specs.NetworkNamespace: configs.NEWNET,
specs.MountNamespace: configs.NEWNS,
specs.UserNamespace: configs.NEWUSER,
specs.IPCNamespace: configs.NEWIPC,
specs.UTSNamespace: configs.NEWUTS,
specs.CgroupNamespace: configs.NEWCGROUP,
}

mountPropagationMapping = map[string]int{
"rprivate": unix.MS_PRIVATE | unix.MS_REC,
"private": unix.MS_PRIVATE,
"rslave": unix.MS_SLAVE | unix.MS_REC,
"slave": unix.MS_SLAVE,
"rshared": unix.MS_SHARED | unix.MS_REC,
"shared": unix.MS_SHARED,
"runbindable": unix.MS_UNBINDABLE | unix.MS_REC,
"unbindable": unix.MS_UNBINDABLE,
"": 0,
}

mountFlags = map[string]struct {
clear bool
flag int
}{
"acl": {false, unix.MS_POSIXACL},
"async": {true, unix.MS_SYNCHRONOUS},
"atime": {true, unix.MS_NOATIME},
"bind": {false, unix.MS_BIND},
"defaults": {false, 0},
"dev": {true, unix.MS_NODEV},
"diratime": {true, unix.MS_NODIRATIME},
"dirsync": {false, unix.MS_DIRSYNC},
"exec": {true, unix.MS_NOEXEC},
"iversion": {false, unix.MS_I_VERSION},
"lazytime": {false, unix.MS_LAZYTIME},
"loud": {true, unix.MS_SILENT},
"mand": {false, unix.MS_MANDLOCK},
"noacl": {true, unix.MS_POSIXACL},
"noatime": {false, unix.MS_NOATIME},
"nodev": {false, unix.MS_NODEV},
"nodiratime": {false, unix.MS_NODIRATIME},
"noexec": {false, unix.MS_NOEXEC},
"noiversion": {true, unix.MS_I_VERSION},
"nolazytime": {true, unix.MS_LAZYTIME},
"nomand": {true, unix.MS_MANDLOCK},
"norelatime": {true, unix.MS_RELATIME},
"nostrictatime": {true, unix.MS_STRICTATIME},
"nosuid": {false, unix.MS_NOSUID},
"rbind": {false, unix.MS_BIND | unix.MS_REC},
"relatime": {false, unix.MS_RELATIME},
"remount": {false, unix.MS_REMOUNT},
"ro": {false, unix.MS_RDONLY},
"rw": {true, unix.MS_RDONLY},
"silent": {false, unix.MS_SILENT},
"strictatime": {false, unix.MS_STRICTATIME},
"suid": {true, unix.MS_NOSUID},
"sync": {false, unix.MS_SYNCHRONOUS},
}
extensionFlags = map[string]struct {
clear bool
flag int
}{
"tmpcopyup": {false, configs.EXT_COPYUP},
}
})
}

// AllowedDevices is the set of devices which are automatically included for
Expand Down Expand Up @@ -256,6 +315,8 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) {
config.Cgroups = c
// set linux-specific config
if spec.Linux != nil {
initMaps()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the improvement that's brought by this, but (as a follow-up) perhaps we should consider having utilities / accessor functions for these variables, so that the initialisation happens more 'organically' (reducing the cognitive overload of having to thing about first calling initMap() whenever we need to use one of the variables.

In this case it could be a function to check if foo exists in mountPropagationMapping

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean something like

func getNamespaceMapping() map[specs.LinuxNamespaceType]configs.NamespaceType {
    initMaps()
    return namespaceMapping
}

// ... and so on for every other map we lazy init.
....

nsMap := getNamespaceMapping()

To me it looks like a bloat, since the nsMap := getNamespaceMapping() is just a way to sugar-coat initMaps().

A runtime assertion would be nice. If only accessing a nil map would panic....

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of anything better than

if namespaceMapping == nil {
   panic("uninitialied map") // we have a bug in the code
}

but it's still inferior to

initMaps()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some kind of a linter to which we can add a rule (in the source code) like "X should be called before using Y" would also be nice to have.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, a lazy way to initialize variables. IOW same as this PR does, but automatically (perhaps via an annotation).

Copy link
Contributor Author

@kolyshkin kolyshkin Nov 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Someone worked on something like this -- alas it does not look good without generics. https://github.com/zetamatta/go-lazy

I guess we have to postpone it till Go 2.0 ))))


var exists bool
if config.RootPropagation, exists = mountPropagationMapping[spec.Linux.RootfsPropagation]; !exists {
return nil, fmt.Errorf("rootfsPropagation=%v is not supported", spec.Linux.RootfsPropagation)
Expand Down Expand Up @@ -332,33 +393,38 @@ func createLibcontainerMount(cwd string, m specs.Mount) (*configs.Mount, error)
// return nil, fmt.Errorf("mount destination %s is not absolute", m.Destination)
logrus.Warnf("mount destination %s is not absolute. Support for non-absolute mount destinations will be removed in a future release.", m.Destination)
}
flags, pgflags, data, ext := parseMountOptions(m.Options)
source := m.Source
device := m.Type
if flags&unix.MS_BIND != 0 {
mnt := parseMountOptions(m.Options)

mnt.Destination = m.Destination
mnt.Source = m.Source
mnt.Device = m.Type
if mnt.Flags&unix.MS_BIND != 0 {
// Any "type" the user specified is meaningless (and ignored) for
// bind-mounts -- so we set it to "bind" because rootfs_linux.go
// (incorrectly) relies on this for some checks.
device = "bind"
if !filepath.IsAbs(source) {
source = filepath.Join(cwd, m.Source)
}
}
return &configs.Mount{
Device: device,
Source: source,
Destination: m.Destination,
Data: data,
Flags: flags,
PropagationFlags: pgflags,
Extensions: ext,
}, nil
mnt.Device = "bind"
if !filepath.IsAbs(mnt.Source) {
mnt.Source = filepath.Join(cwd, m.Source)
}
}
return mnt, nil
}

// systemd property name check: latin letters only, at least 3 of them
var isValidName = regexp.MustCompile(`^[a-zA-Z]{3,}$`).MatchString

var isSecSuffix = regexp.MustCompile(`[a-z]Sec$`).MatchString
// isValidName checks if systemd property name is valid. It should consists
// of latin letters only, and have least 3 of them.
func isValidName(s string) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to my other comment (no need to fix in this PR); perhaps would be nice to have this return an error with the reason for it being invalid (too short or invalid character)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense; opened #3285

if len(s) < 3 {
return false
}
// Check ASCII characters rather than Unicode runes.
for _, ch := range s {
if (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') {
continue
}
return false
}
return true
}

// Some systemd properties are documented as having "Sec" suffix
// (e.g. TimeoutStopSec) but are expected to have "USec" suffix
Expand Down Expand Up @@ -406,11 +472,16 @@ func initSystemdProps(spec *specs.Spec) ([]systemdDbus.Property, error) {
if err != nil {
return nil, fmt.Errorf("Annotation %s=%s value parse error: %w", k, v, err)
}
if isSecSuffix(name) {
name = strings.TrimSuffix(name, "Sec") + "USec"
value, err = convertSecToUSec(value)
if err != nil {
return nil, fmt.Errorf("Annotation %s=%s value parse error: %w", k, v, err)
// Check for Sec suffix.
if trimName := strings.TrimSuffix(name, "Sec"); len(trimName) < len(name) {
// Check for a lowercase ascii a-z just before Sec.
if ch := trimName[len(trimName)-1]; ch >= 'a' && ch <= 'z' {
// Convert from Sec to USec.
name = trimName + "USec"
value, err = convertSecToUSec(value)
if err != nil {
return nil, fmt.Errorf("Annotation %s=%s value parse error: %w", k, v, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not something new, so probably better to fix separately, but this should've been lowercase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you aware of any linter for such things? Would be nice to have

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect the revive linter to be warning about it; 2cca114

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(that's a commit from #2998)

}
}
}
sp = append(sp, systemdDbus.Property{Name: name, Value: value})
Expand Down Expand Up @@ -759,92 +830,38 @@ func setupUserNamespace(spec *specs.Spec, config *configs.Config) error {
return nil
}

// parseMountOptions parses the string and returns the flags, propagation
// flags and any mount data that it contains.
func parseMountOptions(options []string) (int, []int, string, int) {
// parseMountOptions parses options and returns a configs.Mount
// structure with fields that depends on options set accordingly.
func parseMountOptions(options []string) *configs.Mount {
var (
flag int
pgflag []int
data []string
extFlags int
data []string
m configs.Mount
)
flags := map[string]struct {
clear bool
flag int
}{
"acl": {false, unix.MS_POSIXACL},
"async": {true, unix.MS_SYNCHRONOUS},
"atime": {true, unix.MS_NOATIME},
"bind": {false, unix.MS_BIND},
"defaults": {false, 0},
"dev": {true, unix.MS_NODEV},
"diratime": {true, unix.MS_NODIRATIME},
"dirsync": {false, unix.MS_DIRSYNC},
"exec": {true, unix.MS_NOEXEC},
"iversion": {false, unix.MS_I_VERSION},
"lazytime": {false, unix.MS_LAZYTIME},
"loud": {true, unix.MS_SILENT},
"mand": {false, unix.MS_MANDLOCK},
"noacl": {true, unix.MS_POSIXACL},
"noatime": {false, unix.MS_NOATIME},
"nodev": {false, unix.MS_NODEV},
"nodiratime": {false, unix.MS_NODIRATIME},
"noexec": {false, unix.MS_NOEXEC},
"noiversion": {true, unix.MS_I_VERSION},
"nolazytime": {true, unix.MS_LAZYTIME},
"nomand": {true, unix.MS_MANDLOCK},
"norelatime": {true, unix.MS_RELATIME},
"nostrictatime": {true, unix.MS_STRICTATIME},
"nosuid": {false, unix.MS_NOSUID},
"rbind": {false, unix.MS_BIND | unix.MS_REC},
"relatime": {false, unix.MS_RELATIME},
"remount": {false, unix.MS_REMOUNT},
"ro": {false, unix.MS_RDONLY},
"rw": {true, unix.MS_RDONLY},
"silent": {false, unix.MS_SILENT},
"strictatime": {false, unix.MS_STRICTATIME},
"suid": {true, unix.MS_NOSUID},
"sync": {false, unix.MS_SYNCHRONOUS},
}
propagationFlags := map[string]int{
"private": unix.MS_PRIVATE,
"shared": unix.MS_SHARED,
"slave": unix.MS_SLAVE,
"unbindable": unix.MS_UNBINDABLE,
"rprivate": unix.MS_PRIVATE | unix.MS_REC,
"rshared": unix.MS_SHARED | unix.MS_REC,
"rslave": unix.MS_SLAVE | unix.MS_REC,
"runbindable": unix.MS_UNBINDABLE | unix.MS_REC,
}
extensionFlags := map[string]struct {
clear bool
flag int
}{
"tmpcopyup": {false, configs.EXT_COPYUP},
}
initMaps()
for _, o := range options {
// If the option does not exist in the flags table or the flag
// is not supported on the platform,
// then it is a data value for a specific fs type
if f, exists := flags[o]; exists && f.flag != 0 {
// If the option does not exist in the mountFlags table,
// or the flag is not supported on the platform,
// then it is a data value for a specific fs type.
if f, exists := mountFlags[o]; exists && f.flag != 0 {
if f.clear {
flag &= ^f.flag
m.Flags &= ^f.flag
} else {
flag |= f.flag
m.Flags |= f.flag
}
} else if f, exists := propagationFlags[o]; exists && f != 0 {
pgflag = append(pgflag, f)
} else if f, exists := mountPropagationMapping[o]; exists && f != 0 {
m.PropagationFlags = append(m.PropagationFlags, f)
} else if f, exists := extensionFlags[o]; exists && f.flag != 0 {
if f.clear {
extFlags &= ^f.flag
m.Extensions &= ^f.flag
} else {
extFlags |= f.flag
m.Extensions |= f.flag
}
} else {
data = append(data, o)
}
}
return flag, pgflag, strings.Join(data, ","), extFlags
m.Data = strings.Join(data, ",")
return &m
}

func SetupSeccomp(config *specs.LinuxSeccomp) (*configs.Seccomp, error) {
Expand Down
38 changes: 38 additions & 0 deletions libcontainer/specconv/spec_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,11 @@ func TestInitSystemdProps(t *testing.T) {
in: inT{"org.systemd.property.TimeoutStopSec", "'covfefe'"},
exp: expT{true, "", ""},
},
{
desc: "convert USec to Sec (bad variable name, no conversion)",
in: inT{"org.systemd.property.FOOSec", "123"},
exp: expT{false, "FOOSec", 123},
},
{
in: inT{"org.systemd.property.CollectMode", "'inactive-or-failed'"},
exp: expT{false, "CollectMode", "inactive-or-failed"},
Expand Down Expand Up @@ -757,6 +762,39 @@ func TestInitSystemdProps(t *testing.T) {
}
}

func TestIsValidName(t *testing.T) {
testCases := []struct {
in string
valid bool
}{
{"", false}, // too short
{"xx", false}, // too short
{"xxx", true},
{"someValidName", true},
{"A name", false}, // space
{"3335", false}, // numbers
{"Name1", false}, // numbers
{"Кир", false}, // non-ascii
{"მადლობა", false}, // non-ascii
{"合い言葉", false}, // non-ascii
}

for _, tc := range testCases {
valid := isValidName(tc.in)
if valid != tc.valid {
t.Errorf("case %q: expected %v, got %v", tc.in, tc.valid, valid)
}
}
}

func BenchmarkIsValidName(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, s := range []string{"", "xx", "xxx", "someValidName", "A name", "Кир", "მადლობა", "合い言葉"} {
_ = isValidName(s)
}
}
}

func TestNullProcess(t *testing.T) {
spec := Example()
spec.Process = nil
Expand Down