diff -Nru snapd-2.22.6+16.10/asserts/asserts.go snapd-2.23.1+16.10/asserts/asserts.go --- snapd-2.22.6+16.10/asserts/asserts.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/asserts/asserts.go 2017-03-06 13:33:50.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -103,7 +103,10 @@ func init() { // register maxSupportedFormats while breaking initialisation loop - maxSupportedFormat[SnapDeclarationType.Name] = 1 + + // 1: plugs and slots + // 2: support for $SLOT()/$PLUG()/$MISSING + maxSupportedFormat[SnapDeclarationType.Name] = 2 } func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) { @@ -125,7 +128,11 @@ // no analyzer, format 0 is all there is return 0, nil } - return analyzer(headers, body) + formatnum, err = analyzer(headers, body) + if err != nil { + return 0, fmt.Errorf("assertion %s: %v", assertType.Name, err) + } + return formatnum, nil } // Ref expresses a reference to an assertion. @@ -324,15 +331,17 @@ // where: // // HEADER is a set of header entries separated by "\n" -// BODY can be arbitrary, +// BODY can be arbitrary text, // SIGNATURE is the signature // +// Both BODY and HEADER must be UTF8. +// // A header entry for a single line value (no '\n' in it) looks like: // // NAME ": " SIMPLEVALUE // // The format supports multiline text values (with '\n's in them) and -// lists possibly nested with string scalars in them. +// lists or maps, possibly nested, with string scalars in them. // // For those a header entry looks like: // @@ -341,12 +350,17 @@ // where MULTI can be // // * (baseindent + 4)-space indented value (multiline text) +// // * entries of a list each of the form: // -// " "*baseindent " -" ( " " SIMPLEVALUE | "\n" MULTI ) +// " "*baseindent " -" ( " " SIMPLEVALUE | "\n" MULTI ) +// +// * entries of map each of the form: +// +// " "*baseindent " " NAME ":" ( " " SIMPLEVALUE | "\n" MULTI ) // // baseindent starts at 0 and then grows with nesting matching the -// previous level introduction (the " "*baseindent " -" bit) +// previous level introduction (e.g. the " "*baseindent " -" bit) // length minus 1. // // In general the following headers are mandatory: @@ -362,8 +376,10 @@ // // revision (a positive int) // body-length (expected to be equal to the length of BODY) +// format (a positive int for the format iteration of the type used) // // Times are expected to be in the RFC3339 format: "2006-01-02T15:04:05Z07:00". +// func Decode(serializedAssertion []byte) (Assertion, error) { // copy to get an independent backstorage that can't be mutated later assertionSnapshot := make([]byte, len(serializedAssertion)) diff -Nru snapd-2.22.6+16.10/asserts/device_asserts.go snapd-2.23.1+16.10/asserts/device_asserts.go --- snapd-2.22.6+16.10/asserts/device_asserts.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/asserts/device_asserts.go 2017-03-02 06:37:54.000000000 +0000 @@ -30,6 +30,7 @@ // about the properties of a device model. type Model struct { assertionBase + classic bool requiredSnaps []string sysUserAuthority []string timestamp time.Time @@ -60,6 +61,11 @@ return mod.HeaderString("series") } +// Classic returns whether the model is a classic system. +func (mod *Model) Classic() bool { + return mod.classic +} + // Architecture returns the archicteture the model is based on. func (mod *Model) Architecture() string { return mod.HeaderString("architecture") @@ -153,7 +159,10 @@ return nil, fmt.Errorf("%q header must be '*' or a list of account ids", name) } -var modelMandatory = []string{"architecture", "gadget", "kernel"} +var ( + modelMandatory = []string{"architecture", "gadget", "kernel"} + classicModelOptional = []string{"architecture", "gadget"} +) func assembleModel(assert assertionBase) (Assertion, error) { err := checkAuthorityMatchesBrand(&assert) @@ -166,8 +175,26 @@ return nil, err } - for _, mandatory := range modelMandatory { - if _, err := checkNotEmptyString(assert.headers, mandatory); err != nil { + classic, err := checkOptionalBool(assert.headers, "classic") + if err != nil { + return nil, err + } + + if classic { + if _, ok := assert.headers["kernel"]; ok { + return nil, fmt.Errorf("cannot specify a kernel with a classic model") + } + } + + checker := checkNotEmptyString + toCheck := modelMandatory + if classic { + checker = checkOptionalString + toCheck = classicModelOptional + } + + for _, h := range toCheck { + if _, err := checker(assert.headers, h); err != nil { return nil, err } } @@ -209,6 +236,7 @@ // ignore extra headers and non-empty body for future compatibility return &Model{ assertionBase: assert, + classic: classic, requiredSnaps: reqSnaps, sysUserAuthority: sysUserAuthority, timestamp: timestamp, diff -Nru snapd-2.22.6+16.10/asserts/device_asserts_test.go snapd-2.23.1+16.10/asserts/device_asserts_test.go --- snapd-2.22.6+16.10/asserts/device_asserts_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/asserts/device_asserts_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -48,23 +48,42 @@ sysUserAuths = "system-user-authority: *\n" ) -const modelExample = "type: model\n" + - "authority-id: brand-id1\n" + - "series: 16\n" + - "brand-id: brand-id1\n" + - "model: baz-3000\n" + - "display-name: Baz 3000\n" + - "architecture: amd64\n" + - "gadget: brand-gadget\n" + - "kernel: baz-linux\n" + - "store: brand-store\n" + - sysUserAuths + - reqSnaps + - "TSLINE" + - "body-length: 0\n" + - "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + - "\n\n" + - "AXNpZw==" +const ( + modelExample = "type: model\n" + + "authority-id: brand-id1\n" + + "series: 16\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "display-name: Baz 3000\n" + + "architecture: amd64\n" + + "gadget: brand-gadget\n" + + "kernel: baz-linux\n" + + "store: brand-store\n" + + sysUserAuths + + reqSnaps + + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + classicModelExample = "type: model\n" + + "authority-id: brand-id1\n" + + "series: 16\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "display-name: Baz 3000\n" + + "classic: true\n" + + "architecture: amd64\n" + + "gadget: brand-gadget\n" + + "store: brand-store\n" + + reqSnaps + + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" +) func (mods *modelSuite) TestDecodeOK(c *C) { encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) @@ -219,6 +238,56 @@ c.Assert(err, ErrorMatches, `model assertion timestamp outside of signing key validity \(key valid since.*\)`) } +func (mods *modelSuite) TestClassicDecodeOK(c *C) { + encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.AuthorityID(), Equals, "brand-id1") + c.Check(model.Timestamp(), Equals, mods.ts) + c.Check(model.Series(), Equals, "16") + c.Check(model.BrandID(), Equals, "brand-id1") + c.Check(model.Model(), Equals, "baz-3000") + c.Check(model.DisplayName(), Equals, "Baz 3000") + c.Check(model.Classic(), Equals, true) + c.Check(model.Architecture(), Equals, "amd64") + c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.Kernel(), Equals, "") + c.Check(model.Store(), Equals, "brand-store") + c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"}) +} + +func (mods *modelSuite) TestClassicDecodeInvalid(c *C) { + encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1) + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"classic: true\n", "classic: foo\n", `"classic" header must be 'true' or 'false'`}, + {"architecture: amd64\n", "architecture:\n - foo\n", `"architecture" header must be a string`}, + {"gadget: brand-gadget\n", "gadget:\n - foo\n", `"gadget" header must be a string`}, + {"gadget: brand-gadget\n", "kernel: brand-kernel\n", `cannot specify a kernel with a classic model`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr) + } +} + +func (mods *modelSuite) TestClassicDecodeGadgetAndArchOptional(c *C) { + encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "gadget: brand-gadget\n", "", 1) + encoded = strings.Replace(encoded, "architecture: amd64\n", "", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.Classic(), Equals, true) + c.Check(model.Architecture(), Equals, "") + c.Check(model.Gadget(), Equals, "") +} + type serialSuite struct { ts time.Time tsLine string diff -Nru snapd-2.22.6+16.10/asserts/export_test.go snapd-2.23.1+16.10/asserts/export_test.go --- snapd-2.22.6+16.10/asserts/export_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/asserts/export_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -188,3 +188,11 @@ CompilePlugRule = compilePlugRule CompileSlotRule = compileSlotRule ) + +type featureExposer interface { + feature(flabel string) bool +} + +func RuleFeature(rule featureExposer, flabel string) bool { + return rule.feature(flabel) +} diff -Nru snapd-2.22.6+16.10/asserts/ifacedecls.go snapd-2.23.1+16.10/asserts/ifacedecls.go --- snapd-2.22.6+16.10/asserts/ifacedecls.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/asserts/ifacedecls.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015 Canonical Ltd + * Copyright (C) 2015-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -22,13 +22,27 @@ import ( "errors" "fmt" + "reflect" "regexp" "strconv" "strings" ) +// AttrMatchContext has contextual helpers for evaluating attribute constraints. +type AttrMatchContext interface { + PlugAttr(arg string) (interface{}, error) + SlotAttr(arg string) (interface{}, error) +} + +const ( + // feature label for $SLOT()/$PLUG()/$MISSING + dollarAttrConstraintsFeature = "dollar-attr-constraints" +) + type attrMatcher interface { - match(apath string, v interface{}) error + match(apath string, v interface{}, ctx AttrMatchContext) error + + feature(flabel string) bool } func chain(path, k string) string { @@ -78,6 +92,12 @@ if !cc.hadMap { return nil, fmt.Errorf("first level of non alternative constraints must be a set of key-value contraints") } + if strings.HasPrefix(x, "$") { + if x == "$MISSING" { + return missingAttrMatcher{}, nil + } + return compileEvalAttrMatcher(cc, x) + } return compileRegexpAttrMatcher(cc, x) default: return nil, fmt.Errorf("constraint %q must be a key-value map, regexp or a list of alternative constraints: %v", cc, x) @@ -98,42 +118,111 @@ return matcher, nil } -func matchEntry(apath, k string, matcher1 attrMatcher, v interface{}) error { +func matchEntry(apath, k string, matcher1 attrMatcher, v interface{}, ctx AttrMatchContext) error { apath = chain(apath, k) - if v == nil { + // every entry matcher expects the attribute to be set except for $MISSING + if _, ok := matcher1.(missingAttrMatcher); !ok && v == nil { return fmt.Errorf("attribute %q has constraints but is unset", apath) } - if err := matcher1.match(apath, v); err != nil { + if err := matcher1.match(apath, v, ctx); err != nil { return err } return nil } -func matchList(apath string, matcher attrMatcher, l []interface{}) error { +func matchList(apath string, matcher attrMatcher, l []interface{}, ctx AttrMatchContext) error { for i, elem := range l { - if err := matcher.match(chain(apath, strconv.Itoa(i)), elem); err != nil { + if err := matcher.match(chain(apath, strconv.Itoa(i)), elem, ctx); err != nil { return err } } return nil } -func (matcher mapAttrMatcher) match(apath string, v interface{}) error { +func (matcher mapAttrMatcher) feature(flabel string) bool { + for _, matcher1 := range matcher { + if matcher1.feature(flabel) { + return true + } + } + return false +} + +func (matcher mapAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { switch x := v.(type) { case map[string]interface{}: // maps in attributes look like this for k, matcher1 := range matcher { - if err := matchEntry(apath, k, matcher1, x[k]); err != nil { + if err := matchEntry(apath, k, matcher1, x[k], ctx); err != nil { return err } } case []interface{}: - return matchList(apath, matcher, x) + return matchList(apath, matcher, x, ctx) default: return fmt.Errorf("attribute %q must be a map", apath) } return nil } +type missingAttrMatcher struct{} + +func (matcher missingAttrMatcher) feature(flabel string) bool { + return flabel == dollarAttrConstraintsFeature +} + +func (matcher missingAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + if v != nil { + return fmt.Errorf("attribute %q is constrained to be missing but is set", apath) + } + return nil +} + +type evalAttrMatcher struct { + // first iteration supports just $(SLOT|PLUG)(arg) + op string + arg string +} + +var ( + validEvalAttrMatcher = regexp.MustCompile(`^\$(SLOT|PLUG)\((.+)\)$`) +) + +func compileEvalAttrMatcher(cc compileContext, s string) (attrMatcher, error) { + ops := validEvalAttrMatcher.FindStringSubmatch(s) + if len(ops) == 0 { + return nil, fmt.Errorf("cannot compile %q constraint %q: not a valid $SLOT()/$PLUG() constraint", cc, s) + } + return evalAttrMatcher{ + op: ops[1], + arg: ops[2], + }, nil +} + +func (matcher evalAttrMatcher) feature(flabel string) bool { + return flabel == dollarAttrConstraintsFeature +} + +func (matcher evalAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + if ctx == nil { + return fmt.Errorf("attribute %q cannot be matched without context", apath) + } + var comp func(string) (interface{}, error) + switch matcher.op { + case "SLOT": + comp = ctx.SlotAttr + case "PLUG": + comp = ctx.PlugAttr + } + v1, err := comp(matcher.arg) + if err != nil { + return fmt.Errorf("attribute %q constraint $%s(%s) cannot be evaluated: %v", apath, matcher.op, matcher.arg, err) + } + if !reflect.DeepEqual(v, v1) { + return fmt.Errorf("attribute %q does not match $%s(%s): %v != %v", apath, matcher.op, matcher.arg, v, v1) + } + return nil +} + type regexpAttrMatcher struct { *regexp.Regexp } @@ -146,7 +235,11 @@ return regexpAttrMatcher{rx}, nil } -func (matcher regexpAttrMatcher) match(apath string, v interface{}) error { +func (matcher regexpAttrMatcher) feature(flabel string) bool { + return false +} + +func (matcher regexpAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { var s string switch x := v.(type) { case string: @@ -156,7 +249,7 @@ case int64: s = strconv.FormatInt(x, 10) case []interface{}: - return matchList(apath, matcher, x) + return matchList(apath, matcher, x, ctx) default: return fmt.Errorf("attribute %q must be a scalar or list", apath) } @@ -184,10 +277,19 @@ } -func (matcher altAttrMatcher) match(apath string, v interface{}) error { +func (matcher altAttrMatcher) feature(flabel string) bool { + for _, alt := range matcher.alts { + if alt.feature(flabel) { + return true + } + } + return false +} + +func (matcher altAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { var firstErr error for _, alt := range matcher.alts { - err := alt.match(apath, v) + err := alt.match(apath, v, ctx) if err == nil { return nil } @@ -207,6 +309,10 @@ matcher attrMatcher } +func (ac *AttributeConstraints) feature(flabel string) bool { + return ac.matcher.feature(flabel) +} + // compileAttributeConstraints checks and compiles a mapping or list // from the assertion format into AttributeConstraints. func compileAttributeConstraints(constraints interface{}) (*AttributeConstraints, error) { @@ -221,7 +327,11 @@ result error } -func (matcher fixedAttrMatcher) match(apath string, v interface{}) error { +func (matcher fixedAttrMatcher) feature(flabel string) bool { + return false +} + +func (matcher fixedAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { return matcher.result } @@ -231,8 +341,8 @@ ) // Check checks whether attrs don't match the constraints. -func (c *AttributeConstraints) Check(attrs map[string]interface{}) error { - return c.matcher.match("", attrs) +func (c *AttributeConstraints) Check(attrs map[string]interface{}, ctx AttrMatchContext) error { + return c.matcher.match("", attrs, ctx) } // OnClassicConstraint specifies a constraint based whether the system is classic and optional specific distros' sets. @@ -438,6 +548,26 @@ DenyAutoConnection []*PlugConnectionConstraints } +func (r *PlugRule) feature(flabel string) bool { + for _, cs := range [][]*PlugInstallationConstraints{r.AllowInstallation, r.DenyInstallation} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + for _, cs := range [][]*PlugConnectionConstraints{r.AllowConnection, r.DenyConnection, r.AllowAutoConnection, r.DenyAutoConnection} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + return false +} + func castPlugInstallationConstraints(cstrs []constraintsHolder) (res []*PlugInstallationConstraints) { res = make([]*PlugInstallationConstraints, len(cstrs)) for i, cstr := range cstrs { @@ -496,6 +626,10 @@ OnClassic *OnClassicConstraint } +func (c *PlugInstallationConstraints) feature(flabel string) bool { + return c.PlugAttributes.feature(flabel) +} + func (c *PlugInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { switch field { case "plug-attributes": @@ -541,6 +675,10 @@ OnClassic *OnClassicConstraint } +func (c *PlugConnectionConstraints) feature(flabel string) bool { + return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) +} + func (c *PlugConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { switch field { case "plug-attributes": @@ -649,6 +787,26 @@ return res } +func (r *SlotRule) feature(flabel string) bool { + for _, cs := range [][]*SlotInstallationConstraints{r.AllowInstallation, r.DenyInstallation} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + for _, cs := range [][]*SlotConnectionConstraints{r.AllowConnection, r.DenyConnection, r.AllowAutoConnection, r.DenyAutoConnection} { + for _, c := range cs { + if c.feature(flabel) { + return true + } + } + } + + return false +} + func castSlotConnectionConstraints(cstrs []constraintsHolder) (res []*SlotConnectionConstraints) { res = make([]*SlotConnectionConstraints, len(cstrs)) for i, cstr := range cstrs { @@ -700,6 +858,10 @@ OnClassic *OnClassicConstraint } +func (c *SlotInstallationConstraints) feature(flabel string) bool { + return c.SlotAttributes.feature(flabel) +} + func (c *SlotInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { switch field { case "slot-attributes": @@ -745,6 +907,10 @@ OnClassic *OnClassicConstraint } +func (c *SlotConnectionConstraints) feature(flabel string) bool { + return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) +} + func (c *SlotConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { switch field { case "plug-attributes": diff -Nru snapd-2.22.6+16.10/asserts/ifacedecls_test.go snapd-2.23.1+16.10/asserts/ifacedecls_test.go --- snapd-2.22.6+16.10/asserts/ifacedecls_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/asserts/ifacedecls_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015 Canonical Ltd + * Copyright (C) 2015-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -21,6 +21,7 @@ import ( "fmt" + "regexp" . "gopkg.in/check.v1" "gopkg.in/yaml.v2" @@ -71,20 +72,20 @@ "foo": "FOO", "bar": "BAR", "baz": "BAZ", - }) + }, nil) c.Check(err, IsNil) err = cstrs.Check(map[string]interface{}{ "foo": "FOO", "bar": "BAZ", "baz": "BAZ", - }) + }, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BAZ" does not match \^\(BAR\)\$`) err = cstrs.Check(map[string]interface{}{ "foo": "FOO", "baz": "BAZ", - }) + }, nil) c.Check(err, ErrorMatches, `attribute "bar" has constraints but is unset`) } @@ -98,27 +99,27 @@ err = cstrs.Check(map[string]interface{}{ "bar": "BAR", - }) + }, nil) c.Check(err, IsNil) err = cstrs.Check(map[string]interface{}{ "bar": "BARR", - }) + }, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BARR" does not match \^\(BAR|BAZ\)\$`) err = cstrs.Check(map[string]interface{}{ "bar": "BBAZ", - }) + }, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BAZZ" does not match \^\(BAR|BAZ\)\$`) err = cstrs.Check(map[string]interface{}{ "bar": "BABAZ", - }) + }, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BABAZ" does not match \^\(BAR|BAZ\)\$`) err = cstrs.Check(map[string]interface{}{ "bar": "BARAZ", - }) + }, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BARAZ" does not match \^\(BAR|BAZ\)\$`) } @@ -140,14 +141,14 @@ bar2: BAR2 bar3: BAR3 baz: BAZ -`)) +`), nil) c.Check(err, IsNil) err = cstrs.Check(attrs(` foo: FOO bar: BAZ baz: BAZ -`)) +`), nil) c.Check(err, ErrorMatches, `attribute "bar" must be a map`) err = cstrs.Check(attrs(` @@ -157,7 +158,7 @@ bar2: BAR22 bar3: BAR3 baz: BAZ -`)) +`), nil) c.Check(err, ErrorMatches, `attribute "bar\.bar2" value "BAR22" does not match \^\(BAR2\)\$`) err = cstrs.Check(attrs(` @@ -168,7 +169,7 @@ bar22: true bar3: BAR3 baz: BAZ -`)) +`), nil) c.Check(err, ErrorMatches, `attribute "bar\.bar2" must be a scalar or list`) } @@ -189,21 +190,21 @@ "foo": "FOO", "bar": "BAR", "baz": "BAZ", - }) + }, nil) c.Check(err, IsNil) err = cstrs.Check(map[string]interface{}{ "foo": "FOO", "bar": "BAZ", "baz": "BAZ", - }) + }, nil) c.Check(err, IsNil) err = cstrs.Check(map[string]interface{}{ "foo": "FOO", "bar": "BARR", "baz": "BAR", - }) + }, nil) c.Check(err, ErrorMatches, `no alternative matches: attribute "bar" value "BARR" does not match \^\(BAR\)\$`) } @@ -225,7 +226,7 @@ bar: bar1: BAR1 bar2: BAR2 -`)) +`), nil) c.Check(err, IsNil) err = cstrs.Check(attrs(` @@ -233,7 +234,7 @@ bar: bar1: BAR1 bar2: BAR22 -`)) +`), nil) c.Check(err, IsNil) err = cstrs.Check(attrs(` @@ -241,7 +242,7 @@ bar: bar1: BAR1 bar2: BAR3 -`)) +`), nil) c.Check(err, ErrorMatches, `no alternative for attribute "bar\.bar2" matches: attribute "bar\.bar2" value "BAR3" does not match \^\(BAR2\)\$`) } @@ -257,13 +258,13 @@ err = cstrs.Check(attrs(` foo: 1 bar: true -`)) +`), nil) c.Check(err, IsNil) err = cstrs.Check(map[string]interface{}{ "foo": int64(1), "bar": true, - }) + }, nil) c.Check(err, IsNil) } @@ -288,6 +289,21 @@ _, err = asserts.CompileAttributeConstraints([]interface{}{"FOO"}) c.Check(err, ErrorMatches, `first level of non alternative constraints must be a set of key-value contraints`) + + wrongDollarConstraints := []string{ + "$", + "$FOO(a)", + "$SLOT", + "$SLOT()", + } + + for _, wrong := range wrongDollarConstraints { + _, err := asserts.CompileAttributeConstraints(map[string]interface{}{ + "foo": wrong, + }) + c.Check(err, ErrorMatches, fmt.Sprintf(`cannot compile "foo" constraint "%s": not a valid \$SLOT\(\)/\$PLUG\(\) constraint`, regexp.QuoteMeta(wrong))) + + } } func (s *attrConstraintsSuite) TestMatchingListsSimple(c *C) { @@ -300,15 +316,107 @@ err = cstrs.Check(attrs(` foo: ["/foo/x", "/foo/y"] -`)) +`), nil) c.Check(err, IsNil) err = cstrs.Check(attrs(` foo: ["/foo/x", "/foo"] -`)) +`), nil) c.Check(err, ErrorMatches, `attribute "foo\.1" value "/foo" does not match \^\(/foo/\.\*\)\$`) } +func (s *attrConstraintsSuite) TestMissingCheck(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: $MISSING`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(cstrs, "dollar-attr-constraints"), Equals, true) + + err = cstrs.Check(attrs(` +bar: baz +`), nil) + c.Check(err, IsNil) + + err = cstrs.Check(attrs(` +foo: ["x"] +`), nil) + c.Check(err, ErrorMatches, `attribute "foo" is constrained to be missing but is set`) +} + +type testEvalAttr struct { + comp func(side string, arg string) (interface{}, error) +} + +func (ca testEvalAttr) SlotAttr(arg string) (interface{}, error) { + return ca.comp("slot", arg) +} + +func (ca testEvalAttr) PlugAttr(arg string) (interface{}, error) { + return ca.comp("plug", arg) +} + +func (s *attrConstraintsSuite) TestEvalCheck(c *C) { + m, err := asserts.ParseHeaders([]byte(`attrs: + foo: $SLOT(foo) + bar: $PLUG(bar.baz)`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(cstrs, "dollar-attr-constraints"), Equals, true) + + err = cstrs.Check(attrs(` +foo: foo +bar: bar +`), nil) + c.Check(err, ErrorMatches, `attribute "(foo|bar)" cannot be matched without context`) + + calls := make(map[[2]string]bool) + comp1 := func(op string, arg string) (interface{}, error) { + calls[[2]string{op, arg}] = true + return arg, nil + } + + err = cstrs.Check(attrs(` +foo: foo +bar: bar.baz +`), testEvalAttr{comp1}) + c.Check(err, IsNil) + + c.Check(calls, DeepEquals, map[[2]string]bool{ + {"slot", "foo"}: true, + {"plug", "bar.baz"}: true, + }) + + comp2 := func(op string, arg string) (interface{}, error) { + if op == "plug" { + return nil, fmt.Errorf("boom") + } + return arg, nil + } + + err = cstrs.Check(attrs(` +foo: foo +bar: bar.baz +`), testEvalAttr{comp2}) + c.Check(err, ErrorMatches, `attribute "bar" constraint \$PLUG\(bar\.baz\) cannot be evaluated: boom`) + + comp3 := func(op string, arg string) (interface{}, error) { + if op == "slot" { + return "other-value", nil + } + return arg, nil + } + + err = cstrs.Check(attrs(` +foo: foo +bar: bar.baz +`), testEvalAttr{comp3}) + c.Check(err, ErrorMatches, `attribute "foo" does not match \$SLOT\(foo\): foo != other-value`) +} + func (s *attrConstraintsSuite) TestMatchingListsMap(c *C) { m, err := asserts.ParseHeaders([]byte(`attrs: foo: @@ -320,21 +428,21 @@ err = cstrs.Check(attrs(` foo: [{p: "/foo/x"}, {p: "/foo/y"}] -`)) +`), nil) c.Check(err, IsNil) err = cstrs.Check(attrs(` foo: [{p: "zzz"}, {p: "/foo/y"}] -`)) +`), nil) c.Check(err, ErrorMatches, `attribute "foo\.0\.p" value "zzz" does not match \^\(/foo/\.\*\)\$`) } func (s *attrConstraintsSuite) TestAlwaysMatchAttributeConstraints(c *C) { - c.Check(asserts.AlwaysMatchAttributes.Check(nil), IsNil) + c.Check(asserts.AlwaysMatchAttributes.Check(nil, nil), IsNil) } func (s *attrConstraintsSuite) TestNeverMatchAttributeConstraints(c *C) { - c.Check(asserts.NeverMatchAttributes.Check(nil), NotNil) + c.Check(asserts.NeverMatchAttributes.Check(nil, nil), NotNil) } type plugSlotRulesSuite struct{} @@ -342,10 +450,10 @@ func checkAttrs(c *C, attrs *asserts.AttributeConstraints, witness, expected string) { c.Check(attrs.Check(map[string]interface{}{ witness: "XYZ", - }), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness)) + }, nil), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness)) c.Check(attrs.Check(map[string]interface{}{ witness: expected, - }), IsNil) + }, nil), IsNil) } func checkBoolPlugConnConstraints(c *C, cstrs []*asserts.PlugConnectionConstraints, always bool) { @@ -778,6 +886,50 @@ } } +func (s *plugSlotRulesSuite) TestPlugRuleFeatures(c *C) { + combos := []struct { + subrule string + attrConstraints []string + }{ + {"allow-installation", []string{"plug-attributes"}}, + {"deny-installation", []string{"plug-attributes"}}, + {"allow-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-connection", []string{"plug-attributes", "slot-attributes"}}, + {"allow-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + } + + for _, combo := range combos { + for _, attrConstr := range combo.attrConstraints { + attrConstraintMap := map[string]interface{}{ + "a": "ATTR", + "other": []interface{}{"x", "y"}, + } + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + attrConstr: attrConstraintMap, + }, + } + + rule, err := asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, false, Commentf("%v", ruleMap)) + + attrConstraintMap["a"] = "$MISSING" + rule, err = asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + + // covers also alternation + attrConstraintMap["a"] = []interface{}{"$SLOT(a)"} + rule, err = asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + + } + } +} + func (s *plugSlotRulesSuite) TestCompileSlotRuleAllAllowDenyStanzas(c *C) { m, err := asserts.ParseHeaders([]byte(`iface: allow-installation: @@ -1171,3 +1323,40 @@ c.Check(err, ErrorMatches, t.err, Commentf(t.stanza)) } } + +func (s *plugSlotRulesSuite) TestSlotRuleFeatures(c *C) { + combos := []struct { + subrule string + attrConstraints []string + }{ + {"allow-installation", []string{"slot-attributes"}}, + {"deny-installation", []string{"slot-attributes"}}, + {"allow-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-connection", []string{"plug-attributes", "slot-attributes"}}, + {"allow-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + {"deny-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + } + + for _, combo := range combos { + for _, attrConstr := range combo.attrConstraints { + attrConstraintMap := map[string]interface{}{ + "a": "ATTR", + } + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + attrConstr: attrConstraintMap, + }, + } + + rule, err := asserts.CompileSlotRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, false, Commentf("%v", ruleMap)) + + attrConstraintMap["a"] = "$PLUG(a)" + rule, err = asserts.CompileSlotRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) + + } + } +} diff -Nru snapd-2.22.6+16.10/asserts/snap_asserts.go snapd-2.23.1+16.10/asserts/snap_asserts.go --- snapd-2.22.6+16.10/asserts/snap_asserts.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/asserts/snap_asserts.go 2017-03-02 06:37:54.000000000 +0000 @@ -117,13 +117,63 @@ } } +func compilePlugRules(plugs map[string]interface{}, compiled func(iface string, plugRule *PlugRule)) error { + for iface, rule := range plugs { + plugRule, err := compilePlugRule(iface, rule) + if err != nil { + return err + } + compiled(iface, plugRule) + } + return nil +} + +func compileSlotRules(slots map[string]interface{}, compiled func(iface string, slotRule *SlotRule)) error { + for iface, rule := range slots { + slotRule, err := compileSlotRule(iface, rule) + if err != nil { + return err + } + compiled(iface, slotRule) + } + return nil +} + func snapDeclarationFormatAnalyze(headers map[string]interface{}, body []byte) (formatnum int, err error) { _, plugsOk := headers["plugs"] _, slotsOk := headers["slots"] - if plugsOk || slotsOk { - return 1, nil + if !(plugsOk || slotsOk) { + return 0, nil + } + formatnum = 1 + + plugs, err := checkMap(headers, "plugs") + if err != nil { + return 0, err } - return 0, nil + err = compilePlugRules(plugs, func(_ string, rule *PlugRule) { + if rule.feature(dollarAttrConstraintsFeature) { + formatnum = 2 + } + }) + if err != nil { + return 0, err + } + + slots, err := checkMap(headers, "slots") + if err != nil { + return 0, err + } + err = compileSlotRules(slots, func(_ string, rule *SlotRule) { + if rule.feature(dollarAttrConstraintsFeature) { + formatnum = 2 + } + }) + if err != nil { + return 0, err + } + + return formatnum, nil } var validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") @@ -159,12 +209,11 @@ } if plugs != nil { plugRules = make(map[string]*PlugRule, len(plugs)) - for iface, rule := range plugs { - plugRule, err := compilePlugRule(iface, rule) - if err != nil { - return nil, err - } - plugRules[iface] = plugRule + err := compilePlugRules(plugs, func(iface string, rule *PlugRule) { + plugRules[iface] = rule + }) + if err != nil { + return nil, err } } @@ -174,12 +223,11 @@ } if slots != nil { slotRules = make(map[string]*SlotRule, len(slots)) - for iface, rule := range slots { - slotRule, err := compileSlotRule(iface, rule) - if err != nil { - return nil, err - } - slotRules[iface] = slotRule + err := compileSlotRules(slots, func(iface string, rule *SlotRule) { + slotRules[iface] = rule + }) + if err != nil { + return nil, err } } @@ -563,12 +611,11 @@ } if plugs != nil { plugRules = make(map[string]*PlugRule, len(plugs)) - for iface, rule := range plugs { - plugRule, err := compilePlugRule(iface, rule) - if err != nil { - return nil, err - } - plugRules[iface] = plugRule + err := compilePlugRules(plugs, func(iface string, rule *PlugRule) { + plugRules[iface] = rule + }) + if err != nil { + return nil, err } } @@ -579,12 +626,11 @@ } if slots != nil { slotRules = make(map[string]*SlotRule, len(slots)) - for iface, rule := range slots { - slotRule, err := compileSlotRule(iface, rule) - if err != nil { - return nil, err - } - slotRules[iface] = slotRule + err := compileSlotRules(slots, func(iface string, rule *SlotRule) { + slotRules[iface] = rule + }) + if err != nil { + return nil, err } } diff -Nru snapd-2.22.6+16.10/asserts/snap_asserts_test.go snapd-2.23.1+16.10/asserts/snap_asserts_test.go --- snapd-2.22.6+16.10/asserts/snap_asserts_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/asserts/snap_asserts_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -265,23 +265,23 @@ c.Assert(plugRule1.DenyInstallation, HasLen, 1) c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) - c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) - c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) plugRule2 := snapDecl.PlugRule("interface2") c.Assert(plugRule2, NotNil) c.Assert(plugRule2.AllowInstallation, HasLen, 1) c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) c.Assert(plugRule2.AllowConnection, HasLen, 1) - c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) c.Assert(plugRule2.DenyConnection, HasLen, 1) - c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) slotRule3 := snapDecl.SlotRule("interface3") @@ -289,24 +289,24 @@ c.Assert(slotRule3.DenyInstallation, HasLen, 1) c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) - c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) - c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) slotRule4 := snapDecl.SlotRule("interface4") c.Assert(slotRule4, NotNil) c.Assert(slotRule4.AllowAutoConnection, HasLen, 1) - c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) c.Assert(slotRule4.DenyAutoConnection, HasLen, 1) - c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) c.Assert(slotRule4.AllowInstallation, HasLen, 1) - c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "e1".*`) c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) } @@ -332,6 +332,49 @@ fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) c.Assert(err, IsNil) c.Check(fmtnum, Equals, 1) + + headers = map[string]interface{}{ + "plugs": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "plug-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 2) + + headers = map[string]interface{}{ + "slots": map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + "plug-attributes": map[string]interface{}{ + "x": "$SLOT(x)", + }, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 2) + + // errors + headers = map[string]interface{}{ + "plugs": "what", + } + _, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, ErrorMatches, `assertion snap-declaration: "plugs" header must be a map`) + + headers = map[string]interface{}{ + "slots": "what", + } + _, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, ErrorMatches, `assertion snap-declaration: "slots" header must be a map`) } func prereqDevAccount(c *C, storeDB assertstest.SignerDB, db *asserts.Database) { @@ -1123,23 +1166,23 @@ c.Assert(plugRule1.DenyInstallation, HasLen, 1) c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) - c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) - c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) plugRule2 := baseDecl.PlugRule("interface2") c.Assert(plugRule2, NotNil) c.Assert(plugRule2.AllowInstallation, HasLen, 1) c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) c.Assert(plugRule2.AllowConnection, HasLen, 1) - c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) c.Assert(plugRule2.DenyConnection, HasLen, 1) - c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) slotRule3 := baseDecl.SlotRule("interface3") @@ -1147,24 +1190,24 @@ c.Assert(slotRule3.DenyInstallation, HasLen, 1) c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) - c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) - c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) slotRule4 := baseDecl.SlotRule("interface4") c.Assert(slotRule4, NotNil) c.Assert(slotRule4.AllowConnection, HasLen, 1) - c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) c.Assert(slotRule4.DenyConnection, HasLen, 1) - c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) c.Assert(slotRule4.AllowInstallation, HasLen, 1) - c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "e1".*`) c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) } diff -Nru snapd-2.22.6+16.10/client/client.go snapd-2.23.1+16.10/client/client.go --- snapd-2.22.6+16.10/client/client.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/client/client.go 2017-03-06 13:33:50.000000000 +0000 @@ -343,6 +343,8 @@ ErrorKindSnapAlreadyInstalled = "snap-already-installed" ErrorKindSnapNotInstalled = "snap-not-installed" ErrorKindNoUpdateAvailable = "snap-no-update-available" + + ErrorKindNotSnap = "snap-not-a-snap" ) // IsTwoFactorError returns whether the given error is due to problems diff -Nru snapd-2.22.6+16.10/client/login.go snapd-2.23.1+16.10/client/login.go --- snapd-2.22.6+16.10/client/login.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/client/login.go 2017-03-02 06:37:54.000000000 +0000 @@ -87,7 +87,7 @@ return u } -const authFileEnvKey = "SNAPPY_STORE_AUTH_DATA_FILENAME" +const authFileEnvKey = "SNAPD_AUTH_DATA_FILENAME" func storeAuthDataFilename(homeDir string) string { if fn := os.Getenv(authFileEnvKey); fn != "" { diff -Nru snapd-2.22.6+16.10/client/packages.go snapd-2.23.1+16.10/client/packages.go --- snapd-2.22.6+16.10/client/packages.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/client/packages.go 2017-03-08 13:28:17.000000000 +0000 @@ -52,6 +52,7 @@ TryMode bool `json:"trymode"` Apps []AppInfo `json:"apps"` Broken string `json:"broken"` + Contact string `json:"contact"` Prices map[string]float64 `json:"prices"` Screenshots []Screenshot `json:"screenshots"` diff -Nru snapd-2.22.6+16.10/cmd/autogen.sh snapd-2.23.1+16.10/cmd/autogen.sh --- snapd-2.22.6+16.10/cmd/autogen.sh 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/autogen.sh 2017-03-06 13:33:50.000000000 +0000 @@ -27,6 +27,16 @@ ubuntu) extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-ubuntu" ;; + fedora|centos|rhel) + extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor" + ;; + opensuse) + # NOTE: we need to disable apparmor as the version on OpenSUSE + # is too old to confine snap-confine and installed snaps + # themselves. This should be changed once all the kernel + # patches find their way into the distribution. + extra_opts="--libexecdir=/usr/lib/snapd --disable-apparmor" + ;; esac ./configure --enable-maintainer-mode --prefix=/usr $extra_opts diff -Nru snapd-2.22.6+16.10/cmd/cmd.go snapd-2.23.1+16.10/cmd/cmd.go --- snapd-2.22.6+16.10/cmd/cmd.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/cmd.go 2017-03-06 13:33:50.000000000 +0000 @@ -60,6 +60,13 @@ return } + // can we re-exec? some distributions will need extra work before re-exec really works. + switch release.ReleaseInfo.ID { + case "fedora", "centos", "rhel", "opensuse", "suse": + logger.Debugf("re-exec not supported on distro %q yet", release.ReleaseInfo.ID) + return + } + // did we already re-exec? if osutil.GetenvBool("SNAP_DID_REEXEC") { return diff -Nru snapd-2.22.6+16.10/cmd/configure.ac snapd-2.23.1+16.10/cmd/configure.ac --- snapd-2.22.6+16.10/cmd/configure.ac 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/configure.ac 2017-03-02 06:37:54.000000000 +0000 @@ -18,6 +18,8 @@ # Checks for header files. AC_CHECK_HEADERS([fcntl.h limits.h stdlib.h string.h sys/mount.h unistd.h]) +AC_CHECK_HEADERS([sys/quota.h], [], [AC_MSG_ERROR(sys/quota.h unavailable)]) +AC_CHECK_HEADERS([xfs/xqm.h], [], [AC_MSG_ERROR(xfs/xqm.h unavailable)]) # Checks for typedefs, structures, and compiler characteristics. AC_CHECK_HEADER_STDBOOL @@ -101,6 +103,9 @@ PKG_CHECK_MODULES([LIBUDEV], [libudev]) PKG_CHECK_MODULES([UDEV], [udev]) +# Check if libcap is available. +# PKG_CHECK_MODULES([LIBCAP], [libcap]) + # Enable special support for hosts with proprietary nvidia drivers on Ubuntu. AC_ARG_ENABLE([nvidia-ubuntu], AS_HELP_STRING([--enable-nvidia-ubuntu], [Support for proprietary nvidia drivers (Ubuntu)]), @@ -169,5 +174,9 @@ AM_CONDITIONAL([HAVE_SHELLCHECK], [test "x${HAVE_SHELLCHECK}" != "x"]) AS_IF([test "x$HAVE_SHELLCHECK" = "x"], [AC_MSG_WARN(["cannot find the shellcheck tool, will not run syntax checks for shell code"])]) +AC_PATH_PROG([HAVE_VALGRIND],[valgrind]) +AM_CONDITIONAL([HAVE_VALGRIND], [test "x${HAVE_VALGRIND}" != "x"]) +AS_IF([test "x$HAVE_VALGRIND" = "x"], [AC_MSG_WARN(["cannot find the valgrind tool, will not run unit tests through valgrind"])]) + AC_CONFIG_FILES([Makefile]) AC_OUTPUT diff -Nru snapd-2.22.6+16.10/cmd/decode-mount-opts/decode-mount-opts.c snapd-2.23.1+16.10/cmd/decode-mount-opts/decode-mount-opts.c --- snapd-2.22.6+16.10/cmd/decode-mount-opts/decode-mount-opts.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/decode-mount-opts/decode-mount-opts.c 2017-03-02 06:37:54.000000000 +0000 @@ -32,6 +32,7 @@ fprintf(stderr, "cannot parse given argument as a number\n"); return 1; } - printf("%#lx is %s\n", mountflags, sc_mount_opt2str(mountflags)); + char buf[1000]; + printf("%#lx is %s\n", mountflags, sc_mount_opt2str(buf, sizeof buf, mountflags)); return 0; } diff -Nru snapd-2.22.6+16.10/cmd/.indent.pro snapd-2.23.1+16.10/cmd/.indent.pro --- snapd-2.22.6+16.10/cmd/.indent.pro 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/.indent.pro 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,34 @@ +-nbad +-bap +-nbc +-bbo +-hnl +-br +-brs +-c33 +-cd33 +-ncdb +-ce +-ci4 +-cli0 +-d0 +-di1 +-nfc1 +-i8 +-ip0 +-l80 +-lp +-npcs +-nprs +-npsl +-sai +-saf +-saw +-ncs +-nsc +-sob +-nfca +-cp33 +-ss +-ts8 +-il1 diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/cleanup-funcs.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/cleanup-funcs.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/cleanup-funcs.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/cleanup-funcs.c 2017-03-02 06:37:54.000000000 +0000 @@ -22,29 +22,35 @@ void sc_cleanup_string(char **ptr) { - free(*ptr); + if (ptr != NULL) { + free(*ptr); + } } void sc_cleanup_file(FILE ** ptr) { - if (*ptr != NULL) + if (ptr != NULL && *ptr != NULL) { fclose(*ptr); + } } void sc_cleanup_endmntent(FILE ** ptr) { - if (*ptr != NULL) + if (ptr != NULL && *ptr != NULL) { endmntent(*ptr); + } } void sc_cleanup_closedir(DIR ** ptr) { - if (*ptr != NULL) { + if (ptr != NULL && *ptr != NULL) { closedir(*ptr); } } void sc_cleanup_close(int *ptr) { - close(*ptr); + if (ptr != NULL && *ptr != -1) { + close(*ptr); + } } diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/mountinfo.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/mountinfo.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/mountinfo.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/mountinfo.c 2017-03-08 13:28:17.000000000 +0000 @@ -21,11 +21,13 @@ #include #include -struct mountinfo { - struct mountinfo_entry *first; +#include "cleanup-funcs.h" + +struct sc_mountinfo { + struct sc_mountinfo_entry *first; }; -struct mountinfo_entry { +struct sc_mountinfo_entry { int mount_id; int parent_id; unsigned dev_major, dev_minor; @@ -37,7 +39,7 @@ char *mount_source; char *super_opts; - struct mountinfo_entry *next; + struct sc_mountinfo_entry *next; // Buffer holding all of the text data above. // // The buffer must be the last element of the structure. It is allocated @@ -66,119 +68,117 @@ * (10) mount source: filesystem specific information or "none" * (11) super options: per super block options **/ -static struct mountinfo_entry *parse_mountinfo_entry(const char *line) +static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) __attribute__ ((nonnull(1))); /** - * Free a mountinfo structure and all its entries. + * Free a sc_mountinfo structure and all its entries. **/ -static void free_mountinfo(struct mountinfo *info) +static void sc_free_mountinfo(struct sc_mountinfo *info) __attribute__ ((nonnull(1))); /** - * Free a mountinfo entry. + * Free a sc_mountinfo entry. **/ -static void free_mountinfo_entry(struct mountinfo_entry *entry) +static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); -static void cleanup_fclose(FILE ** ptr); -static void cleanup_free(char **ptr); - -struct mountinfo_entry *first_mountinfo_entry(struct mountinfo *info) +struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info) { return info->first; } -struct mountinfo_entry *next_mountinfo_entry(struct mountinfo_entry - *entry) +struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry + *entry) { return entry->next; } -int mountinfo_entry_mount_id(struct mountinfo_entry *entry) +int sc_mountinfo_entry_mount_id(struct sc_mountinfo_entry *entry) { return entry->mount_id; } -int mountinfo_entry_parent_id(struct mountinfo_entry *entry) +int sc_mountinfo_entry_parent_id(struct sc_mountinfo_entry *entry) { return entry->parent_id; } -unsigned mountinfo_entry_dev_major(struct mountinfo_entry *entry) +unsigned sc_mountinfo_entry_dev_major(struct sc_mountinfo_entry *entry) { return entry->dev_major; } -unsigned mountinfo_entry_dev_minor(struct mountinfo_entry *entry) +unsigned sc_mountinfo_entry_dev_minor(struct sc_mountinfo_entry *entry) { return entry->dev_minor; } -const char *mountinfo_entry_root(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_root(struct sc_mountinfo_entry *entry) { return entry->root; } -const char *mountinfo_entry_mount_dir(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_mount_dir(struct sc_mountinfo_entry *entry) { return entry->mount_dir; } -const char *mountinfo_entry_mount_opts(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_mount_opts(struct sc_mountinfo_entry *entry) { return entry->mount_opts; } -const char *mountinfo_entry_optional_fields(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_optional_fields(struct sc_mountinfo_entry *entry) { return entry->optional_fields; } -const char *mountinfo_entry_fs_type(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_fs_type(struct sc_mountinfo_entry *entry) { return entry->fs_type; } -const char *mountinfo_entry_mount_source(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_mount_source(struct sc_mountinfo_entry *entry) { return entry->mount_source; } -const char *mountinfo_entry_super_opts(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_super_opts(struct sc_mountinfo_entry *entry) { return entry->super_opts; } -struct mountinfo *parse_mountinfo(const char *fname) +struct sc_mountinfo *sc_parse_mountinfo(const char *fname) { - struct mountinfo *info = calloc(1, sizeof *info); + struct sc_mountinfo *info = calloc(1, sizeof *info); if (info == NULL) { return NULL; } if (fname == NULL) { fname = "/proc/self/mountinfo"; } - FILE *f __attribute__ ((cleanup(cleanup_fclose))) = fopen(fname, "rt"); + FILE *f __attribute__ ((cleanup(sc_cleanup_file))) = NULL; + f = fopen(fname, "rt"); if (f == NULL) { free(info); return NULL; } - char *line __attribute__ ((cleanup(cleanup_free))) = NULL; + char *line __attribute__ ((cleanup(sc_cleanup_string))) = NULL; size_t line_size = 0; - struct mountinfo_entry *entry, *last = NULL; + struct sc_mountinfo_entry *entry, *last = NULL; for (;;) { errno = 0; if (getline(&line, &line_size, f) == -1) { if (errno != 0) { - free_mountinfo(info); + sc_free_mountinfo(info); return NULL; } break; }; - entry = parse_mountinfo_entry(line); + entry = sc_parse_mountinfo_entry(line); if (entry == NULL) { - free_mountinfo(info); + sc_free_mountinfo(info); return NULL; } if (last != NULL) { @@ -191,9 +191,9 @@ return info; } -static struct mountinfo_entry *parse_mountinfo_entry(const char *line) +static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) { - // NOTE: the mountinfo structure is allocated along with enough extra + // NOTE: the sc_mountinfo structure is allocated along with enough extra // storage to hold the whole line we are parsing. This is used as backing // store for all text fields. // @@ -206,27 +206,73 @@ // this extra memory to hold data parsed from the original line. In the // end, the result is similar to using strtok except that the source and // destination buffers are separate. - struct mountinfo_entry *entry = + // + // At the end of the parsing process, the input buffer (line) and the + // output buffer (entry->line_buf) are the same except for where spaces + // were converted into NUL bytes (string terminators) and except for the + // leading part of the buffer that contains mount_id, parent_id, dev_major + // and dev_minor integer fields that are parsed separately. + // + // If MOUNTINFO_DEBUG is defined then extra debugging is printed to stderr + // and this allows for visual analysis of what is going on. + struct sc_mountinfo_entry *entry = calloc(1, sizeof *entry + strlen(line) + 1); if (entry == NULL) { return NULL; } +#ifdef MOUNTINFO_DEBUG + // Poison the buffer with '\1' bytes that are printed as '#' characters + // by show_buffers() below. This is "unaltered" memory. + memset(entry->line_buf, 1, strlen(line)); +#endif // MOUNTINFO_DEBUG int nscanned; - int offset, total_offset = 0; + int offset_delta, offset = 0; nscanned = sscanf(line, "%d %d %u:%u %n", &entry->mount_id, &entry->parent_id, - &entry->dev_major, &entry->dev_minor, &offset); + &entry->dev_major, &entry->dev_minor, &offset_delta); if (nscanned != 4) goto fail; - total_offset += offset; - int total_used = 0; + offset += offset_delta; + + void show_buffers() { +#ifdef MOUNTINFO_DEBUG + fprintf(stderr, "Input buffer (first), with offset arrow\n"); + fprintf(stderr, "Output buffer (second)\n"); + + fputc(' ', stderr); + for (int i = 0; i < offset - 1; ++i) + fputc('-', stderr); + fputc('v', stderr); + fputc('\n', stderr); + + fprintf(stderr, ">%s<\n", line); + + fputc('>', stderr); + for (int i = 0; i < strlen(line); ++i) { + int c = entry->line_buf[i]; + fputc(c == 0 ? '@' : c == 1 ? '#' : c, stderr); + } + fputc('<', stderr); + fputc('\n', stderr); + + fputc('>', stderr); + for (int i = 0; i < strlen(line); ++i) + fputc('=', stderr); + fputc('<', stderr); + fputc('\n', stderr); +#endif // MOUNTINFO_DEBUG + } + + show_buffers(); + char *parse_next_string_field() { - char *field = &entry->line_buf[0] + total_used; - nscanned = sscanf(line + total_offset, "%s %n", field, &offset); + char *field = &entry->line_buf[0] + offset; + int nscanned = + sscanf(line + offset, "%s %n", field, &offset_delta); if (nscanned != 1) return NULL; - total_offset += offset; - total_used += offset + 1; + offset += offset_delta; + show_buffers(); return field; } if ((entry->root = parse_next_string_field()) == NULL) @@ -235,21 +281,21 @@ goto fail; if ((entry->mount_opts = parse_next_string_field()) == NULL) goto fail; - entry->optional_fields = &entry->line_buf[0] + total_used++; + entry->optional_fields = &entry->line_buf[0] + offset; // NOTE: This ensures that optional_fields is never NULL. If this changes, // must adjust all callers of parse_mountinfo_entry() accordingly. - strcpy(entry->optional_fields, ""); - for (;;) { + char *to = entry->optional_fields; + for (int field_num = 0;; ++field_num) { char *opt_field = parse_next_string_field(); if (opt_field == NULL) goto fail; if (strcmp(opt_field, "-") == 0) { + opt_field[0] = 0; break; } - if (*entry->optional_fields) { - strcat(entry->optional_fields, " "); + if (field_num > 0) { + opt_field[-1] = ' '; } - strcat(entry->optional_fields, opt_field); } if ((entry->fs_type = parse_next_string_field()) == NULL) goto fail; @@ -257,41 +303,32 @@ goto fail; if ((entry->super_opts = parse_next_string_field()) == NULL) goto fail; + show_buffers(); return entry; fail: free(entry); return NULL; } -void cleanup_mountinfo(struct mountinfo **ptr) +void sc_cleanup_mountinfo(struct sc_mountinfo **ptr) { if (*ptr != NULL) { - free_mountinfo(*ptr); + sc_free_mountinfo(*ptr); *ptr = NULL; } } -static void free_mountinfo(struct mountinfo *info) +static void sc_free_mountinfo(struct sc_mountinfo *info) { - struct mountinfo_entry *entry, *next; + struct sc_mountinfo_entry *entry, *next; for (entry = info->first; entry != NULL; entry = next) { next = entry->next; - free_mountinfo_entry(entry); + sc_free_mountinfo_entry(entry); } free(info); } -static void free_mountinfo_entry(struct mountinfo_entry *entry) +static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry) { free(entry); } - -static void cleanup_fclose(FILE ** ptr) -{ - fclose(*ptr); -} - -static void cleanup_free(char **ptr) -{ - free(*ptr); -} diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/mountinfo.h snapd-2.23.1+16.10/cmd/libsnap-confine-private/mountinfo.h --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/mountinfo.h 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/mountinfo.h 2017-03-02 06:37:54.000000000 +0000 @@ -14,91 +14,92 @@ * along with this program. If not, see . */ -#ifndef SC_MOUNTINFO_H -#define SC_MOUNTINFO_H +#ifndef SNAP_CONFINE_MOUNTINFO_H +#define SNAP_CONFINE_MOUNTINFO_H /** - * Structure describing entire /proc/self/mountinfo file + * Structure describing entire /proc/self/sc_mountinfo file **/ -struct mountinfo; +struct sc_mountinfo; /** - * Structure describing a single entry in /proc/self/mountinfo + * Structure describing a single entry in /proc/self/sc_mountinfo **/ -struct mountinfo_entry; +struct sc_mountinfo_entry; /** - * Parse a file in according to mountinfo syntax. + * Parse a file in according to sc_mountinfo syntax. * * The argument can be used to parse an arbitrary file. NULL can be used to - * implicitly parse /proc/self/mountinfo, that is the mount information + * implicitly parse /proc/self/sc_mountinfo, that is the mount information * associated with the current process. **/ -struct mountinfo *parse_mountinfo(const char *fname); +struct sc_mountinfo *sc_parse_mountinfo(const char *fname); /** - * Free a mountinfo structure. + * Free a sc_mountinfo structure. * * This function is designed to be used with __attribute__((cleanup)) so it * takes a pointer to the freed object (which is also a pointer). **/ -void cleanup_mountinfo(struct mountinfo **ptr) __attribute__ ((nonnull(1))); +void sc_cleanup_mountinfo(struct sc_mountinfo **ptr) + __attribute__ ((nonnull(1))); /** - * Get the first mountinfo entry. + * Get the first sc_mountinfo entry. * * The returned value may be NULL if the parsed file contained no entries. The - * returned value is bound to the lifecycle of the whole mountinfo structure + * returned value is bound to the lifecycle of the whole sc_mountinfo structure * and should not be freed explicitly. **/ -struct mountinfo_entry *first_mountinfo_entry(struct mountinfo *info) +struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info) __attribute__ ((nonnull(1))); /** - * Get the next mountinfo entry. + * Get the next sc_mountinfo entry. * - * The returned value is a pointer to the next mountinfo entry or NULL if this + * The returned value is a pointer to the next sc_mountinfo entry or NULL if this * was the last entry. The returned value is bound to the lifecycle of the - * whole mountinfo structure and should not be freed explicitly. + * whole sc_mountinfo structure and should not be freed explicitly. **/ -struct mountinfo_entry *next_mountinfo_entry(struct mountinfo_entry - *entry) +struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry + *entry) __attribute__ ((nonnull(1))); /** * Get the mount identifier of a given mount entry. **/ -int mountinfo_entry_mount_id(struct mountinfo_entry *entry) +int sc_mountinfo_entry_mount_id(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); /** * Get the parent mount identifier of a given mount entry. **/ -int mountinfo_entry_parent_id(struct mountinfo_entry *entry) +int sc_mountinfo_entry_parent_id(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); -unsigned mountinfo_entry_dev_major(struct mountinfo_entry *entry) +unsigned sc_mountinfo_entry_dev_major(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); -unsigned mountinfo_entry_dev_minor(struct mountinfo_entry *entry) +unsigned sc_mountinfo_entry_dev_minor(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); /** * Get the root directory of a given mount entry. **/ -const char *mountinfo_entry_root(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_root(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); /** * Get the mount point of a given mount entry. **/ -const char *mountinfo_entry_mount_dir(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_mount_dir(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); /** * Get the mount options of a given mount entry. **/ -const char *mountinfo_entry_mount_opts(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_mount_opts(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); /** @@ -121,25 +122,25 @@ * group under the same root, then only the "master:X" field is present and not * the "propagate_from:X" field. **/ -const char *mountinfo_entry_optional_fields(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_optional_fields(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); /** * Get the file system type of a given mount entry. **/ -const char *mountinfo_entry_fs_type(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_fs_type(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); /** * Get the source of a given mount entry. **/ -const char *mountinfo_entry_mount_source(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_mount_source(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); /** * Get the super block options of a given mount entry. **/ -const char *mountinfo_entry_super_opts(struct mountinfo_entry *entry) +const char *sc_mountinfo_entry_super_opts(struct sc_mountinfo_entry *entry) __attribute__ ((nonnull(1))); #endif diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/mountinfo-test.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/mountinfo-test.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/mountinfo-test.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/mountinfo-test.c 2017-03-02 06:37:54.000000000 +0000 @@ -24,9 +24,9 @@ { const char *line = "19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw"; - struct mountinfo_entry *entry = parse_mountinfo_entry(line); + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); - g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 19); g_assert_cmpint(entry->parent_id, ==, 25); g_assert_cmpint(entry->dev_major, ==, 0); @@ -48,9 +48,9 @@ { const char *line = "104 23 0:19 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=99840k,mode=755"; - struct mountinfo_entry *entry = parse_mountinfo_entry(line); + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); - g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 104); g_assert_cmpint(entry->parent_id, ==, 23); g_assert_cmpint(entry->dev_major, ==, 0); @@ -69,9 +69,9 @@ { const char *line = "256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw"; - struct mountinfo_entry *entry = parse_mountinfo_entry(line); + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); - g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 256); g_assert_cmpint(entry->parent_id, ==, 104); g_assert_cmpint(entry->dev_major, ==, 0); @@ -89,7 +89,7 @@ static void test_parse_mountinfo_entry__garbage() { const char *line = "256 104 0:3"; - struct mountinfo_entry *entry = parse_mountinfo_entry(line); + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_null(entry); } @@ -97,9 +97,9 @@ { const char *line = "1 2 3:4 root mount-dir mount-opts - fs-type mount-source super-opts"; - struct mountinfo_entry *entry = parse_mountinfo_entry(line); + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); - g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 1); g_assert_cmpint(entry->parent_id, ==, 2); g_assert_cmpint(entry->dev_major, ==, 3); @@ -118,9 +118,9 @@ { const char *line = "1 2 3:4 root mount-dir mount-opts tag:1 - fs-type mount-source super-opts"; - struct mountinfo_entry *entry = parse_mountinfo_entry(line); + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); - g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 1); g_assert_cmpint(entry->parent_id, ==, 2); g_assert_cmpint(entry->dev_major, ==, 3); @@ -135,13 +135,13 @@ g_assert_null(entry->next); } -static void test_parse_mountinfo_entry__two_tags() +static void test_parse_mountinfo_entry__many_tags() { const char *line = - "1 2 3:4 root mount-dir mount-opts tag:1 tag:2 - fs-type mount-source super-opts"; - struct mountinfo_entry *entry = parse_mountinfo_entry(line); + "1 2 3:4 root mount-dir mount-opts tag:1 tag:2 tag:3 tag:4 - fs-type mount-source super-opts"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); - g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 1); g_assert_cmpint(entry->parent_id, ==, 2); g_assert_cmpint(entry->dev_major, ==, 3); @@ -149,7 +149,7 @@ g_assert_cmpstr(entry->root, ==, "root"); g_assert_cmpstr(entry->mount_dir, ==, "mount-dir"); g_assert_cmpstr(entry->mount_opts, ==, "mount-opts"); - g_assert_cmpstr(entry->optional_fields, ==, "tag:1 tag:2"); + g_assert_cmpstr(entry->optional_fields, ==, "tag:1 tag:2 tag:3 tag:4"); g_assert_cmpstr(entry->fs_type, ==, "fs-type"); g_assert_cmpstr(entry->mount_source, ==, "mount-source"); g_assert_cmpstr(entry->super_opts, ==, "super-opts"); @@ -160,22 +160,22 @@ { const char *line = "256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw"; - struct mountinfo_entry *entry = parse_mountinfo_entry(line); + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); - g_test_queue_destroy((GDestroyNotify) free_mountinfo_entry, entry); - g_assert_cmpint(mountinfo_entry_mount_id(entry), ==, 256); - g_assert_cmpint(mountinfo_entry_parent_id(entry), ==, 104); - g_assert_cmpint(mountinfo_entry_dev_major(entry), ==, 0); - g_assert_cmpint(mountinfo_entry_dev_minor(entry), ==, 3); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(sc_mountinfo_entry_mount_id(entry), ==, 256); + g_assert_cmpint(sc_mountinfo_entry_parent_id(entry), ==, 104); + g_assert_cmpint(sc_mountinfo_entry_dev_major(entry), ==, 0); + g_assert_cmpint(sc_mountinfo_entry_dev_minor(entry), ==, 3); - g_assert_cmpstr(mountinfo_entry_root(entry), ==, "mnt:[4026532509]"); - g_assert_cmpstr(mountinfo_entry_mount_dir(entry), ==, + g_assert_cmpstr(sc_mountinfo_entry_root(entry), ==, "mnt:[4026532509]"); + g_assert_cmpstr(sc_mountinfo_entry_mount_dir(entry), ==, "/run/snapd/ns/hello-world.mnt"); - g_assert_cmpstr(mountinfo_entry_mount_opts(entry), ==, "rw"); - g_assert_cmpstr(mountinfo_entry_optional_fields(entry), ==, ""); - g_assert_cmpstr(mountinfo_entry_fs_type(entry), ==, "nsfs"); - g_assert_cmpstr(mountinfo_entry_mount_source(entry), ==, "nsfs"); - g_assert_cmpstr(mountinfo_entry_super_opts(entry), ==, "rw"); + g_assert_cmpstr(sc_mountinfo_entry_mount_opts(entry), ==, "rw"); + g_assert_cmpstr(sc_mountinfo_entry_optional_fields(entry), ==, ""); + g_assert_cmpstr(sc_mountinfo_entry_fs_type(entry), ==, "nsfs"); + g_assert_cmpstr(sc_mountinfo_entry_mount_source(entry), ==, "nsfs"); + g_assert_cmpstr(sc_mountinfo_entry_super_opts(entry), ==, "rw"); } static void __attribute__ ((constructor)) init() @@ -192,7 +192,7 @@ test_parse_mountinfo_entry__no_tags); g_test_add_func("/mountinfo/parse_mountinfo_entry/one_tags", test_parse_mountinfo_entry__one_tag); - g_test_add_func("/mountinfo/parse_mountinfo_entry/two_tags", - test_parse_mountinfo_entry__two_tags); + g_test_add_func("/mountinfo/parse_mountinfo_entry/many_tags", + test_parse_mountinfo_entry__many_tags); g_test_add_func("/mountinfo/accessor_funcs", test_accessor_funcs); } diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/mount-opt.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/mount-opt.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/mount-opt.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/mount-opt.c 2017-03-06 13:33:50.000000000 +0000 @@ -17,18 +17,23 @@ #include "mount-opt.h" +#include +#include #include +#include #include #include -#include "../libsnap-confine-private/utils.h" +#include "fault-injection.h" +#include "privs.h" +#include "string-utils.h" +#include "utils.h" -const char *sc_mount_opt2str(unsigned long flags) +const char *sc_mount_opt2str(char *buf, size_t buf_size, unsigned long flags) { - static char buf[1000]; unsigned long used = 0; - strcpy(buf, ""); -#define F(FLAG, TEXT) do if (flags & (FLAG)) { strcat(buf, #TEXT ","); flags ^= (FLAG); } while (0) + sc_string_init(buf, buf_size); +#define F(FLAG, TEXT) do if (flags & (FLAG)) { sc_string_append(buf, buf_size, #TEXT ","); flags ^= (FLAG); } while (0) F(MS_RDONLY, ro); F(MS_NOSUID, nosuid); F(MS_NODEV, nodev); @@ -41,10 +46,10 @@ F(MS_NODIRATIME, nodiratime); if (flags & MS_BIND) { if (flags & MS_REC) { - strcat(buf, "rbind,"); + sc_string_append(buf, buf_size, "rbind,"); used |= MS_REC; } else { - strcat(buf, "bind,"); + sc_string_append(buf, buf_size, "bind,"); } flags ^= MS_BIND; } @@ -57,28 +62,28 @@ F(MS_UNBINDABLE, unbindable); if (flags & MS_PRIVATE) { if (flags & MS_REC) { - strcat(buf, "rprivate,"); + sc_string_append(buf, buf_size, "rprivate,"); used |= MS_REC; } else { - strcat(buf, "private,"); + sc_string_append(buf, buf_size, "private,"); } flags ^= MS_PRIVATE; } if (flags & MS_SLAVE) { if (flags & MS_REC) { - strcat(buf, "rslave,"); + sc_string_append(buf, buf_size, "rslave,"); used |= MS_REC; } else { - strcat(buf, "slave,"); + sc_string_append(buf, buf_size, "slave,"); } flags ^= MS_SLAVE; } if (flags & MS_SHARED) { if (flags & MS_REC) { - strcat(buf, "rshared,"); + sc_string_append(buf, buf_size, "rshared,"); used |= MS_REC; } else { - strcat(buf, "shared,"); + sc_string_append(buf, buf_size, "shared,"); } flags ^= MS_SHARED; } @@ -105,13 +110,215 @@ // Render any flags that are unaccounted for. if (flags) { char of[128]; - sprintf(of, "%#lx", flags); - strcat(buf, of); + sc_must_snprintf(of, sizeof of, "%#lx", flags); + sc_string_append(buf, buf_size, of); } // Chop the excess comma from the end. - size_t len = strlen(buf); + size_t len = strnlen(buf, buf_size); if (len > 0 && buf[len - 1] == ',') { buf[len - 1] = 0; } return buf; } + +const char *sc_mount_cmd(char *buf, size_t buf_size, const char *source, const char + *target, const char *fs_type, unsigned long mountflags, const + void *data) +{ + sc_string_init(buf, buf_size); + sc_string_append(buf, buf_size, "mount"); + + // Add filesysystem type if it's there and doesn't have the special value "none" + if (fs_type != NULL && strncmp(fs_type, "none", 5) != 0) { + sc_string_append(buf, buf_size, " -t "); + sc_string_append(buf, buf_size, fs_type); + } + // Check for some special, dedicated options, that aren't represented with + // the generic mount option argument (mount -o ...), by collecting those + // options that we will display as command line arguments in + // used_special_flags. This is used below to filter out these arguments + // from mount_flags when calling sc_mount_opt2str(). + int used_special_flags = 0; + + // Bind-ounts (bind) + if (mountflags & MS_BIND) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --rbind"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --bind"); + } + used_special_flags |= MS_BIND; + } + // Moving mount point location (move) + if (mountflags & MS_MOVE) { + sc_string_append(buf, buf_size, " --move"); + used_special_flags |= MS_MOVE; + } + // Shared subtree operations (shared, slave, private, unbindable). + if (MS_SHARED & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-rshared"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-shared"); + } + used_special_flags |= MS_SHARED; + } + + if (MS_SLAVE & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-rslave"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-slave"); + } + used_special_flags |= MS_SLAVE; + } + + if (MS_PRIVATE & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-rprivate"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-private"); + } + used_special_flags |= MS_PRIVATE; + } + + if (MS_UNBINDABLE & mountflags) { + if (mountflags & MS_REC) { + sc_string_append(buf, buf_size, " --make-runbindable"); + used_special_flags |= MS_REC; + } else { + sc_string_append(buf, buf_size, " --make-unbindable"); + } + used_special_flags |= MS_UNBINDABLE; + } + // If regular option syntax exists then use it. + if (mountflags & ~used_special_flags) { + char opts_buf[1000]; + sc_mount_opt2str(opts_buf, sizeof opts_buf, mountflags & + ~used_special_flags); + sc_string_append(buf, buf_size, " -o "); + sc_string_append(buf, buf_size, opts_buf); + } + // Add source and target locations + if (source != NULL && strncmp(source, "none", 5) != 0) { + sc_string_append(buf, buf_size, " "); + sc_string_append(buf, buf_size, source); + } + if (target != NULL && strncmp(target, "none", 5) != 0) { + sc_string_append(buf, buf_size, " "); + sc_string_append(buf, buf_size, target); + } + + return buf; +} + +const char *sc_umount_cmd(char *buf, size_t buf_size, const char *target, + int flags) +{ + sc_string_init(buf, buf_size); + sc_string_append(buf, buf_size, "umount"); + + if (flags & MNT_FORCE) { + sc_string_append(buf, buf_size, " --force"); + } + + if (flags & MNT_DETACH) { + sc_string_append(buf, buf_size, " --lazy"); + } + if (flags & MNT_EXPIRE) { + // NOTE: there's no real command line option for MNT_EXPIRE + sc_string_append(buf, buf_size, " --expire"); + } + if (flags & UMOUNT_NOFOLLOW) { + // NOTE: there's no real command line option for UMOUNT_NOFOLLOW + sc_string_append(buf, buf_size, " --no-follow"); + } + if (target != NULL) { + sc_string_append(buf, buf_size, " "); + sc_string_append(buf, buf_size, target); + } + + return buf; +} + +void sc_do_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data) +{ + char buf[10000]; + const char *mount_cmd = NULL; + + void ensure_mount_cmd() { + if (mount_cmd != NULL) { + return; + } + mount_cmd = sc_mount_cmd(buf, sizeof buf, source, + target, fs_type, mountflags, data); + } + + if (sc_is_debug_enabled()) { +#ifdef SNAP_CONFINE_DEBUG_BUILD + ensure_mount_cmd(); +#else + mount_cmd = "(disabled) use debug build to see details"; +#endif + debug("performing operation: %s", mount_cmd); + } + if (sc_faulty("mount", NULL) + || mount(source, target, fs_type, mountflags, data) < 0) { + // Save errno as ensure can clobber it. + int saved_errno = errno; + + // Drop privileges so that we can compute our nice error message + // without risking an attack on one of the string functions there. + sc_privs_drop(); + + // Compute the equivalent mount command. + ensure_mount_cmd(); + + // Restore errno and die. + errno = saved_errno; + die("cannot perform operation: %s", mount_cmd); + } +} + +void sc_do_umount(const char *target, int flags) +{ + char buf[10000]; + const char *umount_cmd = NULL; + + void ensure_umount_cmd() { + if (umount_cmd != NULL) { + return; + } + umount_cmd = sc_umount_cmd(buf, sizeof buf, target, flags); + } + + if (sc_is_debug_enabled()) { +#ifdef SNAP_CONFINE_DEBUG_BUILD + ensure_umount_cmd(); +#else + umount_cmd = "(disabled) use debug build to see details"; +#endif + debug("performing operation: %s", umount_cmd); + } + if (sc_faulty("umount", NULL) || umount2(target, flags) < 0) { + // Save errno as ensure can clobber it. + int saved_errno = errno; + + // Drop privileges so that we can compute our nice error message + // without risking an attack on one of the string functions there. + sc_privs_drop(); + + // Compute the equivalent umount command. + ensure_umount_cmd(); + + // Restore errno and die. + errno = saved_errno; + die("cannot perform operation: %s", umount_cmd); + } +} diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/mount-opt.h snapd-2.23.1+16.10/cmd/libsnap-confine-private/mount-opt.h --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/mount-opt.h 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/mount-opt.h 2017-03-06 13:33:50.000000000 +0000 @@ -18,12 +18,58 @@ #ifndef SNAP_CONFINE_MOUNT_OPT_H #define SNAP_CONFINE_MOUNT_OPT_H +#include + /** * Convert flags for mount(2) system call to a string representation. + **/ +const char *sc_mount_opt2str(char *buf, size_t buf_size, unsigned long flags); + +/** + * Compute an equivalent mount(8) command from mount(2) arguments. + * + * This function serves as a human-readable representation of the mount system + * call. The return value is a string that looks like a shell mount command. + * + * Note that the returned command is may not be a valid mount command. No + * sanity checking is performed on the mount flags, source or destination + * arguments. + * + * The returned value is always buf, it is provided as a convenience. + **/ +const char *sc_mount_cmd(char *buf, size_t buf_size, const char *source, const char + *target, const char *fs_type, unsigned long mountflags, + const void *data); + +/** + * Compute an equivalent umount(8) command from umount2(2) arguments. * - * The function uses an internal static buffer that is overwritten on each - * request. + * This function serves as a human-readable representation of the unmount + * system call. The return value is a string that looks like a shell unmount + * command. + * + * Note that some flags are not surfaced at umount command line level. For + * those flags a fake option is synthesized. + * + * Note that the returned command is may not be a valid umount command. No + * sanity checking is performed on the mount flags, source or destination + * arguments. + * + * The returned value is always buf, it is provided as a convenience. + **/ +const char *sc_umount_cmd(char *buf, size_t buf_size, const char *target, + int flags); + +/** + * A thin wrapper around mount(2) with logging and error checks. + **/ +void sc_do_mount(const char *source, const char *target, + const char *fs_type, unsigned long mountflags, + const void *data); + +/** + * A thin wrapper around umount(2) with logging and error checks. **/ -const char *sc_mount_opt2str(unsigned long flags); +void sc_do_umount(const char *target, int flags); #endif // SNAP_CONFINE_MOUNT_OPT_H diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/mount-opt-test.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/mount-opt-test.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/mount-opt-test.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/mount-opt-test.c 2017-03-06 13:33:50.000000000 +0000 @@ -18,50 +18,240 @@ #include "mount-opt.h" #include "mount-opt.c" +#include #include + #include static void test_sc_mount_opt2str() { - g_assert_cmpstr(sc_mount_opt2str(0), ==, ""); - g_assert_cmpstr(sc_mount_opt2str(MS_RDONLY), ==, "ro"); - g_assert_cmpstr(sc_mount_opt2str(MS_NOSUID), ==, "nosuid"); - g_assert_cmpstr(sc_mount_opt2str(MS_NODEV), ==, "nodev"); - g_assert_cmpstr(sc_mount_opt2str(MS_NOEXEC), ==, "noexec"); - g_assert_cmpstr(sc_mount_opt2str(MS_SYNCHRONOUS), ==, "sync"); - g_assert_cmpstr(sc_mount_opt2str(MS_REMOUNT), ==, "remount"); - g_assert_cmpstr(sc_mount_opt2str(MS_MANDLOCK), ==, "mand"); - g_assert_cmpstr(sc_mount_opt2str(MS_DIRSYNC), ==, "dirsync"); - g_assert_cmpstr(sc_mount_opt2str(MS_NOATIME), ==, "noatime"); - g_assert_cmpstr(sc_mount_opt2str(MS_NODIRATIME), ==, "nodiratime"); - g_assert_cmpstr(sc_mount_opt2str(MS_BIND), ==, "bind"); - g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_BIND), ==, "rbind"); - g_assert_cmpstr(sc_mount_opt2str(MS_MOVE), ==, "move"); - g_assert_cmpstr(sc_mount_opt2str(MS_SILENT), ==, "silent"); - g_assert_cmpstr(sc_mount_opt2str(MS_POSIXACL), ==, "acl"); - g_assert_cmpstr(sc_mount_opt2str(MS_UNBINDABLE), ==, "unbindable"); - g_assert_cmpstr(sc_mount_opt2str(MS_PRIVATE), ==, "private"); - g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_PRIVATE), ==, "rprivate"); - g_assert_cmpstr(sc_mount_opt2str(MS_SLAVE), ==, "slave"); - g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_SLAVE), ==, "rslave"); - g_assert_cmpstr(sc_mount_opt2str(MS_SHARED), ==, "shared"); - g_assert_cmpstr(sc_mount_opt2str(MS_REC | MS_SHARED), ==, "rshared"); - g_assert_cmpstr(sc_mount_opt2str(MS_RELATIME), ==, "relatime"); - g_assert_cmpstr(sc_mount_opt2str(MS_KERNMOUNT), ==, "kernmount"); - g_assert_cmpstr(sc_mount_opt2str(MS_I_VERSION), ==, "iversion"); - g_assert_cmpstr(sc_mount_opt2str(MS_STRICTATIME), ==, "strictatime"); - g_assert_cmpstr(sc_mount_opt2str(MS_LAZYTIME), ==, "lazytime"); + char buf[1000]; + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, 0), ==, ""); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_RDONLY), ==, "ro"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOSUID), ==, + "nosuid"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NODEV), ==, + "nodev"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOEXEC), ==, + "noexec"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SYNCHRONOUS), ==, + "sync"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REMOUNT), ==, + "remount"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_MANDLOCK), ==, + "mand"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_DIRSYNC), ==, + "dirsync"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOATIME), ==, + "noatime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NODIRATIME), ==, + "nodiratime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_BIND), ==, "bind"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_BIND), ==, + "rbind"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_MOVE), ==, "move"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SILENT), ==, + "silent"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_POSIXACL), ==, + "acl"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_UNBINDABLE), ==, + "unbindable"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_PRIVATE), ==, + "private"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_PRIVATE), + ==, "rprivate"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SLAVE), ==, + "slave"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_SLAVE), + ==, "rslave"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_SHARED), ==, + "shared"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_REC | MS_SHARED), + ==, "rshared"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_RELATIME), ==, + "relatime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_KERNMOUNT), ==, + "kernmount"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_I_VERSION), ==, + "iversion"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_STRICTATIME), ==, + "strictatime"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_LAZYTIME), ==, + "lazytime"); // MS_NOSEC is not defined in userspace // MS_BORN is not defined in userspace - g_assert_cmpstr(sc_mount_opt2str(MS_ACTIVE), ==, "active"); - g_assert_cmpstr(sc_mount_opt2str(MS_NOUSER), ==, "nouser"); - g_assert_cmpstr(sc_mount_opt2str(0x300), ==, "0x300"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_ACTIVE), ==, + "active"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, MS_NOUSER), ==, + "nouser"); + g_assert_cmpstr(sc_mount_opt2str(buf, sizeof buf, 0x300), ==, "0x300"); // random compositions do work - g_assert_cmpstr(sc_mount_opt2str(MS_RDONLY | MS_NOEXEC | MS_BIND), ==, + g_assert_cmpstr(sc_mount_opt2str + (buf, sizeof buf, MS_RDONLY | MS_NOEXEC | MS_BIND), ==, "ro,noexec,bind"); } +static void test_sc_mount_cmd() +{ + char cmd[10000]; + + // Typical mount + sc_mount_cmd(cmd, sizeof cmd, "/dev/sda3", "/mnt", "ext4", MS_RDONLY, + NULL); + g_assert_cmpstr(cmd, ==, "mount -t ext4 -o ro /dev/sda3 /mnt"); + + // Bind mount + sc_mount_cmd(cmd, sizeof cmd, "/source", "/target", NULL, MS_BIND, + NULL); + g_assert_cmpstr(cmd, ==, "mount --bind /source /target"); + + // + recursive + sc_mount_cmd(cmd, sizeof cmd, "/source", "/target", NULL, + MS_BIND | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --rbind /source /target"); + + // Shared subtree mount + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_SHARED, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-shared /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_SLAVE, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-slave /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_PRIVATE, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-private /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_UNBINDABLE, + NULL); + g_assert_cmpstr(cmd, ==, "mount --make-unbindable /place"); + + // + recursive + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, + MS_SHARED | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-rshared /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, MS_SLAVE | MS_REC, + NULL); + g_assert_cmpstr(cmd, ==, "mount --make-rslave /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, + MS_PRIVATE | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-rprivate /place"); + + sc_mount_cmd(cmd, sizeof cmd, "/place", "none", NULL, + MS_UNBINDABLE | MS_REC, NULL); + g_assert_cmpstr(cmd, ==, "mount --make-runbindable /place"); + + // Move + sc_mount_cmd(cmd, sizeof cmd, "/from", "/to", NULL, MS_MOVE, NULL); + g_assert_cmpstr(cmd, ==, "mount --move /from /to"); + + // Monster (invalid but let's format it) + char from[PATH_MAX]; + char to[PATH_MAX]; + for (int i = 1; i < PATH_MAX - 1; ++i) { + from[i] = 'a'; + to[i] = 'b'; + } + from[0] = '/'; + to[0] = '/'; + from[PATH_MAX - 1] = 0; + to[PATH_MAX - 1] = 0; + int opts = MS_BIND | MS_MOVE | MS_SHARED | MS_SLAVE | MS_PRIVATE | + MS_UNBINDABLE | MS_REC | MS_RDONLY | MS_NOSUID | MS_NODEV | + MS_NOEXEC | MS_SYNCHRONOUS | MS_REMOUNT | MS_MANDLOCK | MS_DIRSYNC | + MS_NOATIME | MS_NODIRATIME | MS_BIND | MS_SILENT | MS_POSIXACL | + MS_RELATIME | MS_KERNMOUNT | MS_I_VERSION | MS_STRICTATIME | + MS_LAZYTIME; + const char *fstype = "fstype"; + sc_mount_cmd(cmd, sizeof cmd, from, to, fstype, opts, NULL); + const char *expected = + "mount -t fstype " + "--rbind --move --make-rshared --make-rslave --make-rprivate --make-runbindable " + "-o ro,nosuid,nodev,noexec,sync,remount,mand,dirsync,noatime,nodiratime,silent," + "acl,relatime,kernmount,iversion,strictatime,lazytime " + "/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + "/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + g_assert_cmpstr(cmd, ==, expected); +} + +static void test_sc_umount_cmd() +{ + char cmd[1000]; + + // Typical umount + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", 0); + g_assert_cmpstr(cmd, ==, "umount /mnt/foo"); + + // Force + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", MNT_FORCE); + g_assert_cmpstr(cmd, ==, "umount --force /mnt/foo"); + + // Detach + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", MNT_DETACH); + g_assert_cmpstr(cmd, ==, "umount --lazy /mnt/foo"); + + // Expire + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", MNT_EXPIRE); + g_assert_cmpstr(cmd, ==, "umount --expire /mnt/foo"); + + // O_NOFOLLOW variant for umount + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", UMOUNT_NOFOLLOW); + g_assert_cmpstr(cmd, ==, "umount --no-follow /mnt/foo"); + + // Everything at once + sc_umount_cmd(cmd, sizeof cmd, "/mnt/foo", + MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW); + g_assert_cmpstr(cmd, ==, + "umount --force --lazy --expire --no-follow /mnt/foo"); +} + +static void test_sc_do_mount() +{ + if (g_test_subprocess()) { + bool broken_mount(struct sc_fault_state *state, void *ptr) { + errno = EACCES; + return true; + } + sc_break("mount", broken_mount); + sc_do_mount("/foo", "/bar", "ext4", MS_RDONLY, NULL); + + g_test_message("expected sc_do_mount not to return"); + sc_reset_faults(); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot perform operation: mount -t ext4 -o ro /foo /bar: Permission denied\n"); +} + +static void test_sc_do_umount() +{ + if (g_test_subprocess()) { + bool broken_umount(struct sc_fault_state *state, void *ptr) { + errno = EACCES; + return true; + } + sc_break("umount", broken_umount); + sc_do_umount("/foo", MNT_DETACH); + + g_test_message("expected sc_do_umount not to return"); + sc_reset_faults(); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot perform operation: umount --lazy /foo: Permission denied\n"); +} + static void __attribute__ ((constructor)) init() { g_test_add_func("/mount/sc_mount_opt2str", test_sc_mount_opt2str); + g_test_add_func("/mount/sc_mount_cmd", test_sc_mount_cmd); + g_test_add_func("/mount/sc_umount_cmd", test_sc_umount_cmd); + g_test_add_func("/mount/sc_do_mount", test_sc_do_mount); + g_test_add_func("/mount/sc_do_umount", test_sc_do_umount); } diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/privs.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/privs.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/privs.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/privs.c 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "privs.h" + +#define _GNU_SOURCE + +#include + +#include +#include +#include +#include +#include + +#include "utils.h" + +static bool sc_has_capability(const char *cap_name) +{ + // Lookup capability with the given name. + cap_value_t cap; + if (cap_from_name(cap_name, &cap) < 0) { + die("cannot resolve capability name %s", cap_name); + } + // Get the capability state of the current process. + cap_t caps; + if ((caps = cap_get_proc()) == NULL) { + die("cannot obtain capability state (cap_get_proc)"); + } + // Read the effective value of the flag we're dealing with + cap_flag_value_t cap_flags_value; + if (cap_get_flag(caps, cap, CAP_EFFECTIVE, &cap_flags_value) < 0) { + cap_free(caps); // don't bother checking, we die anyway. + die("cannot obtain value of capability flag (cap_get_flag)"); + } + // Free the representation of the capability state of the current process. + if (cap_free(caps) < 0) { + die("cannot free capability flag (cap_free)"); + } + // Check if the effective bit of the capability is set. + return cap_flags_value == CAP_SET; +} + +void sc_privs_drop() +{ + gid_t gid = getgid(); + uid_t uid = getuid(); + + // Drop extra group membership if we can. + if (sc_has_capability("cap_setgid")) { + gid_t gid_list[1] = { gid }; + if (setgroups(1, gid_list) < 0) { + die("cannot set supplementary group identifiers"); + } + } + // Switch to real group ID + if (setgid(getgid()) < 0) { + die("cannot set group identifier to %d", gid); + } + // Switch to real user ID + if (setuid(getuid()) < 0) { + die("cannot set user identifier to %d", uid); + } +} diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/privs.h snapd-2.23.1+16.10/cmd/libsnap-confine-private/privs.h --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/privs.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/privs.h 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SNAP_CONFINE_PRIVS_H +#define SNAP_CONFINE_PRIVS_H + +/** + * Permanently drop elevated permissions. + * + * If the user has elevated permission as a result of running a setuid root + * application then such permission are permanently dropped. + * + * The set of dropped permissions include: + * - user and group identifier + * - supplementary group identifiers + * + * The function ensures that the elevated permission are dropped or dies if + * this cannot be achieved. Note that only the elevated permissions are + * dropped. When the process itself was started by root then this function does + * nothing at all. + **/ +void sc_privs_drop(); + +#endif diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/privs-test.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/privs-test.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/privs-test.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/privs-test.c 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "privs.h" +#include "privs.c" + +#include + +// Test that dropping permissions really works +static void test_sc_privs_drop() +{ + if (geteuid() != 0 || getuid() == 0) { + g_test_skip("run this test after chown root.root; chmod u+s"); + return; + } + if (getegid() != 0 || getgid() == 0) { + g_test_skip("run this test after chown root.root; chmod g+s"); + return; + } + if (g_test_subprocess()) { + // We start as a regular user with effective-root identity. + g_assert_cmpint(getuid(), !=, 0); + g_assert_cmpint(getgid(), !=, 0); + + g_assert_cmpint(geteuid(), ==, 0); + g_assert_cmpint(getegid(), ==, 0); + + // We drop the privileges. + sc_privs_drop(); + + // The we are no longer root. + g_assert_cmpint(getuid(), !=, 0); + g_assert_cmpint(geteuid(), !=, 0); + g_assert_cmpint(getgid(), !=, 0); + g_assert_cmpint(getegid(), !=, 0); + + // We don't have any supplementary groups. + gid_t groups[2]; + int num_groups = getgroups(1, groups); + g_assert_cmpint(num_groups, ==, 1); + g_assert_cmpint(groups[0], ==, getgid()); + + // All done. + return; + } + g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_INHERIT_STDERR); + g_test_trap_assert_passed(); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/privs/sc_privs_drop", test_sc_privs_drop); +} diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/string-utils.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/string-utils.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/string-utils.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/string-utils.c 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "string-utils.h" + +#include +#include +#include +#include + +#include "utils.h" + +bool sc_streq(const char *a, const char *b) +{ + if (!a || !b) { + return false; + } + + size_t alen = strlen(a); + size_t blen = strlen(b); + + if (alen != blen) { + return false; + } + + return strncmp(a, b, alen) == 0; +} + +bool sc_endswith(const char *str, const char *suffix) +{ + if (!str || !suffix) { + return false; + } + + size_t xlen = strlen(suffix); + size_t slen = strlen(str); + + if (slen < xlen) { + return false; + } + + return strncmp(str - xlen + slen, suffix, xlen) == 0; +} + +int sc_must_snprintf(char *str, size_t size, const char *format, ...) +{ + int n; + + va_list va; + va_start(va, format); + n = vsnprintf(str, size, format, va); + va_end(va); + + if (n < 0 || n >= size) + die("cannot format string: %s", str); + + return n; +} + +size_t sc_string_append(char *dst, size_t dst_size, const char *str) +{ + // Set errno in case we die. + errno = 0; + if (dst == NULL) { + die("cannot append string: buffer is NULL"); + } + if (str == NULL) { + die("cannot append string: string is NULL"); + } + size_t dst_len = strnlen(dst, dst_size); + if (dst_len == dst_size) { + die("cannot append string: dst is unterminated"); + } + + size_t max_str_len = dst_size - dst_len; + size_t str_len = strnlen(str, max_str_len); + if (str_len == max_str_len) { + die("cannot append string: str is too long or unterminated"); + } + // Append the string + memcpy(dst + dst_len, str, str_len); + // Ensure we are terminated + dst[dst_len + str_len] = '\0'; + // return the new size + return strlen(dst); +} + +size_t sc_string_append_char(char *dst, size_t dst_size, char c) +{ + // Set errno in case we die. + errno = 0; + if (dst == NULL) { + die("cannot append character: buffer is NULL"); + } + size_t dst_len = strnlen(dst, dst_size); + if (dst_len == dst_size) { + die("cannot append character: dst is unterminated"); + } + size_t max_str_len = dst_size - dst_len; + if (max_str_len < 2) { + die("cannot append character: not enough space"); + } + if (c == 0) { + die("cannot append character: cannot append string terminator"); + } + // Append the character and terminate the string. + dst[dst_len + 0] = c; + dst[dst_len + 1] = '\0'; + // Return the new size + return dst_len + 1; +} + +size_t sc_string_append_char_pair(char *dst, size_t dst_size, char c1, char c2) +{ + // Set errno in case we die. + errno = 0; + if (dst == NULL) { + die("cannot append character pair: buffer is NULL"); + } + size_t dst_len = strnlen(dst, dst_size); + if (dst_len == dst_size) { + die("cannot append character pair: dst is unterminated"); + } + size_t max_str_len = dst_size - dst_len; + if (max_str_len < 3) { + die("cannot append character pair: not enough space"); + } + if (c1 == 0 || c2 == 0) { + die("cannot append character pair: cannot append string terminator"); + } + // Append the two characters and terminate the string. + dst[dst_len + 0] = c1; + dst[dst_len + 1] = c2; + dst[dst_len + 2] = '\0'; + // Return the new size + return dst_len + 2; +} + +void sc_string_init(char *buf, size_t buf_size) +{ + errno = 0; + if (buf == NULL) { + die("cannot initialize string, buffer is NULL"); + } + if (buf_size == 0) { + die("cannot initialize string, buffer is too small"); + } + buf[0] = '\0'; +} diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/string-utils.h snapd-2.23.1+16.10/cmd/libsnap-confine-private/string-utils.h --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/string-utils.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/string-utils.h 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SNAP_CONFINE_STRING_UTILS_H +#define SNAP_CONFINE_STRING_UTILS_H + +#include +#include + +/** + * Check if two strings are equal. + **/ +bool sc_streq(const char *a, const char *b); + +/** + * Check if a string has a given suffix. + **/ +bool sc_endswith(const char *str, const char *suffix); + +/** + * Safer version of snprintf. + * + * This version dies on any error condition. + **/ +__attribute__ ((format(printf, 3, 4))) +int sc_must_snprintf(char *str, size_t size, const char *format, ...); + +/** + * Append a string to a buffer containing a string. + * + * This version is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL a buffer overflow is detected + * then the function dies. + * + * The buffers cannot overlap. + **/ +size_t sc_string_append(char *dst, size_t dst_size, const char *str); + +/** + * Append a single character to a buffer containing a string. + * + * This version is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * The character cannot be the string terminator. + * + * The return value is the new length of the string. + **/ +size_t sc_string_append_char(char *dst, size_t dst_size, char c); + +/** + * Append a pair of characters to a buffer containing a string. + * + * This version is fully aware of the destination buffer and is extra careful + * not to overflow it. If any argument is NULL or a buffer overflow is detected + * then the function dies. + * + * Neither character can be the string terminator. + * + * The return value is the new length of the string. + **/ +size_t sc_string_append_char_pair(char *dst, size_t dst_size, char c1, char c2); + +/** + * Initialize a string (make it empty). + * + * Initialize a string as empty, ensuring buf is non-NULL buf_size is > 0. + **/ +void sc_string_init(char *buf, size_t buf_size); + +#endif diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/string-utils-test.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/string-utils-test.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/string-utils-test.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/string-utils-test.c 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "string-utils.h" +#include "string-utils.c" + +#include + +static void test_sc_streq() +{ + g_assert_false(sc_streq(NULL, NULL)); + g_assert_false(sc_streq(NULL, "text")); + g_assert_false(sc_streq("text", NULL)); + g_assert_false(sc_streq("foo", "bar")); + g_assert_false(sc_streq("foo", "barbar")); + g_assert_false(sc_streq("foofoo", "bar")); + g_assert_true(sc_streq("text", "text")); + g_assert_true(sc_streq("", "")); +} + +static void test_sc_endswith() +{ + // NULL doesn't end with anything, nothing ends with NULL + g_assert_false(sc_endswith("", NULL)); + g_assert_false(sc_endswith(NULL, "")); + g_assert_false(sc_endswith(NULL, NULL)); + // Empty string ends with an empty string + g_assert_true(sc_endswith("", "")); + // Ends-with (matches) + g_assert_true(sc_endswith("foobar", "bar")); + g_assert_true(sc_endswith("foobar", "ar")); + g_assert_true(sc_endswith("foobar", "r")); + g_assert_true(sc_endswith("foobar", "")); + g_assert_true(sc_endswith("bar", "bar")); + // Ends-with (non-matches) + g_assert_false(sc_endswith("foobar", "quux")); + g_assert_false(sc_endswith("", "bar")); + g_assert_false(sc_endswith("b", "bar")); + g_assert_false(sc_endswith("ba", "bar")); +} + +static void test_sc_must_snprintf() +{ + char buf[5]; + sc_must_snprintf(buf, sizeof buf, "1234"); + g_assert_cmpstr(buf, ==, "1234"); +} + +static void test_sc_must_snprintf__fail() +{ + if (g_test_subprocess()) { + char buf[5]; + sc_must_snprintf(buf, sizeof buf, "12345"); + g_test_message("expected sc_must_snprintf not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot format string: 1234\n"); +} + +// Check that appending to a buffer works OK. +static void test_sc_string_append() +{ + union { + char bigbuf[6]; + struct { + signed char canary1; + char buf[4]; + signed char canary2; + }; + } data = { + .buf = { + 'f', '\0', 0xFF, 0xFF},.canary1 = ~0,.canary2 = ~0,}; + + // Sanity check, ensure that the layout of structures is as spelled above. + // (first canary1, then buf and finally canary2. + g_assert_cmpint(((char *)&data.buf[0]) - ((char *)&data.canary1), ==, + 1); + g_assert_cmpint(((char *)&data.buf[4]) - ((char *)&data.canary2), ==, + 0); + + sc_string_append(data.buf, sizeof data.buf, "oo"); + + // Check that we didn't corrupt either canary. + g_assert_cmpint(data.canary1, ==, ~0); + g_assert_cmpint(data.canary2, ==, ~0); + + // Check that we got the result that was expected. + g_assert_cmpstr(data.buf, ==, "foo"); +} + +// Check that appending an empty string to a full buffer is valid. +static void test_sc_string_append__empty_to_full() +{ + union { + char bigbuf[6]; + struct { + signed char canary1; + char buf[4]; + signed char canary2; + }; + } data = { + .buf = { + 'f', 'o', 'o', '\0'},.canary1 = ~0,.canary2 = ~0,}; + + // Sanity check, ensure that the layout of structures is as spelled above. + // (first canary1, then buf and finally canary2. + g_assert_cmpint(((char *)&data.buf[0]) - ((char *)&data.canary1), ==, + 1); + g_assert_cmpint(((char *)&data.buf[4]) - ((char *)&data.canary2), ==, + 0); + + sc_string_append(data.buf, sizeof data.buf, ""); + + // Check that we didn't corrupt either canary. + g_assert_cmpint(data.canary1, ==, ~0); + g_assert_cmpint(data.canary2, ==, ~0); + + // Check that we got the result that was expected. + g_assert_cmpstr(data.buf, ==, "foo"); +} + +// Check that the overflow detection works. +static void test_sc_string_append__overflow() +{ + if (g_test_subprocess()) { + char buf[4] = { 0, }; + + // Try to append a string that's one character too long. + sc_string_append(buf, sizeof buf, "1234"); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append string: str is too long or unterminated\n"); +} + +// Check that the uninitialized buffer detection works. +static void test_sc_string_append__uninitialized_buf() +{ + if (g_test_subprocess()) { + char buf[4] = { 0xFF, 0xFF, 0xFF, 0xFF }; + + // Try to append a string to a buffer which is not a valic C-string. + sc_string_append(buf, sizeof buf, ""); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append string: dst is unterminated\n"); +} + +// Check that `buf' cannot be NULL. +static void test_sc_string_append__NULL_buf() +{ + if (g_test_subprocess()) { + char buf[4]; + + sc_string_append(NULL, sizeof buf, "foo"); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot append string: buffer is NULL\n"); +} + +// Check that `src' cannot be NULL. +static void test_sc_string_append__NULL_str() +{ + if (g_test_subprocess()) { + char buf[4]; + + sc_string_append(buf, sizeof buf, NULL); + + g_test_message("expected sc_string_append not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot append string: string is NULL\n"); +} + +static void test_sc_string_init__normal() +{ + char buf[1] = { 0xFF }; + + sc_string_init(buf, sizeof buf); + g_assert_cmpint(buf[0], ==, 0); +} + +static void test_sc_string_init__empty_buf() +{ + if (g_test_subprocess()) { + char buf[1] = { 0xFF }; + + sc_string_init(buf, 0); + + g_test_message("expected sc_string_init not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot initialize string, buffer is too small\n"); +} + +static void test_sc_string_init__NULL_buf() +{ + if (g_test_subprocess()) { + sc_string_init(NULL, 1); + + g_test_message("expected sc_string_init not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot initialize string, buffer is NULL\n"); +} + +static void test_sc_string_append_char__uninitialized_buf() +{ + if (g_test_subprocess()) { + char buf[2] = { 0xFF, 0xFF }; + sc_string_append_char(buf, sizeof buf, 'a'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character: dst is unterminated\n"); +} + +static void test_sc_string_append_char__NULL_buf() +{ + if (g_test_subprocess()) { + sc_string_append_char(NULL, 2, 'a'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr("cannot append character: buffer is NULL\n"); +} + +static void test_sc_string_append_char__overflow() +{ + if (g_test_subprocess()) { + char buf[1] = { 0 }; + sc_string_append_char(buf, sizeof buf, 'a'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character: not enough space\n"); +} + +static void test_sc_string_append_char__invalid_zero() +{ + if (g_test_subprocess()) { + char buf[2] = { 0 }; + sc_string_append_char(buf, sizeof buf, '\0'); + + g_test_message("expected sc_string_append_char not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character: cannot append string terminator\n"); +} + +static void test_sc_string_append_char__normal() +{ + char buf[16]; + size_t len; + sc_string_init(buf, sizeof buf); + + len = sc_string_append_char(buf, sizeof buf, 'h'); + g_assert_cmpstr(buf, ==, "h"); + g_assert_cmpint(len, ==, 1); + len = sc_string_append_char(buf, sizeof buf, 'e'); + g_assert_cmpstr(buf, ==, "he"); + g_assert_cmpint(len, ==, 2); + len = sc_string_append_char(buf, sizeof buf, 'l'); + g_assert_cmpstr(buf, ==, "hel"); + g_assert_cmpint(len, ==, 3); + len = sc_string_append_char(buf, sizeof buf, 'l'); + g_assert_cmpstr(buf, ==, "hell"); + g_assert_cmpint(len, ==, 4); + len = sc_string_append_char(buf, sizeof buf, 'o'); + g_assert_cmpstr(buf, ==, "hello"); + g_assert_cmpint(len, ==, 5); +} + +static void test_sc_string_append_char_pair__uninitialized_buf() +{ + if (g_test_subprocess()) { + char buf[3] = { 0xFF, 0xFF, 0xFF }; + sc_string_append_char_pair(buf, sizeof buf, 'a', 'b'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: dst is unterminated\n"); +} + +static void test_sc_string_append_char_pair__NULL_buf() +{ + if (g_test_subprocess()) { + sc_string_append_char_pair(NULL, 3, 'a', 'b'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: buffer is NULL\n"); +} + +static void test_sc_string_append_char_pair__overflow() +{ + if (g_test_subprocess()) { + char buf[2] = { 0 }; + sc_string_append_char_pair(buf, sizeof buf, 'a', 'b'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: not enough space\n"); +} + +static void test_sc_string_append_char_pair__invalid_zero_c1() +{ + if (g_test_subprocess()) { + char buf[3] = { 0 }; + sc_string_append_char_pair(buf, sizeof buf, '\0', 'a'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: cannot append string terminator\n"); +} + +static void test_sc_string_append_char_pair__invalid_zero_c2() +{ + if (g_test_subprocess()) { + char buf[3] = { 0 }; + sc_string_append_char_pair(buf, sizeof buf, 'a', '\0'); + + g_test_message + ("expected sc_string_append_char_pair not to return"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("cannot append character pair: cannot append string terminator\n"); +} + +static void test_sc_string_append_char_pair__normal() +{ + char buf[16]; + size_t len; + sc_string_init(buf, sizeof buf); + + len = sc_string_append_char_pair(buf, sizeof buf, 'h', 'e'); + g_assert_cmpstr(buf, ==, "he"); + g_assert_cmpint(len, ==, 2); + len = sc_string_append_char_pair(buf, sizeof buf, 'l', 'l'); + g_assert_cmpstr(buf, ==, "hell"); + g_assert_cmpint(len, ==, 4); + len = sc_string_append_char_pair(buf, sizeof buf, 'o', '!'); + g_assert_cmpstr(buf, ==, "hello!"); + g_assert_cmpint(len, ==, 6); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/string-utils/sc_streq", test_sc_streq); + g_test_add_func("/string-utils/sc_endswith", test_sc_endswith); + g_test_add_func("/string-utils/sc_must_snprintf", + test_sc_must_snprintf); + g_test_add_func("/string-utils/sc_must_snprintf/fail", + test_sc_must_snprintf__fail); + g_test_add_func("/string-utils/sc_string_append/normal", + test_sc_string_append); + g_test_add_func("/string-utils/sc_string_append/empty_to_full", + test_sc_string_append__empty_to_full); + g_test_add_func("/string-utils/sc_string_append/overflow", + test_sc_string_append__overflow); + g_test_add_func("/string-utils/sc_string_append/uninitialized_buf", + test_sc_string_append__uninitialized_buf); + g_test_add_func("/string-utils/sc_string_append/NULL_buf", + test_sc_string_append__NULL_buf); + g_test_add_func("/string-utils/sc_string_append/NULL_str", + test_sc_string_append__NULL_str); + g_test_add_func("/string-utils/sc_string_init/normal", + test_sc_string_init__normal); + g_test_add_func("/string-utils/sc_string_init/empty_buf", + test_sc_string_init__empty_buf); + g_test_add_func("/string-utils/sc_string_init/NULL_buf", + test_sc_string_init__NULL_buf); + g_test_add_func + ("/string-utils/sc_string_append_char__uninitialized_buf", + test_sc_string_append_char__uninitialized_buf); + g_test_add_func("/string-utils/sc_string_append_char__NULL_buf", + test_sc_string_append_char__NULL_buf); + g_test_add_func("/string-utils/sc_string_append_char__overflow", + test_sc_string_append_char__overflow); + g_test_add_func("/string-utils/sc_string_append_char__invalid_zero", + test_sc_string_append_char__invalid_zero); + g_test_add_func("/string-utils/sc_string_append_char__normal", + test_sc_string_append_char__normal); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__NULL_buf", + test_sc_string_append_char_pair__NULL_buf); + g_test_add_func("/string-utils/sc_string_append_char_pair__overflow", + test_sc_string_append_char_pair__overflow); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__invalid_zero_c1", + test_sc_string_append_char_pair__invalid_zero_c1); + g_test_add_func + ("/string-utils/sc_string_append_char_pair__invalid_zero_c2", + test_sc_string_append_char_pair__invalid_zero_c2); + g_test_add_func("/string-utils/sc_string_append_char_pair__normal", + test_sc_string_append_char_pair__normal); +} diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/utils.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/utils.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/utils.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/utils.c 2017-03-02 06:37:54.000000000 +0000 @@ -116,9 +116,14 @@ return value; } +bool sc_is_debug_enabled() +{ + return getenv_bool("SNAP_CONFINE_DEBUG"); +} + void debug(const char *msg, ...) { - if (getenv_bool("SNAP_CONFINE_DEBUG")) { + if (sc_is_debug_enabled()) { va_list va; va_start(va, msg); fprintf(stderr, "DEBUG: "); @@ -142,21 +147,6 @@ die("fclose failed"); } -int must_snprintf(char *str, size_t size, const char *format, ...) -{ - int n; - - va_list va; - va_start(va, format); - n = vsnprintf(str, size, format, va); - va_end(va); - - if (n < 0 || n >= size) - die("failed to snprintf %s", str); - - return n; -} - int sc_nonfatal_mkpath(const char *const path, mode_t mode) { // If asked to create an empty path, return immediately. diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/utils.h snapd-2.23.1+16.10/cmd/libsnap-confine-private/utils.h --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/utils.h 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/utils.h 2017-03-02 06:37:54.000000000 +0000 @@ -14,12 +14,12 @@ * along with this program. If not, see . * */ -#include -#include - #ifndef CORE_LAUNCHER_UTILS_H #define CORE_LAUNCHER_UTILS_H +#include +#include + __attribute__ ((noreturn)) __attribute__ ((format(printf, 1, 2))) void die(const char *fmt, ...); @@ -30,11 +30,14 @@ __attribute__ ((format(printf, 1, 2))) void debug(const char *fmt, ...); -void write_string_to_file(const char *filepath, const char *buf); +/** + * Return true if debugging is enabled. + * + * This can used to avoid costly computation that is only useful for debugging. + **/ +bool sc_is_debug_enabled(); -// snprintf version that dies on any error condition -__attribute__ ((format(printf, 3, 4))) -int must_snprintf(char *str, size_t size, const char *format, ...); +void write_string_to_file(const char *filepath, const char *buf); /** * Safely create a given directory. diff -Nru snapd-2.22.6+16.10/cmd/libsnap-confine-private/utils-test.c snapd-2.23.1+16.10/cmd/libsnap-confine-private/utils-test.c --- snapd-2.22.6+16.10/cmd/libsnap-confine-private/utils-test.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/libsnap-confine-private/utils-test.c 2017-03-02 06:37:54.000000000 +0000 @@ -100,10 +100,10 @@ int err = chdir(temp_dir); g_assert_cmpint(err, ==, 0); - g_test_queue_destroy((GDestroyNotify) rmdir, temp_dir); g_test_queue_free(temp_dir); - g_test_queue_destroy((GDestroyNotify) chdir, orig_dir); + g_test_queue_destroy((GDestroyNotify) rmdir, temp_dir); g_test_queue_free(orig_dir); + g_test_queue_destroy((GDestroyNotify) chdir, orig_dir); } /** diff -Nru snapd-2.22.6+16.10/cmd/Makefile.am snapd-2.23.1+16.10/cmd/Makefile.am --- snapd-2.22.6+16.10/cmd/Makefile.am 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/Makefile.am 2017-03-08 13:28:17.000000000 +0000 @@ -20,7 +20,7 @@ for f in $(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])) ; do \ out="$$d/`basename $$f.out`"; \ echo "Checking $$f ... "; \ - indent -linux "$$f" -o "$$out"; \ + HOME=$(srcdir) indent "$$f" -o "$$out"; \ diff -Naur "$$f" "$$out" || exit 1; \ done; @@ -35,10 +35,11 @@ .PHONY: check-unit-tests if WITH_UNIT_TESTS -check-unit-tests: snap-confine/snap-confine-unit-tests system-shutdown/system-shutdown-unit-tests libsnap-confine-private/unit-tests - ./snap-confine/snap-confine-unit-tests - ./system-shutdown/system-shutdown-unit-tests - ./libsnap-confine-private/unit-tests +check-unit-tests: snap-confine/snap-confine-unit-tests system-shutdown/system-shutdown-unit-tests libsnap-confine-private/unit-tests snap-update-ns/unit-tests + $(HAVE_VALGRIND) ./libsnap-confine-private/unit-tests + $(HAVE_VALGRIND) ./snap-confine/snap-confine-unit-tests + $(HAVE_VALGRIND) ./snap-update-ns/unit-tests + $(HAVE_VALGRIND) ./system-shutdown/system-shutdown-unit-tests else check-unit-tests: echo "unit tests are disabled (rebuild with --enable-unit-tests)" @@ -46,7 +47,7 @@ .PHONY: fmt fmt: $(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch])) - indent -linux $^ + HOME=$(srcdir) indent $^ # The hack target helps devlopers work on snap-confine on their live system by # installing a fresh copy of snap confine and the appropriate apparmor profile. @@ -69,14 +70,20 @@ libsnap-confine-private/cleanup-funcs.h \ libsnap-confine-private/error.c \ libsnap-confine-private/error.h \ + libsnap-confine-private/fault-injection.c \ + libsnap-confine-private/fault-injection.h \ libsnap-confine-private/mount-opt.c \ libsnap-confine-private/mount-opt.h \ libsnap-confine-private/mountinfo.c \ libsnap-confine-private/mountinfo.h \ + libsnap-confine-private/privs.c \ + libsnap-confine-private/privs.h \ libsnap-confine-private/secure-getenv.c \ libsnap-confine-private/secure-getenv.h \ libsnap-confine-private/snap.c \ libsnap-confine-private/snap.h \ + libsnap-confine-private/string-utils.c \ + libsnap-confine-private/string-utils.h \ libsnap-confine-private/utils.c \ libsnap-confine-private/utils.h @@ -86,11 +93,13 @@ libsnap-confine-private/classic-test.c \ libsnap-confine-private/cleanup-funcs-test.c \ libsnap-confine-private/error-test.c \ + libsnap-confine-private/fault-injection-test.c \ libsnap-confine-private/mount-opt-test.c \ libsnap-confine-private/mountinfo-test.c \ - libsnap-confine-private/fault-injection-test.c \ + libsnap-confine-private/privs-test.c \ libsnap-confine-private/secure-getenv-test.c \ libsnap-confine-private/snap-test.c \ + libsnap-confine-private/string-utils-test.c \ libsnap-confine-private/unit-tests-main.c \ libsnap-confine-private/unit-tests.c \ libsnap-confine-private/unit-tests.h \ @@ -98,6 +107,17 @@ libsnap_confine_private_unit_tests_CFLAGS = $(GLIB_CFLAGS) libsnap_confine_private_unit_tests_LDADD = $(GLIB_LIBS) libsnap_confine_private_unit_tests_CFLAGS += -D_ENABLE_FAULT_INJECTION + +# XXX: This injects a dependency on libcap in a way that makes automake happy +# and allows us to link libcap statically. We need to link in libcap statically +# as at this time adding runtime dependencies to snap-confine is tricky. + +libsnap-confine-private/unit-tests$(EXEEXT): $(libsnap_confine_private_unit_tests_OBJECTS) $(libsnap_confine_private_unit_tests_DEPENDENCIES) $(EXTRA_libsnap_confine_private_unit_tests_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f libsnap-confine-private/unit-tests$(EXEEXT) + $(AM_V_CCLD)$(libsnap_confine_private_unit_tests_LINK) $(libsnap_confine_private_unit_tests_OBJECTS) $(libsnap_confine_private_unit_tests_LDADD) $(LIBS) + +libsnap-confine-private/unit-tests$(EXEEXT): LIBS += -Wl,-Bstatic -lcap -Wl,-Bdynamic + endif ## @@ -109,6 +129,14 @@ decode_mount_opts_decode_mount_opts_SOURCES = \ decode-mount-opts/decode-mount-opts.c decode_mount_opts_decode_mount_opts_LDADD = libsnap-confine-private.a +# XXX: this makes automake generate decode_mount_opts_decode_mount_opts_LINK +decode_mount_opts_decode_mount_opts_CFLAGS = -D_fake + +decode-mount-opts/decode-mount-opts$(EXEEXT): $(decode_mount_opts_decode_mount_opts_OBJECTS) $(decode_mount_opts_decode_mount_opts_DEPENDENCIES) $(EXTRA_decode_mount_opts_decode_mount_opts_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f decode-mount-opts/decode-mount-opts$(EXEEXT) + $(AM_V_CCLD)$(decode_mount_opts_decode_mount_opts_LINK) $(decode_mount_opts_decode_mount_opts_OBJECTS) $(decode_mount_opts_decode_mount_opts_LDADD) $(LIBS) + +decode-mount-opts/decode-mount-opts$(EXEEXT): LIBS += -Wl,-Bstatic -lcap -Wl,-Bdynamic ## ## snap-confine @@ -120,7 +148,9 @@ snap-confine/tests/test_bad_seccomp_filter_args_null \ snap-confine/tests/test_bad_seccomp_filter_args_prctl \ snap-confine/tests/test_bad_seccomp_filter_args_prio \ + snap-confine/tests/test_bad_seccomp_filter_args_quotactl \ snap-confine/tests/test_bad_seccomp_filter_args_socket \ + snap-confine/tests/test_bad_seccomp_filter_args_termios \ snap-confine/tests/test_bad_seccomp_filter_length \ snap-confine/tests/test_bad_seccomp_filter_missing_trailing_newline \ snap-confine/tests/test_complain \ @@ -132,7 +162,9 @@ snap-confine/tests/test_restrictions_working_args_clone \ snap-confine/tests/test_restrictions_working_args_prctl \ snap-confine/tests/test_restrictions_working_args_prio \ + snap-confine/tests/test_restrictions_working_args_quotactl \ snap-confine/tests/test_restrictions_working_args_socket \ + snap-confine/tests/test_restrictions_working_args_termios \ snap-confine/tests/test_unrestricted \ snap-confine/tests/test_unrestricted_missed \ snap-confine/tests/test_whitelist @@ -160,6 +192,8 @@ snap-confine/ns-support.h \ snap-confine/quirks.c \ snap-confine/quirks.h \ + snap-confine/snap-confine-args.c \ + snap-confine/snap-confine-args.h \ snap-confine/snap-confine.c \ snap-confine/udev-support.c \ snap-confine/udev-support.h \ @@ -172,6 +206,12 @@ snap_confine_snap_confine_CFLAGS += $(LIBUDEV_CFLAGS) snap_confine_snap_confine_LDADD += $(LIBUDEV_LIBS) +snap-confine/snap-confine$(EXEEXT): $(snap_confine_snap_confine_OBJECTS) $(snap_confine_snap_confine_DEPENDENCIES) $(EXTRA_snap_confine_snap_confine_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f snap-confine/snap-confine$(EXEEXT) + $(AM_V_CCLD)$(snap_confine_snap_confine_LINK) $(snap_confine_snap_confine_OBJECTS) $(snap_confine_snap_confine_LDADD) $(LIBS) + +snap-confine/snap-confine$(EXEEXT): LIBS += -Wl,-Bstatic -lcap -Wl,-Bdynamic + # This is here to help fix rpmlint hardening issue. # https://en.opensuse.org/openSUSE:Packaging_checks#non-position-independent-executable snap_confine_snap_confine_CFLAGS += $(SUID_CFLAGS) @@ -190,6 +230,21 @@ snap_confine_snap_confine_LDADD += $(APPARMOR_LIBS) endif +# an extra build that has additional debugging enabled at compile time + +noinst_PROGRAMS += snap-confine/snap-confine-debug +snap_confine_snap_confine_debug_SOURCES = $(snap_confine_snap_confine_SOURCES) +snap_confine_snap_confine_debug_CFLAGS = $(snap_confine_snap_confine_CFLAGS) +snap_confine_snap_confine_debug_LDFLAGS = $(snap_confine_snap_confine_LDFLAGS) +snap_confine_snap_confine_debug_LDADD = $(snap_confine_snap_confine_LDADD) +snap_confine_snap_confine_debug_CFLAGS += -DSNAP_CONFINE_DEBUG_BUILD=1 + +snap-confine/snap-confine-debug$(EXEEXT): $(snap_confine_snap_confine_debug_OBJECTS) $(snap_confine_snap_confine_debug_DEPENDENCIES) $(EXTRA_snap_confine_snap_confine_debug_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f snap-confine/snap-confine-debug$(EXEEXT) + $(AM_V_CCLD)$(snap_confine_snap_confine_debug_LINK) $(snap_confine_snap_confine_debug_OBJECTS) $(snap_confine_snap_confine_debug_LDADD) $(LIBS) + +snap-confine/snap-confine-debug$(EXEEXT): LIBS += -Wl,-Bstatic -lcap -Wl,-Bdynamic + if WITH_UNIT_TESTS noinst_PROGRAMS += snap-confine/snap-confine-unit-tests snap_confine_snap_confine_unit_tests_SOURCES = \ @@ -201,10 +256,19 @@ snap-confine/mount-support-test.c \ snap-confine/ns-support-test.c \ snap-confine/quirks.c \ - snap-confine/quirks.h + snap-confine/quirks.h \ + snap-confine/snap-confine-args-test.c snap_confine_snap_confine_unit_tests_CFLAGS = $(snap_confine_snap_confine_CFLAGS) $(GLIB_CFLAGS) snap_confine_snap_confine_unit_tests_LDADD = $(snap_confine_snap_confine_LDADD) $(GLIB_LIBS) snap_confine_snap_confine_unit_tests_LDFLAGS = $(snap_confine_snap_confine_LDFLAGS) + + +snap-confine/snap-confine-unit-tests$(EXEEXT): $(snap_confine_snap_confine_unit_tests_OBJECTS) $(snap_confine_snap_confine_unit_tests_DEPENDENCIES) $(EXTRA_snap_confine_snap_confine_unit_tests_DEPENDENCIES) libsnap-confine-private/$(am__dirstamp) + @rm -f snap-confine/snap-confine-unit-tests$(EXEEXT) + $(AM_V_CCLD)$(snap_confine_snap_confine_unit_tests_LINK) $(snap_confine_snap_confine_unit_tests_OBJECTS) $(snap_confine_snap_confine_unit_tests_LDADD) $(LIBS) + +snap-confine/snap-confine-unit-tests$(EXEEXT): LIBS += -Wl,-Bstatic -lcap -Wl,-Bdynamic + endif snap-confine/%.5: snap-confine/%.rst @@ -220,8 +284,10 @@ # foo.bar.froz The inner subst replaces slashes with dots and the outer # patsubst strips the leading dot install-data-local:: snap-confine/snap-confine.apparmor +if APPARMOR install -d -m 755 $(DESTDIR)/etc/apparmor.d/ install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine +endif # NOTE: The 'void' directory *has to* be chmod 000 install-data-local:: @@ -256,7 +322,7 @@ # http://www.gnu.org/software/automake/manual/automake.html#Hard_002dCoded-Install-Paths # # Install udev rules -install-data-local:: snap-confine/snap-confine.apparmor +install-data-local:: install -d -m 755 $(DESTDIR)$(shell pkg-config udev --variable=udevdir)/rules.d install -m 644 $(srcdir)/snap-confine/80-snappy-assign.rules $(DESTDIR)$(shell pkg-config udev --variable=udevdir)/rules.d @@ -329,6 +395,26 @@ snap_update_ns_snap_update_ns_LDADD = libsnap-confine-private.a +snap_update_ns_snap_update_ns_SOURCES = \ + snap-update-ns/snap-update-ns.c \ + snap-update-ns/mount-entry.h \ + snap-update-ns/mount-entry.c + snap-update-ns/%.5: snap-update-ns/%.rst mkdir -p snap-update-ns $(HAVE_RST2MAN) $^ > $@ + +if WITH_UNIT_TESTS +noinst_PROGRAMS += snap-update-ns/unit-tests +snap_update_ns_unit_tests_SOURCES = \ + libsnap-confine-private/unit-tests-main.c \ + libsnap-confine-private/unit-tests.c \ + snap-update-ns/mount-entry-test.c \ + snap-update-ns/test-data.c \ + snap-update-ns/test-data.h \ + snap-update-ns/test-utils.c \ + snap-update-ns/test-utils.h +snap_update_ns_unit_tests_LDADD = libsnap-confine-private.a +snap_update_ns_unit_tests_CFLAGS = $(GLIB_CFLAGS) +snap_update_ns_unit_tests_LDADD += $(GLIB_LIBS) +endif diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_auto_import_test.go snapd-2.23.1+16.10/cmd/snap/cmd_auto_import_test.go --- snapd-2.22.6+16.10/cmd/snap/cmd_auto_import_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_auto_import_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -154,7 +154,7 @@ l, err := snap.AutoImportCandidates() c.Check(err, IsNil) - c.Check(l, DeepEquals, files[1:len(files)]) + c.Check(l, DeepEquals, files[1:]) } func (s *SnapSuite) TestAutoImportAssertsHappyNotOnClassic(c *C) { diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_booted.go snapd-2.23.1+16.10/cmd/snap/cmd_booted.go --- snapd-2.22.6+16.10/cmd/snap/cmd_booted.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_booted.go 2017-03-02 06:37:54.000000000 +0000 @@ -45,6 +45,6 @@ if len(args) > 0 { return ErrExtraArgs } - fmt.Fprintf(Stderr, "booted command is deprecated") + fmt.Fprintf(Stderr, "booted command is deprecated\n") return nil } diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_buy.go snapd-2.23.1+16.10/cmd/snap/cmd_buy.go --- snapd-2.22.6+16.10/cmd/snap/cmd_buy.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_buy.go 2017-03-06 13:33:50.000000000 +0000 @@ -94,11 +94,13 @@ if e, ok := err.(*client.Error); ok { switch e.Kind { case client.ErrorKindNoPaymentMethods: - return fmt.Errorf(i18n.G(`You do not have a payment method associated with your account, visit https://my.ubuntu.com/payment/edit to add one. -Once completed, return here and run 'snap buy %s' again.`), snap.Name) + return fmt.Errorf(i18n.G(`You need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. + +Once you’ve added your payment details, you just need to run 'snap buy %s' again.`), snap.Name) case client.ErrorKindTermsNotAccepted: - return fmt.Errorf(i18n.G(`Please visit https://my.ubuntu.com/payment/edit to agree to the latest terms and conditions. -Once completed, return here and run 'snap buy %s' again.`), snap.Name) + return fmt.Errorf(i18n.G(`In order to buy %q, you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. + +Once completed, return here and run 'snap buy %s' again.`), snap.Name, snap.Name) } } return err diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_buy_test.go snapd-2.23.1+16.10/cmd/snap/cmd_buy_test.go --- snapd-2.22.6+16.10/cmd/snap/cmd_buy_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_buy_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -394,8 +394,9 @@ rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) - c.Check(err.Error(), check.Equals, `You do not have a payment method associated with your account, visit https://my.ubuntu.com/payment/edit to add one. -Once completed, return here and run 'snap buy hello' again.`) + c.Check(err.Error(), check.Equals, `You need to have a payment method associated with your account in order to buy a snap, please visit https://my.ubuntu.com/payment/edit to add one. + +Once you’ve added your payment details, you just need to run 'snap buy hello' again.`) c.Check(rest, check.DeepEquals, []string{"hello"}) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") @@ -428,7 +429,8 @@ rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) c.Assert(err, check.NotNil) - c.Check(err.Error(), check.Equals, `Please visit https://my.ubuntu.com/payment/edit to agree to the latest terms and conditions. + c.Check(err.Error(), check.Equals, `In order to buy "hello", you need to agree to the latest terms and conditions. Please visit https://my.ubuntu.com/payment/edit to do this. + Once completed, return here and run 'snap buy hello' again.`) c.Check(rest, check.DeepEquals, []string{"hello"}) c.Check(s.Stdout(), check.Equals, "") diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_connect.go snapd-2.23.1+16.10/cmd/snap/cmd_connect.go --- snapd-2.22.6+16.10/cmd/snap/cmd_connect.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_connect.go 2017-03-02 06:37:54.000000000 +0000 @@ -27,7 +27,7 @@ type cmdConnect struct { Positionals struct { - PlugSpec SnapAndName `required:"yes"` + PlugSpec connectPlugSpec `required:"yes"` SlotSpec SnapAndName } `positional-args:"true"` } diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_download.go snapd-2.23.1+16.10/cmd/snap/cmd_download.go --- snapd-2.22.6+16.10/cmd/snap/cmd_download.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_download.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -31,9 +31,7 @@ "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/image" - "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/snap" - "github.com/snapcore/snapd/store" ) type cmdDownload struct { @@ -62,7 +60,7 @@ }}) } -func fetchSnapAssertions(sto *store.Store, snapPath string, snapInfo *snap.Info, dlOpts *image.DownloadOptions) error { +func fetchSnapAssertions(tsto *image.ToolingStore, snapPath string, snapInfo *snap.Info) error { db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ Backstore: asserts.NewMemoryBackstore(), Trusted: sysdb.Trusted(), @@ -82,9 +80,10 @@ save := func(a asserts.Assertion) error { return encoder.Encode(a) } - f := image.StoreAssertionFetcher(sto, dlOpts, db, save) + f := tsto.AssertionFetcher(db, save) - return image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db) + _, err = image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db) + return err } func (x *cmdDownload) Execute(args []string) error { @@ -109,29 +108,23 @@ snapName := string(x.Positional.Snap) - // FIXME: set auth context - var authContext auth.AuthContext - var user *auth.UserState - - sto := store.New(nil, authContext) - // we always allow devmode for downloads - devMode := true + tsto, err := image.NewToolingStore() + if err != nil { + return err + } + fmt.Fprintf(Stderr, i18n.G("Fetching snap %q\n"), snapName) dlOpts := image.DownloadOptions{ TargetDir: "", // cwd - DevMode: devMode, Channel: x.Channel, - User: user, } - - fmt.Fprintf(Stderr, i18n.G("Fetching snap %q\n"), snapName) - snapPath, snapInfo, err := image.DownloadSnap(sto, snapName, revision, &dlOpts) + snapPath, snapInfo, err := tsto.DownloadSnap(snapName, revision, &dlOpts) if err != nil { return err } fmt.Fprintf(Stderr, i18n.G("Fetching assertions for %q\n"), snapName) - err = fetchSnapAssertions(sto, snapPath, snapInfo, &dlOpts) + err = fetchSnapAssertions(tsto, snapPath, snapInfo) if err != nil { return err } diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_first_boot.go snapd-2.23.1+16.10/cmd/snap/cmd_first_boot.go --- snapd-2.22.6+16.10/cmd/snap/cmd_first_boot.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_first_boot.go 2017-03-02 06:37:54.000000000 +0000 @@ -44,6 +44,6 @@ if len(args) > 0 { return ErrExtraArgs } - fmt.Fprintf(Stderr, "firstboot command is deprecated") + fmt.Fprintf(Stderr, "firstboot command is deprecated\n") return nil } diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_info.go snapd-2.23.1+16.10/cmd/snap/cmd_info.go --- snapd-2.22.6+16.10/cmd/snap/cmd_info.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_info.go 2017-03-02 06:37:54.000000000 +0000 @@ -222,6 +222,9 @@ // TODO: have publisher; use publisher here, // and additionally print developer if publisher != developer fmt.Fprintf(w, "publisher:\t%s\n", both.Developer) + if both.Contact != "" { + fmt.Fprintf(w, "contact:\t%s\n", strings.TrimPrefix(both.Contact, "mailto:")) + } maybePrintPrice(w, remote, resInfo) // FIXME: find out for real termWidth := 77 diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_list.go snapd-2.23.1+16.10/cmd/snap/cmd_list.go --- snapd-2.22.6+16.10/cmd/snap/cmd_list.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_list.go 2017-03-06 13:33:50.000000000 +0000 @@ -67,17 +67,23 @@ return listSnaps(names, x.All) } +var ErrNoMatchingSnaps = errors.New(i18n.G("no matching snaps installed")) + func listSnaps(names []string, all bool) error { cli := Client() snaps, err := cli.List(names, &client.ListOptions{All: all}) if err != nil { if err == client.ErrNoSnapsInstalled { - fmt.Fprintln(Stderr, i18n.G("No snaps are installed yet. Try \"snap install hello-world\".")) - return nil + if len(names) == 0 { + fmt.Fprintln(Stderr, i18n.G("No snaps are installed yet. Try \"snap install hello-world\".")) + return nil + } else { + return ErrNoMatchingSnaps + } } return err } else if len(snaps) == 0 { - return errors.New(i18n.G("no matching snaps installed")) + return ErrNoMatchingSnaps } sort.Sort(snapsByName(snaps)) diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_list_test.go snapd-2.23.1+16.10/cmd/snap/cmd_list_test.go --- snapd-2.22.6+16.10/cmd/snap/cmd_list_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_list_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -131,11 +131,8 @@ n++ }) - rest, err := snap.Parser().ParseArgs([]string{"list", "quux"}) - c.Assert(err, check.IsNil) - c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Equals, "") - c.Check(s.Stderr(), check.Equals, "No snaps are installed yet. Try \"snap install hello-world\".\n") + _, err := snap.Parser().ParseArgs([]string{"list", "quux"}) + c.Assert(err, check.ErrorMatches, `no matching snaps installed`) } func (s *SnapSuite) TestListWithNoMatchingQuery(c *check.C) { diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_run.go snapd-2.23.1+16.10/cmd/snap/cmd_run.go --- snapd-2.22.6+16.10/cmd/snap/cmd_run.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_run.go 2017-03-02 06:37:54.000000000 +0000 @@ -118,6 +118,50 @@ return info, nil } +func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error { + // 'current' symlink for user data (SNAP_USER_DATA) + userData := info.UserDataDir(usr.HomeDir) + wantedSymlinkValue := filepath.Base(userData) + currentActiveSymlink := filepath.Join(userData, "..", "current") + + var err error + var currentSymlinkValue string + for i := 0; i < 5; i++ { + currentSymlinkValue, err = os.Readlink(currentActiveSymlink) + // Failure other than non-existing symlink is fatal + if err != nil && !os.IsNotExist(err) { + // TRANSLATORS: %v the error message + return fmt.Errorf(i18n.G("cannot read symlink: %v"), err) + } + + if currentSymlinkValue == wantedSymlinkValue { + break + } + + if err == nil { + // We may be racing with other instances of snap-run that try to do the same thing + // If the symlink is already removed then we can ignore this error. + err = os.Remove(currentActiveSymlink) + if err != nil && !os.IsNotExist(err) { + // abort with error + break + } + } + + err = os.Symlink(wantedSymlinkValue, currentActiveSymlink) + // Error other than symlink already exists will abort and be propagated + if err == nil || !os.IsExist(err) { + break + } + // If we arrived here it means the symlink couldn't be created because it got created + // in the meantime by another instance, so we will try again. + } + if err != nil { + return fmt.Errorf(i18n.G("cannot update the 'current' symlink of %q: %v"), currentActiveSymlink, err) + } + return nil +} + func createUserDataDirs(info *snap.Info) error { usr, err := userCurrent() if err != nil { @@ -133,7 +177,8 @@ return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err) } } - return nil + + return createOrUpdateUserDataSymlink(info, usr) } func snapRunApp(snapApp, command string, args []string) error { @@ -176,13 +221,13 @@ } cmd := []string{ - filepath.Join(dirs.LibExecDir, "snap-confine"), + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), } if info.NeedsClassic() { cmd = append(cmd, "--classic") } cmd = append(cmd, securityTag) - cmd = append(cmd, filepath.Join(dirs.LibExecDir, "snap-exec")) + cmd = append(cmd, filepath.Join(dirs.CoreLibExecDir, "snap-exec")) if command != "" { cmd = append(cmd, "--command="+command) diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_run_test.go snapd-2.23.1+16.10/cmd/snap/cmd_run_test.go --- snapd-2.22.6+16.10/cmd/snap/cmd_run_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_run_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -89,11 +89,11 @@ rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) - c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ - filepath.Join(dirs.LibExecDir, "snap-confine"), + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", - filepath.Join(dirs.LibExecDir, "snap-exec"), + filepath.Join(dirs.DistroLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } @@ -125,11 +125,11 @@ rest, err := snaprun.Parser().ParseArgs([]string{"run", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) - c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ - filepath.Join(dirs.LibExecDir, "snap-confine"), "--classic", + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "--classic", "snap.snapname.app", - filepath.Join(dirs.LibExecDir, "snap-exec"), + filepath.Join(dirs.DistroLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } @@ -160,11 +160,11 @@ // and run it! err = snaprun.SnapRunApp("snapname.app", "my-command", []string{"arg1", "arg2"}) c.Assert(err, check.IsNil) - c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ - filepath.Join(dirs.LibExecDir, "snap-confine"), + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", - filepath.Join(dirs.LibExecDir, "snap-exec"), + filepath.Join(dirs.DistroLibExecDir, "snap-exec"), "--command=my-command", "snapname.app", "arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } @@ -212,11 +212,11 @@ // Run a hook from the active revision _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "snapname"}) c.Assert(err, check.IsNil) - c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ - filepath.Join(dirs.LibExecDir, "snap-confine"), + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.hook.configure", - filepath.Join(dirs.LibExecDir, "snap-exec"), + filepath.Join(dirs.DistroLibExecDir, "snap-exec"), "--hook=configure", "snapname"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } @@ -247,11 +247,11 @@ // Specifically pass "unset" which would use the active version. _, err = snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=unset", "snapname"}) c.Assert(err, check.IsNil) - c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ - filepath.Join(dirs.LibExecDir, "snap-confine"), + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.hook.configure", - filepath.Join(dirs.LibExecDir, "snap-exec"), + filepath.Join(dirs.DistroLibExecDir, "snap-exec"), "--hook=configure", "snapname"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } @@ -284,11 +284,11 @@ // Run a hook on revision 41 _, err := snaprun.Parser().ParseArgs([]string{"run", "--hook=configure", "-r=41", "snapname"}) c.Assert(err, check.IsNil) - c.Check(execArg0, check.Equals, filepath.Join(dirs.LibExecDir, "snap-confine")) + c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine")) c.Check(execArgs, check.DeepEquals, []string{ - filepath.Join(dirs.LibExecDir, "snap-confine"), + filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.hook.configure", - filepath.Join(dirs.LibExecDir, "snap-exec"), + filepath.Join(dirs.DistroLibExecDir, "snap-exec"), "--hook=configure", "snapname"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41") } diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_snap_op.go snapd-2.23.1+16.10/cmd/snap/cmd_snap_op.go --- snapd-2.22.6+16.10/cmd/snap/cmd_snap_op.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_snap_op.go 2017-03-08 13:28:14.000000000 +0000 @@ -50,6 +50,26 @@ pollTime = 100 * time.Millisecond ) +type waitMixin struct { + NoWait bool `long:"no-wait" hidden:"true"` +} + +// TODO: use waitMixin outside of cmd_snap_op.go + +var waitDescs = mixinDescs{ + "no-wait": i18n.G("Do not wait for the operation to finish but just print the change id."), +} + +var noWait = errors.New("no wait for op") + +func (wmx *waitMixin) wait(cli *client.Client, id string) (*client.Change, error) { + if wmx.NoWait { + fmt.Fprintf(Stdout, "%s\n", id) + return nil, noWait + } + return wait(cli, id) +} + func wait(cli *client.Client, id string) (*client.Change, error) { pb := progress.NewTextProgress() defer func() { @@ -174,6 +194,8 @@ `) type cmdRemove struct { + waitMixin + Revision string `long:"revision"` Positional struct { Snaps []installedSnapName `positional-arg-name:"" required:"1"` @@ -193,7 +215,11 @@ return err } - if _, err := wait(cli, changeID); err != nil { + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { return err } @@ -213,7 +239,10 @@ return err } - chg, err := wait(cli, changeID) + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } if err != nil { return err } @@ -374,6 +403,8 @@ } type cmdInstall struct { + waitMixin + channelMixin modeMixin Revision string `long:"revision"` @@ -424,7 +455,10 @@ setupAbortHandler(changeID) - chg, err := wait(cli, changeID) + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } if err != nil { return err } @@ -458,7 +492,10 @@ setupAbortHandler(changeID) - chg, err := wait(cli, changeID) + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } if err != nil { return err } @@ -523,6 +560,8 @@ } type cmdRefresh struct { + waitMixin + channelMixin modeMixin @@ -534,14 +573,17 @@ } `positional-args:"yes"` } -func refreshMany(snaps []string, opts *client.SnapOptions) error { +func (x *cmdRefresh) refreshMany(snaps []string, opts *client.SnapOptions) error { cli := Client() changeID, err := cli.RefreshMany(snaps, opts) if err != nil { return err } - chg, err := wait(cli, changeID) + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } if err != nil { return err } @@ -560,7 +602,7 @@ return nil } -func refreshOne(name string, opts *client.SnapOptions) error { +func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error { cli := Client() changeID, err := cli.Refresh(name, opts) if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindNoUpdateAvailable { @@ -571,14 +613,18 @@ return err } - if _, err := wait(cli, changeID); err != nil { + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { return err } return showDone([]string{name}, "refresh") } -func listRefresh() error { +func (x *cmdRefresh) listRefresh() error { cli := Client() snaps, _, err := cli.Find(&client.FindOptions{ Refresh: true, @@ -617,8 +663,14 @@ return errors.New(i18n.G("--list does not take mode nor channel flags")) } - return listRefresh() + return x.listRefresh() + } + + if len(x.Positional.Snaps) == 0 && os.Getenv("SNAP_REFRESH_FROM_TIMER") == "1" { + fmt.Fprintf(Stdout, "Ignoring `snap refresh` from the systemd timer") + return nil } + names := make([]string, len(x.Positional.Snaps)) for i, name := range x.Positional.Snaps { names[i] = string(name) @@ -630,7 +682,7 @@ Revision: x.Revision, } x.setModes(opts) - return refreshOne(names[0], opts) + return x.refreshOne(names[0], opts) } if x.asksForMode() || x.asksForChannel() { @@ -641,10 +693,12 @@ return errors.New(i18n.G("a single snap name must be specified when ignoring validation")) } - return refreshMany(names, nil) + return x.refreshMany(names, nil) } type cmdTry struct { + waitMixin + modeMixin Positional struct { SnapDir string `positional-arg-name:""` @@ -680,11 +734,19 @@ } changeID, err := cli.Try(path, opts) + if e, ok := err.(*client.Error); ok && e.Kind == client.ErrorKindNotSnap { + return fmt.Errorf(i18n.G(`%q does not contain an unpacked snap. + +Try "snapcraft prime" in your project directory, then "snap try" again.`), path) + } if err != nil { return err } - chg, err := wait(cli, changeID) + chg, err := x.wait(cli, changeID) + if err == noWait { + return nil + } if err != nil { return err } @@ -713,6 +775,8 @@ } type cmdEnable struct { + waitMixin + Positional struct { Snap installedSnapName `positional-arg-name:""` } `positional-args:"yes" required:"yes"` @@ -727,7 +791,10 @@ return err } - _, err = wait(cli, changeID) + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } if err != nil { return err } @@ -737,6 +804,8 @@ } type cmdDisable struct { + waitMixin + Positional struct { Snap installedSnapName `positional-arg-name:""` } `positional-args:"yes" required:"yes"` @@ -751,7 +820,10 @@ return err } - _, err = wait(cli, changeID) + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } if err != nil { return err } @@ -761,6 +833,8 @@ } type cmdRevert struct { + waitMixin + modeMixin Revision string `long:"revision"` Positional struct { @@ -796,7 +870,11 @@ return err } - if _, err := wait(cli, changeID); err != nil { + _, err = x.wait(cli, changeID) + if err == noWait { + return nil + } + if err != nil { return err } @@ -816,23 +894,23 @@ func init() { addCommand("remove", shortRemoveHelp, longRemoveHelp, func() flags.Commander { return &cmdRemove{} }, - map[string]string{"revision": i18n.G("Remove only the given revision")}, nil) + waitDescs.also(map[string]string{"revision": i18n.G("Remove only the given revision")}), nil) addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} }, - channelDescs.also(modeDescs).also(map[string]string{ + waitDescs.also(channelDescs).also(modeDescs).also(map[string]string{ "revision": i18n.G("Install the given revision of a snap, to which you must have developer access"), "dangerous": i18n.G("Install the given snap file even if there are no pre-acknowledged signatures for it, meaning it was not verified and could be dangerous (--devmode implies this)"), "force-dangerous": i18n.G("Alias for --dangerous (DEPRECATED)"), }), nil) addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, - channelDescs.also(modeDescs).also(map[string]string{ + waitDescs.also(channelDescs).also(modeDescs).also(map[string]string{ "revision": i18n.G("Refresh to the given revision"), "list": i18n.G("Show available snaps for refresh"), "ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"), }), nil) - addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, modeDescs, nil) - addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, nil, nil) - addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, nil, nil) - addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, modeDescs.also(map[string]string{ + addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, waitDescs.also(modeDescs), nil) + addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, waitDescs, nil) + addCommand("disable", shortDisableHelp, longDisableHelp, func() flags.Commander { return &cmdDisable{} }, waitDescs, nil) + addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, waitDescs.also(modeDescs).also(map[string]string{ "revision": "Revert to the given revision", }), nil) } diff -Nru snapd-2.22.6+16.10/cmd/snap/cmd_snap_op_test.go snapd-2.23.1+16.10/cmd/snap/cmd_snap_op_test.go --- snapd-2.22.6+16.10/cmd/snap/cmd_snap_op_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/cmd_snap_op_test.go 2017-03-08 13:28:14.000000000 +0000 @@ -673,6 +673,30 @@ s.runTryTest(c, &client.SnapOptions{Classic: true}) } +func (s *SnapOpSuite) TestTryNoSnapDirErrors(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + w.WriteHeader(http.StatusAccepted) + fmt.Fprintln(w, ` +{ + "type": "error", + "result": { + "message":"error from server", + "kind":"snap-not-a-snap" + }, + "status-code": 400 +} +`) + + }) + + cmd := []string{"try", "/"} + _, err := snap.Parser().ParseArgs(cmd) + c.Assert(err, check.ErrorMatches, `"/" does not contain an unpacked snap. + +Try "snapcraft prime" in your project directory, then "snap try" again.`) +} + func (s *SnapSuite) TestInstallChannelDuplicationError(c *check.C) { _, err := snap.Parser().ParseArgs([]string{"install", "--edge", "--beta", "some-snap"}) c.Assert(err, check.ErrorMatches, "Please specify a single channel") @@ -863,3 +887,33 @@ // ensure that the fake server api was actually hit c.Check(n, check.Equals, total) } + +func (s *SnapOpSuite) TestNoWait(c *check.C) { + s.srv.checker = func(r *http.Request) {} + + cmds := [][]string{ + {"remove", "--no-wait", "foo"}, + {"remove", "--no-wait", "foo", "bar"}, + {"install", "--no-wait", "foo"}, + {"install", "--no-wait", "foo", "bar"}, + {"revert", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo"}, + {"refresh", "--no-wait", "foo", "bar"}, + {"enable", "--no-wait", "foo"}, + {"disable", "--no-wait", "foo"}, + {"try", "--no-wait", "."}, + } + + for _, cmd := range cmds { + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser().ParseArgs(cmd) + c.Assert(err, check.IsNil, check.Commentf("%v", cmd)) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, "(?sm)42\n") + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.srv.n, check.Equals, 1) + // reset + s.srv.n = 0 + s.stdout.Reset() + } +} diff -Nru snapd-2.22.6+16.10/cmd/snap/complete.go snapd-2.23.1+16.10/cmd/snap/complete.go --- snapd-2.22.6+16.10/cmd/snap/complete.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/complete.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,6 +1,7 @@ package main import ( + "fmt" "strings" "github.com/jessevdk/go-flags" @@ -98,3 +99,72 @@ }) return res } + +type connectPlugSpec struct { + SnapAndName +} + +func (cps connectPlugSpec) Complete(match string) []flags.Completion { + // Parse what the user typed so far, it can be either + // nothing (""), a "snap", a "snap:" or a "snap:name". + parts := strings.SplitN(match, ":", 2) + + // Ask snapd about available interfaces. + cli := Client() + ifaces, err := cli.Interfaces() + if err != nil { + return nil + } + + snaps := make(map[string]bool) + + var ret []flags.Completion + + var snapPrefix, plugPrefix string + if len(parts) == 2 { + // The user typed the colon, means they know the snap they want; + // go with that. + plugPrefix = parts[1] + snaps[parts[0]] = true + } else { + // The user is about to or has started typing a snap name but didn't + // reach the colon yet. Offer plugs for snaps with names that start + // like that. + snapPrefix = parts[0] + for _, plug := range ifaces.Plugs { + if strings.HasPrefix(plug.Snap, snapPrefix) { + snaps[plug.Snap] = true + } + } + } + + if len(snaps) == 1 { + for snapName := range snaps { + for _, plug := range ifaces.Plugs { + if plug.Snap == snapName && strings.HasPrefix(plug.Name, plugPrefix) { + // TODO: in the future annotate plugs that can take + // multiple connection sensibly and don't skip those even + // if they have connections already. + if len(plug.Connections) == 0 { + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:%s", plug.Snap, plug.Name)}) + } + } + } + } + } else { + for snapName := range snaps { + for _, plug := range ifaces.Plugs { + if plug.Snap == snapName { + if len(plug.Connections) == 0 { + // TODO: in the future annotate plugs that can take + // multiple connection sensibly and don't skip those + // even if they have connections already. + ret = append(ret, flags.Completion{Item: fmt.Sprintf("%s:", snapName)}) + } + } + } + } + } + + return ret +} diff -Nru snapd-2.22.6+16.10/cmd/snap/main_test.go snapd-2.23.1+16.10/cmd/snap/main_test.go --- snapd-2.22.6+16.10/cmd/snap/main_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap/main_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -151,7 +151,7 @@ return func() { cmd.Version = old } } -const TestAuthFileEnvKey = "SNAPPY_STORE_AUTH_DATA_FILENAME" +const TestAuthFileEnvKey = "SNAPD_AUTH_DATA_FILENAME" const TestAuthFileContents = `{"id":123,"email":"hello@mail.com","macaroon":"MDAxM2xvY2F0aW9uIHNuYXBkCjAwMTJpZGVudGlmaWVyIDQzCjAwMmZzaWduYXR1cmUg5RfMua72uYop4t3cPOBmGUuaoRmoDH1HV62nMJq7eqAK"}` func (s *SnapSuite) TestErrorResult(c *C) { diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/mount-support.c snapd-2.23.1+16.10/cmd/snap-confine/mount-support.c --- snapd-2.22.6+16.10/cmd/snap-confine/mount-support.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/mount-support.c 2017-03-08 13:28:17.000000000 +0000 @@ -33,7 +33,9 @@ #include "../libsnap-confine-private/classic.h" #include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/mount-opt.h" #include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/string-utils.h" #include "../libsnap-confine-private/utils.h" #include "mount-support-nvidia.h" #include "quirks.h" @@ -76,7 +78,7 @@ // TODO: simplify this, after all it is just a tmpfs // TODO: fold this into bootstrap -static void setup_private_mount(const char *security_tag) +static void setup_private_mount(const char *snap_name) { uid_t uid = getuid(); gid_t gid = getgid(); @@ -87,8 +89,8 @@ // // Under that basedir, we put a 1777 /tmp dir that is then bind // mounted for the applications to use - must_snprintf(tmpdir, sizeof(tmpdir), "/tmp/snap.%d_%s_XXXXXX", uid, - security_tag); + sc_must_snprintf(tmpdir, sizeof(tmpdir), "/tmp/snap.%d_%s_XXXXXX", uid, + snap_name); if (mkdtemp(tmpdir) == NULL) { die("cannot create temporary directory essential for private /tmp"); } @@ -98,7 +100,7 @@ if (!d) { die("cannot allocate memory for string copy"); } - must_snprintf(tmpdir, sizeof(tmpdir), "%s/tmp", d); + sc_must_snprintf(tmpdir, sizeof(tmpdir), "%s/tmp", d); free(d); if (mkdir(tmpdir, 01777) != 0) { @@ -189,16 +191,16 @@ * either the core snap on an all-snap system or the core snap + punched holes * on a classic system. **/ -static void sc_setup_mount_profiles(const char *security_tag) +static void sc_setup_mount_profiles(const char *snap_name) { - debug("%s: %s", __FUNCTION__, security_tag); + debug("%s: %s", __FUNCTION__, snap_name); FILE *f __attribute__ ((cleanup(sc_cleanup_endmntent))) = NULL; const char *mount_profile_dir = "/var/lib/snapd/mount"; char profile_path[PATH_MAX]; - must_snprintf(profile_path, sizeof(profile_path), "%s/%s.fstab", - mount_profile_dir, security_tag); + sc_must_snprintf(profile_path, sizeof(profile_path), "%s/snap.%s.fstab", + mount_profile_dir, snap_name); debug("opening mount profile %s", profile_path); f = setmntent(profile_path, "r"); @@ -360,7 +362,8 @@ errno != EEXIST) { die("cannot create %s", mnt->path); } - must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, mnt->path); + sc_must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, + mnt->path); debug("performing operation: mount --rbind %s %s", mnt->path, dst); if (mount(mnt->path, dst, NULL, MS_REC | MS_BIND, NULL) < 0) { @@ -387,10 +390,10 @@ // https://bugs.launchpad.net/snap-confine/+bug/1580018 const char *etc_alternatives = "/etc/alternatives"; if (access(etc_alternatives, F_OK) == 0) { - must_snprintf(src, sizeof src, "%s%s", config->rootfs_dir, - etc_alternatives); - must_snprintf(dst, sizeof dst, "%s%s", scratch_dir, - etc_alternatives); + sc_must_snprintf(src, sizeof src, "%s%s", config->rootfs_dir, + etc_alternatives); + sc_must_snprintf(dst, sizeof dst, "%s%s", scratch_dir, + etc_alternatives); debug("performing operation: mount --bind %s %s", src, dst); if (mount(src, dst, NULL, MS_BIND, NULL) != 0) { die("cannot perform operation: mount --bind %s %s", src, @@ -407,7 +410,7 @@ // the desired root filesystem. In the "core" and "ubuntu-core" snaps the // directory is always /snap. On the host it is a build-time configuration // option stored in SNAP_MOUNT_DIR. - must_snprintf(dst, sizeof dst, "%s/snap", scratch_dir); + sc_must_snprintf(dst, sizeof dst, "%s/snap", scratch_dir); debug("performing operation: mount --rbind %s %s", SNAP_MOUNT_DIR, dst); if (mount(SNAP_MOUNT_DIR, dst, NULL, MS_BIND | MS_REC | MS_SLAVE, NULL) < 0) { @@ -427,11 +430,25 @@ SC_HOSTFS_DIR); } } + // Ensure that hostfs isgroup owned by root. We may have (now or earlier) + // created the directory as the user who first ran a snap on a given + // system and the group identity of that user is visilbe on disk. + // This was LP:#1665004 + struct stat sb; + if (stat(SC_HOSTFS_DIR, &sb) < 0) { + die("cannot stat %s", SC_HOSTFS_DIR); + } + if (sb.st_uid != 0 || sb.st_gid != 0) { + if (chown(SC_HOSTFS_DIR, 0, 0) < 0) { + die("cannot change user/group owner of %s to root", + SC_HOSTFS_DIR); + } + } // Make the upcoming "put_old" directory for pivot_root private so that // mount events don't propagate to any peer group. In practice pivot root // has a number of undocumented requirements and one of them is that the // "put_old" directory (the second argument) cannot be shared in any way. - must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, SC_HOSTFS_DIR); + sc_must_snprintf(dst, sizeof dst, "%s/%s", scratch_dir, SC_HOSTFS_DIR); debug("performing operation: mount --bind %s %s", dst, dst); if (mount(dst, dst, NULL, MS_BIND, NULL) < 0) { die("cannot perform operation: mount --bind %s %s", dst, dst); @@ -478,7 +495,7 @@ // in the original root filesystem (which is now mounted on SC_HOSTFS_DIR). // This way we can remove the temporary directory we created and "clean up" // after ourselves nicely. - must_snprintf(dst, sizeof dst, "%s/%s", SC_HOSTFS_DIR, scratch_dir); + sc_must_snprintf(dst, sizeof dst, "%s/%s", SC_HOSTFS_DIR, scratch_dir); debug("performing operation: umount %s", dst); if (umount2(dst, 0) < 0) { die("cannot perform operation: umount %s", dst); @@ -502,21 +519,21 @@ // Detach the redundant hostfs version of sysfs since it shows up in the // mount table and software inspecting the mount table may become confused // (eg, docker and LP:# 162601). - must_snprintf(src, sizeof src, "%s/sys", SC_HOSTFS_DIR); + sc_must_snprintf(src, sizeof src, "%s/sys", SC_HOSTFS_DIR); debug("performing operation: umount --lazy %s", src); if (umount2(src, UMOUNT_NOFOLLOW | MNT_DETACH) < 0) { die("cannot perform operation: umount --lazy %s", src); } // Detach the redundant hostfs version of /dev since it shows up in the // mount table and software inspecting the mount table may become confused. - must_snprintf(src, sizeof src, "%s/dev", SC_HOSTFS_DIR); + sc_must_snprintf(src, sizeof src, "%s/dev", SC_HOSTFS_DIR); debug("performing operation: umount --lazy %s", src); if (umount2(src, UMOUNT_NOFOLLOW | MNT_DETACH) < 0) { die("cannot perform operation: umount --lazy %s", src); } // Detach the redundant hostfs version of /proc since it shows up in the // mount table and software inspecting the mount table may become confused. - must_snprintf(src, sizeof src, "%s/proc", SC_HOSTFS_DIR); + sc_must_snprintf(src, sizeof src, "%s/proc", SC_HOSTFS_DIR); debug("performing operation: umount --lazy %s", src); if (umount2(src, UMOUNT_NOFOLLOW | MNT_DETACH) < 0) { die("cannot perform operation: umount --lazy %s", src); @@ -579,7 +596,7 @@ return false; } -void sc_populate_mount_ns(const char *security_tag) +void sc_populate_mount_ns(const char *snap_name) { // Get the current working directory before we start fiddling with // mounts and possibly pivot_root. At the end of the whole process, we @@ -641,7 +658,7 @@ // set up private mounts // TODO: rename this and fold it into bootstrap - setup_private_mount(security_tag); + setup_private_mount(snap_name); // set up private /dev/pts // TODO: fold this into bootstrap @@ -652,7 +669,7 @@ sc_setup_quirks(); } // setup the security backend bind mounts - sc_setup_mount_profiles(security_tag); + sc_setup_mount_profiles(snap_name); // Try to re-locate back to vanilla working directory. This can fail // because that directory is no longer present. diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/mount-support.h snapd-2.23.1+16.10/cmd/snap-confine/mount-support.h --- snapd-2.22.6+16.10/cmd/snap-confine/mount-support.h 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/mount-support.h 2017-03-02 06:37:54.000000000 +0000 @@ -31,6 +31,6 @@ * The function will also try to preserve the current working directory but if * this is impossible it will chdir to SC_VOID_DIR. **/ -void sc_populate_mount_ns(const char *security_tag); +void sc_populate_mount_ns(const char *snap_name); #endif diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/mount-support-nvidia.c snapd-2.23.1+16.10/cmd/snap-confine/mount-support-nvidia.c --- snapd-2.22.6+16.10/cmd/snap-confine/mount-support-nvidia.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/mount-support-nvidia.c 2017-03-06 13:33:50.000000000 +0000 @@ -30,6 +30,7 @@ #include "../libsnap-confine-private/classic.h" #include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/string-utils.h" #include "../libsnap-confine-private/utils.h" #ifdef NVIDIA_ARCH @@ -134,28 +135,28 @@ } hostfs_symlink_target[num_read] = 0; if (hostfs_symlink_target[0] == '/') { - must_snprintf(symlink_target, - sizeof symlink_target, - "/var/lib/snapd/hostfs%s", - hostfs_symlink_target); + sc_must_snprintf(symlink_target, + sizeof symlink_target, + "/var/lib/snapd/hostfs%s", + hostfs_symlink_target); } else { // Keep relative symlinks as-is, so that they point to -> libfoo.so.0.123 - must_snprintf(symlink_target, - sizeof symlink_target, "%s", - hostfs_symlink_target); + sc_must_snprintf(symlink_target, + sizeof symlink_target, "%s", + hostfs_symlink_target); } break; case S_IFREG: - must_snprintf(symlink_target, - sizeof symlink_target, - "/var/lib/snapd/hostfs%s", pathname); + sc_must_snprintf(symlink_target, + sizeof symlink_target, + "/var/lib/snapd/hostfs%s", pathname); break; default: debug("ignoring unsupported entry: %s", pathname); continue; } - must_snprintf(symlink_name, sizeof symlink_name, - "%s/%s", libgl_dir, filename); + sc_must_snprintf(symlink_name, sizeof symlink_name, + "%s/%s", libgl_dir, filename); debug("creating symbolic link %s -> %s", symlink_name, symlink_target); if (symlink(symlink_target, symlink_name) != 0) { @@ -169,8 +170,8 @@ { // Bind mount a tmpfs on $rootfs_dir/var/lib/snapd/lib/gl char buf[512]; - must_snprintf(buf, sizeof(buf), "%s%s", rootfs_dir, - "/var/lib/snapd/lib/gl"); + sc_must_snprintf(buf, sizeof(buf), "%s%s", rootfs_dir, + "/var/lib/snapd/lib/gl"); const char *libgl_dir = buf; debug("mounting tmpfs at %s", libgl_dir); if (mount("none", libgl_dir, "tmpfs", MS_NODEV | MS_NOEXEC, NULL) != 0) { @@ -226,19 +227,33 @@ static void sc_mount_nvidia_driver_ubuntu(const char *rootfs_dir) { struct sc_nvidia_driver driver; + + // Probe sysfs to get the version of the driver that is currently inserted. sc_probe_nvidia_driver(&driver); - if (driver.major_version != 0) { - // Bind mount the binary nvidia driver into /var/lib/snapd/lib/gl. - char src[PATH_MAX], dst[PATH_MAX]; - must_snprintf(src, sizeof src, "/usr/lib/nvidia-%d", - driver.major_version); - must_snprintf(dst, sizeof dst, "%s%s", rootfs_dir, - SC_LIBGL_DIR); - debug("bind mounting nvidia driver %s -> %s", src, dst); - if (mount(src, dst, NULL, MS_BIND, NULL) != 0) { - die("cannot bind mount nvidia driver %s -> %s", src, - dst); - } + + // If there's driver in the kernel then don't mount userspace. + if (driver.major_version == 0) { + return; + } + // Construct the paths for the driver userspace libraries + // and for the gl directory. + char src[PATH_MAX], dst[PATH_MAX]; + sc_must_snprintf(src, sizeof src, "/usr/lib/nvidia-%d", + driver.major_version); + sc_must_snprintf(dst, sizeof dst, "%s%s", rootfs_dir, SC_LIBGL_DIR); + + // If there is no userspace driver available then don't try to mount it. + // This can happen for any number of reasons but one interesting one is + // that that snapd runs in a lxd container on a host that uses nvidia. In + // that case the container may not have the userspace library installed but + // the kernel will still have the module around. + if (access(src, F_OK) != 0) { + return; + } + // Bind mount the binary nvidia driver into /var/lib/snapd/lib/gl. + debug("bind mounting nvidia driver %s -> %s", src, dst); + if (mount(src, dst, NULL, MS_BIND, NULL) != 0) { + die("cannot bind mount nvidia driver %s -> %s", src, dst); } } #endif // ifdef NVIDIA_UBUNTU diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/ns-support.c snapd-2.23.1+16.10/cmd/snap-confine/ns-support.c --- snapd-2.22.6+16.10/cmd/snap-confine/ns-support.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/ns-support.c 2017-03-02 06:37:54.000000000 +0000 @@ -39,6 +39,7 @@ #include "../libsnap-confine-private/cleanup-funcs.h" #include "../libsnap-confine-private/mountinfo.h" +#include "../libsnap-confine-private/string-utils.h" #include "../libsnap-confine-private/utils.h" #include "user-support.h" @@ -147,24 +148,24 @@ **/ static bool sc_is_ns_group_dir_private() { - struct mountinfo *info - __attribute__ ((cleanup(cleanup_mountinfo))) = NULL; - info = parse_mountinfo(NULL); + struct sc_mountinfo *info + __attribute__ ((cleanup(sc_cleanup_mountinfo))) = NULL; + info = sc_parse_mountinfo(NULL); if (info == NULL) { die("cannot parse /proc/self/mountinfo"); } - struct mountinfo_entry *entry = first_mountinfo_entry(info); + struct sc_mountinfo_entry *entry = sc_first_mountinfo_entry(info); while (entry != NULL) { - const char *mount_dir = mountinfo_entry_mount_dir(entry); + const char *mount_dir = sc_mountinfo_entry_mount_dir(entry); const char *optional_fields = - mountinfo_entry_optional_fields(entry); + sc_mountinfo_entry_optional_fields(entry); if (strcmp(mount_dir, sc_ns_dir) == 0 && strcmp(optional_fields, "") == 0) { // If /run/snapd/ns has no optional fields, we know it is mounted // private and there is nothing else to do. return true; } - entry = next_mountinfo_entry(entry); + entry = sc_next_mountinfo_entry(entry); } return false; } @@ -265,8 +266,8 @@ die("cannot open directory for namespace group %s", group_name); } char lock_fname[PATH_MAX]; - must_snprintf(lock_fname, sizeof lock_fname, "%s%s", group_name, - SC_NS_LOCK_FILE); + sc_must_snprintf(lock_fname, sizeof lock_fname, "%s%s", group_name, + SC_NS_LOCK_FILE); debug("opening lock file for namespace group %s", group_name); group->lock_fd = openat(group->dir_fd, lock_fname, @@ -285,9 +286,9 @@ { debug("releasing resources associated with namespace group %s", group->name); - close(group->dir_fd); - close(group->lock_fd); - close(group->event_fd); + sc_cleanup_close(&group->dir_fd); + sc_cleanup_close(&group->lock_fd); + sc_cleanup_close(&group->event_fd); free(group->name); free(group); } @@ -324,8 +325,8 @@ { // Open the mount namespace file. char mnt_fname[PATH_MAX]; - must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name, - SC_NS_MNT_FILE); + sc_must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name, + SC_NS_MNT_FILE); int mnt_fd __attribute__ ((cleanup(sc_cleanup_close))) = -1; // NOTE: There is no O_EXCL here because the file can be around but // doesn't have to be a mounted namespace. @@ -343,6 +344,8 @@ } // Check if we got an nsfs-based file or a regular file. This can be // reliably tested because nsfs has an unique filesystem type NSFS_MAGIC. + // On older kernels that don't support nsfs yet we can look for + // PROC_SUPER_MAGIC instead. // We can just ensure that this is the case thanks to fstatfs. struct statfs buf; if (fstatfs(mnt_fd, &buf) < 0) { @@ -352,7 +355,7 @@ // Account for kernel headers old enough to not know about NSFS_MAGIC. #define NSFS_MAGIC 0x6e736673 #endif - if (buf.f_type == NSFS_MAGIC) { + if (buf.f_type == NSFS_MAGIC || buf.f_type == PROC_SUPER_MAGIC) { char *vanilla_cwd __attribute__ ((cleanup(sc_cleanup_string))) = NULL; vanilla_cwd = get_current_dir_name(); @@ -456,9 +459,10 @@ (int)parent, group->name); char src[PATH_MAX]; char dst[PATH_MAX]; - must_snprintf(src, sizeof src, "/proc/%d/ns/mnt", (int)parent); - must_snprintf(dst, sizeof dst, "%s%s", group->name, - SC_NS_MNT_FILE); + sc_must_snprintf(src, sizeof src, "/proc/%d/ns/mnt", + (int)parent); + sc_must_snprintf(dst, sizeof dst, "%s%s", group->name, + SC_NS_MNT_FILE); if (mount(src, dst, NULL, MS_BIND, NULL) < 0) { die("cannot bind-mount the mount namespace file %s -> %s", src, dst); } @@ -525,8 +529,8 @@ } // Unmount ${group_name}.mnt which holds the preserved namespace char mnt_fname[PATH_MAX]; - must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name, - SC_NS_MNT_FILE); + sc_must_snprintf(mnt_fname, sizeof mnt_fname, "%s%s", group->name, + SC_NS_MNT_FILE); debug("unmounting preserved mount namespace file %s", mnt_fname); if (umount2(mnt_fname, UMOUNT_NOFOLLOW) < 0) { switch (errno) { diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/quirks.c snapd-2.23.1+16.10/cmd/snap-confine/quirks.c --- snapd-2.22.6+16.10/cmd/snap-confine/quirks.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/quirks.c 2017-03-02 06:37:54.000000000 +0000 @@ -28,6 +28,7 @@ #include "../libsnap-confine-private/classic.h" #include "../libsnap-confine-private/cleanup-funcs.h" #include "../libsnap-confine-private/mount-opt.h" +#include "../libsnap-confine-private/string-utils.h" #include "../libsnap-confine-private/utils.h" // XXX: for smaller patch, this should be in utils.h later #include "user-support.h" @@ -89,7 +90,8 @@ if (sc_nonfatal_mkpath(dest_dir, 0755) < 0) { die("cannot create empty directory at %s", dest_dir); } - const char *flags_str = sc_mount_opt2str(flags); + char buf[1000]; + const char *flags_str = sc_mount_opt2str(buf, sizeof buf, flags); debug("performing operation: mount %s %s -o %s", src_dir, dest_dir, flags_str); if (mount(src_dir, dest_dir, NULL, flags, NULL) != 0) { @@ -143,10 +145,10 @@ die("unsupported entry type of file %s (%d)", entryp->d_name, entryp->d_type); } - must_snprintf(src_name, sizeof src_name, "%s/%s", ref_dir, - entryp->d_name); - must_snprintf(dest_name, sizeof dest_name, "%s/%s", mimic_dir, - entryp->d_name); + sc_must_snprintf(src_name, sizeof src_name, "%s/%s", ref_dir, + entryp->d_name); + sc_must_snprintf(dest_name, sizeof dest_name, "%s/%s", + mimic_dir, entryp->d_name); sc_quirk_mkdir_bind(src_name, dest_name, flags); } while (entryp != NULL); } @@ -189,8 +191,8 @@ } // now let's make /var/lib the vanilla /var/lib from the core snap char buf[PATH_MAX]; - must_snprintf(buf, sizeof buf, "%s/var/lib", - sc_get_inner_core_mount_point()); + sc_must_snprintf(buf, sizeof buf, "%s/var/lib", + sc_get_inner_core_mount_point()); sc_quirk_create_writable_mimic("/var/lib", buf, MS_RDONLY | MS_REC | MS_SLAVE | MS_NODEV | MS_NOSUID); diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/seccomp-support.c snapd-2.23.1+16.10/cmd/snap-confine/seccomp-support.c --- snapd-2.22.6+16.10/cmd/snap-confine/seccomp-support.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/seccomp-support.c 2017-03-02 06:37:54.000000000 +0000 @@ -17,6 +17,7 @@ #include "config.h" #include "seccomp-support.h" +#include #include #include #include // needed for search mappings @@ -27,15 +28,19 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include #include "../libsnap-confine-private/secure-getenv.h" +#include "../libsnap-confine-private/string-utils.h" #include "../libsnap-confine-private/utils.h" #define sc_map_add(X) sc_map_add_kvp(#X, X) @@ -94,16 +99,16 @@ * sc_map_search(s) - if found, return scmp_datum_t for key, else set errno * sc_map_destroy() - destroy the hash map and linked list */ -static scmp_datum_t sc_map_search(char *s) +static scmp_datum_t sc_map_search(const char *s) { ENTRY e; ENTRY *ep = NULL; scmp_datum_t val = 0; errno = 0; - e.key = s; + e.key = (char *)s; if (hsearch_r(e, FIND, &ep, &sc_map_htab) == 0) - die("hsearch_r failed"); + die("hsearch_r failed for %s", s); if (ep != NULL) { scmp_datum_t *val_p = NULL; @@ -163,19 +168,32 @@ // man 2 socket - domain sc_map_add(AF_UNIX); + sc_map_add(PF_UNIX); sc_map_add(AF_LOCAL); + sc_map_add(PF_LOCAL); sc_map_add(AF_INET); + sc_map_add(PF_INET); sc_map_add(AF_INET6); + sc_map_add(PF_INET6); sc_map_add(AF_IPX); + sc_map_add(PF_IPX); sc_map_add(AF_NETLINK); + sc_map_add(PF_NETLINK); sc_map_add(AF_X25); + sc_map_add(PF_X25); sc_map_add(AF_AX25); + sc_map_add(PF_AX25); sc_map_add(AF_ATMPVC); + sc_map_add(PF_ATMPVC); sc_map_add(AF_APPLETALK); + sc_map_add(PF_APPLETALK); sc_map_add(AF_PACKET); + sc_map_add(PF_PACKET); sc_map_add(AF_ALG); + sc_map_add(PF_ALG); // linux/can.h sc_map_add(AF_CAN); + sc_map_add(PF_CAN); // man 2 socket - type sc_map_add(SOCK_STREAM); @@ -281,6 +299,25 @@ sc_map_add(CLONE_NEWUSER); sc_map_add(CLONE_NEWUTS); + // man 4 tty_ioctl + sc_map_add(TIOCSTI); + + // man 2 quotactl (with what Linux supports) + sc_map_add(Q_SYNC); + sc_map_add(Q_QUOTAON); + sc_map_add(Q_QUOTAOFF); + sc_map_add(Q_GETFMT); + sc_map_add(Q_GETINFO); + sc_map_add(Q_SETINFO); + sc_map_add(Q_GETQUOTA); + sc_map_add(Q_SETQUOTA); + sc_map_add(Q_XQUOTAON); + sc_map_add(Q_XQUOTAOFF); + sc_map_add(Q_XGETQUOTA); + sc_map_add(Q_XSETQLIM); + sc_map_add(Q_XGETQSTAT); + sc_map_add(Q_XQUOTARM); + // initialize the htab for our map memset((void *)&sc_map_htab, 0, sizeof(sc_map_htab)); if (hcreate_r(sc_map_entries.count, &sc_map_htab) == 0) @@ -318,7 +355,7 @@ } /* Caller must check if errno != 0 */ -static scmp_datum_t read_number(char *s) +static scmp_datum_t read_number(const char *s) { scmp_datum_t val = 0; @@ -640,8 +677,8 @@ secure_getenv("SNAPPY_LAUNCHER_SECCOMP_PROFILE_DIR"); char profile_path[512]; // arbitrary path name limit - must_snprintf(profile_path, sizeof(profile_path), "%s/%s", - filter_profile_dir, filter_profile); + sc_must_snprintf(profile_path, sizeof(profile_path), "%s/%s", + filter_profile_dir, filter_profile); f = fopen(profile_path, "r"); if (f == NULL) { diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/snap-confine-args.c snapd-2.23.1+16.10/cmd/snap-confine/snap-confine-args.c --- snapd-2.22.6+16.10/cmd/snap-confine/snap-confine-args.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/snap-confine-args.c 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "snap-confine-args.h" + +#include + +#include "../libsnap-confine-private/utils.h" + +struct sc_args { + // The security tag that the application is intended to run with + char *security_tag; + // The executable that should be invoked + char *executable; + + // Flag indicating that --version was passed on command line. + bool is_version_query; + // Flag indicating that --classic was passed on command line. + bool is_classic_confinement; +}; + +struct sc_args *sc_nonfatal_parse_args(int *argcp, char ***argvp, + struct sc_error **errorp) +{ + struct sc_args *args = NULL; + struct sc_error *err = NULL; + + if (argcp == NULL || argvp == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, 0, + "cannot parse arguments, argcp or argvp is NULL"); + goto out; + } + // Use dereferenced versions of argcp and argvp for convenience. + int argc = *argcp; + char **const argv = *argvp; + + if (argc == 0 || argv == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, 0, + "cannot parse arguments, argc is zero or argv is NULL"); + goto out; + } + // Sanity check, look for NULL argv entries. + for (int i = 0; i < argc; ++i) { + if (argv[i] == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, 0, + "cannot parse arguments, argument at index %d is NULL", + i); + goto out; + } + } + + args = calloc(1, sizeof *args); + if (args == NULL) { + die("cannot allocate memory for command line arguments object"); + } + // Check if we're being called through the ubuntu-core-launcher symlink. + // When this happens we want to skip the first positional argument as it is + // the security tag repeated (legacy). + bool ignore_first_tag = false; + char *basename = strrchr(argv[0], '/'); + if (basename != NULL) { + // NOTE: this is safe because we, at most, may move to the NUL byte + // that compares to an empty string. + basename += 1; + if (strcmp(basename, "ubuntu-core-launcher") == 0) { + ignore_first_tag = true; + } + } + // Parse option switches. + int optind; + for (optind = 1; optind < argc; ++optind) { + // Look at all the options switches that start with the minus sign ('-') + if (argv[optind][0] != '-') { + // On first non-switch argument break the loop. The next loop looks + // just for non-option arguments. This ensures that options and + // positional arguments cannot be mixed. + break; + } + // Handle option switches + if (strcmp(argv[optind], "--version") == 0) { + args->is_version_query = true; + // NOTE: --version short-circuits the parser to finish + goto done; + } else if (strcmp(argv[optind], "--classic") == 0) { + args->is_classic_confinement = true; + } else { + // Report unhandled option switches + err = sc_error_init(SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE, + "unrecognized command line option: %s", + argv[optind]); + goto out; + } + } + + // Parse positional arguments. + // + // NOTE: optind is not reset, we just continue from where we left off in + // the loop above. + for (; optind < argc; ++optind) { + if (args->security_tag == NULL) { + // The first positional argument becomes the security tag. + if (ignore_first_tag) { + // Unless we are called as ubuntu-core-launcher, then we just + // swallow and ignore that security tag altogether. + ignore_first_tag = false; + continue; + } + args->security_tag = strdup(argv[optind]); + if (args->security_tag == NULL) { + die("cannot allocate memory for security tag"); + } + } else if (args->executable == NULL) { + // The second positional argument becomes the executable name. + args->executable = strdup(argv[optind]); + if (args->executable == NULL) { + die("cannot allocate memory for executable name"); + } + // No more positional arguments are required. + // Stop the parsing process. + break; + } + } + + // Verify that all mandatory positional arguments are present. + // Ensure that we have the security tag + if (args->security_tag == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE, + "application or hook security tag was not provided"); + goto out; + } + // Ensure that we have the executable name + if (args->executable == NULL) { + err = sc_error_init(SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE, + "executable name was not provided"); + goto out; + } + + int i; + done: + // "shift" the argument vector left, except for argv[0], to "consume" the + // arguments that were scanned / parsed correctly. + for (i = 1; optind + i < argc; ++i) { + argv[i] = argv[optind + i]; + } + argv[i] = NULL; + + // Write the updated argc back, argv is never modified. + *argcp = argc - optind; + + out: + // Don't return anything in case of an error. + if (err != NULL) { + sc_cleanup_args(&args); + } + // Forward the error and return + sc_error_forward(errorp, err); + return args; +} + +void sc_args_free(struct sc_args *args) +{ + if (args != NULL) { + free(args->security_tag); + args->security_tag = NULL; + free(args->executable); + args->executable = NULL; + free(args); + } +} + +void sc_cleanup_args(struct sc_args **ptr) +{ + sc_args_free(*ptr); + *ptr = NULL; +} + +bool sc_args_is_version_query(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain version query flag from NULL argument parser"); + } + return args->is_version_query; +} + +bool sc_args_is_classic_confinement(struct sc_args * args) +{ + if (args == NULL) { + die("cannot obtain classic confinement flag from NULL argument parser"); + } + return args->is_classic_confinement; +} + +const char *sc_args_security_tag(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain security tag from NULL argument parser"); + } + return args->security_tag; +} + +const char *sc_args_executable(struct sc_args *args) +{ + if (args == NULL) { + die("cannot obtain executable from NULL argument parser"); + } + return args->executable; +} diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/snap-confine-args.h snapd-2.23.1+16.10/cmd/snap-confine/snap-confine-args.h --- snapd-2.22.6+16.10/cmd/snap-confine/snap-confine-args.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/snap-confine-args.h 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SC_SNAP_CONFINE_ARGS_H +#define SC_SNAP_CONFINE_ARGS_H + +#include + +#include "../libsnap-confine-private/error.h" + +/** + * Error domain for errors related to argument parsing. + **/ +#define SC_ARGS_DOMAIN "args" + +enum { + /** + * Error indicating that the command line arguments could not be parsed + * correctly and usage message should be displayed to the user. + **/ + SC_ARGS_ERR_USAGE = 1, +}; + +/** + * Opaque structure describing command-line arguments to snap-confine. + **/ +struct sc_args; + +/** + * Parse command line arguments for snap-confine. + * + * Snap confine understands very specific arguments. + * + * The argument vector can begin with "ubuntu-core-launcher" (with an optional + * path) which implies that the first arctual argument (argv[1]) is a copy of + * argv[2] and can be discarded. + * + * The argument vector is scanned, left to right, to look for switches that + * start with the minus sign ('-'). Recognized options are stored and + * memorized. Unrecognized options return an appropriate error object. + * + * Currently only one option is understood, that is "--version". It is simply + * scanned, memorized and discarded. The presence of this switch can be + * retrieved with sc_args_is_version_query(). + * + * After all the option switches are scanned it is expected to scan two more + * arguments: the security tag and the name of the executable to run. An error + * object is returned when those is missing. + * + * Both argc and argv are modified so the caller can look at the first unparsed + * argument at argc[0]. This is only done if argument parsing is successful. + **/ +__attribute__ ((warn_unused_result)) +struct sc_args *sc_nonfatal_parse_args(int *argcp, char ***argvp, + struct sc_error **errorp); + +/** + * Free the object describing command-line arguments to snap-confine. + **/ +void sc_args_free(struct sc_args *args); + +/** + * Cleanup an error with sc_args_free() + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_args))). + **/ +void sc_cleanup_args(struct sc_args **ptr); + +/** + * Check if snap-confine was invoked with the --version switch. + **/ +bool sc_args_is_version_query(struct sc_args *args); + +/** + * Check if snap-confine was invoked with the --classic switch. + **/ +bool sc_args_is_classic_confinement(struct sc_args *args); + +/** + * Get the security tag passed to snap-confine. + * + * The return value may be NULL if snap-confine was invoked with --version. It + * is never NULL otherwise. + * + * The return value must not be freed(). It is bound to the lifetime of + * the argument parser. + **/ +const char *sc_args_security_tag(struct sc_args *args); + +/** + * Get the executable name passed to snap-confine. + * + * The return value may be NULL if snap-confine was invoked with --version. It + * is never NULL otherwise. + * + * The return value must not be freed(). It is bound to the lifetime of + * the argument parser. + **/ +const char *sc_args_executable(struct sc_args *args); + +#endif diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/snap-confine-args-test.c snapd-2.23.1+16.10/cmd/snap-confine/snap-confine-args-test.c --- snapd-2.22.6+16.10/cmd/snap-confine/snap-confine-args-test.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/snap-confine-args-test.c 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "snap-confine-args.h" +#include "snap-confine-args.c" + +#include + +#include + +/** + * Create an argc + argv pair out of a NULL terminated argument list. + **/ +static void + __attribute__ ((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...) +{ + int argc = 0; + char **argv = NULL; + g_test_queue_free(argv); + + va_list ap; + va_start(ap, argvp); + const char *arg; + do { + arg = va_arg(ap, const char *); + // XXX: yeah, wrong way but the worse that can happen is for test to fail + argv = realloc(argv, sizeof(const char **) * (argc + 1)); + g_assert_nonnull(argv); + if (arg != NULL) { + char *arg_copy = strdup(arg); + g_test_queue_free(arg_copy); + argv[argc] = arg_copy; + argc += 1; + } else { + argv[argc] = NULL; + } + } while (arg != NULL); + va_end(ap); + + *argcp = argc; + *argvp = argv; +} + +static void test_test_argc_argv() +{ + // Check that test_argc_argv() correctly stores data + int argc; + char **argv; + + test_argc_argv(&argc, &argv, NULL); + g_assert_cmpint(argc, ==, 0); + g_assert_null(argv[0]); + + test_argc_argv(&argc, &argv, "zero", "one", "two", NULL); + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "zero"); + g_assert_cmpstr(argv[1], ==, "one"); + g_assert_cmpstr(argv[2], ==, "two"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__typical() +{ + // Test that typical invocation of snap-confine is parsed correctly. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "snap.SNAP_NAME.APP_NAME", + "/usr/lib/snapd/snap-exec", "--option", "arg", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/lib/snapd/snap-confine"); + g_assert_cmpstr(argv[1], ==, "--option"); + g_assert_cmpstr(argv[2], ==, "arg"); + g_assert_null(argv[3]); +} + +static void test_sc_cleanup_args() +{ + // Check that NULL argument parser can be cleaned up + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args = NULL; + sc_cleanup_args(&args); + + // Check that a non-NULL argument parser can be cleaned up + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "snap.SNAP_NAME.APP_NAME", "/usr/lib/snapd/snap-exec", + NULL); + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + sc_cleanup_args(&args); + g_assert_null(args); +} + +static void test_sc_nonfatal_parse_args__typical_classic() +{ + // Test that typical invocation of snap-confine is parsed correctly. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--classic", + "snap.SNAP_NAME.APP_NAME", "/usr/lib/snapd/snap-exec", + "--option", "arg", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, true); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/lib/snapd/snap-confine"); + g_assert_cmpstr(argv[1], ==, "--option"); + g_assert_cmpstr(argv[2], ==, "arg"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__ubuntu_core_launcher() +{ + // Test that typical legacy invocation of snap-confine via the + // ubuntu-core-launcher symlink, with duplicated security tag, is parsed + // correctly. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/bin/ubuntu-core-launcher", + "snap.SNAP_NAME.APP_NAME", "snap.SNAP_NAME.APP_NAME", + "/usr/lib/snapd/snap-exec", "--option", "arg", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_cmpstr(sc_args_security_tag(args), ==, + "snap.SNAP_NAME.APP_NAME"); + g_assert_cmpstr(sc_args_executable(args), ==, + "/usr/lib/snapd/snap-exec"); + g_assert_cmpint(sc_args_is_version_query(args), ==, false); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/bin/ubuntu-core-launcher"); + g_assert_cmpstr(argv[1], ==, "--option"); + g_assert_cmpstr(argv[2], ==, "arg"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__version() +{ + // Test that snap-confine --version is detected. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--version", "ignored", + "garbage", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_null(err); + g_assert_nonnull(args); + + // Check supported switches and arguments + g_assert_null(sc_args_security_tag(args)); + g_assert_null(sc_args_executable(args)); + g_assert_cmpint(sc_args_is_version_query(args), ==, true); + g_assert_cmpint(sc_args_is_classic_confinement(args), ==, false); + + // Check remaining arguments + g_assert_cmpint(argc, ==, 3); + g_assert_cmpstr(argv[0], ==, "/usr/lib/snapd/snap-confine"); + g_assert_cmpstr(argv[1], ==, "ignored"); + g_assert_cmpstr(argv[2], ==, "garbage"); + g_assert_null(argv[3]); +} + +static void test_sc_nonfatal_parse_args__evil_input() +{ + // Check that calling without any arguments is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + + // NULL argcp/argvp attack + args = sc_nonfatal_parse_args(NULL, NULL, &err); + + g_assert_nonnull(err); + g_assert_null(args); + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argcp or argvp is NULL"); + + int argc; + char **argv; + + // NULL argv attack + argc = 0; + argv = NULL; + args = sc_nonfatal_parse_args(&argc, &argv, &err); + + g_assert_nonnull(err); + g_assert_null(args); + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argc is zero or argv is NULL"); + + // NULL argv[i] attack + test_argc_argv(&argc, &argv, + "/usr/lib/snapd/snap-confine", "--version", "ignored", + "garbage", NULL); + argv[1] = NULL; // overwrite --version with NULL + args = sc_nonfatal_parse_args(&argc, &argv, &err); + + g_assert_nonnull(err); + g_assert_null(args); + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argument at index 1 is NULL"); +} + +static void test_sc_nonfatal_parse_args__nothing_to_parse() +{ + // Check that calling without any arguments is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "cannot parse arguments, argc is zero or argv is NULL"); +} + +static void test_sc_nonfatal_parse_args__no_security_tag() +{ + // Check that lack of security tag is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "application or hook security tag was not provided"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__no_executable() +{ + // Check that lack of security tag is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "snap.SNAP_NAME.APP_NAME", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "executable name was not provided"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__unknown_option() +{ + // Check that unrecognized option switch is reported as error. + struct sc_error *err __attribute__ ((cleanup(sc_cleanup_error))) = NULL; + struct sc_args *args __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "--frozbonicator", NULL); + + args = sc_nonfatal_parse_args(&argc, &argv, &err); + g_assert_nonnull(err); + g_assert_null(args); + + // Check the error that we've got + g_assert_cmpstr(sc_error_msg(err), ==, + "unrecognized command line option: --frozbonicator"); + g_assert_true(sc_error_match(err, SC_ARGS_DOMAIN, SC_ARGS_ERR_USAGE)); +} + +static void test_sc_nonfatal_parse_args__forwards_error() +{ + // Check that sc_nonfatal_parse_args() forwards errors. + if (g_test_subprocess()) { + int argc; + char **argv; + test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", + "--frozbonicator", NULL); + + // Call sc_nonfatal_parse_args() without an error handle + struct sc_args *args + __attribute__ ((cleanup(sc_cleanup_args))) = NULL; + args = sc_nonfatal_parse_args(&argc, &argv, NULL); + (void)args; + + g_test_message("expected not to reach this place"); + g_test_fail(); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("unrecognized command line option: --frozbonicator\n"); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/args/test_argc_argv", test_test_argc_argv); + g_test_add_func("/args/sc_cleanup_args", test_sc_cleanup_args); + g_test_add_func("/args/sc_nonfatal_parse_args/typical", + test_sc_nonfatal_parse_args__typical); + g_test_add_func("/args/sc_nonfatal_parse_args/typical_classic", + test_sc_nonfatal_parse_args__typical_classic); + g_test_add_func("/args/sc_nonfatal_parse_args/ubuntu_core_launcher", + test_sc_nonfatal_parse_args__ubuntu_core_launcher); + g_test_add_func("/args/sc_nonfatal_parse_args/version", + test_sc_nonfatal_parse_args__version); + g_test_add_func("/args/sc_nonfatal_parse_args/nothing_to_parse", + test_sc_nonfatal_parse_args__nothing_to_parse); + g_test_add_func("/args/sc_nonfatal_parse_args/evil_input", + test_sc_nonfatal_parse_args__evil_input); + g_test_add_func("/args/sc_nonfatal_parse_args/no_security_tag", + test_sc_nonfatal_parse_args__no_security_tag); + g_test_add_func("/args/sc_nonfatal_parse_args/no_executable", + test_sc_nonfatal_parse_args__no_executable); + g_test_add_func("/args/sc_nonfatal_parse_args/unknown_option", + test_sc_nonfatal_parse_args__unknown_option); + g_test_add_func("/args/sc_nonfatal_parse_args/forwards_error", + test_sc_nonfatal_parse_args__forwards_error); +} diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/snap-confine.c snapd-2.23.1+16.10/cmd/snap-confine/snap-confine.c --- snapd-2.22.6+16.10/cmd/snap-confine/snap-confine.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/snap-confine.c 2017-03-02 06:37:54.000000000 +0000 @@ -120,7 +120,8 @@ debug ("skipping sandbox setup, classic confinement in use"); } else { - const char *group_name = getenv("SNAP_NAME"); + const char *snap_name = getenv("SNAP_NAME"); + const char *group_name = snap_name; if (group_name == NULL) { die("SNAP_NAME is not set"); } @@ -130,7 +131,7 @@ sc_lock_ns_mutex(group); sc_create_or_join_ns_group(group, &apparmor); if (sc_should_populate_ns_group(group)) { - sc_populate_mount_ns(security_tag); + sc_populate_mount_ns(snap_name); sc_preserve_populated_ns_group(group); } sc_unlock_ns_mutex(group); @@ -143,7 +144,12 @@ debug ("resetting PATH to values in sync with core snap"); setenv("PATH", - "/usr/sbin:/usr/bin:/sbin:/bin:/usr/games", 1); + "/usr/local/sbin:" + "/usr/local/bin:" + "/usr/sbin:" + "/usr/bin:" + "/sbin:" + "/bin:" "/usr/games:" "/usr/local/games", 1); struct snappy_udev udev_s; if (snappy_udev_init(security_tag, &udev_s) == 0) setup_devices_cgroup(security_tag, &udev_s); diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_quotactl snapd-2.23.1+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_quotactl --- snapd-2.22.6+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_quotactl 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_quotactl 2017-03-08 13:28:17.000000000 +0000 @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/snap-confine/tests/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "quotactl $i" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL + fi + + # all good + PASS +done diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_socket snapd-2.23.1+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_socket --- snapd-2.22.6+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_socket 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_socket 2017-03-08 13:28:17.000000000 +0000 @@ -9,7 +9,7 @@ # what we are testing EOF -for i in 'AF_UNI' 'AF_UNIXX' 'AF_UN!X' ; do +for i in 'AF_UNI' 'AF_UNIXX' 'AF_UN!X' 'PF_UNI' ; do printf "Test bad seccomp arg filtering (socket %s)" "$i" cat "$TMP"/tmpl >"$TMP"/snap.name.app echo "socket $i" >>"$TMP"/snap.name.app diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_termios snapd-2.23.1+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_termios --- snapd-2.22.6+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_termios 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/tests/test_bad_seccomp_filter_args_termios 2017-03-08 13:28:17.000000000 +0000 @@ -0,0 +1,25 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/snap-confine/tests/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "ioctl - $i" >>"$TMP"/snap.name.app + + if $L snap.name.app /bin/true 2>/dev/null; then + # true returned successfully, bad arg test failed + cat "$TMP"/snap.name.app + FAIL + fi + + # all good + PASS +done diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/tests/test_restrictions_working_args_quotactl snapd-2.23.1+16.10/cmd/snap-confine/tests/test_restrictions_working_args_quotactl --- snapd-2.22.6+16.10/cmd/snap-confine/tests/test_restrictions_working_args_quotactl 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/tests/test_restrictions_working_args_quotactl 2017-03-08 13:28:17.000000000 +0000 @@ -0,0 +1,24 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/snap-confine/tests/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app + echo "quotactl $i" >>"$TMP"/snap.name.app + + printf "Test good seccomp arg filtering (quotactl %s)" "$i" + # ensure that the command "true" can run with the right filter + if $L snap.name.app /bin/true ; then + PASS + else + dmesg|tail -n1 + FAIL + fi +done diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/tests/test_restrictions_working_args_socket snapd-2.23.1+16.10/cmd/snap-confine/tests/test_restrictions_working_args_socket --- snapd-2.22.6+16.10/cmd/snap-confine/tests/test_restrictions_working_args_socket 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/tests/test_restrictions_working_args_socket 2017-03-08 13:28:17.000000000 +0000 @@ -9,7 +9,7 @@ getpriority EOF -for i in AF_UNIX AF_LOCAL AF_INET AF_INET6 AF_IPX AF_NETLINK AF_X25 AF_AX25 AF_ATMPVC AF_APPLETALK AF_PACKET AF_ALG AF_CAN ; do +for i in AF_UNIX AF_LOCAL AF_INET AF_INET6 AF_IPX AF_NETLINK AF_X25 AF_AX25 AF_ATMPVC AF_APPLETALK AF_PACKET AF_ALG AF_CAN PF_UNIX PF_LOCAL PF_INET PF_INET6 PF_IPX PF_NETLINK PF_X25 PF_AX25 PF_ATMPVC PF_APPLETALK PF_PACKET PF_ALG PF_CAN ; do cat "$TMP"/tmpl >"$TMP"/snap.name.app echo "socket $i" >>"$TMP"/snap.name.app diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/tests/test_restrictions_working_args_termios snapd-2.23.1+16.10/cmd/snap-confine/tests/test_restrictions_working_args_termios --- snapd-2.22.6+16.10/cmd/snap-confine/tests/test_restrictions_working_args_termios 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/tests/test_restrictions_working_args_termios 2017-03-08 13:28:17.000000000 +0000 @@ -0,0 +1,22 @@ +#!/bin/sh + +set -e + +. "${srcdir:-.}/snap-confine/tests/common.sh" + +get_common_syscalls >"$TMP"/tmpl +cat >>"$TMP"/tmpl <"$TMP"/snap.name.app +echo "ioctl - TIOCSTI" >>"$TMP"/snap.name.app + +printf "Test good seccomp arg filtering (ioctl - %s)" "TIOCSTI" +# ensure that the command "true" can run with the right filter +if $L snap.name.app /bin/true ; then + PASS +else + dmesg|tail -n1 + FAIL +fi diff -Nru snapd-2.22.6+16.10/cmd/snap-confine/udev-support.c snapd-2.23.1+16.10/cmd/snap-confine/udev-support.c --- snapd-2.22.6+16.10/cmd/snap-confine/udev-support.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-confine/udev-support.c 2017-03-02 06:37:54.000000000 +0000 @@ -28,6 +28,7 @@ #include #include "../libsnap-confine-private/snap.h" +#include "../libsnap-confine-private/string-utils.h" #include "../libsnap-confine-private/utils.h" #include "udev-support.h" @@ -73,7 +74,7 @@ char *env[] = { NULL }; unsigned major = MAJOR(devnum); unsigned minor = MINOR(devnum); - must_snprintf(buf, sizeof(buf), "%u:%u", major, minor); + sc_must_snprintf(buf, sizeof(buf), "%u:%u", major, minor); execle("/lib/udev/snappy-app-dev", "/lib/udev/snappy-app-dev", "add", udev_s->tagname, path, buf, NULL, env); die("execl failed"); @@ -103,8 +104,8 @@ udev_s->tagname[0] = '\0'; udev_s->tagname_len = 0; // TAG+="snap_" (udev doesn't like '.' in the tag name) - udev_s->tagname_len = must_snprintf(udev_s->tagname, MAX_BUF, - "%s", security_tag); + udev_s->tagname_len = sc_must_snprintf(udev_s->tagname, MAX_BUF, + "%s", security_tag); for (int i = 0; i < udev_s->tagname_len; i++) if (udev_s->tagname[i] == '.') udev_s->tagname[i] = '_'; @@ -176,27 +177,27 @@ // create devices cgroup controller char cgroup_dir[PATH_MAX]; - must_snprintf(cgroup_dir, sizeof(cgroup_dir), - "/sys/fs/cgroup/devices/%s/", security_tag); + sc_must_snprintf(cgroup_dir, sizeof(cgroup_dir), + "/sys/fs/cgroup/devices/%s/", security_tag); if (mkdir(cgroup_dir, 0755) < 0 && errno != EEXIST) die("mkdir failed"); // move ourselves into it char cgroup_file[PATH_MAX]; - must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, - "tasks"); + sc_must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, + "tasks"); char buf[128]; - must_snprintf(buf, sizeof(buf), "%i", getpid()); + sc_must_snprintf(buf, sizeof(buf), "%i", getpid()); write_string_to_file(cgroup_file, buf); // deny by default. Write 'a' to devices.deny to remove all existing // devices that were added in previous launcher invocations, then add // the static and assigned devices. This ensures that at application // launch the cgroup only has what is currently assigned. - must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, - "devices.deny"); + sc_must_snprintf(cgroup_file, sizeof(cgroup_file), "%s%s", cgroup_dir, + "devices.deny"); write_string_to_file(cgroup_file, "a"); // add the common devices diff -Nru snapd-2.22.6+16.10/cmd/snapctl/main_test.go snapd-2.23.1+16.10/cmd/snapctl/main_test.go --- snapd-2.22.6+16.10/cmd/snapctl/main_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snapctl/main_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -75,14 +75,14 @@ s.expectedArgs = []string{} fakeAuthPath := filepath.Join(c.MkDir(), "auth.json") - os.Setenv("SNAPPY_STORE_AUTH_DATA_FILENAME", fakeAuthPath) + os.Setenv("SNAPD_AUTH_DATA_FILENAME", fakeAuthPath) err := ioutil.WriteFile(fakeAuthPath, []byte(`{"macaroon":"user-macaroon"}`), 0644) c.Assert(err, IsNil) } func (s *snapctlSuite) TearDownTest(c *C) { os.Unsetenv("SNAP_CONTEXT") - os.Unsetenv("SNAPPY_STORE_AUTH_DATA_FILENAME") + os.Unsetenv("SNAPD_AUTH_DATA_FILENAME") clientConfig.BaseURL = "" s.server.Close() os.Args = s.oldArgs diff -Nru snapd-2.22.6+16.10/cmd/snap-exec/main.go snapd-2.23.1+16.10/cmd/snap-exec/main.go --- snapd-2.22.6+16.10/cmd/snap-exec/main.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-exec/main.go 2017-03-02 06:37:54.000000000 +0000 @@ -28,6 +28,7 @@ "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" ) @@ -141,7 +142,7 @@ cmdArgs := cmdArgv[1:] // build the environment from the yaml - env := append(os.Environ(), app.Env()...) + env := append(os.Environ(), osutil.SubstituteEnv(app.Env())...) // run the command fullCmd := filepath.Join(app.Snap.MountDir(), cmd) diff -Nru snapd-2.22.6+16.10/cmd/snap-exec/main_test.go snapd-2.23.1+16.10/cmd/snap-exec/main_test.go --- snapd-2.22.6+16.10/cmd/snap-exec/main_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-exec/main_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -62,7 +62,9 @@ stop-command: stop-app post-stop-command: post-stop-app environment: - LD_LIBRARY_PATH: /some/path + BASE_PATH: /some/path + LD_LIBRARY_PATH: ${BASE_PATH}/lib + MY_PATH: $PATH nostop: command: nostop `) @@ -149,7 +151,9 @@ c.Assert(err, IsNil) c.Check(execArgv0, Equals, fmt.Sprintf("%s/snapname/42/stop-app", dirs.SnapMountDir)) c.Check(execArgs, DeepEquals, []string{execArgv0, "arg1", "arg2"}) - c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path\n") + c.Check(execEnv, testutil.Contains, "BASE_PATH=/some/path") + c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path/lib") + c.Check(execEnv, testutil.Contains, fmt.Sprintf("MY_PATH=%s", os.Getenv("PATH"))) } func (s *snapExecSuite) TestSnapExecHookIntegration(c *C) { @@ -307,5 +311,5 @@ c.Assert(err, IsNil) c.Check(execArgv0, Equals, "/bin/bash") c.Check(execArgs, DeepEquals, []string{execArgv0, "-c", "echo foo"}) - c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path\n") + c.Check(execEnv, testutil.Contains, "LD_LIBRARY_PATH=/some/path/lib") } diff -Nru snapd-2.22.6+16.10/cmd/snap-update-ns/mount-entry.c snapd-2.23.1+16.10/cmd/snap-update-ns/mount-entry.c --- snapd-2.22.6+16.10/cmd/snap-update-ns/mount-entry.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-update-ns/mount-entry.c 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "snap-update-ns/mount-entry.h" + +#include +#include +#include +#include + +#include "../libsnap-confine-private/utils.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/cleanup-funcs.h" + +/** + * Compare two mount entries (through indirect pointers). + **/ +static int +sc_indirect_compare_mount_entry(const struct sc_mount_entry **a, + const struct sc_mount_entry **b) +{ + return sc_compare_mount_entry(*a, *b); +} + +/** + * Copy struct mntent into a freshly-allocated struct sc_mount_entry. + * + * The next pointer is initialized to NULL, it should be managed by the caller. + * If anything goes wrong the routine die()s. + **/ +static struct sc_mount_entry *sc_clone_mount_entry_from_mntent(const struct + mntent *entry) +{ + struct sc_mount_entry *result; + result = calloc(1, sizeof *result); + if (result == NULL) { + die("cannot allocate memory"); + } + result->entry.mnt_fsname = strdup(entry->mnt_fsname ? : ""); + if (result->entry.mnt_fsname == NULL) { + die("cannot copy string"); + } + result->entry.mnt_dir = strdup(entry->mnt_dir ? : ""); + if (result->entry.mnt_dir == NULL) { + die("cannot copy string"); + } + result->entry.mnt_type = strdup(entry->mnt_type ? : ""); + if (result->entry.mnt_type == NULL) { + die("cannot copy string"); + } + result->entry.mnt_opts = strdup(entry->mnt_opts ? : ""); + if (result->entry.mnt_opts == NULL) { + die("cannot copy string"); + } + result->entry.mnt_freq = entry->mnt_freq; + result->entry.mnt_passno = entry->mnt_passno; + return result; +} + +static struct sc_mount_entry *sc_get_next_and_free_mount_entry(struct + sc_mount_entry + *entry) +{ + struct sc_mount_entry *next = entry->next; + free(entry->entry.mnt_fsname); + free(entry->entry.mnt_dir); + free(entry->entry.mnt_type); + free(entry->entry.mnt_opts); + memset(entry, 0, sizeof *entry); + free(entry); + return next; +} + +void sc_free_mount_entry_list(struct sc_mount_entry *entry) +{ + while (entry != NULL) { + entry = sc_get_next_and_free_mount_entry(entry); + } +} + +void sc_cleanup_mount_entry_list(struct sc_mount_entry **entryp) +{ + if (entryp != NULL) { + sc_free_mount_entry_list(*entryp); + *entryp = NULL; + } +} + +int sc_compare_mount_entry(const struct sc_mount_entry *a, + const struct sc_mount_entry *b) +{ + int result; + if (a == NULL || b == NULL) { + die("cannot compare NULL mount entry"); + } + result = strcmp(a->entry.mnt_fsname, b->entry.mnt_fsname); + if (result != 0) { + return result; + } + result = strcmp(a->entry.mnt_dir, b->entry.mnt_dir); + if (result != 0) { + return result; + } + result = strcmp(a->entry.mnt_type, b->entry.mnt_type); + if (result != 0) { + return result; + } + result = strcmp(a->entry.mnt_opts, b->entry.mnt_opts); + if (result != 0) { + return result; + } + result = a->entry.mnt_freq - b->entry.mnt_freq; + if (result != 0) { + return result; + } + result = a->entry.mnt_passno - b->entry.mnt_passno; + return result; +} + +struct sc_mount_entry *sc_load_mount_profile(const char *pathname) +{ + FILE *f __attribute__ ((cleanup(sc_cleanup_endmntent))) = NULL; + + f = setmntent(pathname, "rt"); + if (f == NULL) { + // NOTE: it is fine if the profile doesn't exist. + // It is equivalent to having no entries. + if (errno != ENOENT) { + die("cannot open mount profile %s for reading", + pathname); + } + return NULL; + } + // Loop over the entries in the file and copy them to a singly-linked list. + struct sc_mount_entry *entry, *first = NULL, *prev = NULL; + struct mntent *mntent_entry; + while (((mntent_entry = getmntent(f)) != NULL)) { + entry = sc_clone_mount_entry_from_mntent(mntent_entry); + if (prev != NULL) { + prev->next = entry; + } + if (first == NULL) { + first = entry; + } + prev = entry; + } + return first; +} + +void sc_save_mount_profile(const struct sc_mount_entry *first, + const char *pathname) +{ + FILE *f __attribute__ ((cleanup(sc_cleanup_endmntent))) = NULL; + + f = setmntent(pathname, "wt"); + if (f == NULL) { + die("cannot open mount profile %s for writing", pathname); + } + + const struct sc_mount_entry *entry; + for (entry = first; entry != NULL; entry = entry->next) { + if (addmntent(f, &entry->entry) != 0) { + die("cannot add mount entry to %s", pathname); + } + } +} + +void sc_sort_mount_entries(struct sc_mount_entry **first) +{ + if (*first == NULL) { + // NULL list is an empty list + return; + } + // Count the items + size_t count; + struct sc_mount_entry *entry; + for (count = 0, entry = *first; entry != NULL; + ++count, entry = entry->next) ; + + // Allocate an array of pointers + struct sc_mount_entry **entryp_array = NULL; + entryp_array = calloc(count, sizeof *entryp_array); + if (entryp_array == NULL) { + die("cannot allocate memory"); + } + // Populate the array + entry = *first; + for (size_t i = 0; i < count; ++i) { + entryp_array[i] = entry; + entry = entry->next; + } + + // Sort the array according to lexical sorting of all the elements. + qsort(entryp_array, count, sizeof(void *), + (int (*)(const void *, const void *)) + sc_indirect_compare_mount_entry); + + // Rewrite all the next pointers of each element. + for (size_t i = 0; i < count - 1; ++i) { + entryp_array[i]->next = entryp_array[i + 1]; + } + entryp_array[count - 1]->next = NULL; + + // Rewrite the pointer to the head of the list. + *first = entryp_array[0]; + free(entryp_array); +} diff -Nru snapd-2.22.6+16.10/cmd/snap-update-ns/mount-entry.h snapd-2.23.1+16.10/cmd/snap-update-ns/mount-entry.h --- snapd-2.22.6+16.10/cmd/snap-update-ns/mount-entry.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-update-ns/mount-entry.h 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SNAP_CONFINE_MOUNT_ENTRY_H +#define SNAP_CONFINE_MOUNT_ENTRY_H + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +/** + * A fstab-like mount entry. + **/ +struct sc_mount_entry { + struct mntent entry; + struct sc_mount_entry *next; +}; + +/** + * Parse a given fstab-like file into a list of sc_mount_entry objects. + * + * If the given file does not exist then the result is a NULL (empty) list. + * If anything goes wrong the routine die()s. + **/ +struct sc_mount_entry *sc_load_mount_profile(const char *pathname); + +/** + * Save a list of sc_mount_entry objects to a fstab-like file. + * + * If anything goes wrong the routine die()s. + **/ +void sc_save_mount_profile(const struct sc_mount_entry *first, + const char *pathname); + +/** + * Compare two mount entries. + * + * Returns 0 if both entries are equal, a number less than zero if the first + * entry sorts before the second entry or a number greater than zero if the + * second entry sorts before the second entry. + **/ +int +sc_compare_mount_entry(const struct sc_mount_entry *a, + const struct sc_mount_entry *b); + +/** + * Sort the linked list of mount entries. + * + * The initial argument is a pointer to the first element (which can be NULL). + * The list is sorted and all the next pointers are updated to point to the + * lexically subsequent element. + **/ +void sc_sort_mount_entries(struct sc_mount_entry **first); + +/** + * Free a dynamically allocated list of strct sc_mount_entry objects. + * + * This function is designed to be used with + * __attribute__((cleanup(sc_cleanup_mount_entry_list))). + **/ +void sc_cleanup_mount_entry_list(struct sc_mount_entry **entryp); + +/** + * Free a dynamically allocated list of strct sc_mount_entry objects. + **/ +void sc_free_mount_entry_list(struct sc_mount_entry *entry); + +#endif diff -Nru snapd-2.22.6+16.10/cmd/snap-update-ns/mount-entry-test.c snapd-2.23.1+16.10/cmd/snap-update-ns/mount-entry-test.c --- snapd-2.22.6+16.10/cmd/snap-update-ns/mount-entry-test.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-update-ns/mount-entry-test.c 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "mount-entry.h" +#include "mount-entry.c" + +#include + +#include + +#include "test-utils.h" +#include "test-data.h" + +static void test_sc_load_mount_profile() +{ + struct sc_mount_entry *fstab + __attribute__ ((cleanup(sc_cleanup_mount_entry_list))) = NULL; + struct sc_mount_entry *entry; + sc_test_write_lines("test.fstab", test_entry_str_1, test_entry_str_2, + NULL); + fstab = sc_load_mount_profile("test.fstab"); + g_assert_nonnull(fstab); + + entry = fstab; + test_looks_like_test_entry_1(entry); + g_assert_nonnull(entry->next); + + entry = entry->next; + test_looks_like_test_entry_2(entry); + g_assert_null(entry->next); +} + +static void test_sc_load_mount_profile__no_such_file() +{ + struct sc_mount_entry *fstab + __attribute__ ((cleanup(sc_cleanup_mount_entry_list))) = NULL; + fstab = sc_load_mount_profile("test.does-not-exist.fstab"); + g_assert_null(fstab); +} + +static void test_sc_save_mount_profile() +{ + struct sc_mount_entry entry_1 = test_entry_1; + struct sc_mount_entry entry_2 = test_entry_2; + entry_1.next = &entry_2; + entry_2.next = NULL; + + // We can save the profile defined above. + sc_save_mount_profile(&entry_1, "test.fstab"); + + // Cast-away the const qualifier. This just calls unlink and we don't + // modify the name in any way. This way the signature is compatible with + // that of GDestroyNotify. + g_test_queue_destroy((GDestroyNotify) sc_test_remove_file, + (char *)"test.fstab"); + + // After reading the generated file it looks as expected. + FILE *f = fopen("test.fstab", "rt"); + g_assert_nonnull(f); + char *line = NULL; + size_t n = 0; + ssize_t num_read; + + num_read = getline(&line, &n, f); + g_assert_cmpint(num_read, >, -0); + g_assert_cmpstr(line, ==, "fsname-1 dir-1 type-1 opts-1 1 2\n"); + + num_read = getline(&line, &n, f); + g_assert_cmpint(num_read, >, -0); + g_assert_cmpstr(line, ==, "fsname-2 dir-2 type-2 opts-2 3 4\n"); + + num_read = getline(&line, &n, f); + g_assert_cmpint(num_read, ==, -1); + + free(line); + fclose(f); +} + +static void test_sc_compare_mount_entry() +{ + // Do trivial comparison checks. + g_assert_cmpint(sc_compare_mount_entry(&test_entry_1, &test_entry_1), + ==, 0); + g_assert_cmpint(sc_compare_mount_entry(&test_entry_1, &test_entry_2), <, + 0); + g_assert_cmpint(sc_compare_mount_entry(&test_entry_2, &test_entry_1), >, + 0); + g_assert_cmpint(sc_compare_mount_entry(&test_entry_2, &test_entry_2), + ==, 0); + + // Ensure that each field is compared. + struct sc_mount_entry a = test_entry_1; + struct sc_mount_entry b = test_entry_1; + g_assert_cmpint(sc_compare_mount_entry(&a, &b), ==, 0); + + b.entry.mnt_fsname = test_entry_2.entry.mnt_fsname; + g_assert_cmpint(sc_compare_mount_entry(&a, &b), <, 0); + b = test_entry_1; + + b.entry.mnt_dir = test_entry_2.entry.mnt_dir; + g_assert_cmpint(sc_compare_mount_entry(&a, &b), <, 0); + b = test_entry_1; + + b.entry.mnt_opts = test_entry_2.entry.mnt_opts; + g_assert_cmpint(sc_compare_mount_entry(&a, &b), <, 0); + b = test_entry_1; + + b.entry.mnt_freq = test_entry_2.entry.mnt_freq; + g_assert_cmpint(sc_compare_mount_entry(&a, &b), <, 0); + b = test_entry_1; + + b.entry.mnt_passno = test_entry_2.entry.mnt_passno; + g_assert_cmpint(sc_compare_mount_entry(&a, &b), <, 0); + b = test_entry_1; +} + +static void test_sc_clone_mount_entry_from_mntent() +{ + struct sc_mount_entry *entry = + sc_clone_mount_entry_from_mntent(&test_mnt_1); + test_looks_like_test_entry_1(entry); + g_assert_null(entry->next); + + struct sc_mount_entry *next = sc_get_next_and_free_mount_entry(entry); + g_assert_null(next); +} + +static void test_sc_sort_mount_entries() +{ + struct sc_mount_entry *list; + + // Sort an empty list, it should not blow up. + list = NULL; + sc_sort_mount_entries(&list); + g_assert(list == NULL); + + // Create a list with two items in wrong order (backwards). + struct sc_mount_entry entry_1 = test_entry_1; + struct sc_mount_entry entry_2 = test_entry_2; + list = &entry_2; + entry_2.next = &entry_1; + entry_1.next = NULL; + + // Sort the list + sc_sort_mount_entries(&list); + + // Ensure that the linkage now follows the right order. + g_assert(list == &entry_1); + g_assert(entry_1.next == &entry_2); + g_assert(entry_2.next == NULL); +} + +static void __attribute__ ((constructor)) init() +{ + g_test_add_func("/mount-entry/sc_load_mount_profile", + test_sc_load_mount_profile); + g_test_add_func("/mount-entry/sc_load_mount_profile/no_such_file", + test_sc_load_mount_profile__no_such_file); + g_test_add_func("/mount-entry/sc_save_mount_profile", + test_sc_save_mount_profile); + g_test_add_func("/mount-entry/sc_compare_mount_entry", + test_sc_compare_mount_entry); + g_test_add_func("/mount-entry/test_sc_clone_mount_entry_from_mntent", + test_sc_clone_mount_entry_from_mntent); + g_test_add_func("/mount-entry/test_sort_mount_entries", + test_sc_sort_mount_entries); +} diff -Nru snapd-2.22.6+16.10/cmd/snap-update-ns/test-data.c snapd-2.23.1+16.10/cmd/snap-update-ns/test-data.c --- snapd-2.22.6+16.10/cmd/snap-update-ns/test-data.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-update-ns/test-data.c 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "test-data.h" + +#include + +const char *test_entry_str_1 = "fsname-1 dir-1 type-1 opts-1 1 2"; +const char *test_entry_str_2 = "fsname-2 dir-2 type-2 opts-2 3 4"; + +const struct sc_mount_entry test_entry_1 = {.entry = { + .mnt_fsname = "fsname-1", + .mnt_dir = "dir-1", + .mnt_type = "type-1", + .mnt_opts = "opts-1", + .mnt_freq = 1, + .mnt_passno = 2, + } +}; + +const struct sc_mount_entry test_entry_2 = {.entry = { + .mnt_fsname = "fsname-2", + .mnt_dir = "dir-2", + .mnt_type = "type-2", + .mnt_opts = "opts-2", + .mnt_freq = 3, + .mnt_passno = 4, + } +}; + +const struct mntent test_mnt_1 = { + .mnt_fsname = "fsname-1", + .mnt_dir = "dir-1", + .mnt_type = "type-1", + .mnt_opts = "opts-1", + .mnt_freq = 1, + .mnt_passno = 2, +}; + +const struct mntent test_mnt_2 = { + .mnt_fsname = "fsname-2", + .mnt_dir = "dir-2", + .mnt_type = "type-2", + .mnt_opts = "opts-2", + .mnt_freq = 3, + .mnt_passno = 4, +}; + +void test_looks_like_test_entry_1(const struct sc_mount_entry *entry) +{ + g_assert_cmpstr(entry->entry.mnt_fsname, ==, "fsname-1"); + g_assert_cmpstr(entry->entry.mnt_dir, ==, "dir-1"); + g_assert_cmpstr(entry->entry.mnt_type, ==, "type-1"); + g_assert_cmpstr(entry->entry.mnt_opts, ==, "opts-1"); + g_assert_cmpint(entry->entry.mnt_freq, ==, 1); + g_assert_cmpint(entry->entry.mnt_passno, ==, 2); +} + +void test_looks_like_test_entry_2(const struct sc_mount_entry *entry) +{ + g_assert_cmpstr(entry->entry.mnt_fsname, ==, "fsname-2"); + g_assert_cmpstr(entry->entry.mnt_dir, ==, "dir-2"); + g_assert_cmpstr(entry->entry.mnt_type, ==, "type-2"); + g_assert_cmpstr(entry->entry.mnt_opts, ==, "opts-2"); + g_assert_cmpint(entry->entry.mnt_freq, ==, 3); + g_assert_cmpint(entry->entry.mnt_passno, ==, 4); +} diff -Nru snapd-2.22.6+16.10/cmd/snap-update-ns/test-data.h snapd-2.23.1+16.10/cmd/snap-update-ns/test-data.h --- snapd-2.22.6+16.10/cmd/snap-update-ns/test-data.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-update-ns/test-data.h 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SNAP_CONFINE_TEST_DATA_H +#define SNAP_CONFINE_TEST_DATA_H + +#include "mount-entry.h" + +extern const char *test_entry_str_1; +extern const char *test_entry_str_2; + +extern const struct sc_mount_entry test_entry_1; +extern const struct sc_mount_entry test_entry_2; + +extern const struct mntent test_mnt_1; +extern const struct mntent test_mnt_2; + +void test_looks_like_test_entry_1(const struct sc_mount_entry *entry); +void test_looks_like_test_entry_2(const struct sc_mount_entry *entry); + +#endif diff -Nru snapd-2.22.6+16.10/cmd/snap-update-ns/test-utils.c snapd-2.23.1+16.10/cmd/snap-update-ns/test-utils.c --- snapd-2.22.6+16.10/cmd/snap-update-ns/test-utils.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-update-ns/test-utils.c 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#include "test-utils.h" + +#include + +#include +#include +#include + +void sc_test_remove_file(const char *name) +{ + int err = remove(name); + g_assert_cmpint(err, ==, 0); +} + +void sc_test_write_lines(const char *name, ...) +{ + FILE *f = fopen(name, "wt"); + g_assert_nonnull(f); + + va_list ap; + va_start(ap, name); + const char *line; + while ((line = va_arg(ap, const char *)) != NULL) { + fprintf(f, "%s\n", line); + } + va_end(ap); + fclose(f); + + // Cast-away the const qualifier. This just calls unlink and we don't + // modify the name in any way. This way the signature is compatible with + // that of GDestroyNotify. + g_test_queue_destroy((GDestroyNotify) sc_test_remove_file, + (char *)name); +} diff -Nru snapd-2.22.6+16.10/cmd/snap-update-ns/test-utils.h snapd-2.23.1+16.10/cmd/snap-update-ns/test-utils.h --- snapd-2.22.6+16.10/cmd/snap-update-ns/test-utils.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/snap-update-ns/test-utils.h 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef SC_SNAP_CONFINE_TEST_UTILS_H +#define SC_SNAP_CONFINE_TEST_UTILS_H + +/** + * Write a sequence of lines to the given file. + * + * Lines are provided as arguments. The list must be terminated with a NULL + * pointer. Lines should not contain a trailing newline as that is added + * automatically. + * + * The written file is automatically removed when the test terminates. + **/ +void sc_test_write_lines(const char *name, ...) __attribute__ ((sentinel)); + +/** + * Remove a file created during testing. + * + * This function is compatible with GDestroyNotify. It just calls + * remove(3) but has a different signature. + **/ +void sc_test_remove_file(const char *name); + +#endif diff -Nru snapd-2.22.6+16.10/cmd/system-shutdown/system-shutdown.c snapd-2.23.1+16.10/cmd/system-shutdown/system-shutdown.c --- snapd-2.22.6+16.10/cmd/system-shutdown/system-shutdown.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/system-shutdown/system-shutdown.c 2017-03-02 06:37:54.000000000 +0000 @@ -29,6 +29,7 @@ #include // errno, sys_errlist #include "system-shutdown-utils.h" +#include "../libsnap-confine-private/string-utils.h" int main(int argc, char *argv[]) { @@ -78,13 +79,13 @@ if (argc < 2) { kmsg("* called without verb; halting."); } else { - if (streq("reboot", argv[1])) { + if (sc_streq("reboot", argv[1])) { cmd = RB_AUTOBOOT; kmsg("- rebooting."); - } else if (streq("poweroff", argv[1])) { + } else if (sc_streq("poweroff", argv[1])) { cmd = RB_POWER_OFF; kmsg("- powering off."); - } else if (streq("halt", argv[1])) { + } else if (sc_streq("halt", argv[1])) { kmsg("- halting."); } else { kmsg("* called with unsupported verb %s; halting.", diff -Nru snapd-2.22.6+16.10/cmd/system-shutdown/system-shutdown-utils.c snapd-2.23.1+16.10/cmd/system-shutdown/system-shutdown-utils.c --- snapd-2.22.6+16.10/cmd/system-shutdown/system-shutdown-utils.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/system-shutdown/system-shutdown-utils.c 2017-03-02 06:37:54.000000000 +0000 @@ -32,38 +32,7 @@ #include // getpid, close #include "../libsnap-confine-private/mountinfo.h" - -bool streq(const char *a, const char *b) -{ - if (!a || !b) { - return false; - } - - size_t alen = strlen(a); - size_t blen = strlen(b); - - if (alen != blen) { - return false; - } - - return strncmp(a, b, alen) == 0; -} - -static bool endswith(const char *str, const char *suffix) -{ - if (!str || !suffix) { - return false; - } - - size_t xlen = strlen(suffix); - size_t slen = strlen(str); - - if (slen < xlen) { - return false; - } - - return strncmp(str - xlen + slen, suffix, xlen) == 0; -} +#include "../libsnap-confine-private/string-utils.h" __attribute__ ((format(printf, 1, 2))) void kmsg(const char *fmt, ...) @@ -120,36 +89,37 @@ bool had_writable = false; for (int i = 0; i < 10 && did_umount; i++) { - struct mountinfo *mounts = parse_mountinfo(NULL); + struct sc_mountinfo *mounts = sc_parse_mountinfo(NULL); if (!mounts) { // oh dear die("unable to get mount info; giving up"); } - struct mountinfo_entry *cur = first_mountinfo_entry(mounts); + struct sc_mountinfo_entry *cur = + sc_first_mountinfo_entry(mounts); had_writable = false; did_umount = false; while (cur) { - const char *dir = mountinfo_entry_mount_dir(cur); - const char *src = mountinfo_entry_mount_source(cur); - unsigned major = mountinfo_entry_dev_major(cur); + const char *dir = sc_mountinfo_entry_mount_dir(cur); + const char *src = sc_mountinfo_entry_mount_source(cur); + unsigned major = sc_mountinfo_entry_dev_major(cur); - cur = next_mountinfo_entry(cur); + cur = sc_next_mountinfo_entry(cur); - if (streq("/", dir)) { + if (sc_streq("/", dir)) { continue; } - if (streq("/dev", dir)) { + if (sc_streq("/dev", dir)) { continue; } - if (streq("/proc", dir)) { + if (sc_streq("/proc", dir)) { continue; } if (major != 0 && major != LOOP_MAJOR - && endswith(dir, "/writable")) { + && sc_endswith(dir, "/writable")) { had_writable = true; } @@ -161,7 +131,7 @@ did_umount = true; } } - cleanup_mountinfo(&mounts); + sc_cleanup_mountinfo(&mounts); } return !had_writable; diff -Nru snapd-2.22.6+16.10/cmd/system-shutdown/system-shutdown-utils.h snapd-2.23.1+16.10/cmd/system-shutdown/system-shutdown-utils.h --- snapd-2.22.6+16.10/cmd/system-shutdown/system-shutdown-utils.h 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/system-shutdown/system-shutdown-utils.h 2017-03-02 06:37:54.000000000 +0000 @@ -24,8 +24,6 @@ // no longer found writable. bool umount_all(); -bool streq(const char *a, const char *b); - __attribute__ ((noreturn)) void die(const char *msg); __attribute__ ((format(printf, 1, 2))) diff -Nru snapd-2.22.6+16.10/cmd/system-shutdown/system-shutdown-utils-test.c snapd-2.23.1+16.10/cmd/system-shutdown/system-shutdown-utils-test.c --- snapd-2.22.6+16.10/cmd/system-shutdown/system-shutdown-utils-test.c 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/cmd/system-shutdown/system-shutdown-utils-test.c 2017-03-02 06:37:54.000000000 +0000 @@ -19,35 +19,3 @@ #include "system-shutdown-utils.c" #include - -static void test_streq() -{ - g_assert_false(streq(NULL, NULL)); - g_assert_false(streq(NULL, "text")); - g_assert_false(streq("text", NULL)); - g_assert_false(streq("foo", "bar")); - g_assert_false(streq("foo", "barbar")); - g_assert_false(streq("foofoo", "bar")); - g_assert_true(streq("text", "text")); - g_assert_true(streq("", "")); -} - -static void test_endswith() -{ - g_assert_false(endswith("", NULL)); - g_assert_false(endswith(NULL, "")); - g_assert_false(endswith(NULL, NULL)); - g_assert_true(endswith("", "")); - g_assert_true(endswith("foobar", "bar")); - g_assert_true(endswith("foobar", "ar")); - g_assert_true(endswith("foobar", "r")); - g_assert_true(endswith("foobar", "")); - g_assert_false(endswith("foobar", "quux")); - g_assert_false(endswith("", "bar")); -} - -static void __attribute__ ((constructor)) init() -{ - g_test_add_func("/system_shutdown/streq", test_streq); - g_test_add_func("/system_shutdown/endswith", test_endswith); -} diff -Nru snapd-2.22.6+16.10/daemon/api.go snapd-2.23.1+16.10/daemon/api.go --- snapd-2.22.6+16.10/daemon/api.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/daemon/api.go 2017-03-08 13:28:17.000000000 +0000 @@ -50,6 +50,7 @@ "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/configstate" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" "github.com/snapcore/snapd/overlord/ifacestate" @@ -827,7 +828,7 @@ // confinement. flags.JailMode = jailMode flags.Classic = classic - flags.DevMode = devMode || devModeOS && !classic + flags.DevMode = devMode return flags, nil } @@ -844,12 +845,12 @@ switch len(updated) { case 0: - // not really needed but be paranoid if len(inst.Snaps) != 0 { - return "", nil, nil, fmt.Errorf("internal error: when asking for a refresh of %s no update was found but no error was generated", strutil.Quoted(inst.Snaps)) + // TRANSLATORS: the %s is a comma-separated list of quoted snap names + msg = fmt.Sprintf(i18n.G("Refresh snaps %s: no updates"), strutil.Quoted(inst.Snaps)) + } else { + msg = fmt.Sprintf(i18n.G("Refresh all snaps: no updates")) } - // FIXME: instead don't generated a change(?) at all - msg = fmt.Sprintf(i18n.G("Refresh all snaps: no updates")) case 1: msg = fmt.Sprintf(i18n.G("Refresh snap %q"), updated[0]) default: @@ -1121,6 +1122,16 @@ // the developer asked us to do this with a trusted snap dir info, err := unsafeReadSnapInfo(trydir) + if _, ok := err.(snap.NotSnapError); ok { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Result: &errorResult{ + Message: err.Error(), + Kind: errorKindNotSnap, + }, + Status: http.StatusBadRequest, + }, nil) + } if err != nil { return BadRequest("cannot read snap info for %s: %s", trydir, err) } @@ -1423,13 +1434,13 @@ s := c.d.overlord.State() s.Lock() - transaction := configstate.NewTransaction(s) + tr := config.NewTransaction(s) s.Unlock() currentConfValues := make(map[string]interface{}) for _, key := range keys { var value interface{} - if err := transaction.Get(snapName, key, &value); err != nil { + if err := tr.Get(snapName, key, &value); err != nil { return BadRequest("%s", err) } @@ -2173,7 +2184,7 @@ return BadRequest("cannot get ucrednet uid: %v", err) } if uid != 0 { - return BadRequest("cannot use create-user as non-root") + return BadRequest("cannot get users as non-root") } st := c.d.overlord.State() diff -Nru snapd-2.22.6+16.10/daemon/api_test.go snapd-2.23.1+16.10/daemon/api_test.go --- snapd-2.22.6+16.10/daemon/api_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/daemon/api_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -54,7 +54,7 @@ "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" - "github.com/snapcore/snapd/overlord/configstate" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/ifacestate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" @@ -142,6 +142,10 @@ ID: "ubuntu", VersionID: "mocked", }) + + snapstate.CanAutoRefresh = func(*state.State) (bool, error) { + return false, nil + } } func (s *apiBaseSuite) TearDownSuite(c *check.C) { @@ -225,6 +229,8 @@ st.Lock() defer st.Unlock() snapstate.ReplaceStore(st, s) + // mark as already seeded + st.Set("seeded", true) s.d = d return d @@ -386,6 +392,7 @@ "trymode": false, "apps": []appJSON{}, "broken": "", + "contact": "", }, Meta: meta, } @@ -1729,7 +1736,7 @@ head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} restore := release.MockReleaseInfo(&release.OS{ID: "x-devmode-distro"}) defer restore() - flags := snapstate.Flags{DevMode: true, RemoveSnapPath: true} + flags := snapstate.Flags{RemoveSnapPath: true} chgSummary := s.sideloadCheck(c, body, head, flags, false) c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) } @@ -1792,7 +1799,7 @@ "\r\n" + "true\r\n" + "----hello--\r\n" - d := newTestDaemon(c) + d := s.daemon(c) d.overlord.Loop() defer d.overlord.Stop() @@ -1816,7 +1823,7 @@ "\r\n" + "true\r\n" + "----hello--\r\n" - d := newTestDaemon(c) + d := s.daemon(c) d.overlord.Loop() defer d.overlord.Stop() @@ -1833,7 +1840,7 @@ } func (s *apiSuite) TestLocalInstallSnapDeriveSideInfo(c *check.C) { - d := newTestDaemon(c) + d := s.daemon(c) d.overlord.Loop() defer d.overlord.Stop() // add the assertions first @@ -1915,7 +1922,7 @@ "\r\n" + "xyzzy\r\n" + "----hello--\r\n" - d := newTestDaemon(c) + d := s.daemon(c) d.overlord.Loop() defer d.overlord.Stop() @@ -1958,7 +1965,7 @@ } func (s *apiSuite) TestTrySnap(c *check.C) { - d := newTestDaemon(c) + d := s.daemon(c) d.overlord.Loop() defer d.overlord.Stop() @@ -2073,7 +2080,7 @@ } func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedFlags snapstate.Flags, hasCoreSnap bool) string { - d := newTestDaemon(c) + d := s.daemon(c) d.overlord.Loop() defer d.overlord.Stop() @@ -2183,10 +2190,10 @@ // Set a config that we'll get in a moment d.overlord.State().Lock() - transaction := configstate.NewTransaction(d.overlord.State()) - transaction.Set("test-snap", "test-key1", "test-value1") - transaction.Set("test-snap", "test-key2", "test-value2") - transaction.Commit() + tr := config.NewTransaction(d.overlord.State()) + tr.Set("test-snap", "test-key1", "test-value1") + tr.Set("test-snap", "test-key2", "test-value2") + tr.Commit() d.overlord.State().Unlock() result := s.runGetConf(c, []string{"test-key1"}) @@ -2331,9 +2338,7 @@ s.testInstall(c, &release.OS{ID: "ubuntu"}, snapstate.Flags{}, snap.R(0)) } func (s *apiSuite) TestInstallOnDevModeDistro(c *check.C) { - flags := snapstate.Flags{} - flags.DevMode = true - s.testInstall(c, &release.OS{ID: "x-devmode-distro"}, flags, snap.R(0)) + s.testInstall(c, &release.OS{ID: "x-devmode-distro"}, snapstate.Flags{}, snap.R(0)) } func (s *apiSuite) TestInstallRevision(c *check.C) { s.testInstall(c, &release.OS{ID: "ubuntu"}, snapstate.Flags{}, snap.R(42)) diff -Nru snapd-2.22.6+16.10/daemon/daemon_test.go snapd-2.23.1+16.10/daemon/daemon_test.go --- snapd-2.22.6+16.10/daemon/daemon_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/daemon/daemon_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -225,6 +225,12 @@ func (s *daemonSuite) TestStartStop(c *check.C) { d := newTestDaemon(c) + st := d.overlord.State() + // mark as already seeded + st.Lock() + st.Set("seeded", true) + st.Unlock() + l, err := net.Listen("tcp", "127.0.0.1:0") c.Assert(err, check.IsNil) @@ -265,6 +271,12 @@ func (s *daemonSuite) TestRestartWiring(c *check.C) { d := newTestDaemon(c) + // mark as already seeded + st := d.overlord.State() + st.Lock() + st.Set("seeded", true) + st.Unlock() + l, err := net.Listen("tcp", "127.0.0.1:0") c.Assert(err, check.IsNil) diff -Nru snapd-2.22.6+16.10/daemon/response.go snapd-2.23.1+16.10/daemon/response.go --- snapd-2.22.6+16.10/daemon/response.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/daemon/response.go 2017-03-02 06:37:54.000000000 +0000 @@ -51,7 +51,7 @@ type resp struct { Status int `json:"status-code"` Type ResponseType `json:"type"` - Result interface{} `json:"result"` + Result interface{} `json:"result,omitempty"` *Meta } @@ -129,6 +129,8 @@ errorKindSnapNotInstalled = errorKind("snap-not-installed") errorKindSnapNoUpdateAvailable = errorKind("snap-no-update-available") + errorKindNotSnap = errorKind("snap-not-a-snap") + errorKindSnapNeedsMode = errorKind("snap-needs-mode") errorKindSnapNeedsClassicSystem = errorKind("snap-needs-classic-system") ) diff -Nru snapd-2.22.6+16.10/daemon/snap.go snapd-2.23.1+16.10/daemon/snap.go --- snapd-2.22.6+16.10/daemon/snap.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/daemon/snap.go 2017-03-08 13:28:17.000000000 +0000 @@ -156,7 +156,7 @@ type appJSON struct { Name string `json:"name"` Daemon string `json:"daemon"` - Aliases []string `json:"aliases"` + Aliases []string `json:"aliases,omitempty"` } // screenshotJSON contains the json for snap.ScreenshotInfo @@ -204,6 +204,7 @@ "private": localSnap.Private, "apps": apps, "broken": localSnap.Broken, + "contact": localSnap.Contact, } } @@ -242,6 +243,7 @@ "channel": remoteSnap.Channel, "private": remoteSnap.Private, "confinement": confinement, + "contact": remoteSnap.Contact, } if len(screenshots) > 0 { diff -Nru snapd-2.22.6+16.10/data/completion/snap snapd-2.23.1+16.10/data/completion/snap --- snapd-2.22.6+16.10/data/completion/snap 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/data/completion/snap 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,8 @@ # -*- sh -*- _complete() { + # TODO: add support for sourcing this function from the core snap. local cur prev words cword - _init_completion || return + _init_completion -n : || return local command if [[ ${#words[@]} -gt 2 ]]; then @@ -32,8 +33,14 @@ try) _filedir -d ;; + connect) + if [[ "$COMPREPLY" == *: ]]; then + compopt -o nospace + fi esac + __ltrim_colon_completions "$cur" + return 0 } diff -Nru snapd-2.22.6+16.10/data/selinux/COPYING snapd-2.23.1+16.10/data/selinux/COPYING --- snapd-2.22.6+16.10/data/selinux/COPYING 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/data/selinux/COPYING 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff -Nru snapd-2.22.6+16.10/data/selinux/INSTALL.md snapd-2.23.1+16.10/data/selinux/INSTALL.md --- snapd-2.22.6+16.10/data/selinux/INSTALL.md 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/data/selinux/INSTALL.md 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,32 @@ +# Installing the policy + + +## Building the policy module + +There's only a few requirements for building the policy module: + +* GNU make (Fedora / Debian: `make`) +* SELinux policy development package (Fedora: `selinux-policy-devel` / Debian: `selinux-policy-dev`) + +Install the packages as appropriate, then run `make`. + +For example, on Fedora: + +```bash +$ sudo dnf install make selinux-policy-devel +$ make +``` + +## Install the module to the system + +To install the module, run the following as root: + +```bash +$ semodule -i snappy.pp +``` + +When shipping this module in a distribution package, use the following command instead: + +```bash +$ semodule -X 200 -i snappy.pp +``` diff -Nru snapd-2.22.6+16.10/data/selinux/Makefile snapd-2.23.1+16.10/data/selinux/Makefile --- snapd-2.22.6+16.10/data/selinux/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/data/selinux/Makefile 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,35 @@ +# This file is part of snapd-selinux +# Copyright (C) 2016 Neal Gompa +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +TARGETS?= snappy +SHARE?=/usr/share +MODULES?=${TARGETS:=.pp.bz2} + +all: ${TARGETS:=.pp.bz2} + +%.pp.bz2: %.pp + @echo Compressing $^ -\ $@ + bzip2 -9 $^ + +%.pp: %.te + make -f ${SHARE}/selinux/devel/Makefile $@ + +clean: + rm -f *~ *.tc *.pp *.pp.bz2 + rm -rf tmp + diff -Nru snapd-2.22.6+16.10/data/selinux/README.md snapd-2.23.1+16.10/data/selinux/README.md --- snapd-2.22.6+16.10/data/selinux/README.md 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/data/selinux/README.md 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,25 @@ +# snapd-selinux + +This is the SELinux policy for Snapd and related Snappy components + +# Licensing + +As the work from this is derived from modules from [Fedora's SELinux policy +project](https://github.com/fedora-selinux/selinux-policy), it is licensed in +the same manner. + +Copyright (C) 2016 Neal Gompa + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. diff -Nru snapd-2.22.6+16.10/data/selinux/snappy.fc snapd-2.23.1+16.10/data/selinux/snappy.fc --- snapd-2.22.6+16.10/data/selinux/snappy.fc 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/data/selinux/snappy.fc 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,40 @@ +# This file is part of snapd-selinux +# Skeleton derived from Fedora selinux-policy, Copyright (C) 2016 Red Hat, Inc. +# Copyright (C) 2016 Neal Gompa +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +HOME_DIR/snap(/.*)? gen_context(system_u:object_r:snappy_home_t,s0) + + +/usr/bin/snap -- gen_context(system_u:object_r:snappy_exec_t,s0) + +ifdef(`distro_redhat',` +/usr/libexec/snapd/.* -- gen_context(system_u:object_r:snappy_exec_t,s0) +/etc/sysconfig/snapd -- gen_context(system_u:object_r:snappy_config_t,s0) +/usr/lib/systemd/system/snapd.* -- gen_context(system_u:object_r:snappy_unit_file_t,s0) +') + +ifdef(`distro_debian',` +/usr/lib/snapd/.* -- gen_context(system_u:object_r:snappy_exec_t,s0) +/etc/default/snapd -- gen_context(system_u:object_r:snappy_config_t,s0) +/lib/systemd/system/snapd.* -- gen_context(system_u:object_r:snappy_unit_file_t,s0) +') + +/var/run/snapd\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0) +/var/run/snapd-snap\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0) +/var/lib/snapd(/.*)? gen_context(system_u:object_r:snappy_var_lib_t,s0) +/var/snap(/.*)? gen_context(system_u:object_r:snappy_var_t,s0) diff -Nru snapd-2.22.6+16.10/data/selinux/snappy.if snapd-2.23.1+16.10/data/selinux/snappy.if --- snapd-2.22.6+16.10/data/selinux/snappy.if 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/data/selinux/snappy.if 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,273 @@ +# This file is part of snapd-selinux +# Skeleton derived from Fedora selinux-policy, Copyright (C) 2016 Red Hat, Inc. +# Copyright (C) 2016 Neal Gompa +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +######################################## +## +## Execute snapd in the snappy domain. +## +## +## +## Domain allowed to transition. +## +## +# +interface(`snappy_domtrans',` + gen_require(` + type snappy_t, snappy_exec_t; + ') + + corecmd_search_bin($1) + domtrans_pattern($1, snappy_exec_t, snappy_t) +') + +####################################### +## +## Execute snapd server in the snappy domain. +## +## +## +## Domain allowed to transition. +## +## +# +interface(`snappy_systemctl',` + gen_require(` + type snappy_t; + type snappy_unit_file_t; + ') + + systemd_exec_systemctl($1) + init_reload_services($1) + allow $1 snappy_unit_file_t:unix_stream_socket create_stream_socket_perms; + allow $1 snappy_unit_file_t:file read_file_perms; + allow $1 snappy_unit_file_t:service manage_service_perms; + + ps_process_pattern($1, snappy_t) +') + + +######################################## +## +## Permit the reading of snapd config files +## +## +## +## Domain allowed to access. +## +## +# +interface(`snappy_read_config',` + gen_require(` + type snappy_config_t; + ') + + files_search_etc($1) + allow $1 snappy_config_t:dir list_dir_perms; + allow $1 snappy_config_t:file read_file_perms; + allow $1 snappy_config_t:lnk_file read_lnk_file_perms; +') + + +######################################## +## +## Create snappy content in the user home directory +## with an correct label. +## +## +## +## Domain allowed access. +## +## +# +interface(`snappy_filetrans_home_content',` + + gen_require(` + type snappy_home_t; + ') + + userdom_user_home_dir_filetrans($1, snappy_home_t, dir, "snap") +') + + +######################################## +## +## Read snappy home directory content +## +## +## +## Domain allowed access. +## +## +# +interface(`snappy_read_user_home_files',` + gen_require(` + type snappy_home_t; + ') + + allow $1 snappy_home_t:dir list_dir_perms; + allow $1 snappy_home_t:file read_file_perms; + allow $1 snappy_home_t:lnk_file read_lnk_file_perms; + userdom_search_user_home_dirs($1) +') + +######################################## +## +## Write snappy home directory content +## +## +## +## Domain allowed access. +## +## +# +interface(`snappy_write_user_home_files',` + gen_require(` + type snappy_home_t; + ') + + write_files_pattern($1, snappy_home_t, snappy_home_t) + userdom_search_user_home_dirs($1) +') + +######################################## +## +## Dontaudit attempts to read/write snappy home directory content +## +## +## +## Domain to not audit. +## +## +# +interface(`snappy_dontaudit_rw_user_home_files',` + gen_require(` + type snappy_home_t; + ') + + dontaudit $1 snappy_home_t:file rw_inherited_file_perms; +') + +######################################## +## +## Dontaudit attempts to write snappy home directory content +## +## +## +## Domain to not audit. +## +## +# +interface(`snappy_dontaudit_manage_user_home_files',` + gen_require(` + type snappy_home_t; + ') + + dontaudit $1 snappy_home_t:dir manage_dir_perms; + dontaudit $1 snappy_home_t:file manage_file_perms; +') + +######################################## +## +## Execute snappy home directory content. +## +## +## +## Domain allowed access. +## +## +# +interface(`snappy_exec_user_home_files',` + gen_require(` + type snappy_home_t; + ') + + can_exec($1, snappy_home_t) +') + +######################################## +## +## Execmod snappy home directory content. +## +## +## +## Domain allowed access. +## +## +# +interface(`snappy_execmod_user_home_files',` + gen_require(` + type snappy_home_t; + ') + + allow $1 snappy_home_t:file execmod; +') + + +######################################## +## +## Connect to snapd over a unix stream socket. +## +## +## +## Domain allowed access. +## +## +# +interface(`snappy_stream_connect',` + gen_require(` + type snappy_t, snappy_var_run_t; + ') + + files_search_pids($1) + stream_connect_pattern($1, snappy_var_run_t, snappy_var_run_t, snappy_t) +') + +####################################### +## +## All of the rules required to +## administrate a snappy environment. +## +## +## +## Domain allowed access. +## +## +## +## +## Role allowed access. +## +## +## +# + +interface(`snappy_admin', + gen_require(` + type snappy_t, snappy_config_t; + type snappy_var_run_t; + ') + + allow $1 snappy_t:process signal_perms; + + ps_process_pattern($1, snappy_t); + + admin_pattern($1, snappy_config_t); + + files_list_pids($1, snappy_var_run_t); + admin_pattern($1, snappy_var_run_t); +') diff -Nru snapd-2.22.6+16.10/data/selinux/snappy.te snapd-2.23.1+16.10/data/selinux/snappy.te --- snapd-2.22.6+16.10/data/selinux/snappy.te 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/data/selinux/snappy.te 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,217 @@ +# This file is part of snapd-selinux +# Skeleton derived from Fedora selinux-policy, Copyright (C) 2016 Red Hat, Inc. +# Copyright (C) 2016 Neal Gompa +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +policy_module(snappy,0.0.12) + +######################################## +# +# Declarations +# + +type snappy_t; +type snappy_exec_t; +init_daemon_domain(snappy_t, snappy_exec_t) + +type snappy_config_t; +files_config_file(snappy_config_t) + +type snappy_home_t; +typealias snappy_home_t alias { user_snappy_home_t staff_snappy_home_t sysadm_snappy_home_t }; +typealias snappy_home_t alias { auditadm_snappy_home_t secadm_snappy_home_t }; +userdom_user_home_content(snappy_home_t) + +type snappy_var_t; +files_type(snappy_var_t) + +type snappy_var_lib_t; +files_type(snappy_var_lib_t) + +type snappy_var_run_t; +files_pid_file(snappy_var_run_t) + +type snappy_unit_file_t; +systemd_unit_file(snappy_unit_file_t) + +######################################## +# +# snappy local policy +# + +# For development purposes, snappy_t domain is to be marked permissive +permissive snappy_t; + +# Allow transitions from init_t to snappy for sockets +gen_require(` type init_t; type var_run_t; ') +filetrans_pattern(init_t, var_run_t, snappy_var_run_t, sock_file, "snapd.socket") +filetrans_pattern(init_t, var_run_t, snappy_var_run_t, sock_file, "snapd-snap.socket") + +# Allow init_t to read snappy data +allow init_t snappy_var_lib_t:dir read; + +# Allow snapd to read init socket +gen_require(` type init_t; ') +allow snappy_t init_t:file { getattr open read }; +allow snappy_t init_t:lnk_file read; +allow snappy_t init_t:unix_stream_socket connectto; + +# Allow snapd to read file contexts +gen_require(` type file_context_t; ') +allow snappy_t file_context_t:dir search; +allow snappy_t file_context_t:file { getattr open read }; + +# Allow snapd to read procfs +gen_require(` type proc_t; ') +allow snappy_t proc_t:file { getattr open read }; + +# Allow snapd to read sysfs +gen_require(` type sysfs_t; ') +allow snappy_t sysfs_t:dir read; +allow snappy_t sysfs_t:file { getattr setattr open read write }; +allow snappy_t sysfs_t:lnk_file read; + + +# Allow snapd to read SSL cert store +gen_require(` type cert_t; ') +allow snappy_t cert_t:dir search; +allow snappy_t cert_t:file { getattr open read }; +allow snappy_t cert_t:lnk_file read; + +# Allow snapd to read config files +read_files_pattern(snappy_t, snappy_config_t, snappy_config_t) + +# Allow snapd to manage snaps' homedir data +manage_dirs_pattern(snappy_t, snappy_home_t, snappy_home_t) +manage_files_pattern(snappy_t, snappy_home_t, snappy_home_t) +manage_lnk_files_pattern(snappy_t, snappy_home_t, snappy_home_t) +userdom_search_user_home_dirs(snappy_t) +userdom_user_home_dir_filetrans(snappy_t, snappy_home_t, dir, "snap") + +# Allow snapd to read DNS config +gen_require(` type net_conf_t; ') +allow snappy_t net_conf_t:file { getattr open read }; +allow snappy_t net_conf_t:lnk_file { read }; + +# When managed by NetworkManager, DNS config is in its rundata +gen_require(` type NetworkManager_var_run_t; ') +allow snappy_t NetworkManager_var_run_t:dir search; + +# Allow snapd to read sysctl files +gen_require(` type sysctl_net_t; ') +allow snappy_t sysctl_net_t:dir search; +allow snappy_t sysctl_net_t:file { open read }; + +# Allow snapd to manage D-Bus config files for snaps +gen_require(` type dbusd_etc_t; ') +allow snappy_t dbusd_etc_t:dir { getattr open read search }; +allow snappy_t dbusd_etc_t:file { getattr open read write create rename unlink }; +allow snappy_t dbusd_etc_t:lnk_file { read }; + +# Allow snapd to manage udev rules for snaps +gen_require(` type udev_rules_t; type udev_exec_t; ') +allow snappy_t udev_rules_t:dir { getattr open read write search add_name remove_name }; +allow snappy_t udev_rules_t:file { getattr open read write create rename unlink }; +allow snappy_t udev_exec_t:file { execute execute_no_trans getattr open read }; + +# Allow snapd to manipulate udev +gen_require(` type udev_t; type udev_var_run_t; ') +allow snappy_t udev_t:unix_stream_socket connectto; +allow snappy_t udev_var_run_t:file { getattr open read }; +allow snappy_t udev_var_run_t:sock_file { getattr open read write }; + +# Allow snapd to read/write systemd units and use systemctl for managing snaps +gen_require(` type systemd_unit_file_t; type systemd_systemctl_exec_t; ') +allow snappy_t systemd_unit_file_t:dir { search open read write add_name remove_name }; +allow snappy_t systemd_unit_file_t:file { getattr open read write create rename unlink }; +allow snappy_t systemd_systemctl_exec_t:file { execute execute_no_trans getattr open read }; + +# Allow snapd to mount snaps +gen_require(` type mount_exec_t; ') +allow snappy_t mount_exec_t:file { execute execute_no_trans getattr open read }; + +# Allow snapd to execute unsquashfs +gen_require(` type bin_t; ') +allow snappy_t bin_t:file { execute execute_no_trans }; + +# Allow snapd to get FUSE device attributes +gen_require(` type fuse_device_t; ') +allow snappy_t fuse_device_t:chr_file getattr; + +# Read l10n files? +miscfiles_read_localization(snappy_t) + +# Allow snapd to manage its socket files +manage_files_pattern(snappy_t, snappy_var_run_t, snappy_var_run_t) + +# Allow snapd to manage mounts +gen_require(` type fs_t; type mount_var_run_t; ') +allow snappy_t fs_t:filesystem { mount unmount }; +allow snappy_t mount_var_run_t:dir search; +allow snappy_t mount_var_run_t:file { getattr setattr open read write }; + +# Allow snapd to manage its persistent data +manage_dirs_pattern(snappy_t, snappy_var_lib_t, snappy_var_lib_t) +manage_files_pattern(snappy_t, snappy_var_lib_t, snappy_var_lib_t) +manage_lnk_files_pattern(snappy_t, snappy_var_lib_t, snappy_var_lib_t) +manage_dirs_pattern(snappy_t, snappy_var_t, snappy_var_t) +manage_files_pattern(snappy_t, snappy_var_t, snappy_var_t) +manage_lnk_files_pattern(snappy_t, snappy_var_t, snappy_var_t) + +# Grant snapd access to /tmp +gen_require(` type tmp_t; ') +allow snappy_t tmp_t:dir { getattr setattr add_name create read remove_name rmdir write }; +allow snappy_t tmp_t:file { getattr setattr create open unlink write }; + +# Until we can figure out how to apply the label to mounted snaps, +# we need to grant snapd access to "unlabeled files" +gen_require(` type unlabeled_t; ') +allow snappy_t unlabeled_t:dir { getattr search open read }; +allow snappy_t unlabeled_t:file { getattr open read }; + +logging_send_syslog_msg(snappy_t); + +allow snappy_t self:capability { sys_admin dac_override chown kill fowner fsetid mknod net_admin net_bind_service net_raw setfcap }; +allow snappy_t self:tun_socket relabelto; +allow snappy_t self:process { getcap signal_perms setrlimit setfscreate }; + +allow snappy_t self:fifo_file rw_fifo_file_perms; +allow snappy_t self:unix_stream_socket create_stream_socket_perms; +allow snappy_t self:tcp_socket create_stream_socket_perms; +allow snappy_t self:udp_socket create_stream_socket_perms; +allow snappy_t self:unix_dgram_socket create_socket_perms; +allow snappy_t self:capability2 block_suspend; + +# snapd needs to check for ipv6 support +gen_require(` type node_t; ') +allow snappy_t node_t:tcp_socket node_bind; + +corenet_all_recvfrom_unlabeled(snappy_t) +corenet_all_recvfrom_netlabel(snappy_t) +corenet_tcp_sendrecv_generic_if(snappy_t) +corenet_tcp_sendrecv_generic_node(snappy_t) + +corenet_tcp_sendrecv_http_port(snappy_t) +corenet_tcp_connect_http_port(snappy_t) +corenet_tcp_sendrecv_http_cache_port(snappy_t) +corenet_tcp_connect_http_cache_port(snappy_t) + +# snapd has its own internal DNS resolver +corenet_tcp_sendrecv_dns_port(snappy_t) +corenet_udp_sendrecv_dns_port(snappy_t) +corenet_tcp_connect_dns_port(snappy_t) +corenet_sendrecv_dns_client_packets(snappy_t) diff -Nru snapd-2.22.6+16.10/data/systemd/snapd.refresh.service snapd-2.23.1+16.10/data/systemd/snapd.refresh.service --- snapd-2.22.6+16.10/data/systemd/snapd.refresh.service 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/data/systemd/snapd.refresh.service 2017-03-06 13:33:50.000000000 +0000 @@ -8,4 +8,4 @@ [Service] Type=oneshot ExecStart=/usr/bin/snap refresh -Environment=SNAP_REFRESH_FROM_TIMER=1 +Environment=SNAP_REFRESH_FROM_EMERGENCY_TIMER=1 diff -Nru snapd-2.22.6+16.10/data/systemd/snapd.refresh.timer snapd-2.23.1+16.10/data/systemd/snapd.refresh.timer --- snapd-2.22.6+16.10/data/systemd/snapd.refresh.timer 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/data/systemd/snapd.refresh.timer 2017-03-06 13:33:50.000000000 +0000 @@ -2,9 +2,9 @@ Description=Timer to automatically refresh installed snaps [Timer] -# spread the requests gently -# https://bugs.launchpad.net/snappy/+bug/1537793 -OnCalendar=23,05,11,17:00 +# do a weekly refresh using the time to ensure that we can still +# fix any potential errors in the internal timer handling +OnCalendar=weekly RandomizedDelaySec=6h AccuracySec=10min Persistent=true diff -Nru snapd-2.22.6+16.10/debian/changelog snapd-2.23.1+16.10/debian/changelog --- snapd-2.22.6+16.10/debian/changelog 2017-02-22 22:34:23.000000000 +0000 +++ snapd-2.23.1+16.10/debian/changelog 2017-03-08 13:29:56.000000000 +0000 @@ -1,4 +1,244 @@ -snapd (2.22.6+16.10) yakkety; urgency=medium +snapd (2.23.1+16.10) yakkety; urgency=medium + + * New upstream release, LP: #1665608 + - packaging, tests: use "systemctl list-unit-files --full" + everywhere + - interfaces: fix default content attribute value + - tests: do not nuke the entire snapd.conf.d dir when changing + store settings + - hookstate: run the right "snap" command in the hookmanager + - snapstate: revert PR#2958, run configure hook again everywhere + + -- Michael Vogt Wed, 08 Mar 2017 14:29:56 +0100 + +snapd (2.23) xenial; urgency=medium + + * New upstream release, LP: #1665608 + - overlord: phase 2 with 2nd setup-profiles and hook done after + restart for core installation + - data: re-add snapd.refresh.{timer,service} with weekly schedule + - interfaces: allow 'getent' by default with some missing dbs to + various interfaces + - overlord/snapstate: drop forced devmode + - snapstate: disable running the configure hook on classic for the + core snap + - ifacestate: re-generate apparmor in InterfaceManager.initialize() + - daemon: DevModeDistro does not imply snapstate.Flags{DevMode:true} + - interfaces/bluez,network-manager: implement ConnectedSlot policy + - cmd: add helpers for mounting / unmounting + - snapstate: error in LinkSnap() if revision is unset + - release: add linuxmint 18 to the non-devmode distros + - cmd: fixes to run correctly on opensuse + - interfaces: consistently use 'const' instead of 'var' for security + policy + - interfaces: miscellaneous policy updates for unity7, udisks2 and + browser-support + - interfaces/apparmor: compensate for kernel behavior change + - many: only tweak core config if hook exists + - overlord/hookstate: don't report a run hook output error without + any context + - cmd/snap-update-ns: move test data and helpers to new module + - vet: fix vet error on mount test. + - tests: empty init (systemd) failover test + - cmd: add .indent.pro file to the tree + - interfaces: specs for apparmor, seccomp, udev + - wrappers/services: RemainAfterExit=yes for oneshot daemons w/ stop + cmds + - tests: several improvements to the nested suite + - tests: do not use core for "All snaps up to date" check + - cmd/snap-update-ns: add function for sorting mount entries + - httputil: copy some headers over redirects + - data/selinux: merge SELinux policy module + - kmod: added Specification for kmod security backend + - tests: failover test for rc.local crash + - debian/tests: map snapd deb pockets to core snap channels for + autopkgtest + - many: switch channels on refresh if needed + - interfaces/builtin: add /boot/uboot/config.txt access to core- + support + - release: assume higher version of supported distros will still + work + - cmd/snap-update-ns: add compare function for mount entries + - tests: enable docker test + - tests: bail out if core snap is not installed + - interfaces: use mount.Entry instead of string snippets. + - osutil: trivial tweaks to build ID support + - many: display kernel version in 'snap version' + - osutil: add package for reading Build-ID + - snap: error when `snap list foo` is run and no snap is installed + - cmd/snap-confine: don't crash if nvidia module is loaded but + drivers are not available + - tests: update listing test for latest core snap version update + - overlord/hookstate/ctlcmd: helper function for creating a deep + copy of interface attributes + - interfaces: add a linux framebuffer interface + - cmd/snap, store: change error messages to reflect latest UX doc + - interfaces: initial unity8 interface + - asserts: improved information about assertions format in the + Decode doc comment + - snapstate: ensure snapstate.CanAutoRefresh is nil in tests + - mkversion.sh: Add support for taking the version as a parameter + - interfaces: add an interface for use by thumbnailer + - cmd/snap-confine: ensure that hostfs is root owned. + - screen-inhibit-control: add methods for delaying screensavers + - overlord: optional device registration and gadget support on + classic + - overlord: make seeding work also on classic, optionally + - image,cmd/snap: refactoring and initial envvar support to use + stores needing auth + - tests: add libvirt interface spread test + - cmd/libsnap: add helper for dropping permissions + - interfaces: misc updates for network-control, firewall-control, + unity7 and default policy + - interfaces: allow recv* and send* by default, accept4 with accept + and other cleanups + - interfaces/builtin: add classic-support interface + - store: use xdelta3 from core if available and not on the regular + system + - snap: add contact: line in `snap info` + - interfaces/builtin: add network-setup-control which allows rw + access to netplan + - unity7: support missing signals and methods for status icons + - cmd: autoconf for RHEL + - cmd/snap-confine: look for PROCFS_SUPER_MAGIC + - dirs: use the right snap mount dir for the distribution + - many: differentiate between "distro" and "core" libexecdir + - cmd: don't reexec on RHEL family + - config: make helpers reusable + - snap-exec: support nested environment variables in environment + - release: add galliumos support + - interfaces/builtin: more path options for serial + - i18n: look into core snaps when checking for translations + - tests: nested image testing + - tests: add basic test for docker + - hookstate,ifacestate: support snapctl set/get slot and plug attrs + (step 3) + - cmd/snap: add shell completion to connect + - cmd: add functions to load/save fstab-like files + - snap run: create "current" symlink in user data dir + - cmd: autoconf for centos + - tests: add more debug if ubuntu-core-upgrade fails + - tests: increase service retries + - packaging/ubuntu-14.04: inform user how to extend PATH with + /snap/bin. + - cmd: add helpers for working with mount/umount commands + - overlord/snapstate: prepare for using snap-update-ns + - cmd: use per-snap mount profile to populate the mount namespace + - overlord/ifacestate: setup seccomp security on startup + - interface/seccomp: sort combined snippets + - release: don't force devmode on LinuxMint "serena" + - tests: filter ubuntu-core systems for authenticated find-private + test + - interfaces/builtin/core-support: Allow modifying logind + configuration from the core snap + - tests: fix "snap managed" output check and suppress output from + expect in the authenticated login tests + - interfaces: shutdown: also allow shutdown/reboot/suspend via + logind + - cmd/snap-confine-tests: reformat test to pass shellcheck + - cmd: add sc_is_debug_enabled + - interfaces/mount: add dedicated mount entry type + - interfaces/core-support: allow modifying systemd-timesyncd and + sysctl configuration + - snap: improve message after `snap refresh pkg1 pkg2` + - tests: improve snap-env test + - interfaces/io-ports-control: use /dev/port, not /dev/ports + - interfaces/mount-observe: add quotactl with arg filtering (LP: + #1626359) + - interfaces/mount: generate per-snap mount profile + - tests: add spread test for delta downloads + - daemon: show "$snapname (delta)" in progress when downloading + deltas + - cmd: use safer functions in sc_mount_opt2str + - asserts: introduce a variant of model assertions for classic + systems + - interfaces/core-support: allow modifying snap rsyslog + configuration + - interfaces: remove some syscalls already in the default policy + plus comment cleanups + - interfaces: miscellaneous updates for hardware-observe, kernel- + module-control, unity7 and default + - snap-confine: add the key for which hsearch_r fails + - snap: improve the error message for `snap try` + - tests: fix pattern and use MATCH in find-private + - tests: stop tying setting up staging store access to the setup of + the state tarball + - tests: add regression spread test for #1660941 + - interfaces/default: don't allow TIOCSTI ioctl + - interfaces: allow nice/setpriority to 0-19 values for calling + process by default + - tests: improve debug when the core transition test hangs + - tests: disable ubuntu-core->core transition on ppc64el (its just + too slow) + - snapstate: move refresh from a systemd timer to the internal + snapstate Ensure() + - tests/lib/fakestore/refresh: some more info when we fail to copy + asserts + - overlord/devicestate: backoff between retries if the server seems + to have refused the serial-request + - image: check kernel/gadget publisher vs model brand, warn on store + disconnected snaps + - vendor: move gettext.go back to github.com/ojii/gettext.go + - store: retry on 502 http response as well + - tests: increase snap-service kill-timeout + - store,osutil: use new osutil.ExecutableExists(exe) check to only + use deltas if xdelta3 is present + - cmd: fix autogen.sh on fedora + - overlord/devicemgr: fix test: setup account-key before using the + key for signing + - cmd: add /usr/local/* to PATH + - cmd: add sc_string_append + - asserts: support for correctly suggesting format 2 for snap- + declaration + - interfaces: port mount backend to new APIs, unify content of per + app/hook profiles + - overlord/devicestate: implement policy about gadget and kernel + matching the model + - interfaces: allow sched_setscheduler again by default + - debian: update breaks/replaces for snap-confine->snapd + - debian: move the snap-confine packaging into snapd + - 14.04/integrationtests: rely on upstart to restart ssh. + - store: enable download deltas on classic by default + - spread: add unit suite + - snapctl: add config in client to disable auth and use it in + snapctl + - overlord/ifacestate: register all security backends with the + repository + - overlord,tests: have enable/disable affect security profiles + - tests: install ubuntu-core from the same channel as core + - overlord: move configstate.Transaction into config package + - seccomp-support.c: add PF_* domains which can be used instead of + AF_* + - store: always log retry summary when SNAPD_DEBUG is set + - tests: parameterize kernel snap channel + - snapenv: do not append ":" to the SNAP_LIBRARY_PATH + - interfaces/builtin: refine the content interface rules using $SLOT + - asserts,interfaces/policy: add support for + $SLOT()/$PLUG()/$MISSING in *-attributes constraintsThis adds + support for $SLOT(arg), $PLUG(arg) and $MISSING attribute + constraints in plugs and slots rules in snap-declarations: + - cmd/snap-confine: add snap-confine command line parser module + - tests: remove (some) garbage files found by restore cleanup + analysis + - cmd: fix issues uncovered by valgrind + - tests: fix typo in systems name + - cmd: collect string utilities in one module, add missing tests + - cmd: rename mountinfo to sc_mountinfo + - tests: allow to install snapd debs from a ppa instead of building + them + - spread: remove state tar on project restore + + -- Michael Vogt Fri, 17 Feb 2017 12:21:42 +0100 + +snapd (2.22.7) xenial; urgency=medium + + * New upstream release: + - errtracker,overlord/snapstate: more info in errtracker reports + - interfaces/apparmor: compensate for kernel behavior change + + -- Michael Vogt Fri, 24 Feb 2017 19:24:11 +0100 + +snapd (2.22.6) xenial; urgency=medium * New upstream release, LP: #1667105 - overlord/ifacestate: don't fail if affected snap is gone diff -Nru snapd-2.22.6+16.10/debian/control snapd-2.23.1+16.10/debian/control --- snapd-2.22.6+16.10/debian/control 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/debian/control 2017-03-02 06:37:54.000000000 +0000 @@ -18,6 +18,7 @@ golang-any (>=2:1.6) | golang-1.6, indent, init-system-helpers, + libcap-dev, libapparmor-dev, libglib2.0-dev, libseccomp-dev, @@ -27,7 +28,8 @@ python3-docutils, python3-markdown, squashfs-tools, - udev + udev, + xfslibs-dev Standards-Version: 3.9.7 Homepage: https://github.com/snapcore/snapd Vcs-Browser: https://github.com/snapcore/snapd @@ -56,13 +58,12 @@ apparmor (>= 2.10.95-0ubuntu2.2), ca-certificates, gnupg1 | gnupg, - snap-confine (= ${binary:Version}), squashfs-tools, systemd, ${misc:Depends}, ${shlibs:Depends} -Replaces: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9) -Breaks: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9) +Replaces: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22) +Breaks: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22) Conflicts: snap (<< 2013-11-29-1ubuntu1) Built-Using: ${misc:Built-Using} Description: Tool to interact with Ubuntu Core Snappy. @@ -97,19 +98,15 @@ Package: snap-confine Architecture: any -Breaks: ubuntu-core-launcher (<< 2.22) -Replaces: ubuntu-core-launcher (<< 2.22) -Depends: apparmor (>= 2.10.95-0ubuntu2.2), ${misc:Depends}, ${shlibs:Depends} -Description: Support executable to apply confinement for snappy apps - This package contains an internal tool for applying confinement to snappy app. - The executable (snap-confine) is ran internally by snapd to apply confinement - to the started application process. The tool is written in C and carefully - reviewed to limit the attack surface on the security model of snapd. +Section: oldlibs +Depends: snapd (= ${binary:Version}), ${misc:Depends} +Description: Transitional package for snapd + This is a transitional dummy package. It can safely be removed. Package: ubuntu-core-launcher Architecture: any -Depends: snap-confine (= ${binary:Version}), ${misc:Depends} +Depends: snapd (= ${binary:Version}), ${misc:Depends} Section: oldlibs Pre-Depends: dpkg (>= 1.15.7.2) -Description: Transitional package for snap-confine +Description: Transitional package for snapd This is a transitional dummy package. It can safely be removed. diff -Nru snapd-2.22.6+16.10/debian/rules snapd-2.23.1+16.10/debian/rules --- snapd-2.22.6+16.10/debian/rules 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/debian/rules 2017-03-06 13:33:50.000000000 +0000 @@ -59,7 +59,7 @@ dh_fixperms -Xusr/lib/snapd/snap-confine override_dh_installdeb: - dh_apparmor --profile-name=usr.lib.snapd.snap-confine -psnap-confine + dh_apparmor --profile-name=usr.lib.snapd.snap-confine -psnapd dh_installdeb override_dh_clean: diff -Nru snapd-2.22.6+16.10/debian/snap-confine.install snapd-2.23.1+16.10/debian/snap-confine.install --- snapd-2.22.6+16.10/debian/snap-confine.install 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/debian/snap-confine.install 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ -etc/apparmor.d/usr.lib.snapd.snap-confine -lib/udev/rules.d/80-snappy-assign.rules -lib/udev/snappy-app-dev -usr/lib/snapd/snap-confine -usr/lib/snapd/snap-discard-ns -usr/lib/snapd/snap-update-ns -usr/share/man/man5/snap-confine.5 -usr/share/man/man5/snap-update-ns.5 -usr/share/man/man5/snap-discard-ns.5 -# for compatibility with ancient snap installs that wrote the shell based -# wrapper scripts instead of the modern symlinks -usr/bin/ubuntu-core-launcher diff -Nru snapd-2.22.6+16.10/debian/snapd.install snapd-2.23.1+16.10/debian/snapd.install --- snapd-2.22.6+16.10/debian/snapd.install 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/debian/snapd.install 2017-03-02 06:37:54.000000000 +0000 @@ -15,3 +15,17 @@ # snap/snapd version information data/info /usr/lib/snapd/ +# snap-confine stuff +etc/apparmor.d/usr.lib.snapd.snap-confine +lib/udev/rules.d/80-snappy-assign.rules +lib/udev/snappy-app-dev +usr/lib/snapd/snap-confine +usr/lib/snapd/snap-discard-ns +usr/lib/snapd/snap-update-ns +usr/share/man/man5/snap-confine.5 +usr/share/man/man5/snap-update-ns.5 +usr/share/man/man5/snap-discard-ns.5 +# for compatibility with ancient snap installs that wrote the shell based +# wrapper scripts instead of the modern symlinks +usr/bin/ubuntu-core-launcher + diff -Nru snapd-2.22.6+16.10/debian/snapd.postrm snapd-2.23.1+16.10/debian/snapd.postrm --- snapd-2.22.6+16.10/debian/snapd.postrm 2017-02-22 08:24:38.000000000 +0000 +++ snapd-2.23.1+16.10/debian/snapd.postrm 2017-03-08 13:28:21.000000000 +0000 @@ -11,8 +11,8 @@ } if [ "$1" = "purge" ]; then - mounts=$(systemctl list-unit-files | grep '^snap[-.].*\.mount' | cut -f1 -d ' ') - services=$(systemctl list-unit-files | grep '^snap[-.].*\.service' | cut -f1 -d ' ') + mounts=$(systemctl list-unit-files --full | grep '^snap[-.].*\.mount' | cut -f1 -d ' ') + services=$(systemctl list-unit-files --full | grep '^snap[-.].*\.service' | cut -f1 -d ' ') for unit in $services $mounts; do # ensure its really a snapp mount unit or systemd unit if ! grep -q 'What=/var/lib/snapd/snaps/' "/etc/systemd/system/$unit" && ! grep -q 'X-Snappy=yes' "/etc/systemd/system/$unit"; then diff -Nru snapd-2.22.6+16.10/debian/tests/integrationtests snapd-2.23.1+16.10/debian/tests/integrationtests --- snapd-2.22.6+16.10/debian/tests/integrationtests 2017-02-22 08:24:38.000000000 +0000 +++ snapd-2.23.1+16.10/debian/tests/integrationtests 2017-03-06 13:33:50.000000000 +0000 @@ -3,7 +3,7 @@ set -ex # required for the debian adt host -mkdir -p /etc/systemd/system/snapd.service.d/ +mkdir -p /etc/systemd/system/snapd.service.d/ if [ "$http_proxy" != "" ]; then cat <.*/\1 yes/' /etc/ssh/sshd_config systemctl reload sshd.service +# Map snapd deb package pockets to core snap channels. This is intended to cope +# with the autopkgtest execution when testing packages from the different pockets +if apt -qq list snapd | grep -q -- -proposed; then + export SPREAD_CORE_CHANNEL=candidate +elif apt -qq list snapd | grep -q -- -updates; then + export SPREAD_CORE_CHANNEL=stable +fi + # and now run spread against localhost . /etc/os-release export GOPATH=/tmp/go diff -Nru snapd-2.22.6+16.10/dirs/dirs.go snapd-2.23.1+16.10/dirs/dirs.go --- snapd-2.22.6+16.10/dirs/dirs.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/dirs/dirs.go 2017-03-06 13:33:50.000000000 +0000 @@ -24,6 +24,8 @@ "os" "path/filepath" "strings" + + "github.com/snapcore/snapd/release" ) // the various file paths @@ -65,7 +67,8 @@ ClassicDir string - LibExecDir string + DistroLibExecDir string + CoreLibExecDir string XdgRuntimeDirGlob string ) @@ -105,7 +108,13 @@ } GlobalRootDir = rootdir - SnapMountDir = filepath.Join(rootdir, "/snap") + switch release.ReleaseInfo.ID { + case "fedora", "centos", "rhel": + SnapMountDir = filepath.Join(rootdir, "/var/lib/snapd/snap") + default: + SnapMountDir = filepath.Join(rootdir, "/snap") + } + SnapDataDir = filepath.Join(rootdir, "/var/snap") SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/snap/") SnapAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "profiles") @@ -143,7 +152,14 @@ LocaleDir = filepath.Join(rootdir, "/usr/share/locale") ClassicDir = filepath.Join(rootdir, "/writable/classic") - LibExecDir = filepath.Join(rootdir, "/usr/lib/snapd") + switch release.ReleaseInfo.ID { + case "fedora", "centos", "rhel": + DistroLibExecDir = filepath.Join(rootdir, "/usr/libexec/snapd") + default: + DistroLibExecDir = filepath.Join(rootdir, "/usr/lib/snapd") + } + + CoreLibExecDir = filepath.Join(rootdir, "/usr/lib/snapd") XdgRuntimeDirGlob = filepath.Join(rootdir, "/run/user/*/") } diff -Nru snapd-2.22.6+16.10/errtracker/errtracker.go snapd-2.23.1+16.10/errtracker/errtracker.go --- snapd-2.22.6+16.10/errtracker/errtracker.go 2017-02-22 21:55:49.000000000 +0000 +++ snapd-2.23.1+16.10/errtracker/errtracker.go 2017-03-06 13:33:50.000000000 +0000 @@ -58,7 +58,7 @@ return fmt.Sprintf("%s %s", ID, release.ReleaseInfo.VersionID) } -func Report(snap, channel, errMsg string, extra map[string]string) (string, error) { +func Report(snap, errMsg, dupSig string, extra map[string]string) (string, error) { if CrashDbURLBase == "" { return "", nil } @@ -72,7 +72,7 @@ crashDbUrl := fmt.Sprintf("%s/%s", CrashDbURLBase, identifier) - hostSnapdPath := filepath.Join(dirs.LibExecDir, "snapd") + hostSnapdPath := filepath.Join(dirs.DistroLibExecDir, "snapd") coreSnapdPath := filepath.Join(dirs.SnapMountDir, "core/current/usr/lib/snapd/snapd") if mockedHostSnapd != "" { hostSnapdPath = mockedHostSnapd @@ -98,10 +98,9 @@ "CoreSnapdBuildID": coreBuildID, "Date": timeNow().Format(time.ANSIC), "Snap": snap, - "Channel": channel, "KernelVersion": release.KernelVersion(), "ErrorMessage": errMsg, - "DuplicateSignature": fmt.Sprintf("snap-install: %s", errMsg), + "DuplicateSignature": dupSig, } for k, v := range extra { // only set if empty diff -Nru snapd-2.22.6+16.10/errtracker/errtracker_test.go snapd-2.23.1+16.10/errtracker/errtracker_test.go --- snapd-2.22.6+16.10/errtracker/errtracker_test.go 2017-02-22 21:55:49.000000000 +0000 +++ snapd-2.23.1+16.10/errtracker/errtracker_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -95,7 +95,7 @@ "Channel": "beta", "KernelVersion": release.KernelVersion(), "ErrorMessage": "failed to do stuff", - "DuplicateSignature": "snap-install: failed to do stuff", + "DuplicateSignature": "[failed to do stuff]", "Architecture": arch.UbuntuArchitecture(), }) fmt.Fprintf(w, "c14388aa-f78d-11e6-8df0-fa163eaf9b83 OOPSID") @@ -117,13 +117,15 @@ restorer = errtracker.MockTimeNow(func() time.Time { return time.Date(2017, 2, 17, 9, 51, 0, 0, time.UTC) }) defer restorer() - id, err := errtracker.Report("some-snap", "beta", "failed to do stuff", nil) + id, err := errtracker.Report("some-snap", "failed to do stuff", "[failed to do stuff]", map[string]string{ + "Channel": "beta", + }) c.Check(err, IsNil) c.Check(id, Equals, "c14388aa-f78d-11e6-8df0-fa163eaf9b83 OOPSID") c.Check(n, Equals, 1) // run again, verify identifier is unchanged - id, err = errtracker.Report("some-other-snap", "edge", "failed to do more stuff", nil) + id, err = errtracker.Report("some-other-snap", "failed to do more stuff", "[failed to do more stuff]", nil) c.Check(err, IsNil) c.Check(id, Equals, "c14388aa-f78d-11e6-8df0-fa163eaf9b83 OOPSID") c.Check(n, Equals, 2) diff -Nru snapd-2.22.6+16.10/httputil/logger.go snapd-2.23.1+16.10/httputil/logger.go --- snapd-2.22.6+16.10/httputil/logger.go 2017-02-22 21:53:11.000000000 +0000 +++ snapd-2.23.1+16.10/httputil/logger.go 2017-03-06 13:33:50.000000000 +0000 @@ -105,18 +105,17 @@ Key: "SNAPD_DEBUG_HTTP", body: opts.MayLogBody, }, - Timeout: opts.Timeout, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - if len(via) > 10 { - return errors.New("stopped after 10 redirects") - } - // preserve some headers across redirects - // to the CDN - for _, header := range []string{"Range", "User-Agent"} { - v := via[0].Header.Get(header) - req.Header.Set(header, v) - } - return nil - }, + Timeout: opts.Timeout, + CheckRedirect: checkRedirect, + } +} + +func checkRedirect(req *http.Request, via []*http.Request) error { + if len(via) > 10 { + return errors.New("stopped after 10 redirects") } + // fixed in go 1.8 + fixupHeadersForRedirect(req, via) + + return nil } diff -Nru snapd-2.22.6+16.10/httputil/logger_test.go snapd-2.23.1+16.10/httputil/logger_test.go --- snapd-2.22.6+16.10/httputil/logger_test.go 2017-02-22 21:53:11.000000000 +0000 +++ snapd-2.23.1+16.10/httputil/logger_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -141,11 +141,17 @@ c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/") c.Check(r.Header.Get("User-Agent"), check.Equals, "fancy-agent") + c.Check(r.Header.Get("Range"), check.Equals, "42") + c.Check(r.Header.Get("Authorization"), check.Equals, "please") + c.Check(r.Header.Get("Cookie"), check.Equals, "chocolate chip") http.Redirect(w, r, r.URL.Path, 302) case 1: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/") c.Check(r.Header.Get("User-Agent"), check.Equals, "fancy-agent") + c.Check(r.Header.Get("Range"), check.Equals, "42") + c.Check(r.Header.Get("Authorization"), check.Equals, "") + c.Check(r.Header.Get("Cookie"), check.Equals, "") default: c.Fatalf("expected to get 1 requests, now on %d", n+1) } @@ -156,7 +162,13 @@ client := httputil.NewHTTPClient(nil) req, err := http.NewRequest("GET", server.URL, nil) c.Assert(err, check.IsNil) + // some headers that should be copied req.Header.Set("User-Agent", "fancy-agent") + req.Header.Set("Range", "42") + + // some headers that shouldn't + req.Header.Set("Authorization", "please") + req.Header.Set("Cookie", "chocolate chip") _, err = client.Do(req) c.Assert(err, check.IsNil) diff -Nru snapd-2.22.6+16.10/httputil/redirect17.go snapd-2.23.1+16.10/httputil/redirect17.go --- snapd-2.22.6+16.10/httputil/redirect17.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/httputil/redirect17.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,40 @@ +// +build !go1.8 + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package httputil + +import ( + "net/http" + "strings" +) + +func fixupHeadersForRedirect(req *http.Request, via []*http.Request) { + // preserve some headers across redirects (needed for the CDN) + // (this is done automatically, slightly more cleanly, from 1.8) + for k, v := range via[0].Header { + switch strings.ToLower(k) { + case "authorization", "www-authenticate", "cookie", "cookie2": + // Do not copy sensitive headers across + // redirects. For rationale for which headers and + // why, see https://golang.org/pkg/net/http/#Client + default: + req.Header[k] = v + } + } +} diff -Nru snapd-2.22.6+16.10/httputil/redirect18.go snapd-2.23.1+16.10/httputil/redirect18.go --- snapd-2.22.6+16.10/httputil/redirect18.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/httputil/redirect18.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,28 @@ +// +build go1.8 + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package httputil + +import ( + "net/http" +) + +func fixupHeadersForRedirect(req *http.Request, via []*http.Request) { + // no longer needed from 1.8 +} diff -Nru snapd-2.22.6+16.10/i18n/i18n.go snapd-2.23.1+16.10/i18n/i18n.go --- snapd-2.22.6+16.10/i18n/i18n.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/i18n/i18n.go 2017-03-02 06:37:54.000000000 +0000 @@ -27,12 +27,9 @@ "path/filepath" "strings" - /* this is actually "github.com/ojii/gettext.go", however because - https://github.com/ojii/gettext.go/pull/4 - is not merged as of this writtting we want to use this fork - */ - "github.com/mvo5/gettext.go" + "github.com/ojii/gettext.go" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" ) @@ -49,24 +46,31 @@ setLocale("") } -func langpackResolver(root string, locale string, domain string) string { - +func langpackResolver(baseRoot string, locale string, domain string) string { // first check for the real locale (e.g. de_DE) // then try to simplify the locale (e.g. de_DE -> de) locales := []string{locale, strings.SplitN(locale, "_", 2)[0]} for _, locale := range locales { r := filepath.Join(locale, "LC_MESSAGES", fmt.Sprintf("%s.mo", domain)) - // ubuntu uses /usr/lib/locale-langpack and patches the glibc gettext - // implementation - langpack := filepath.Join(root, "..", "locale-langpack", r) - if osutil.FileExists(langpack) { - return langpack + // look into the core snaps first for translations, + // then the main system + candidateDirs := []string{ + filepath.Join(dirs.SnapMountDir, "/core/current/", baseRoot), + baseRoot, } - - regular := filepath.Join(root, r) - if osutil.FileExists(regular) { - return regular + for _, root := range candidateDirs { + // ubuntu uses /usr/lib/locale-langpack and patches the glibc gettext + // implementation + langpack := filepath.Join(root, "..", "locale-langpack", r) + if osutil.FileExists(langpack) { + return langpack + } + + regular := filepath.Join(root, r) + if osutil.FileExists(regular) { + return regular + } } } diff -Nru snapd-2.22.6+16.10/i18n/i18n_test.go snapd-2.23.1+16.10/i18n/i18n_test.go --- snapd-2.22.6+16.10/i18n/i18n_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/i18n/i18n_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -27,6 +27,8 @@ "testing" . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" ) // Hook up check.v1 into the "go test" runner @@ -35,7 +37,7 @@ var mockLocalePo = []byte(` msgid "" msgstr "" -"Project-Id-Version: snappy\n" +"Project-Id-Version: snappy-test\n" "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n" "POT-Creation-Date: 2015-06-16 09:08+0200\n" "Language: en_DK\n" @@ -127,3 +129,34 @@ var Gtest = G c.Assert(Gtest("singular"), Equals, "singular") } + +func (s *i18nTestSuite) TestLangpackResolverFromLangpack(c *C) { + root := c.MkDir() + localeDir := filepath.Join(root, "/usr/share/locale") + err := os.MkdirAll(localeDir, 0755) + c.Assert(err, IsNil) + + d := filepath.Join(root, "/usr/share/locale-langpack") + makeMockTranslations(c, d) + bindTextDomain("snappy-test", localeDir) + setLocale("") + + // no G() to avoid adding the test string to snappy-pot + var Gtest = G + c.Assert(Gtest("singular"), Equals, "translated singular", Commentf("test with %q failed", d)) +} + +func (s *i18nTestSuite) TestLangpackResolverFromCore(c *C) { + origSnapMountDir := dirs.SnapMountDir + defer func() { dirs.SnapMountDir = origSnapMountDir }() + dirs.SnapMountDir = c.MkDir() + + d := filepath.Join(dirs.SnapMountDir, "/core/current/usr/share/locale") + makeMockTranslations(c, d) + bindTextDomain("snappy-test", "/usr/share/locale") + setLocale("") + + // no G() to avoid adding the test string to snappy-pot + var Gtest = G + c.Assert(Gtest("singular"), Equals, "translated singular", Commentf("test with %q failed", d)) +} diff -Nru snapd-2.22.6+16.10/image/export_test.go snapd-2.23.1+16.10/image/export_test.go --- snapd-2.22.6+16.10/image/export_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/image/export_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -19,6 +19,14 @@ package image +import ( + "github.com/snapcore/snapd/overlord/auth" +) + +func MockToolingStore(sto Store) *ToolingStore { + return &ToolingStore{sto: sto} +} + var ( LocalSnaps = localSnaps DecodeModelAssertion = decodeModelAssertion @@ -26,3 +34,7 @@ BootstrapToRootDir = bootstrapToRootDir InstallCloudConfig = installCloudConfig ) + +func (tsto *ToolingStore) User() *auth.UserState { + return tsto.user +} diff -Nru snapd-2.22.6+16.10/image/helpers.go snapd-2.23.1+16.10/image/helpers.go --- snapd-2.22.6+16.10/image/helpers.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/image/helpers.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2016 Canonical Ltd + * Copyright (C) 2014-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -22,6 +22,7 @@ // TODO: put these in appropriate package(s) once they are clarified a bit more import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -30,20 +31,13 @@ "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/store" "golang.org/x/net/context" ) -// DownloadOptions carries options for downloading snaps plus assertions. -type DownloadOptions struct { - TargetDir string - Channel string - DevMode bool - User *auth.UserState -} - // A Store can find metadata on snaps, download snaps and fetch assertions. type Store interface { SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) @@ -52,11 +46,79 @@ Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) } +// ToolingStore wraps access to the store for tools. +type ToolingStore struct { + sto Store + user *auth.UserState +} + +func newToolingStore(arch, storeID string) (*ToolingStore, error) { + cfg := store.DefaultConfig() + cfg.Architecture = arch + cfg.StoreID = storeID + var user *auth.UserState + if authFn := os.Getenv("UBUNTU_STORE_AUTH_DATA_FILENAME"); authFn != "" { + var err error + user, err = readAuthFile(authFn) + if err != nil { + return nil, err + } + } + sto := store.New(cfg, nil) + return &ToolingStore{ + sto: sto, + user: user, + }, nil +} + +type authData struct { + Macaroon string `json:"macaroon"` + Discharges []string `json:"discharges"` +} + +func readAuthFile(authFn string) (*auth.UserState, error) { + f, err := os.Open(authFn) + if err != nil { + return nil, fmt.Errorf("cannot open auth file %q: %v", authFn, err) + } + defer f.Close() + + var creds authData + dec := json.NewDecoder(f) + if err := dec.Decode(&creds); err != nil { + return nil, fmt.Errorf("cannot decode auth file %q: %v", authFn, err) + } + if creds.Macaroon == "" || len(creds.Discharges) == 0 { + return nil, fmt.Errorf("invalid auth file %q: missing fields", authFn) + } + return &auth.UserState{ + StoreMacaroon: creds.Macaroon, + StoreDischarges: creds.Discharges, + }, nil +} + +func NewToolingStoreFromModel(model *asserts.Model) (*ToolingStore, error) { + return newToolingStore(model.Architecture(), model.Store()) +} + +func NewToolingStore() (*ToolingStore, error) { + arch := os.Getenv("UBUNTU_STORE_ARCH") + storeID := os.Getenv("UBUNTU_STORE_ID") + return newToolingStore(arch, storeID) +} + +// DownloadOptions carries options for downloading snaps plus assertions. +type DownloadOptions struct { + TargetDir string + Channel string +} + // DownloadSnap downloads the snap with the given name and optionally revision using the provided store and options. It returns the final full path of the snap inside the opts.TargetDir and a snap.Info for the snap. -func DownloadSnap(sto Store, name string, revision snap.Revision, opts *DownloadOptions) (targetFn string, info *snap.Info, err error) { +func (tsto *ToolingStore) DownloadSnap(name string, revision snap.Revision, opts *DownloadOptions) (targetFn string, info *snap.Info, err error) { if opts == nil { opts = &DownloadOptions{} } + sto := tsto.sto targetDir := opts.TargetDir if targetDir == "" { @@ -72,7 +134,7 @@ Channel: opts.Channel, Revision: revision, } - snap, err := sto.SnapInfo(spec, opts.User) + snap, err := sto.SnapInfo(spec, tsto.user) if err != nil { return "", nil, fmt.Errorf("cannot find snap %q: %v", name, err) } @@ -81,17 +143,17 @@ targetFn = filepath.Join(targetDir, baseName) pb := progress.NewTextProgress() - if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, opts.User); err != nil { + if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, tsto.user); err != nil { return "", nil, err } return targetFn, snap, nil } -// StoreAssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them. -func StoreAssertionFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher { +// AssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them. +func (tsto *ToolingStore) AssertionFetcher(db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher { retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) { - return sto.Assertion(ref.Type, ref.PrimaryKey, dlOpts.User) + return tsto.sto.Assertion(ref.Type, ref.PrimaryKey, tsto.user) } save2 := func(a asserts.Assertion) error { // for checking @@ -108,16 +170,28 @@ } // FetchAndCheckSnapAssertions fetches and cross checks the snap assertions matching the given snap file using the provided asserts.Fetcher and assertion database. -func FetchAndCheckSnapAssertions(snapPath string, info *snap.Info, f asserts.Fetcher, db asserts.RODatabase) error { +func FetchAndCheckSnapAssertions(snapPath string, info *snap.Info, f asserts.Fetcher, db asserts.RODatabase) (*asserts.SnapDeclaration, error) { sha3_384, size, err := asserts.SnapFileSHA3_384(snapPath) if err != nil { - return err + return nil, err } + // this assumes series "16" if err := snapasserts.FetchSnapAssertions(f, sha3_384); err != nil { - return fmt.Errorf("cannot fetch snap signatures/assertions: %v", err) + return nil, fmt.Errorf("cannot fetch snap signatures/assertions: %v", err) } // cross checks - return snapasserts.CrossCheck(info.Name(), sha3_384, size, &info.SideInfo, db) + if err := snapasserts.CrossCheck(info.Name(), sha3_384, size, &info.SideInfo, db); err != nil { + return nil, err + } + + a, err := db.Find(asserts.SnapDeclarationType, map[string]string{ + "series": release.Series, + "snap-id": info.SnapID, + }) + if err != nil { + return nil, fmt.Errorf("internal error: lost snap declaration for %q: %v", info.Name(), err) + } + return a.(*asserts.SnapDeclaration), nil } diff -Nru snapd-2.22.6+16.10/image/image.go snapd-2.23.1+16.10/image/image.go --- snapd-2.22.6+16.10/image/image.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/image/image.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2016 Canonical Ltd + * Copyright (C) 2014-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -32,16 +32,17 @@ "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/partition" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/squashfs" - "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/strutil" ) var ( Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr ) type Options struct { @@ -115,18 +116,30 @@ return err } + // TODO: might make sense to support this later + if model.Classic() { + return fmt.Errorf("cannot prepare image of a classic model") + } + local, err := localSnaps(opts) if err != nil { return err } - sto := makeStore(model) + // FIXME: limitation until we can pass series parametrized much more + if model.Series() != release.Series { + return fmt.Errorf("model with series %q != %q unsupported", model.Series(), release.Series) + } + tsto, err := NewToolingStoreFromModel(model) + if err != nil { + return err + } - if err := downloadUnpackGadget(sto, model, opts, local); err != nil { + if err := downloadUnpackGadget(tsto, model, opts, local); err != nil { return err } - return bootstrapToRootDir(sto, model, opts, local) + return bootstrapToRootDir(tsto, model, opts, local) } // these are postponed, not implemented or abandoned, not finalized, @@ -159,7 +172,7 @@ return modela, nil } -func downloadUnpackGadget(sto Store, model *asserts.Model, opts *Options, local *localInfos) error { +func downloadUnpackGadget(tsto *ToolingStore, model *asserts.Model, opts *Options, local *localInfos) error { if err := os.MkdirAll(opts.GadgetUnpackDir, 0755); err != nil { return fmt.Errorf("cannot create gadget unpack dir %q: %s", opts.GadgetUnpackDir, err) } @@ -168,7 +181,7 @@ TargetDir: opts.GadgetUnpackDir, Channel: opts.Channel, } - snapFn, _, err := acquireSnap(sto, model.Gadget(), dlOpts, local) + snapFn, _, err := acquireSnap(tsto, model.Gadget(), dlOpts, local) if err != nil { return err } @@ -178,7 +191,7 @@ return snap.Unpack("*", opts.GadgetUnpackDir) } -func acquireSnap(sto Store, name string, dlOpts *DownloadOptions, local *localInfos) (downloadedSnap string, info *snap.Info, err error) { +func acquireSnap(tsto *ToolingStore, name string, dlOpts *DownloadOptions, local *localInfos) (downloadedSnap string, info *snap.Info, err error) { if info := local.Info(name); info != nil { // local snap to install (unasserted only for now) p := local.Path(name) @@ -188,7 +201,7 @@ } return dst, info, nil } - return DownloadSnap(sto, name, snap.R(0), dlOpts) + return tsto.DownloadSnap(name, snap.R(0), dlOpts) } type addingFetcher struct { @@ -196,13 +209,13 @@ addedRefs []*asserts.Ref } -func makeFetcher(sto Store, dlOpts *DownloadOptions, db *asserts.Database) *addingFetcher { +func makeFetcher(tsto *ToolingStore, dlOpts *DownloadOptions, db *asserts.Database) *addingFetcher { var f addingFetcher save := func(a asserts.Assertion) error { f.addedRefs = append(f.addedRefs, a.Ref()) return nil } - f.Fetcher = StoreAssertionFetcher(sto, dlOpts, db, save) + f.Fetcher = tsto.AssertionFetcher(db, save) return &f } @@ -228,7 +241,17 @@ const defaultCore = "core" -func bootstrapToRootDir(sto Store, model *asserts.Model, opts *Options, local *localInfos) error { +var trusted = sysdb.Trusted() + +func MockTrusted(mockTrusted []asserts.Assertion) (restore func()) { + prevTrusted := trusted + trusted = mockTrusted + return func() { + trusted = prevTrusted + } +} + +func bootstrapToRootDir(tsto *ToolingStore, model *asserts.Model, opts *Options, local *localInfos) error { // FIXME: try to avoid doing this if opts.RootDir != "" { dirs.SetRootDir(opts.RootDir) @@ -244,18 +267,18 @@ // a bit more API there, potential issues when crossing stores/series) db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ Backstore: asserts.NewMemoryBackstore(), - Trusted: sysdb.Trusted(), + Trusted: trusted, }) if err != nil { return err } - f := makeFetcher(sto, &DownloadOptions{}, db) + f := makeFetcher(tsto, &DownloadOptions{}, db) if err := f.Save(model); err != nil { if !osutil.GetenvBool("UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_MODEL") { return fmt.Errorf("cannot fetch and check prerequisites for the model assertion: %v", err) } else { - logger.Noticef("Cannot fetch and check prerequisites for the model assertion, it will not be copied into the image: %v", err) + fmt.Fprintf(Stderr, "WARNING: Cannot fetch and check prerequisites for the model assertion, it will not be copied into the image making it unusable (unless this is a test): %v\n", err) f.addedRefs = nil } } @@ -270,7 +293,6 @@ dlOpts := &DownloadOptions{ TargetDir: snapSeedDir, Channel: opts.Channel, - DevMode: false, // XXX: should this be true? } for _, d := range []string{snapSeedDir, assertSeedDir} { @@ -291,6 +313,7 @@ snaps = append(snaps, opts.Snaps...) seen := make(map[string]bool) + var locals []string downloadedSnapsInfo := map[string]*snap.Info{} var seedYaml snap.Seed for _, snapName := range snaps { @@ -306,24 +329,40 @@ fmt.Fprintf(Stdout, "Fetching %s\n", snapName) } - fn, info, err := acquireSnap(sto, name, dlOpts, local) + fn, info, err := acquireSnap(tsto, name, dlOpts, local) if err != nil { return err } seen[name] = true + typ := info.Type // if it comes from the store fetch the snap assertions too // TODO: support somehow including available assertions // also for local snaps if info.SnapID != "" { - err = FetchAndCheckSnapAssertions(fn, info, f, db) + snapDecl, err := FetchAndCheckSnapAssertions(fn, info, f, db) if err != nil { return err } + var kind string + switch typ { + case snap.TypeKernel: + kind = "kernel" + case snap.TypeGadget: + kind = "gadget" + } + if kind != "" { // kernel or gadget + // TODO: share helpers with devicestate if the policy becomes much more complicated + publisher := snapDecl.PublisherID() + if publisher != model.BrandID() && publisher != "canonical" { + return fmt.Errorf("cannot use %s %q published by %q for model by %q", kind, name, publisher, model.BrandID()) + } + } + } else { + locals = append(locals, name) } - typ := info.Type // kernel/os are required for booting if typ == snap.TypeKernel || typ == snap.TypeOS { dst := filepath.Join(dirs.SnapBlobDir, filepath.Base(fn)) @@ -342,10 +381,14 @@ Channel: info.Channel, File: filepath.Base(fn), DevMode: info.NeedsDevMode(), + Contact: info.Contact, // no assertions for this snap were put in the seed Unasserted: info.SnapID == "", }) } + if len(locals) > 0 { + fmt.Fprintf(Stderr, "WARNING: %s were installed from local snaps disconnected from a store and cannot be refreshed subsequently!\n", strutil.Quoted(locals)) + } for _, aRef := range f.addedRefs { var afn string @@ -458,11 +501,3 @@ dst := filepath.Join(targetDir, filepath.Base(info.MountFile())) return dst, osutil.CopyFile(snapPath, dst, 0) } - -func makeStore(model *asserts.Model) Store { - cfg := store.DefaultConfig() - cfg.Architecture = model.Architecture() - cfg.Series = model.Series() - cfg.StoreID = model.Store() - return store.New(cfg, nil) -} diff -Nru snapd-2.22.6+16.10/image/image_test.go snapd-2.23.1+16.10/image/image_test.go --- snapd-2.22.6+16.10/image/image_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/image/image_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2016 Canonical Ltd + * Copyright (C) 2014-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -34,7 +34,6 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/assertstest" - "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/boot/boottest" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/image" @@ -55,9 +54,11 @@ bootloader *boottest.MockBootloader stdout *bytes.Buffer + stderr *bytes.Buffer downloadedSnaps map[string]string storeSnapInfo map[string]*snap.Info + tsto *image.ToolingStore storeSigning *assertstest.StoreStack brandSigning *assertstest.SigningDB @@ -72,14 +73,17 @@ s.bootloader = boottest.NewMockBootloader("grub", c.MkDir()) partition.ForceBootloader(s.bootloader) - s.stdout = bytes.NewBuffer(nil) + s.stdout = &bytes.Buffer{} image.Stdout = s.stdout + s.stderr = &bytes.Buffer{} + image.Stderr = s.stderr s.downloadedSnaps = make(map[string]string) s.storeSnapInfo = make(map[string]*snap.Info) + s.tsto = image.MockToolingStore(s) rootPrivKey, _ := assertstest.GenerateKey(1024) storePrivKey, _ := assertstest.GenerateKey(752) - s.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey) + s.storeSigning = assertstest.NewStoreStack("canonical", rootPrivKey, storePrivKey) brandPrivKey, _ := assertstest.GenerateKey(752) s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey) @@ -106,15 +110,20 @@ }, nil, "") c.Assert(err, IsNil) s.model = model.(*asserts.Model) + + otherAcct := assertstest.NewAccount(s.storeSigning, "other", map[string]interface{}{ + "account-id": "other", + }, "") + s.storeSigning.Add(otherAcct) } -func (s *imageSuite) addSystemSnapAssertions(c *C, snapName string) { +func (s *imageSuite) addSystemSnapAssertions(c *C, snapName string, publisher string) { snapID := snapName + "-Id" decl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ "series": "16", "snap-id": snapID, "snap-name": snapName, - "publisher-id": "can0nical", + "publisher-id": publisher, "timestamp": time.Now().UTC().Format(time.RFC3339), }, nil, "") c.Assert(err, IsNil) @@ -129,7 +138,7 @@ "snap-size": fmt.Sprintf("%d", snapSize), "snap-id": snapID, "snap-revision": s.storeSnapInfo[snapName].Revision.String(), - "developer-id": "can0nical", + "developer-id": publisher, "timestamp": time.Now().UTC().Format(time.RFC3339), }, nil, "") c.Assert(err, IsNil) @@ -140,6 +149,7 @@ func (s *imageSuite) TearDownTest(c *C) { partition.ForceBootloader(nil) image.Stdout = os.Stdout + image.Stderr = os.Stderr } // interface for the store @@ -271,7 +281,7 @@ } func (s *imageSuite) TestMissingGadgetUnpackDir(c *C) { - err := image.DownloadUnpackGadget(s, s.model, &image.Options{}, nil) + err := image.DownloadUnpackGadget(s.tsto, s.model, &image.Options{}, nil) c.Assert(err, ErrorMatches, `cannot create gadget unpack dir "": mkdir : no such file or directory`) } @@ -300,7 +310,7 @@ local, err := image.LocalSnaps(opts) c.Assert(err, IsNil) - err = image.DownloadUnpackGadget(s, s.model, opts, local) + err = image.DownloadUnpackGadget(s.tsto, s.model, opts, local) c.Assert(err, IsNil) // verify the right data got unpacked @@ -315,7 +325,7 @@ } } -func (s *imageSuite) setupSnaps(c *C, gadgetUnpackDir string) { +func (s *imageSuite) setupSnaps(c *C, gadgetUnpackDir string, publishers map[string]string) { err := os.MkdirAll(gadgetUnpackDir, 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(filepath.Join(gadgetUnpackDir, "grub.conf"), nil, 0644) @@ -323,23 +333,24 @@ s.downloadedSnaps["pc"] = snaptest.MakeTestSnapWithFiles(c, packageGadget, [][]string{{"grub.cfg", "I'm a grub.cfg"}}) s.storeSnapInfo["pc"] = infoFromSnapYaml(c, packageGadget, snap.R(1)) - s.addSystemSnapAssertions(c, "pc") + s.addSystemSnapAssertions(c, "pc", publishers["pc"]) s.downloadedSnaps["pc-kernel"] = snaptest.MakeTestSnapWithFiles(c, packageKernel, nil) s.storeSnapInfo["pc-kernel"] = infoFromSnapYaml(c, packageKernel, snap.R(2)) - s.addSystemSnapAssertions(c, "pc-kernel") + s.addSystemSnapAssertions(c, "pc-kernel", publishers["pc-kernel"]) s.downloadedSnaps["core"] = snaptest.MakeTestSnapWithFiles(c, packageCore, nil) s.storeSnapInfo["core"] = infoFromSnapYaml(c, packageCore, snap.R(3)) - s.addSystemSnapAssertions(c, "core") + s.addSystemSnapAssertions(c, "core", "canonical") s.downloadedSnaps["required-snap1"] = snaptest.MakeTestSnapWithFiles(c, requiredSnap1, nil) s.storeSnapInfo["required-snap1"] = infoFromSnapYaml(c, requiredSnap1, snap.R(3)) - s.addSystemSnapAssertions(c, "required-snap1") + s.storeSnapInfo["required-snap1"].Contact = "foo@example.com" + s.addSystemSnapAssertions(c, "required-snap1", "other") } func (s *imageSuite) TestBootstrapToRootDir(c *C) { - restore := sysdb.InjectTrusted(s.storeSigning.Trusted) + restore := image.MockTrusted(s.storeSigning.Trusted) defer restore() rootdir := filepath.Join(c.MkDir(), "imageroot") @@ -347,7 +358,10 @@ // FIXME: bootstrapToRootDir needs an unpacked gadget yaml gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget") - s.setupSnaps(c, gadgetUnpackDir) + s.setupSnaps(c, gadgetUnpackDir, map[string]string{ + "pc": "canonical", + "pc-kernel": "canonical", + }) // mock the mount cmds (for the extract kernel assets stuff) c1 := testutil.MockCommand(c, "mount", "") @@ -362,7 +376,7 @@ local, err := image.LocalSnaps(opts) c.Assert(err, IsNil) - err = image.BootstrapToRootDir(s, s.model, opts, local) + err = image.BootstrapToRootDir(s.tsto, s.model, opts, local) c.Assert(err, IsNil) // check seed yaml @@ -384,6 +398,8 @@ File: fn, }) } + c.Check(seed.Snaps[3].Name, Equals, "required-snap1") + c.Check(seed.Snaps[3].Contact, Equals, "foo@example.com") storeAccountKey := s.storeSigning.StoreAccountKey("") brandPubKey, err := s.brandSigning.PublicKey("") @@ -417,10 +433,12 @@ c.Assert(err, IsNil) c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap") c.Check(m["snap_core"], Equals, "core_3.snap") + + c.Check(s.stderr.String(), Equals, "") } -func (s *imageSuite) TestBootstrapToRootDirLocalCore(c *C) { - restore := sysdb.InjectTrusted(s.storeSigning.Trusted) +func (s *imageSuite) TestBootstrapToRootDirLocalCoreBrandKernel(c *C) { + restore := image.MockTrusted(s.storeSigning.Trusted) defer restore() rootdir := filepath.Join(c.MkDir(), "imageroot") @@ -428,7 +446,10 @@ // FIXME: bootstrapToRootDir needs an unpacked gadget yaml gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget") - s.setupSnaps(c, gadgetUnpackDir) + s.setupSnaps(c, gadgetUnpackDir, map[string]string{ + "pc": "canonical", + "pc-kernel": "my-brand", + }) // mock the mount cmds (for the extract kernel assets stuff) c1 := testutil.MockCommand(c, "mount", "") @@ -447,7 +468,7 @@ local, err := image.LocalSnaps(opts) c.Assert(err, IsNil) - err = image.BootstrapToRootDir(s, s.model, opts, local) + err = image.BootstrapToRootDir(s.tsto, s.model, opts, local) c.Assert(err, IsNil) // check seed yaml @@ -532,10 +553,12 @@ // check that cloud-init is setup correctly c.Check(osutil.FileExists(filepath.Join(rootdir, "etc/cloud/cloud-init.disabled")), Equals, true) + + c.Check(s.stderr.String(), Equals, "WARNING: \"core\", \"required-snap1\" were installed from local snaps disconnected from a store and cannot be refreshed subsequently!\n") } func (s *imageSuite) TestBootstrapToRootDirDevmodeSnap(c *C) { - restore := sysdb.InjectTrusted(s.storeSigning.Trusted) + restore := image.MockTrusted(s.storeSigning.Trusted) defer restore() rootdir := filepath.Join(c.MkDir(), "imageroot") @@ -548,7 +571,10 @@ err = ioutil.WriteFile(filepath.Join(gadgetUnpackDir, "grub.conf"), nil, 0644) c.Assert(err, IsNil) - s.setupSnaps(c, gadgetUnpackDir) + s.setupSnaps(c, gadgetUnpackDir, map[string]string{ + "pc": "canonical", + "pc-kernel": "canonical", + }) s.downloadedSnaps["devmode-snap"] = snaptest.MakeTestSnapWithFiles(c, devmodeSnap, nil) s.storeSnapInfo["devmode-snap"] = infoFromSnapYaml(c, devmodeSnap, snap.R(0)) @@ -568,7 +594,7 @@ local, err := image.LocalSnaps(opts) c.Assert(err, IsNil) - err = image.BootstrapToRootDir(s, s.model, opts, local) + err = image.BootstrapToRootDir(s.tsto, s.model, opts, local) c.Assert(err, IsNil) // check seed yaml @@ -598,6 +624,37 @@ }) } +func (s *imageSuite) TestBootstrapToRootDirKernelPublisherMismatch(c *C) { + restore := image.MockTrusted(s.storeSigning.Trusted) + defer restore() + + rootdir := filepath.Join(c.MkDir(), "imageroot") + + // FIXME: bootstrapToRootDir needs an unpacked gadget yaml + gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget") + + s.setupSnaps(c, gadgetUnpackDir, map[string]string{ + "pc": "canonical", + "pc-kernel": "other", + }) + + // mock the mount cmds (for the extract kernel assets stuff) + c1 := testutil.MockCommand(c, "mount", "") + defer c1.Restore() + c2 := testutil.MockCommand(c, "umount", "") + defer c2.Restore() + + opts := &image.Options{ + RootDir: rootdir, + GadgetUnpackDir: gadgetUnpackDir, + } + local, err := image.LocalSnaps(opts) + c.Assert(err, IsNil) + + err = image.BootstrapToRootDir(s.tsto, s.model, opts, local) + c.Assert(err, ErrorMatches, `cannot use kernel "pc-kernel" published by "other" for model by "my-brand"`) +} + func (s *imageSuite) TestInstallCloudConfigNoConfig(c *C) { targetDir := c.MkDir() emptyGadgetDir := c.MkDir() @@ -623,3 +680,22 @@ c.Assert(err, IsNil) c.Check(content, DeepEquals, canary) } + +func (s *imageSuite) TestNewToolingStoreWithAuth(c *C) { + tmpdir := c.MkDir() + authFn := filepath.Join(tmpdir, "auth.json") + err := ioutil.WriteFile(authFn, []byte(`{ +"macaroon": "MACAROON", +"discharges": ["DISCHARGE"] +}`), 0600) + c.Assert(err, IsNil) + + os.Setenv("UBUNTU_STORE_AUTH_DATA_FILENAME", authFn) + defer os.Unsetenv("UBUNTU_STORE_AUTH_DATA_FILENAME") + + tsto, err := image.NewToolingStore() + c.Assert(err, IsNil) + user := tsto.User() + c.Check(user.StoreMacaroon, Equals, "MACAROON") + c.Check(user.StoreDischarges, DeepEquals, []string{"DISCHARGE"}) +} diff -Nru snapd-2.22.6+16.10/interfaces/apparmor/backend_test.go snapd-2.23.1+16.10/interfaces/apparmor/backend_test.go --- snapd-2.22.6+16.10/interfaces/apparmor/backend_test.go 2017-02-22 08:24:38.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/apparmor/backend_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -352,6 +352,9 @@ # Read-only access to the core snap. @{INSTALL_DIR}/core/** r, + # Read only access to the core snap to load libc from. + # This is related to LP: #1666897 + @{INSTALL_DIR}/core/*/{,usr/}lib/@{multiarch}/{,**/}lib*.so* m, snippet } diff -Nru snapd-2.22.6+16.10/interfaces/apparmor/spec.go snapd-2.23.1+16.10/interfaces/apparmor/spec.go --- snapd-2.22.6+16.10/interfaces/apparmor/spec.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/apparmor/spec.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,115 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package apparmor + +import ( + "github.com/snapcore/snapd/interfaces" +) + +// Specification assists in collecting apparmor entries associated with an interface. +type Specification struct { + // snippets are indexed by security tag. + snippets map[string][][]byte + securityTags []string +} + +// AddSnippet adds a new apparmor snippet. +func (spec *Specification) AddSnippet(snippet []byte) error { + if len(spec.securityTags) == 0 { + return nil + } + if spec.snippets == nil { + spec.snippets = make(map[string][][]byte) + } + for _, tag := range spec.securityTags { + spec.snippets[tag] = append(spec.snippets[tag], snippet) + } + + return nil +} + +// Snippets returns a deep copy of all the added snippets. +func (spec *Specification) Snippets() map[string][][]byte { + result := make(map[string][][]byte, len(spec.snippets)) + for k, v := range spec.snippets { + vCopy := make([][]byte, 0, len(v)) + for _, vElem := range v { + vElemCopy := make([]byte, len(vElem)) + copy(vElemCopy, vElem) + vCopy = append(vCopy, vElemCopy) + } + result[k] = vCopy + } + return result +} + +// Implementation of methods required by interfaces.Specification + +// AddConnectedPlug records apparmor-specific side-effects of having a connected plug. +func (spec *Specification) AddConnectedPlug(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + AppArmorConnectedPlug(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + spec.securityTags = plug.SecurityTags() + defer func() { spec.securityTags = nil }() + return iface.AppArmorConnectedPlug(spec, plug, slot) + } + return nil +} + +// AddConnectedSlot records mount-specific side-effects of having a connected slot. +func (spec *Specification) AddConnectedSlot(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + AppArmorConnectedSlot(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + spec.securityTags = slot.SecurityTags() + defer func() { spec.securityTags = nil }() + return iface.AppArmorConnectedSlot(spec, plug, slot) + } + return nil +} + +// AddPermanentPlug records mount-specific side-effects of having a plug. +func (spec *Specification) AddPermanentPlug(iface interfaces.Interface, plug *interfaces.Plug) error { + type definer interface { + AppArmorPermanentPlug(spec *Specification, plug *interfaces.Plug) error + } + if iface, ok := iface.(definer); ok { + spec.securityTags = plug.SecurityTags() + defer func() { spec.securityTags = nil }() + return iface.AppArmorPermanentPlug(spec, plug) + } + return nil +} + +// AddPermanentSlot records mount-specific side-effects of having a slot. +func (spec *Specification) AddPermanentSlot(iface interfaces.Interface, slot *interfaces.Slot) error { + type definer interface { + AppArmorPermanentSlot(spec *Specification, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + spec.securityTags = slot.SecurityTags() + defer func() { spec.securityTags = nil }() + return iface.AppArmorPermanentSlot(spec, slot) + } + return nil +} diff -Nru snapd-2.22.6+16.10/interfaces/apparmor/spec_test.go snapd-2.23.1+16.10/interfaces/apparmor/spec_test.go --- snapd-2.22.6+16.10/interfaces/apparmor/spec_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/apparmor/spec_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,97 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package apparmor_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/snap" +) + +type specSuite struct { + iface *ifacetest.TestInterface + spec *apparmor.Specification + plug *interfaces.Plug + slot *interfaces.Slot +} + +var _ = Suite(&specSuite{ + iface: &ifacetest.TestInterface{ + InterfaceName: "test", + AppArmorConnectedPlugCallback: func(spec *apparmor.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddSnippet([]byte("connected-plug")) + }, + AppArmorConnectedSlotCallback: func(spec *apparmor.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddSnippet([]byte("connected-slot")) + }, + AppArmorPermanentPlugCallback: func(spec *apparmor.Specification, plug *interfaces.Plug) error { + return spec.AddSnippet([]byte("permanent-plug")) + }, + AppArmorPermanentSlotCallback: func(spec *apparmor.Specification, slot *interfaces.Slot) error { + return spec.AddSnippet([]byte("permanent-slot")) + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "snap1"}, + Name: "name", + Interface: "test", + Apps: map[string]*snap.AppInfo{ + "app1": { + Snap: &snap.Info{ + SuggestedName: "snap1", + }, + Name: "app1"}}, + }, + }, + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "snap2"}, + Name: "name", + Interface: "test", + Apps: map[string]*snap.AppInfo{ + "app2": { + Snap: &snap.Info{ + SuggestedName: "snap2", + }, + Name: "app2"}}, + }, + }, +}) + +func (s *specSuite) SetUpTest(c *C) { + s.spec = &apparmor.Specification{} +} + +// The spec.Specification can be used through the interfaces.Specification interface +func (s *specSuite) TestSpecificationIface(c *C) { + var r interfaces.Specification = s.spec + c.Assert(r.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddConnectedSlot(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddPermanentPlug(s.iface, s.plug), IsNil) + c.Assert(r.AddPermanentSlot(s.iface, s.slot), IsNil) + c.Assert(s.spec.Snippets(), DeepEquals, map[string][][]byte{ + "snap.snap1.app1": {[]byte("connected-plug"), []byte("permanent-plug")}, + "snap.snap2.app2": {[]byte("connected-slot"), []byte("permanent-slot")}, + }) +} diff -Nru snapd-2.22.6+16.10/interfaces/apparmor/template.go snapd-2.23.1+16.10/interfaces/apparmor/template.go --- snapd-2.22.6+16.10/interfaces/apparmor/template.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/apparmor/template.go 2017-03-06 13:33:50.000000000 +0000 @@ -121,6 +121,7 @@ /{,usr/}bin/find ixr, /{,usr/}bin/flock ixr, /{,usr/}bin/fmt ixr, + /{,usr/}bin/getent ixr, /{,usr/}bin/getopt ixr, /{,usr/}bin/groups ixr, /{,usr/}bin/gzip ixr, @@ -143,6 +144,7 @@ /{,usr/}bin/mktemp ixr, /{,usr/}bin/more ixr, /{,usr/}bin/mv ixr, + /{,usr/}bin/nice ixr, /{,usr/}bin/openssl ixr, # may cause harmless capability block_suspend denial /{,usr/}bin/pgrep ixr, /{,usr/}bin/printenv ixr, @@ -257,6 +259,7 @@ @{PROC}/version_signature r, /etc/{,writable/}hostname r, /etc/{,writable/}localtime r, + /etc/{,writable/}mailname r, /etc/{,writable/}timezone r, @{PROC}/@{pid}/io r, owner @{PROC}/@{pid}/limits r, @@ -276,6 +279,7 @@ @{PROC}/sys/fs/file-max r, @{PROC}/sys/kernel/pid_max r, @{PROC}/sys/kernel/random/uuid r, + @{PROC}/sys/kernel/random/boot_id r, /sys/devices/virtual/tty/{console,tty*}/active r, /{,usr/}lib/ r, @@ -340,13 +344,18 @@ /{dev,run}/shm/sem.snap.@{SNAP_NAME}.* rwk, # Snap-specific XDG_RUNTIME_DIR that is based on the UID of the user - owner /{dev,run}/user/[0-9]*/snap.@{SNAP_NAME}/ rw, - owner /{dev,run}/user/[0-9]*/snap.@{SNAP_NAME}/** mrwklix, + owner /run/user/[0-9]*/snap.@{SNAP_NAME}/ rw, + owner /run/user/[0-9]*/snap.@{SNAP_NAME}/** mrwklix, # Allow apps from the same package to communicate with each other via an # abstract or anonymous socket unix peer=(label=snap.@{SNAP_NAME}.*), + # Allow apps from the same package to communicate with each other via DBus. + # Note: this does not grant access to the DBus sockets of well known buses + # (will still need to use an appropriate interface for that). + dbus (receive, send) peer=(label=snap.@{SNAP_NAME}.*), + # Allow apps from the same package to signal each other via signals signal peer=snap.@{SNAP_NAME}.*, @@ -426,4 +435,7 @@ var classicJailmodeSnippet = []byte(` # Read-only access to the core snap. @{INSTALL_DIR}/core/** r, + # Read only access to the core snap to load libc from. + # This is related to LP: #1666897 + @{INSTALL_DIR}/core/*/{,usr/}lib/@{multiarch}/{,**/}lib*.so* m, `) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/account_control.go snapd-2.23.1+16.10/interfaces/builtin/account_control.go --- snapd-2.22.6+16.10/interfaces/builtin/account_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/account_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -53,9 +53,6 @@ // Needed because useradd uses a netlink socket const accountControlConnectedPlugSecComp = ` -sendto -recvfrom - # useradd requires chowning to 'shadow' # TODO: dynamically determine the shadow gid to support alternate cores fchown - 0 42 diff -Nru snapd-2.22.6+16.10/interfaces/builtin/account_control_test.go snapd-2.23.1+16.10/interfaces/builtin/account_control_test.go --- snapd-2.22.6+16.10/interfaces/builtin/account_control_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/account_control_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -88,5 +88,5 @@ snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) c.Assert(err, IsNil) - c.Check(string(snippet), testutil.Contains, "\nsendto\nrecvfrom\n") + c.Check(string(snippet), testutil.Contains, "\nfchown - 0 42\n") } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/all.go snapd-2.23.1+16.10/interfaces/builtin/all.go --- snapd-2.22.6+16.10/interfaces/builtin/all.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/all.go 2017-03-08 13:28:17.000000000 +0000 @@ -27,10 +27,12 @@ &BluezInterface{}, &BoolFileInterface{}, &BrowserSupportInterface{}, + NewClassicSupportInterface(), &ContentInterface{}, &DbusInterface{}, &DockerInterface{}, &DockerSupportInterface{}, + &FramebufferInterface{}, &FwupdInterface{}, &GpioInterface{}, &HidrawInterface{}, @@ -51,9 +53,11 @@ &PppInterface{}, &PulseAudioInterface{}, &SerialPortInterface{}, + &ThumbnailerInterface{}, &TimeControlInterface{}, &UDisks2Interface{}, &UbuntuDownloadManagerInterface{}, + &Unity8Interface{}, &UpowerObserveInterface{}, &UhidInterface{}, NewAccountControlInterface(), @@ -78,6 +82,7 @@ NewNetworkControlInterface(), NewNetworkInterface(), NewNetworkObserveInterface(), + NewNetworkSetupControlInterface(), NewNetworkSetupObserveInterface(), NewOpenglInterface(), NewOpenvSwitchInterface(), diff -Nru snapd-2.22.6+16.10/interfaces/builtin/all_test.go snapd-2.23.1+16.10/interfaces/builtin/all_test.go --- snapd-2.22.6+16.10/interfaces/builtin/all_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/all_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -54,9 +54,11 @@ c.Check(all, Contains, &builtin.PhysicalMemoryObserveInterface{}) c.Check(all, Contains, &builtin.PulseAudioInterface{}) c.Check(all, Contains, &builtin.SerialPortInterface{}) + c.Check(all, Contains, &builtin.ThumbnailerInterface{}) c.Check(all, Contains, &builtin.TimeControlInterface{}) c.Check(all, Contains, &builtin.UDisks2Interface{}) c.Check(all, Contains, &builtin.UbuntuDownloadManagerInterface{}) + c.Check(all, Contains, &builtin.Unity8Interface{}) c.Check(all, Contains, &builtin.UpowerObserveInterface{}) c.Check(all, Contains, &builtin.UhidInterface{}) c.Check(all, DeepContains, builtin.NewAccountControlInterface()) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/avahi_observe.go snapd-2.23.1+16.10/interfaces/builtin/avahi_observe.go --- snapd-2.22.6+16.10/interfaces/builtin/avahi_observe.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/avahi_observe.go 2017-03-02 06:37:54.000000000 +0000 @@ -105,25 +105,10 @@ peer=(label=unconfined), ` -const avahiObserveConnectedPlugSecComp = ` -# Description: allows domain browsing, service browsing and service resolving - -# dbus -connect -getsockname -recvfrom -recvmsg -send -sendto -sendmsg -socket -` - func NewAvahiObserveInterface() interfaces.Interface { return &commonInterface{ name: "avahi-observe", connectedPlugAppArmor: avahiObserveConnectedPlugAppArmor, - connectedPlugSecComp: avahiObserveConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/avahi_observe_test.go snapd-2.23.1+16.10/interfaces/builtin/avahi_observe_test.go --- snapd-2.22.6+16.10/interfaces/builtin/avahi_observe_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/avahi_observe_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -85,9 +85,4 @@ c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) c.Check(string(snippet), testutil.Contains, "name=org.freedesktop.Avahi") - - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - c.Check(string(snippet), testutil.Contains, "sendto") } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/basedeclaration.go snapd-2.23.1+16.10/interfaces/builtin/basedeclaration.go --- snapd-2.22.6+16.10/interfaces/builtin/basedeclaration.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/basedeclaration.go 2017-03-08 13:28:17.000000000 +0000 @@ -138,6 +138,9 @@ series: 16 revision: 0 plugs: + classic-support: + allow-installation: false + deny-auto-connection: true core-support: allow-installation: plug-snap-type: @@ -154,6 +157,8 @@ snapd-control: allow-installation: false deny-auto-connection: true + unity8: + allow-installation: false slots: account-control: allow-installation: @@ -199,14 +204,24 @@ slot-snap-type: - core deny-auto-connection: true + classic-support: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true content: allow-installation: slot-snap-type: - app - gadget + allow-connection: + plug-attributes: + content: $SLOT(content) allow-auto-connection: plug-publisher-id: - $SLOT_PUBLISHER_ID + plug-attributes: + content: $SLOT(content) core-support: allow-installation: slot-snap-type: @@ -309,6 +324,11 @@ slot-snap-type: - core deny-auto-connection: true + framebuffer: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true locale-control: allow-installation: slot-snap-type: @@ -392,6 +412,11 @@ slot-snap-type: - core deny-auto-connection: true + network-setup-control: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true network-setup-observe: allow-installation: slot-snap-type: @@ -490,6 +515,12 @@ slot-snap-type: - core deny-auto-connection: true + thumbnailer: + allow-installation: + slot-snap-type: + - app + deny-auto-connection: true + deny-connection: true time-control: allow-installation: slot-snap-type: @@ -525,6 +556,11 @@ allow-installation: slot-snap-type: - core + unity8: + allow-installation: + slot-snap-type: + - app + deny-connection: true unity8-calendar: allow-installation: slot-snap-type: diff -Nru snapd-2.22.6+16.10/interfaces/builtin/basedeclaration_test.go snapd-2.23.1+16.10/interfaces/builtin/basedeclaration_test.go --- snapd-2.22.6+16.10/interfaces/builtin/basedeclaration_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/basedeclaration_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -147,6 +147,7 @@ "pulseaudio": true, "screen-inhibit-control": true, "unity7": true, + "unity8": true, "ubuntu-download-manager": true, "upower-observe": true, "x11": true, @@ -176,9 +177,10 @@ // these have more complex or in flux policies and have their // own separate tests snowflakes := map[string]bool{ - "content": true, - "home": true, - "lxd-support": true, + "classic-support": true, + "content": true, + "home": true, + "lxd-support": true, } for _, iface := range all { @@ -220,11 +222,58 @@ } func (s *baseDeclSuite) TestAutoConnectionContent(c *C) { - // content will also depend for now AutoConnect(plug, slot) // random snaps cannot connect with content + // (Sanitize* will now also block this) cand := s.connectCand(c, "content", "", "") err := cand.CheckAutoConnect() c.Check(err, NotNil) + + slotDecl1 := s.mockSnapDecl(c, "slot-snap", "slot-snap-id", "pub1", "") + plugDecl1 := s.mockSnapDecl(c, "plug-snap", "plug-snap-id", "pub1", "") + plugDecl2 := s.mockSnapDecl(c, "plug-snap", "plug-snap-id", "pub2", "") + + // same publisher, same content + cand = s.connectCand(c, "stuff", ` +name: slot-snap +slots: + stuff: + interface: content + content: mk1 +`, ` +name: plug-snap +plugs: + stuff: + interface: content + content: mk1 +`) + cand.SlotSnapDeclaration = slotDecl1 + cand.PlugSnapDeclaration = plugDecl1 + err = cand.CheckAutoConnect() + c.Check(err, IsNil) + + // different publisher, same content + cand.SlotSnapDeclaration = slotDecl1 + cand.PlugSnapDeclaration = plugDecl2 + err = cand.CheckAutoConnect() + c.Check(err, NotNil) + + // same publisher, different content + cand = s.connectCand(c, "stuff", `name: slot-snap +slots: + stuff: + interface: content + content: mk1 +`, ` +name: plug-snap +plugs: + stuff: + interface: content + content: mk2 +`) + cand.SlotSnapDeclaration = slotDecl1 + cand.PlugSnapDeclaration = plugDecl1 + err = cand.CheckAutoConnect() + c.Check(err, NotNil) } func (s *baseDeclSuite) TestAutoConnectionLxdSupportOverride(c *C) { @@ -296,6 +345,24 @@ c.Check(err, IsNil) } +func (s *baseDeclSuite) TestAutoConnectionClassicSupportOverride(c *C) { + cand := s.connectCand(c, "classic-support", "", "") + err := cand.CheckAutoConnect() + c.Check(err, NotNil) + c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"classic-support\"") + + plugsSlots := ` +plugs: + classic-support: + allow-auto-connection: true +` + + snapDecl := s.mockSnapDecl(c, "classic", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots) + cand.PlugSnapDeclaration = snapDecl + err = cand.CheckAutoConnect() + c.Check(err, IsNil) +} + func (s *baseDeclSuite) TestAutoConnectionOverrideMultiple(c *C) { plugsSlots := ` plugs: @@ -370,15 +437,18 @@ "ppp": {"core"}, "pulseaudio": {"app", "core"}, "serial-port": {"core", "gadget"}, + "thumbnailer": {"app"}, "udisks2": {"app"}, "uhid": {"core"}, + "unity8": {"app"}, "unity8-calendar": {"app"}, "unity8-contacts": {"app"}, "ubuntu-download-manager": {"app"}, "upower-observe": {"app", "core"}, // snowflakes - "docker": nil, - "lxd": nil, + "classic-support": nil, + "docker": nil, + "lxd": nil, } restrictedPlugInstallation = map[string][]string{ @@ -456,10 +526,12 @@ all := builtin.Interfaces() restricted := map[string]bool{ + "classic-support": true, "docker-support": true, "kernel-module-control": true, "lxd-support": true, "snapd-control": true, + "unity8": true, } for _, iface := range all { @@ -499,12 +571,14 @@ // case-by-case basis noconnect := map[string]bool{ "bluez": true, + "content": true, "docker": true, "fwupd": true, "location-control": true, "location-observe": true, "lxd": true, "mir": true, + "thumbnailer": true, "udisks2": true, "unity8-calendar": true, "unity8-contacts": true, @@ -572,11 +646,13 @@ // given how the rules work this can be delicate, // listed here to make sure that was a conscious decision bothSides := map[string]bool{ + "classic-support": true, "core-support": true, "docker-support": true, "kernel-module-control": true, "lxd-support": true, "snapd-control": true, + "unity8": true, } for _, iface := range all { @@ -594,3 +670,59 @@ } } } + +func (s *baseDeclSuite) TestConnectionContent(c *C) { + // we let connect explicitly as long as content matches (or is absent on both sides) + + // random (Sanitize* will now also block this) + cand := s.connectCand(c, "content", "", "") + err := cand.Check() + c.Check(err, NotNil) + + slotDecl1 := s.mockSnapDecl(c, "slot-snap", "slot-snap-id", "pub1", "") + plugDecl1 := s.mockSnapDecl(c, "plug-snap", "plug-snap-id", "pub1", "") + plugDecl2 := s.mockSnapDecl(c, "plug-snap", "plug-snap-id", "pub2", "") + + // same publisher, same content + cand = s.connectCand(c, "stuff", `name: slot-snap +slots: + stuff: + interface: content + content: mk1 +`, ` +name: plug-snap +plugs: + stuff: + interface: content + content: mk1 +`) + cand.SlotSnapDeclaration = slotDecl1 + cand.PlugSnapDeclaration = plugDecl1 + err = cand.Check() + c.Check(err, IsNil) + + // different publisher, same content + cand.SlotSnapDeclaration = slotDecl1 + cand.PlugSnapDeclaration = plugDecl2 + err = cand.Check() + c.Check(err, IsNil) + + // same publisher, different content + cand = s.connectCand(c, "stuff", ` +name: slot-snap +slots: + stuff: + interface: content + content: mk1 +`, ` +name: plug-snap +plugs: + stuff: + interface: content + content: mk2 +`) + cand.SlotSnapDeclaration = slotDecl1 + cand.PlugSnapDeclaration = plugDecl1 + err = cand.Check() + c.Check(err, NotNil) +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/bluetooth_control.go snapd-2.23.1+16.10/interfaces/builtin/bluetooth_control.go --- snapd-2.22.6+16.10/interfaces/builtin/bluetooth_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/bluetooth_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -25,8 +25,7 @@ const bluetoothControlConnectedPlugAppArmor = ` # Description: Allow managing the kernel side Bluetooth stack. Reserved -# because this gives privileged access to the system. -# Usage: reserved +# because this gives privileged access to the system. network bluetooth, # For crypto functionality the kernel offers @@ -47,10 +46,8 @@ const bluetoothControlConnectedPlugSecComp = ` # Description: Allow managing the kernel side Bluetooth stack. Reserved -# because this gives privileged access to the system. -# Usage: reserved +# because this gives privileged access to the system. bind -getsockopt ` func NewBluetoothControlInterface() interfaces.Interface { diff -Nru snapd-2.22.6+16.10/interfaces/builtin/bluez.go snapd-2.23.1+16.10/interfaces/builtin/bluez.go --- snapd-2.22.6+16.10/interfaces/builtin/bluez.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/bluez.go 2017-03-08 13:28:17.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -25,10 +25,9 @@ "github.com/snapcore/snapd/interfaces" ) -var bluezPermanentSlotAppArmor = []byte(` -# Description: Allow operating as the bluez service. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const bluezPermanentSlotAppArmor = ` +# Description: Allow operating as the bluez service. This gives privileged +# access to the system. network bluetooth, @@ -53,7 +52,7 @@ path=/org/freedesktop/DBus interface=org.freedesktop.DBus member={Request,Release}Name - peer=(name=org.freedesktop.DBus), + peer=(name=org.freedesktop.DBus, label=unconfined), dbus (send) bus=system @@ -71,21 +70,27 @@ bus=system name="org.bluez.obex", - # Allow traffic to/from our path and interface with any method + # Allow traffic to/from our path and interface with any method for unconfined + # cliens to talk to our bluez services. dbus (receive, send) bus=system path=/org/bluez{,/**} - interface=org.bluez.*, - - # Allow traffic to/from org.freedesktop.DBus for bluez service + interface=org.bluez.* + peer=(label=unconfined), dbus (receive, send) bus=system - path=/ - interface=org.freedesktop.DBus.**, + path=/org/bluez{,/**} + interface=org.freedesktop.DBus.* + peer=(label=unconfined), + + # Allow traffic to/from org.freedesktop.DBus for bluez service. This rule is + # not snap-specific and grants privileged access to the org.freedesktop.DBus + # on the system bus. dbus (receive, send) bus=system - path=/org/bluez{,/**} - interface=org.freedesktop.DBus.**, + path=/ + interface=org.freedesktop.DBus.* + peer=(label=unconfined), # Allow access to hostname system service dbus (receive, send) @@ -93,12 +98,20 @@ path=/org/freedesktop/hostname1 interface=org.freedesktop.DBus.Properties peer=(label=unconfined), -`) +` -var bluezConnectedPlugAppArmor = []byte(` -# Description: Allow using bluez service. Reserved because this gives -# privileged access to the bluez service. -# Usage: reserved +const bluezConnectedSlotAppArmor = ` +# Allow connected clients to interact with the service + +# Allow all access to bluez service +dbus (receive, send) + bus=system + peer=(label=###PLUG_SECURITY_TAGS###), +` + +const bluezConnectedPlugAppArmor = ` +# Description: Allow using bluez service. This gives privileged access to the +# bluez service. #include @@ -126,52 +139,19 @@ path=/org/bluez{,/**} interface=org.freedesktop.DBus.* peer=(label=unconfined), -`) +` -var bluezPermanentSlotSecComp = []byte(` -# Description: Allow operating as the bluez service. Reserved because this -# gives -# privileged access to the system. -# Usage: reserved +const bluezPermanentSlotSecComp = ` +# Description: Allow operating as the bluez service. This gives privileged +# access to the system. accept accept4 bind -connect -getpeername -getsockname -getsockopt listen -recv -recvfrom -recvmmsg -recvmsg -send -sendmmsg -sendmsg -sendto -setsockopt shutdown -socketpair -socket -`) - -var bluezConnectedPlugSecComp = []byte(` -# Description: Allow using bluez service. Reserved because this gives -# privileged access to the bluez service. -# Usage: reserved - -# Can communicate with DBus system service -connect -getsockname -recv -recvmsg -send -sendto -sendmsg -socket -`) +` -var bluezPermanentSlotDBus = []byte(` +const bluezPermanentSlotDBus = ` @@ -191,7 +171,7 @@ -`) +` type BluezInterface struct{} @@ -208,10 +188,8 @@ case interfaces.SecurityAppArmor: old := []byte("###SLOT_SECURITY_TAGS###") new := slotAppLabelExpr(slot) - snippet := bytes.Replace(bluezConnectedPlugAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(bluezConnectedPlugAppArmor), old, new, -1) return snippet, nil - case interfaces.SecuritySecComp: - return bluezConnectedPlugSecComp, nil } return nil, nil } @@ -219,16 +197,23 @@ func (iface *BluezInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return bluezPermanentSlotAppArmor, nil + return []byte(bluezPermanentSlotAppArmor), nil case interfaces.SecuritySecComp: - return bluezPermanentSlotSecComp, nil + return []byte(bluezPermanentSlotSecComp), nil case interfaces.SecurityDBus: - return bluezPermanentSlotDBus, nil + return []byte(bluezPermanentSlotDBus), nil } return nil, nil } func (iface *BluezInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityAppArmor: + old := []byte("###PLUG_SECURITY_TAGS###") + new := plugAppLabelExpr(plug) + snippet := bytes.Replace([]byte(bluezConnectedSlotAppArmor), old, new, -1) + return snippet, nil + } return nil, nil } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/bluez_test.go snapd-2.23.1+16.10/interfaces/builtin/bluez_test.go --- snapd-2.22.6+16.10/interfaces/builtin/bluez_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/bluez_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -116,21 +116,28 @@ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.bluez.app"),`) } +func (s *BluezInterfaceSuite) TestConnectedSlotSnippetAppArmor(c *C) { + snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + + c.Check(string(snippet), testutil.Contains, "peer=(label=\"snap.bluez.*\")") +} + func (s *BluezInterfaceSuite) TestUsedSecuritySystems(c *C) { - systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor, - interfaces.SecuritySecComp} - for _, system := range systems { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - snippet, err = s.iface.PermanentSlotSnippet(s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - } - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus) + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus) c.Assert(err, IsNil) c.Assert(snippet, IsNil) snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityDBus) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecuritySecComp) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/browser_support.go snapd-2.23.1+16.10/interfaces/builtin/browser_support.go --- snapd-2.22.6+16.10/interfaces/builtin/browser_support.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/browser_support.go 2017-03-08 13:28:17.000000000 +0000 @@ -31,7 +31,6 @@ # Chrome/Chromium and Mozilla) and file paths they expect. This interface is # transitional and is only in place while upstream's work to change their paths # and snappy is updated to properly mediate the APIs. -# Usage: reserved # This allows raising the OOM score of other processes owned by the user. owner @{PROC}/@{pid}/oom_score_adj rw, @@ -46,11 +45,22 @@ owner /{dev,run}/shm/{,.}org.chromium.Chromium.* rw, owner /{dev,run}/shm/{,.}com.google.Chrome.* rw, +# Allow reading platform files +/run/udev/data/+platform:* r, + # Chrome/Chromium should be adjusted to not use gconf. It is only used with # legacy systems that don't have snapd deny dbus (send) bus=session interface="org.gnome.GConf.Server", + +# Lttng tracing is very noisy and should not be allowed by confined apps. Can +# safely deny. LP: #1260491 +deny /{dev,run,var/run}/shm/lttng-ust-* r, + +# webbrowser-app/webapp-container tries to read this file to determine if it is +# confined or not, so explicitly deny to avoid noise in the logs. +deny @{PROC}/@{pid}/attr/current r, ` const browserSupportConnectedPlugAppArmorWithoutSandbox = ` @@ -104,7 +114,6 @@ /run/udev/data/+acpi:* r, /run/udev/data/+hwmon:hwmon[0-9]* r, /run/udev/data/+i2c:* r, -/run/udev/data/+platform:* r, /sys/devices/**/bConfigurationValue r, /sys/devices/**/descriptors r, /sys/devices/**/manufacturer r, @@ -187,12 +196,12 @@ # Chrome/Chromium and Mozilla) and file paths they expect. This interface is # transitional and is only in place while upstream's work to change their paths # and snappy is updated to properly mediate the APIs. -# Usage: reserved # for anonymous sockets bind listen accept +accept4 # TODO: fine-tune when seccomp arg filtering available in stable distro # releases diff -Nru snapd-2.22.6+16.10/interfaces/builtin/classic_support.go snapd-2.23.1+16.10/interfaces/builtin/classic_support.go --- snapd-2.22.6+16.10/interfaces/builtin/classic_support.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/classic_support.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,113 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "github.com/snapcore/snapd/interfaces" +) + +const classicSupportPlugAppArmor = ` +# Description: permissions to use classic dimension. This policy is +# intentionally not restricted. This gives device ownership to +# connected snaps. + +# Description: permissions to use classic dimension. This policy is intentionally +# not restricted. This gives device ownership to connected snaps. + +# for 'create' +/{,usr/}bin/unsquashfs ixr, +/var/lib/snapd/snaps/core_*.snap r, +capability chown, +capability fowner, +capability mknod, + +# This allows running anything unconfined +/{,usr/}bin/sudo Uxr, +capability fsetid, +capability dac_override, + +# Allow copying configuration to the chroot +/etc/{,**} r, +/var/lib/extrausers/{,*} r, + +# Allow bind mounting various directories +capability sys_admin, +/{,usr/}bin/mount ixr, +/{,usr/}bin/mountpoint ixr, +/run/mount/utab rw, +@{PROC}/[0-9]*/mountinfo r, +mount options=(rw bind) /home/ -> /var/snap/@{SNAP_NAME}/**/, +mount options=(rw bind) /run/ -> /var/snap/@{SNAP_NAME}/**/, +mount options=(rw bind) /proc/ -> /var/snap/@{SNAP_NAME}/**/, +mount options=(rw bind) /sys/ -> /var/snap/@{SNAP_NAME}/**/, +mount options=(rw bind) /dev/ -> /var/snap/@{SNAP_NAME}/**/, +mount options=(rw bind) / -> /var/snap/@{SNAP_NAME}/**/, +mount fstype=devpts options=(rw) devpts -> /dev/pts/, +mount options=(rw rprivate) -> /var/snap/@{SNAP_NAME}/**/, + +# reset +/{,usr/}bin/umount ixr, +umount /var/snap/@{SNAP_NAME}/**/, + +# These rules allow running anything unconfined as well as managing systemd +/usr/bin/systemd-run Uxr, +/bin/systemctl Uxr, +` + +const classicSupportPlugSecComp = ` +# Description: permissions to use classic dimension. This policy is intentionally +# not restricted. This gives device ownership to connected snaps. +# create +chown +chown32 +lchown +lchown32 +fchown +fchown32 +fchownat +mknod +chroot + +# sudo +bind +sendmsg +sendmmsg +sendto +recvfrom +recvmsg +setgroups +setgroups32 + +# classic +mount +getsockopt + +# reset +umount +umount2 +` + +func NewClassicSupportInterface() interfaces.Interface { + return &commonInterface{ + name: "classic-support", + connectedPlugAppArmor: classicSupportPlugAppArmor, + connectedPlugSecComp: classicSupportPlugSecComp, + } +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/classic_support_test.go snapd-2.23.1+16.10/interfaces/builtin/classic_support_test.go --- snapd-2.22.6+16.10/interfaces/builtin/classic_support_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/classic_support_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -0,0 +1,88 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type ClassicSupportInterfaceSuite struct { + iface interfaces.Interface + slot *interfaces.Slot + plug *interfaces.Plug +} + +var _ = Suite(&ClassicSupportInterfaceSuite{ + iface: builtin.NewClassicSupportInterface(), + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, + Name: "classic-support", + Interface: "classic-support", + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "other"}, + Name: "classic-support", + Interface: "classic-support", + }, + }, +}) + +func (s *ClassicSupportInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "classic-support") +} + +func (s *ClassicSupportInterfaceSuite) TestSanitizeSlot(c *C) { + err := s.iface.SanitizeSlot(s.slot) + c.Assert(err, IsNil) +} + +func (s *ClassicSupportInterfaceSuite) TestSanitizePlug(c *C) { + err := s.iface.SanitizePlug(s.plug) + c.Assert(err, IsNil) +} + +func (s *ClassicSupportInterfaceSuite) TestSanitizeIncorrectInterface(c *C) { + c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) }, + PanicMatches, `slot is not of interface "classic-support"`) + c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) }, + PanicMatches, `plug is not of interface "classic-support"`) +} + +func (s *ClassicSupportInterfaceSuite) TestUsedSecuritySystems(c *C) { + // connected plugs have a non-nil security snippet for apparmor + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + c.Check(string(snippet), testutil.Contains, "/usr/bin/systemd-run Uxr,\n") + c.Check(string(snippet), testutil.Contains, "/bin/systemctl Uxr,\n") + // connected plugs have a non-nil security snippet for seccomp + snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + c.Check(string(snippet), testutil.Contains, "mount\n") +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/content.go snapd-2.23.1+16.10/interfaces/builtin/content.go --- snapd-2.22.6+16.10/interfaces/builtin/content.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/content.go 2017-03-08 13:28:21.000000000 +0000 @@ -26,6 +26,7 @@ "strings" "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/mount" "github.com/snapcore/snapd/snap" ) @@ -44,6 +45,11 @@ if iface.Name() != slot.Interface { panic(fmt.Sprintf("slot is not of interface %q", iface)) } + content, ok := slot.Attrs["content"].(string) + if !ok || len(content) == 0 { + // content defaults to "slot" name if unspecified + slot.Attrs["content"] = slot.Name + } // check that we have either a read or write path rpath := iface.path(slot, "read") @@ -68,6 +74,11 @@ if iface.Name() != plug.Interface { panic(fmt.Sprintf("plug is not of interface %q", iface)) } + content, ok := plug.Attrs["content"].(string) + if !ok || len(content) == 0 { + // content defaults to "plug" name if unspecified + plug.Attrs["content"] = plug.Name + } target, ok := plug.Attrs["target"].(string) if !ok || len(target) == 0 { return fmt.Errorf("content plug must contain target path") @@ -127,10 +138,15 @@ return filepath.Join(snapInfo.MountDir(), path) } -func mountEntry(plug *interfaces.Plug, slot *interfaces.Slot, relSrc string, mntOpts string) string { - dst := resolveSpecialVariable(plug.Attrs["target"].(string), plug.Snap) - src := resolveSpecialVariable(relSrc, slot.Snap) - return fmt.Sprintf("%s %s none bind%s 0 0", src, dst, mntOpts) +func mountEntry(plug *interfaces.Plug, slot *interfaces.Slot, relSrc string, extraOptions ...string) mount.Entry { + options := make([]string, 0, len(extraOptions)+1) + options = append(options, "bind") + options = append(options, extraOptions...) + return mount.Entry{ + Name: resolveSpecialVariable(relSrc, slot.Snap), + Dir: resolveSpecialVariable(plug.Attrs["target"].(string), plug.Snap), + Options: options, + } } func (iface *ContentInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { @@ -167,14 +183,6 @@ } return contentSnippet.Bytes(), nil - case interfaces.SecurityMount: - for _, r := range iface.path(slot, "read") { - fmt.Fprintln(contentSnippet, mountEntry(plug, slot, r, ",ro")) - } - for _, w := range iface.path(slot, "write") { - fmt.Fprintln(contentSnippet, mountEntry(plug, slot, w, "")) - } - return contentSnippet.Bytes(), nil } return nil, nil } @@ -184,5 +192,24 @@ } func (iface *ContentInterface) AutoConnect(plug *interfaces.Plug, slot *interfaces.Slot) bool { - return plug.Attrs["content"] == slot.Attrs["content"] + // allow what declarations allowed + return true +} + +// Interactions with the mount backend. + +func (iface *ContentInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + for _, r := range iface.path(slot, "read") { + err := spec.AddMountEntry(mountEntry(plug, slot, r, "ro")) + if err != nil { + return err + } + } + for _, w := range iface.path(slot, "write") { + err := spec.AddMountEntry(mountEntry(plug, slot, w)) + if err != nil { + return err + } + } + return nil } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/content_test.go snapd-2.23.1+16.10/interfaces/builtin/content_test.go --- snapd-2.22.6+16.10/interfaces/builtin/content_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/content_test.go 2017-03-08 13:28:21.000000000 +0000 @@ -24,12 +24,13 @@ "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/mount" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" ) type ContentSuite struct { - iface interfaces.Interface + iface *builtin.ContentInterface } var _ = Suite(&ContentSuite{ @@ -46,6 +47,7 @@ slots: content-slot: interface: content + content: mycont read: - shared/read ` @@ -55,12 +57,29 @@ c.Assert(err, IsNil) } +func (s *ContentSuite) TestSanitizeSlotContentLabelDefault(c *C) { + const mockSnapYaml = `name: content-slot-snap +version: 1.0 +slots: + content-slot: + interface: content + read: + - shared/read +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + slot := &interfaces.Slot{SlotInfo: info.Slots["content-slot"]} + err := s.iface.SanitizeSlot(slot) + c.Assert(err, IsNil) + c.Assert(slot.Attrs["content"], Equals, slot.Name) +} + func (s *ContentSuite) TestSanitizeSlotNoPaths(c *C) { const mockSnapYaml = `name: content-slot-snap version: 1.0 slots: content-slot: interface: content + content: mycont ` info := snaptest.MockInfo(c, mockSnapYaml, nil) slot := &interfaces.Slot{SlotInfo: info.Slots["content-slot"]} @@ -74,6 +93,7 @@ slots: content-slot: interface: content + content: mycont read: [] write: [] ` @@ -83,12 +103,13 @@ c.Assert(err, ErrorMatches, "read or write path must be set") } -func (s *ContentSuite) TestSanitizeSlotHasRealtivePath(c *C) { +func (s *ContentSuite) TestSanitizeSlotHasRelativePath(c *C) { const mockSnapYaml = `name: content-slot-snap version: 1.0 slots: content-slot: interface: content + content: mycont ` for _, rw := range []string{"read: [../foo]", "write: [../bar]"} { info := snaptest.MockInfo(c, mockSnapYaml+" "+rw, nil) @@ -104,6 +125,7 @@ plugs: content-plug: interface: content + content: mycont target: import ` info := snaptest.MockInfo(c, mockSnapYaml, nil) @@ -112,12 +134,28 @@ c.Assert(err, IsNil) } +func (s *ContentSuite) TestSanitizePlugContentLabelDefault(c *C) { + const mockSnapYaml = `name: content-slot-snap +version: 1.0 +plugs: + content-plug: + interface: content + target: import +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := &interfaces.Plug{PlugInfo: info.Plugs["content-plug"]} + err := s.iface.SanitizePlug(plug) + c.Assert(err, IsNil) + c.Assert(plug.Attrs["content"], Equals, plug.Name) +} + func (s *ContentSuite) TestSanitizePlugSimpleNoTarget(c *C) { const mockSnapYaml = `name: content-slot-snap version: 1.0 plugs: content-plug: interface: content + content: mycont ` info := snaptest.MockInfo(c, mockSnapYaml, nil) plug := &interfaces.Plug{PlugInfo: info.Plugs["content-plug"]} @@ -131,6 +169,7 @@ plugs: content-plug: interface: content + content: mycont target: ../foo ` info := snaptest.MockInfo(c, mockSnapYaml, nil) @@ -168,10 +207,14 @@ producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)}) slot := &interfaces.Slot{SlotInfo: producerInfo.Slots["content"]} - content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityMount) - c.Assert(err, IsNil) - expected := "/snap/producer/5/export /snap/consumer/7/import none bind,ro 0 0\n" - c.Assert(string(content), Equals, expected) + spec := &mount.Specification{} + c.Assert(s.iface.MountConnectedPlug(spec, plug, slot), IsNil) + expectedMnt := []mount.Entry{{ + Name: "/snap/producer/5/export", + Dir: "/snap/consumer/7/import", + Options: []string{"bind", "ro"}, + }} + c.Assert(spec.MountEntries(), DeepEquals, expectedMnt) } // Check that sharing of read-only snap content is possible @@ -192,14 +235,18 @@ producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)}) slot := &interfaces.Slot{SlotInfo: producerInfo.Slots["content"]} - content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityMount) - c.Assert(err, IsNil) - expected := "/snap/producer/5/export /snap/consumer/7/import none bind,ro 0 0\n" - c.Assert(string(content), Equals, expected) + spec := &mount.Specification{} + c.Assert(s.iface.MountConnectedPlug(spec, plug, slot), IsNil) + expectedMnt := []mount.Entry{{ + Name: "/snap/producer/5/export", + Dir: "/snap/consumer/7/import", + Options: []string{"bind", "ro"}, + }} + c.Assert(spec.MountEntries(), DeepEquals, expectedMnt) - content, err = s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor) + content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) - expected = ` + expected := ` # In addition to the bind mount, add any AppArmor rules so that # snaps may directly access the slot implementation's files # read-only. @@ -226,14 +273,18 @@ producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)}) slot := &interfaces.Slot{SlotInfo: producerInfo.Slots["content"]} - content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityMount) - c.Assert(err, IsNil) - expected := "/var/snap/producer/5/export /var/snap/consumer/7/import none bind 0 0\n" - c.Assert(string(content), Equals, expected) + spec := &mount.Specification{} + c.Assert(s.iface.MountConnectedPlug(spec, plug, slot), IsNil) + expectedMnt := []mount.Entry{{ + Name: "/var/snap/producer/5/export", + Dir: "/var/snap/consumer/7/import", + Options: []string{"bind"}, + }} + c.Assert(spec.MountEntries(), DeepEquals, expectedMnt) - content, err = s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor) + content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) - expected = ` + expected := ` # In addition to the bind mount, add any AppArmor rules so that # snaps may directly access the slot implementation's files. Due # to a limitation in the kernel's LSM hooks for AF_UNIX, these @@ -262,14 +313,18 @@ producerInfo := snaptest.MockInfo(c, producerYaml, &snap.SideInfo{Revision: snap.R(5)}) slot := &interfaces.Slot{SlotInfo: producerInfo.Slots["content"]} - content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityMount) - c.Assert(err, IsNil) - expected := "/var/snap/producer/common/export /var/snap/consumer/common/import none bind 0 0\n" - c.Assert(string(content), Equals, expected) + spec := &mount.Specification{} + c.Assert(s.iface.MountConnectedPlug(spec, plug, slot), IsNil) + expectedMnt := []mount.Entry{{ + Name: "/var/snap/producer/common/export", + Dir: "/var/snap/consumer/common/import", + Options: []string{"bind"}, + }} + c.Assert(spec.MountEntries(), DeepEquals, expectedMnt) - content, err = s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor) + content, err := s.iface.ConnectedPlugSnippet(plug, slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) - expected = ` + expected := ` # In addition to the bind mount, add any AppArmor rules so that # snaps may directly access the slot implementation's files. Due # to a limitation in the kernel's LSM hooks for AF_UNIX, these diff -Nru snapd-2.22.6+16.10/interfaces/builtin/core_support.go snapd-2.23.1+16.10/interfaces/builtin/core_support.go --- snapd-2.22.6+16.10/interfaces/builtin/core_support.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/core_support.go 2017-03-06 13:33:50.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -24,18 +24,51 @@ ) const coreSupportConnectedPlugAppArmor = ` -# Description: Can control all aspects of systemd via the systemctl command. It -# allows execution of the systemctl binary unconfined. As such, this gives device -# ownership to the snap. +# Description: Can control all aspects of systemd via the systemctl command, +# update rsyslog configuration, update systemd-timesyncd configuration and +# update/apply sysctl configuration. The interface allows execution of the +# systemctl binary unconfined and modifying all sysctl configuration. As such, +# this gives device ownership to the snap. /bin/systemctl Uxr, -` -const coreSupportConnectedPlugSecComp = ` -sendmsg -recvmsg -sendto -recvfrom +# Allow modifying rsyslog configuration for such things as remote logging. For +# now, only allow modifying NN-snap*.conf and snap*.conf files. +/etc/rsyslog.d/{,*} r, +/etc/rsyslog.d/{,[0-9][0-9]-}snap*.conf w, + +# Allow modifying /etc/systemd/timesyncd.conf for adjusting systemd-timesyncd's +# timeservers +/etc/systemd/timesyncd.conf rw, + +# Allow modifying sysctl configuration and applying the changes. For now, allow +# reading all sysctl files but only allow modifying NN-snap*.conf and +# snap*.conf files in /etc/sysctl.d. +/etc/sysctl.conf r, +/etc/sysctl.d/{,*} r, +/etc/sysctl.d/{,[0-9][0-9]-}snap*.conf w, +/{,usr/}{,s}bin/sysctl ixr, +@{PROC}/sys/{,**} r, +@{PROC}/sys/** w, + +# Allow modifying logind configuration. For now, allow reading all logind +# configuration but only allow modifying NN-snap*.conf and snap*.conf files +# in /etc/systemd/logind.conf.d. +/etc/systemd/logind.conf r, +/etc/systemd/logind.conf.d/{,*} r, +/etc/systemd/logind.conf.d/{,[0-9][0-9]-}snap*.conf w, + +# Allow managing the hostname with a core config option +/etc/hostname rw, +/{,usr/}{,s}bin/hostnamectl ixr, + +# Allow modifying swapfile configuration for swapfile.service shipped in +# the core snap, general mgmt of the service is handled via systemctl +/etc/default/swapfile rw, + +# Allow read/write access to the pi2 boot config.txt. WARNING: improperly +# editing this file may render the system unbootable. +owner /boot/uboot/config.txt rwk, ` // NewShutdownInterface returns a new "shutdown" interface. @@ -43,7 +76,6 @@ return &commonInterface{ name: "core-support", connectedPlugAppArmor: coreSupportConnectedPlugAppArmor, - connectedPlugSecComp: coreSupportConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/core_support_test.go snapd-2.23.1+16.10/interfaces/builtin/core_support_test.go --- snapd-2.22.6+16.10/interfaces/builtin/core_support_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/core_support_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -84,17 +84,10 @@ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - // connected plugs have a non-nil security snippet for seccomp - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) } func (s *CoreSupportInterfaceSuite) TestConnectedPlugSnippet(c *C) { snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(string(snippet), testutil.Contains, `/bin/systemctl Uxr,`) - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(string(snippet), testutil.Contains, `sendmsg`) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/cups_control.go snapd-2.23.1+16.10/interfaces/builtin/cups_control.go --- snapd-2.22.6+16.10/interfaces/builtin/cups_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/cups_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -28,18 +28,11 @@ #include ` -const cupsControlConnectedPlugSecComp = ` -recvfrom -sendto -setsockopt -` - // NewCupsControlInterface returns a new "cups" interface. func NewCupsControlInterface() interfaces.Interface { return &commonInterface{ name: "cups-control", connectedPlugAppArmor: cupsControlConnectedPlugAppArmor, - connectedPlugSecComp: cupsControlConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/dbus.go snapd-2.23.1+16.10/interfaces/builtin/dbus.go --- snapd-2.22.6+16.10/interfaces/builtin/dbus.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/dbus.go 2017-03-02 06:37:54.000000000 +0000 @@ -104,14 +104,6 @@ peer=(label=unconfined), ` -const dbusPermanentSlotSecComp = ` -# Description: Allow owning a name on DBus public bus -getsockname -recvmsg -sendmsg -sendto -` - const dbusPermanentSlotDBus = ` @@ -184,13 +176,6 @@ peer=(label=###SLOT_SECURITY_TAGS###), ` -const dbusConnectedPlugSecComp = ` -getsockname -recvmsg -sendmsg -sendto -` - type DbusInterface struct{} func (iface *DbusInterface) Name() string { @@ -318,8 +303,6 @@ snippet = bytes.Replace(snippet, old, new, -1) return snippet, nil - case interfaces.SecuritySecComp: - return []byte(dbusConnectedPlugSecComp), nil } return nil, nil } @@ -355,8 +338,6 @@ } return snippets.Bytes(), nil - case interfaces.SecuritySecComp: - return []byte(dbusPermanentSlotSecComp), nil case interfaces.SecurityDBus: bus, name, err := iface.getAttribs(slot.Attrs) if err != nil { diff -Nru snapd-2.22.6+16.10/interfaces/builtin/dbus_test.go snapd-2.23.1+16.10/interfaces/builtin/dbus_test.go --- snapd-2.22.6+16.10/interfaces/builtin/dbus_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/dbus_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -124,10 +124,6 @@ c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - snippet, err = s.iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - snippet, err = s.iface.ConnectedSlotSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) @@ -135,10 +131,6 @@ snippet, err = s.iface.ConnectedPlugSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - - snippet, err = s.iface.ConnectedPlugSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) } func (s *DbusInterfaceSuite) TestValidSessionBusName(c *C) { @@ -370,14 +362,6 @@ c.Check(string(snippet), testutil.Contains, "interface=\"org.test-system-slot{,.*}\"\n") } -func (s *DbusInterfaceSuite) TestPermanentSlotSeccomp(c *C) { - snippet, err := s.iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - - c.Check(string(snippet), testutil.Contains, "getsockname\n") -} - func (s *DbusInterfaceSuite) TestPermanentSlotDBusSession(c *C) { snippet, err := s.iface.PermanentSlotSnippet(s.sessionSlot, interfaces.SecurityDBus) c.Assert(err, IsNil) @@ -477,14 +461,6 @@ c.Check(string(snippet), testutil.Contains, "interface=\"org.test-system-connected{,.*}\"\n") } -func (s *DbusInterfaceSuite) TestConnectedPlugSeccomp(c *C) { - snippet, err := s.iface.ConnectedPlugSnippet(s.connectedSessionPlug, s.connectedSessionSlot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - - c.Check(string(snippet), testutil.Contains, "getsockname\n") -} - func (s *DbusInterfaceSuite) TestConnectionFirst(c *C) { const plugYaml = `name: plugger version: 1.0 diff -Nru snapd-2.22.6+16.10/interfaces/builtin/dcdbas_control.go snapd-2.23.1+16.10/interfaces/builtin/dcdbas_control.go --- snapd-2.22.6+16.10/interfaces/builtin/dcdbas_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/dcdbas_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -33,8 +33,6 @@ # # See http://linux.dell.com/libsmbios/main/ for more information about the libsmbios project. -# Usage: reserved - # entries pertaining to System Management Interrupts (SMI) /sys/devices/platform/dcdbas/smi_data rw, /sys/devices/platform/dcdbas/smi_data_buf_phys_addr rw, diff -Nru snapd-2.22.6+16.10/interfaces/builtin/docker.go snapd-2.23.1+16.10/interfaces/builtin/docker.go --- snapd-2.22.6+16.10/interfaces/builtin/docker.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/docker.go 2017-03-08 13:28:17.000000000 +0000 @@ -28,7 +28,6 @@ const dockerConnectedPlugAppArmor = ` # Description: allow access to the Docker daemon socket. This gives privileged # access to the system via Docker's socket API. -# Usage: reserved # Allow talking to the docker daemon /{,var/}run/docker.sock rw, @@ -37,9 +36,7 @@ const dockerConnectedPlugSecComp = ` # Description: allow access to the Docker daemon socket. This gives privileged # access to the system via Docker's socket API. -# Usage: reserved -setsockopt bind ` diff -Nru snapd-2.22.6+16.10/interfaces/builtin/docker_support.go snapd-2.23.1+16.10/interfaces/builtin/docker_support.go --- snapd-2.22.6+16.10/interfaces/builtin/docker_support.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/docker_support.go 2017-03-08 13:28:17.000000000 +0000 @@ -31,7 +31,6 @@ # errors and not for security confinement. The Docker daemon by design requires # extensive access to the system and cannot be effectively confined against # malicious activity. -# Usage: reserved #include @@ -114,7 +113,6 @@ # errors and not for security confinement. The Docker daemon by design requires # extensive access to the system and cannot be effectively confined against # malicious activity. -# Usage: reserved # Because seccomp may only go more strict, we must allow all syscalls to Docker # that it expects to give to containers in addition to what it needs to run and diff -Nru snapd-2.22.6+16.10/interfaces/builtin/firewall_control.go snapd-2.23.1+16.10/interfaces/builtin/firewall_control.go --- snapd-2.22.6+16.10/interfaces/builtin/firewall_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/firewall_control.go 2017-03-08 13:28:17.000000000 +0000 @@ -27,7 +27,6 @@ const firewallControlConnectedPlugAppArmor = ` # Description: Can configure firewall. This is restricted because it gives # privileged access to networking and should only be used with trusted apps. -# Usage: reserved #include @@ -69,6 +68,10 @@ @{PROC}/sys/net/netfilter/** r, @{PROC}/sys/net/nf_conntrack_max r, +# read netfilter module parameters +/sys/module/nf_*/ r, +/sys/module/nf_*/parameters/{,*} r, + # various firewall related sysctl files @{PROC}/sys/net/ipv4/conf/*/rp_filter w, @{PROC}/sys/net/ipv{4,6}/conf/*/accept_source_route w, @@ -86,23 +89,9 @@ const firewallControlConnectedPlugSecComp = ` # Description: Can configure firewall. This is restricted because it gives # privileged access to networking and should only be used with trusted apps. -# Usage: reserved # for connecting to xtables abstract socket bind -connect -getsockname -getsockopt -recv -recvfrom -recvmsg -recvmmsg -send -sendmmsg -sendmsg -sendto -setsockopt -socket # for ping and ping6 capset diff -Nru snapd-2.22.6+16.10/interfaces/builtin/framebuffer.go snapd-2.23.1+16.10/interfaces/builtin/framebuffer.go --- snapd-2.22.6+16.10/interfaces/builtin/framebuffer.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/framebuffer.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,110 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "bytes" + "fmt" + + "github.com/snapcore/snapd/interfaces" +) + +const framebufferConnectedPlugAppArmor = ` +# Description: Allow reading and writing to the universal framebuffer (/dev/fb*) which +# gives privileged access to the console framebuffer. + +/dev/fb[0-9]* rw, +/run/udev/data/c29:[0-9]* r, +` + +// The type for physical-memory-control interface +type FramebufferInterface struct{} + +// Getter for the name of the physical-memory-control interface +func (iface *FramebufferInterface) Name() string { + return "framebuffer" +} + +func (iface *FramebufferInterface) String() string { + return iface.Name() +} + +// Check validity of the defined slot +func (iface *FramebufferInterface) SanitizeSlot(slot *interfaces.Slot) error { + // Does it have right type? + if iface.Name() != slot.Interface { + panic(fmt.Sprintf("slot is not of interface %q", iface)) + } + + // Creation of the slot of this type + // is allowed only by a gadget or os snap + if !(slot.Snap.Type == "os") { + return fmt.Errorf("%s slots only allowed on core snap", iface.Name()) + } + return nil +} + +// Checks and possibly modifies a plug +func (iface *FramebufferInterface) SanitizePlug(plug *interfaces.Plug) error { + if iface.Name() != plug.Interface { + panic(fmt.Sprintf("plug is not of interface %q", iface)) + } + // Currently nothing is checked on the plug side + return nil +} + +// Returns snippet granted on install +func (iface *FramebufferInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return nil, nil +} + +// Getter for the security snippet specific to the plug +func (iface *FramebufferInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityAppArmor: + return []byte(framebufferConnectedPlugAppArmor), nil + + case interfaces.SecurityUDev: + var tagSnippet bytes.Buffer + const udevRule = `KERNEL=="fb[0-9]*", TAG+="%s"` + for appName := range plug.Apps { + tag := udevSnapSecurityName(plug.Snap.Name(), appName) + tagSnippet.WriteString(fmt.Sprintf(udevRule, tag)) + tagSnippet.WriteString("\n") + } + return tagSnippet.Bytes(), nil + } + return nil, nil +} + +// No extra permissions granted on connection +func (iface *FramebufferInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return nil, nil +} + +// No permissions granted to plug permanently +func (iface *FramebufferInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return nil, nil +} + +func (iface *FramebufferInterface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool { + // Allow what is allowed in the declarations + return true +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/framebuffer_test.go snapd-2.23.1+16.10/interfaces/builtin/framebuffer_test.go --- snapd-2.22.6+16.10/interfaces/builtin/framebuffer_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/framebuffer_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,112 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" +) + +type FramebufferInterfaceSuite struct { + iface interfaces.Interface + slot *interfaces.Slot + plug *interfaces.Plug +} + +var _ = Suite(&FramebufferInterfaceSuite{ + iface: &builtin.FramebufferInterface{}, +}) + +func (s *FramebufferInterfaceSuite) SetUpTest(c *C) { + // Mock for OS Snap + osSnapInfo := snaptest.MockInfo(c, ` +name: ubuntu-core +type: os +slots: + test-framebuffer: + interface: framebuffer +`, nil) + s.slot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-framebuffer"]} + + // Snap Consumers + consumingSnapInfo := snaptest.MockInfo(c, ` +name: client-snap +plugs: + plug-for-framebuffer: + interface: framebuffer +apps: + app-accessing-framebuffer: + command: foo + plugs: [plug-for-framebuffer] +`, nil) + s.plug = &interfaces.Plug{PlugInfo: consumingSnapInfo.Plugs["plug-for-framebuffer"]} +} + +func (s *FramebufferInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "framebuffer") +} + +func (s *FramebufferInterfaceSuite) TestSanitizeSlot(c *C) { + err := s.iface.SanitizeSlot(s.slot) + c.Assert(err, IsNil) + err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "some-snap"}, + Name: "framebuffer", + Interface: "framebuffer", + }}) + c.Assert(err, ErrorMatches, "framebuffer slots only allowed on core snap") +} + +func (s *FramebufferInterfaceSuite) TestSanitizePlug(c *C) { + err := s.iface.SanitizePlug(s.plug) + c.Assert(err, IsNil) +} + +func (s *FramebufferInterfaceSuite) TestSanitizeIncorrectInterface(c *C) { + c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) }, + PanicMatches, `slot is not of interface "framebuffer"`) + c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) }, + PanicMatches, `plug is not of interface "framebuffer"`) +} + +func (s *FramebufferInterfaceSuite) TestUsedSecuritySystems(c *C) { + expectedSnippet1 := []byte(` +# Description: Allow reading and writing to the universal framebuffer (/dev/fb*) which +# gives privileged access to the console framebuffer. + +/dev/fb[0-9]* rw, +/run/udev/data/c29:[0-9]* r, +`) + expectedSnippet2 := []byte(`KERNEL=="fb[0-9]*", TAG+="snap_client-snap_app-accessing-framebuffer" +`) + + // connected plugs have a non-nil security snippet for apparmor + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet)) + + snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityUDev) + c.Assert(err, IsNil) + c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet)) +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/fuse_support.go snapd-2.23.1+16.10/interfaces/builtin/fuse_support.go --- snapd-2.22.6+16.10/interfaces/builtin/fuse_support.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/fuse_support.go 2017-03-02 06:37:54.000000000 +0000 @@ -26,8 +26,6 @@ # not supported at this time. mount -# for communicating with kernel -recvmsg ` const fuseSupportConnectedPlugAppArmor = ` diff -Nru snapd-2.22.6+16.10/interfaces/builtin/fwupd.go snapd-2.23.1+16.10/interfaces/builtin/fwupd.go --- snapd-2.22.6+16.10/interfaces/builtin/fwupd.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/fwupd.go 2017-03-08 13:28:17.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -25,10 +25,9 @@ "github.com/snapcore/snapd/interfaces" ) -var fwupdPermanentSlotAppArmor = []byte(` -# Description: Allow operating as the fwupd service. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const fwupdPermanentSlotAppArmor = ` +# Description: Allow operating as the fwupd service. This gives privileged +# access to the system. # Allow read/write access for old efivars sysfs interface capability sys_admin, @@ -84,12 +83,11 @@ dbus (bind) bus=system name="org.freedesktop.fwupd", -`) +` -var fwupdConnectedPlugAppArmor = []byte(` -# Description: Allow using fwupd service. Reserved because this gives -# privileged access to the fwupd service. -# Usage: reserved +const fwupdConnectedPlugAppArmor = ` +# Description: Allow using fwupd service. This gives # privileged access to the +# fwupd service. #Can access the network #include @@ -110,12 +108,11 @@ path=/ interface=org.freedesktop.DBus.Properties peer=(label=###SLOT_SECURITY_TAGS###), -`) +` -var fwupdConnectedSlotAppArmor = []byte(` -# Description: Allow firmware update using fwupd service. Reserved because this gives -# privileged access to the fwupd service. -# Usage: reserved +const fwupdConnectedSlotAppArmor = ` +# Description: Allow firmware update using fwupd service. This gives privileged +# access to the fwupd service. # Allow traffic to/from org.freedesktop.DBus for fwupd service dbus (receive, send) @@ -142,9 +139,9 @@ path=/org/freedesktop/fwupd{,/**} interface=org.freedesktop.fwupd peer=(label=###PLUG_SECURITY_TAGS###), -`) +` -var fwupdPermanentSlotDBus = []byte(` +const fwupdPermanentSlotDBus = ` @@ -156,35 +153,20 @@ -`) +` -var fwupdPermanentSlotSecComp = []byte(` -# Description: Allow operating as the fwupd service. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const fwupdPermanentSlotSecComp = ` +# Description: Allow operating as the fwupd service. This gives privileged +# access to the system. # Can communicate with DBus system service bind -getsockname -recvfrom -recvmsg -sendmsg -sendto -setsockopt -`) +` -var fwupdConnectedPlugSecComp = []byte(` +const fwupdConnectedPlugSecComp = ` # Description: Allow using fwupd service. Reserved because this gives # privileged access to the fwupd service. -# Usage: reserved bind -getsockname -getsockopt -recvfrom -recvmsg -sendmsg -sendto -setsockopt -`) +` // FwupdInterface type type FwupdInterface struct{} @@ -205,10 +187,10 @@ case interfaces.SecurityAppArmor: old := []byte("###SLOT_SECURITY_TAGS###") new := slotAppLabelExpr(slot) - snippet := bytes.Replace(fwupdConnectedPlugAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(fwupdConnectedPlugAppArmor), old, new, -1) return snippet, nil case interfaces.SecuritySecComp: - return fwupdConnectedPlugSecComp, nil + return []byte(fwupdConnectedPlugSecComp), nil } return nil, nil } @@ -217,11 +199,11 @@ func (iface *FwupdInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return fwupdPermanentSlotAppArmor, nil + return []byte(fwupdPermanentSlotAppArmor), nil case interfaces.SecurityDBus: - return fwupdPermanentSlotDBus, nil + return []byte(fwupdPermanentSlotDBus), nil case interfaces.SecuritySecComp: - return fwupdPermanentSlotSecComp, nil + return []byte(fwupdPermanentSlotSecComp), nil } return nil, nil } @@ -232,7 +214,7 @@ case interfaces.SecurityAppArmor: old := []byte("###PLUG_SECURITY_TAGS###") new := plugAppLabelExpr(plug) - snippet := bytes.Replace(fwupdConnectedSlotAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(fwupdConnectedSlotAppArmor), old, new, -1) return snippet, nil } return nil, nil diff -Nru snapd-2.22.6+16.10/interfaces/builtin/gsettings.go snapd-2.23.1+16.10/interfaces/builtin/gsettings.go --- snapd-2.22.6+16.10/interfaces/builtin/gsettings.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/gsettings.go 2017-03-02 06:37:54.000000000 +0000 @@ -27,7 +27,6 @@ # Description: Can access global gsettings of the user's session. Restricted # because this gives privileged access to sensitive information stored in # gsettings and allows adjusting settings of other applications. -# Usage: reserved #include @@ -40,27 +39,11 @@ peer=(label=unconfined), ` -const gsettingsConnectedPlugSecComp = ` -# Description: Can access global gsettings of the user's session. Restricted -# because this gives privileged access to sensitive information stored in -# gsettings and allows adjusting settings of other applications. - -# dbus -connect -getsockname -recvmsg -send -sendto -sendmsg -socket -` - // NewGsettingsInterface returns a new "gsettings" interface. func NewGsettingsInterface() interfaces.Interface { return &commonInterface{ name: "gsettings", connectedPlugAppArmor: gsettingsConnectedPlugAppArmor, - connectedPlugSecComp: gsettingsConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/hardware_observe.go snapd-2.23.1+16.10/interfaces/builtin/hardware_observe.go --- snapd-2.22.6+16.10/interfaces/builtin/hardware_observe.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/hardware_observe.go 2017-03-02 06:37:54.000000000 +0000 @@ -23,28 +23,36 @@ "github.com/snapcore/snapd/interfaces" ) -// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/log-observe const hardwareObserveConnectedPlugAppArmor = ` # Description: This interface allows for getting hardware information -# from the system. this is reserved because it allows reading potentially sensitive information. -# Usage: reserved +# from the system. This is reserved because it allows reading potentially +# sensitive information. -# used by lscpu +# used by lscpu and 'lspci -A intel-conf1/intel-conf2' capability sys_rawio, -# files in /sys pertaining to hardware +# used by lspci +capability sys_admin, +/etc/modprobe.d/{,*} r, + +# files in /sys pertaining to hardware (eg, 'lspci -A linux-sysfs') /sys/{block,bus,class,devices,firmware}/{,**} r, +# files in /proc/bus/pci (eg, 'lspci -A linux-proc') +@{PROC}/bus/pci/{,**} r, + # DMI tables /sys/firmware/dmi/tables/DMI r, /sys/firmware/dmi/tables/smbios_entry_point r, +# interrupts +@{PROC}/interrupts r, + # Needed for udevadm /run/udev/data/** r, # util-linux /{,usr/}bin/lscpu ixr, -@{PROC}/bus/pci/devices r, # lsusb # Note: lsusb and its database have to be shipped in the snap if not on classic @@ -53,6 +61,23 @@ /dev/ r, /dev/bus/usb/{,**/} r, /etc/udev/udev.conf r, + +# lshw -quiet (note, lshw also tries to create /dev/fb-*, but fails gracefully) +@{PROC}/devices r, +@{PROC}/ide/{,**} r, +@{PROC}/scsi/{,**} r, +@{PROC}/device-tree/{,**} r, +/sys/kernel/debug/usb/devices r, +@{PROC}/sys/abi/{,*} r, +` + +const hardwareObserveConnectedPlugSecComp = ` +# Description: This interface allows for getting hardware information +# from the system. This is reserved because it allows reading potentially +# sensitive information. + +# used by 'lspci -A intel-conf1/intel-conf2' +iopl ` // NewHardwareObserveInterface returns a new "hardware-observe" interface. @@ -60,6 +85,7 @@ return &commonInterface{ name: "hardware-observe", connectedPlugAppArmor: hardwareObserveConnectedPlugAppArmor, + connectedPlugSecComp: hardwareObserveConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/hardware_observe_test.go snapd-2.23.1+16.10/interfaces/builtin/hardware_observe_test.go --- snapd-2.22.6+16.10/interfaces/builtin/hardware_observe_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/hardware_observe_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -25,6 +25,7 @@ "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/builtin" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" ) type HardwareObserveInterfaceSuite struct { @@ -83,4 +84,10 @@ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) + c.Check(string(snippet), testutil.Contains, "capability sys_rawio,\n") + // connected plugs have a non-nil security snippet for seccomp + snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + c.Check(string(snippet), testutil.Contains, "iopl\n") } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/home.go snapd-2.23.1+16.10/interfaces/builtin/home.go --- snapd-2.22.6+16.10/interfaces/builtin/home.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/home.go 2017-03-02 06:37:54.000000000 +0000 @@ -27,7 +27,6 @@ const homeConnectedPlugAppArmor = ` # Description: Can access non-hidden files in user's $HOME. This is restricted # because it gives file access to all of the user's $HOME. -# Usage: reserved # Note, @{HOME} is the user's $HOME, not the snap's $HOME diff -Nru snapd-2.22.6+16.10/interfaces/builtin/iio.go snapd-2.23.1+16.10/interfaces/builtin/iio.go --- snapd-2.22.6+16.10/interfaces/builtin/iio.go 2017-02-14 15:14:13.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/iio.go 2017-03-06 13:33:50.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -29,13 +29,13 @@ "github.com/snapcore/snapd/interfaces" ) -var iioConnectedPlugAppArmor = []byte(` +const iioConnectedPlugAppArmor = ` # Description: Give access to a specific IIO device on the system. ###IIO_DEVICE_PATH### rw, /sys/bus/iio/devices/###IIO_DEVICE_NAME###/ r, /sys/bus/iio/devices/###IIO_DEVICE_NAME###/** rwk, -`) +` // The type for iio interface type IioInterface struct{} @@ -105,7 +105,7 @@ switch securitySystem { case interfaces.SecurityAppArmor: cleanedPath := filepath.Clean(path) - snippet := bytes.Replace(iioConnectedPlugAppArmor, []byte("###IIO_DEVICE_PATH###"), []byte(cleanedPath), -1) + snippet := bytes.Replace([]byte(iioConnectedPlugAppArmor), []byte("###IIO_DEVICE_PATH###"), []byte(cleanedPath), -1) // The path is already verified against a regular expression // in SanitizeSlot so we can rely on its structure here and diff -Nru snapd-2.22.6+16.10/interfaces/builtin/io_ports_control.go snapd-2.23.1+16.10/interfaces/builtin/io_ports_control.go --- snapd-2.22.6+16.10/interfaces/builtin/io_ports_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/io_ports_control.go 2017-03-08 13:28:17.000000000 +0000 @@ -32,7 +32,7 @@ capability sys_rawio, # required by iopl -/dev/ports rw, +/dev/port rw, ` const ioPortsControlConnectedPlugSecComp = ` @@ -97,7 +97,7 @@ case interfaces.SecurityUDev: var tagSnippet bytes.Buffer - const udevRule = `KERNEL=="ports", TAG+="%s"` + const udevRule = `KERNEL=="port", TAG+="%s"` for appName := range plug.Apps { tag := udevSnapSecurityName(plug.Snap.Name(), appName) tagSnippet.WriteString(fmt.Sprintf(udevRule, tag)) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/io_ports_control_test.go snapd-2.23.1+16.10/interfaces/builtin/io_ports_control_test.go --- snapd-2.22.6+16.10/interfaces/builtin/io_ports_control_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/io_ports_control_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -97,7 +97,7 @@ capability sys_rawio, # required by iopl -/dev/ports rw, +/dev/port rw, `) expectedSnippet2 := []byte(` @@ -110,7 +110,7 @@ iopl `) - expectedSnippet3 := []byte(`KERNEL=="ports", TAG+="snap_client-snap_app-accessing-io-ports" + expectedSnippet3 := []byte(`KERNEL=="port", TAG+="snap_client-snap_app-accessing-io-ports" `) // connected plugs have a non-nil security snippet for apparmor diff -Nru snapd-2.22.6+16.10/interfaces/builtin/kernel_module_control.go snapd-2.23.1+16.10/interfaces/builtin/kernel_module_control.go --- snapd-2.22.6+16.10/interfaces/builtin/kernel_module_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/kernel_module_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -29,9 +29,13 @@ capability sys_module, @{PROC}/modules r, - # NOTE: needed by lscpu. In the future this may be moved to system-trace or - # system-observe. + # FIXME: moved to physical-memory-observe (remove this in series 18) /dev/mem r, + + # Required to use SYSLOG_ACTION_READ_ALL and SYSLOG_ACTION_SIZE_BUFFER when + # /proc/sys/kernel/dmesg_restrict is '1' (syslog(2)). These operations are + # required to verify kernel modules that are loaded. + capability syslog, ` const kernelModuleControlConnectedPlugSecComp = ` diff -Nru snapd-2.22.6+16.10/interfaces/builtin/libvirt.go snapd-2.23.1+16.10/interfaces/builtin/libvirt.go --- snapd-2.22.6+16.10/interfaces/builtin/libvirt.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/libvirt.go 2017-03-02 06:37:54.000000000 +0000 @@ -27,17 +27,9 @@ ` const libvirtConnectedPlugSecComp = ` -connect -getsockname -recv -recvmsg -send -sendto -sendmsg -socket -socketpair listen accept +accept4 ` func NewLibvirtInterface() interfaces.Interface { diff -Nru snapd-2.22.6+16.10/interfaces/builtin/locale_control.go snapd-2.23.1+16.10/interfaces/builtin/locale_control.go --- snapd-2.22.6+16.10/interfaces/builtin/locale_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/locale_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -26,7 +26,6 @@ // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/locale-control const localeControlConnectedPlugAppArmor = ` # Description: Can manage locales directly separate from 'config ubuntu-core'. -# Usage: reserved # TODO: this won't work until snappy exposes this configurability /etc/default/locale rw, diff -Nru snapd-2.22.6+16.10/interfaces/builtin/location_control.go snapd-2.23.1+16.10/interfaces/builtin/location_control.go --- snapd-2.22.6+16.10/interfaces/builtin/location_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/location_control.go 2017-03-06 13:33:50.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -25,10 +25,9 @@ "github.com/snapcore/snapd/interfaces" ) -var locationControlPermanentSlotAppArmor = []byte(` -# Description: Allow operating as the location service. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const locationControlPermanentSlotAppArmor = ` +# Description: Allow operating as the location service. This gives privileged +# access to the system. # DBus accesses #include @@ -56,9 +55,9 @@ path=/com/ubuntu/location/Service{,/**} interface=org.freedesktop.DBus** peer=(label=unconfined), -`) +` -var locationControlConnectedSlotAppArmor = []byte(` +const locationControlConnectedSlotAppArmor = ` # Allow connected clients to interact with the service # Allow clients to register providers @@ -83,12 +82,11 @@ interface=org.freedesktop.DBus.Properties member=PropertiesChanged peer=(label=###PLUG_SECURITY_TAGS###), -`) +` -var locationControlConnectedPlugAppArmor = []byte(` -# Description: Allow using location service. Reserved because this gives -# privileged access to the service. -# Usage: reserved +const locationControlConnectedPlugAppArmor = ` +# Description: Allow using location service. This gives privileged access to +# the service. #include @@ -120,37 +118,23 @@ path=/ interface=org.freedesktop.DBus.ObjectManager peer=(label=unconfined), -`) +` -var locationControlPermanentSlotSecComp = []byte(` -getsockname -recvmsg -sendmsg -sendto -`) - -var locationControlConnectedPlugSecComp = []byte(` -getsockname -recvmsg -sendmsg -sendto -`) - -var locationControlPermanentSlotDBus = []byte(` +const locationControlPermanentSlotDBus = ` -`) +` -var locationControlConnectedPlugDBus = []byte(` +const locationControlConnectedPlugDBus = ` -`) +` type LocationControlInterface struct{} @@ -167,12 +151,10 @@ case interfaces.SecurityAppArmor: old := []byte("###SLOT_SECURITY_TAGS###") new := slotAppLabelExpr(slot) - snippet := bytes.Replace(locationControlConnectedPlugAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(locationControlConnectedPlugAppArmor), old, new, -1) return snippet, nil case interfaces.SecurityDBus: - return locationControlConnectedPlugDBus, nil - case interfaces.SecuritySecComp: - return locationControlConnectedPlugSecComp, nil + return []byte(locationControlConnectedPlugDBus), nil } return nil, nil } @@ -180,11 +162,9 @@ func (iface *LocationControlInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return locationControlPermanentSlotAppArmor, nil + return []byte(locationControlPermanentSlotAppArmor), nil case interfaces.SecurityDBus: - return locationControlPermanentSlotDBus, nil - case interfaces.SecuritySecComp: - return locationControlPermanentSlotSecComp, nil + return []byte(locationControlPermanentSlotDBus), nil } return nil, nil } @@ -194,7 +174,7 @@ case interfaces.SecurityAppArmor: old := []byte("###PLUG_SECURITY_TAGS###") new := plugAppLabelExpr(plug) - snippet := bytes.Replace(locationControlConnectedSlotAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(locationControlConnectedSlotAppArmor), old, new, -1) return snippet, nil } return nil, nil diff -Nru snapd-2.22.6+16.10/interfaces/builtin/location_control_test.go snapd-2.23.1+16.10/interfaces/builtin/location_control_test.go --- snapd-2.22.6+16.10/interfaces/builtin/location_control_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/location_control_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -177,17 +177,13 @@ } func (s *LocationControlInterfaceSuite) TestUsedSecuritySystems(c *C) { - systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor, - interfaces.SecuritySecComp, interfaces.SecurityDBus} - for _, system := range systems { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - snippet, err = s.iface.PermanentSlotSnippet(s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - } - snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/location_observe.go snapd-2.23.1+16.10/interfaces/builtin/location_observe.go --- snapd-2.22.6+16.10/interfaces/builtin/location_observe.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/location_observe.go 2017-03-06 13:33:50.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -25,10 +25,9 @@ "github.com/snapcore/snapd/interfaces" ) -var locationObservePermanentSlotAppArmor = []byte(` -# Description: Allow operating as the location service. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const locationObservePermanentSlotAppArmor = ` +# Description: Allow operating as the location service. This gives privileged +# access to the system. # DBus accesses #include @@ -56,9 +55,9 @@ path=/com/ubuntu/location/Service{,/**} interface=org.freedesktop.DBus** peer=(label=unconfined), -`) +` -var locationObserveConnectedSlotAppArmor = []byte(` +const locationObserveConnectedSlotAppArmor = ` # Allow connected clients to interact with the service # Allow the service to host sessions @@ -118,12 +117,11 @@ interface=org.freedesktop.DBus.Properties member=PropertiesChanged peer=(label=###PLUG_SECURITY_TAGS###), -`) +` -var locationObserveConnectedPlugAppArmor = []byte(` -# Description: Allow using location service. Reserved because this gives -# privileged access to the service. -# Usage: reserved +const locationObserveConnectedPlugAppArmor = ` +# Description: Allow using location service. This gives privileged access to +# the service. #include @@ -185,23 +183,9 @@ path=/ interface=org.freedesktop.DBus.ObjectManager peer=(label=unconfined), -`) +` -var locationObservePermanentSlotSecComp = []byte(` -getsockname -recvmsg -sendmsg -sendto -`) - -var locationObserveConnectedPlugSecComp = []byte(` -getsockname -recvmsg -sendmsg -sendto -`) - -var locationObservePermanentSlotDBus = []byte(` +const locationObservePermanentSlotDBus = ` @@ -210,9 +194,9 @@ -`) +` -var locationObserveConnectedPlugDBus = []byte(` +const locationObserveConnectedPlugDBus = ` @@ -220,7 +204,7 @@ -`) +` type LocationObserveInterface struct{} @@ -237,12 +221,10 @@ case interfaces.SecurityAppArmor: old := []byte("###SLOT_SECURITY_TAGS###") new := slotAppLabelExpr(slot) - snippet := bytes.Replace(locationObserveConnectedPlugAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(locationObserveConnectedPlugAppArmor), old, new, -1) return snippet, nil case interfaces.SecurityDBus: - return locationObserveConnectedPlugDBus, nil - case interfaces.SecuritySecComp: - return locationObserveConnectedPlugSecComp, nil + return []byte(locationObserveConnectedPlugDBus), nil default: return nil, nil } @@ -251,11 +233,9 @@ func (iface *LocationObserveInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return locationObservePermanentSlotAppArmor, nil + return []byte(locationObservePermanentSlotAppArmor), nil case interfaces.SecurityDBus: - return locationObservePermanentSlotDBus, nil - case interfaces.SecuritySecComp: - return locationObservePermanentSlotSecComp, nil + return []byte(locationObservePermanentSlotDBus), nil default: return nil, nil } @@ -266,7 +246,7 @@ case interfaces.SecurityAppArmor: old := []byte("###PLUG_SECURITY_TAGS###") new := plugAppLabelExpr(plug) - snippet := bytes.Replace(locationObserveConnectedSlotAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(locationObserveConnectedSlotAppArmor), old, new, -1) return snippet, nil default: return nil, nil diff -Nru snapd-2.22.6+16.10/interfaces/builtin/location_observe_test.go snapd-2.23.1+16.10/interfaces/builtin/location_observe_test.go --- snapd-2.22.6+16.10/interfaces/builtin/location_observe_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/location_observe_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -177,17 +177,13 @@ } func (s *LocationObserveInterfaceSuite) TestUsedSecuritySystems(c *C) { - systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor, - interfaces.SecuritySecComp, interfaces.SecurityDBus} - for _, system := range systems { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - snippet, err = s.iface.PermanentSlotSnippet(s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - } - snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/log_observe.go snapd-2.23.1+16.10/interfaces/builtin/log_observe.go --- snapd-2.22.6+16.10/interfaces/builtin/log_observe.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/log_observe.go 2017-03-02 06:37:54.000000000 +0000 @@ -26,7 +26,6 @@ // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/log-observe const logObserveConnectedPlugAppArmor = ` # Description: Can read system logs and set kernel log rate-limiting -# Usage: reserved /var/log/ r, /var/log/** r, diff -Nru snapd-2.22.6+16.10/interfaces/builtin/mir.go snapd-2.23.1+16.10/interfaces/builtin/mir.go --- snapd-2.22.6+16.10/interfaces/builtin/mir.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/mir.go 2017-03-08 13:28:17.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (c) 2016 Canonical Ltd + * Copyright (c) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -25,68 +25,49 @@ "github.com/snapcore/snapd/interfaces" ) -var mirPermanentSlotAppArmor = []byte(` -# Description: Allow operating as the Mir server. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const mirPermanentSlotAppArmor = ` +# Description: Allow operating as the Mir server. This gives privileged access +# to the system. + # needed since Mir is the display server, to configure tty devices capability sys_tty_config, -/{dev,run}/shm/\#* rw, /dev/tty[0-9]* rw, -network netlink raw, + +/{dev,run}/shm/\#* rw, /run/mir_socket rw, -#NOTE: this allows reading and inserting all input events + +# NOTE: this allows reading and inserting all input events /dev/input/* rw, + +# For using udev +network netlink raw, /run/udev/data/c13:[0-9]* r, /run/udev/data/+input:input[0-9]* r, -`) +` -var mirPermanentSlotSecComp = []byte(` -# Description: Allow operating as the mir server. Reserved because this -# gives privileged access to the system. +const mirPermanentSlotSecComp = ` +# Description: Allow operating as the mir server. This gives privileged access +# to the system. # Needed for server launch bind listen -setsockopt -getsockname # Needed by server upon client connect -send -sendto -sendmsg accept +accept4 shmctl -open -getsockopt -recv -recvmsg -recvfrom -`) +` -var mirConnectedSlotAppArmor = []byte(` +const mirConnectedSlotAppArmor = ` # Description: Permit clients to use Mir -# Usage: reserved unix (receive, send) type=seqpacket addr=none peer=(label=###PLUG_SECURITY_TAGS###), -`) +` -var mirConnectedPlugAppArmor = []byte(` +const mirConnectedPlugAppArmor = ` # Description: Permit clients to use Mir -# Usage: common unix (receive, send) type=seqpacket addr=none peer=(label=###SLOT_SECURITY_TAGS###), /run/mir_socket rw, /run/user/[0-9]*/mir_socket rw, -`) - -var mirConnectedPlugSecComp = []byte(` -# Description: Permit clients to use Mir -# Usage: common -recv -recvfrom -recvmsg -send -sendto -sendmsg - -`) +` type MirInterface struct{} @@ -103,10 +84,8 @@ case interfaces.SecurityAppArmor: old := []byte("###SLOT_SECURITY_TAGS###") new := slotAppLabelExpr(slot) - snippet := bytes.Replace(mirConnectedPlugAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(mirConnectedPlugAppArmor), old, new, -1) return snippet, nil - case interfaces.SecuritySecComp: - return mirConnectedPlugSecComp, nil } return nil, nil } @@ -116,9 +95,9 @@ securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return mirPermanentSlotAppArmor, nil + return []byte(mirPermanentSlotAppArmor), nil case interfaces.SecuritySecComp: - return mirPermanentSlotSecComp, nil + return []byte(mirPermanentSlotSecComp), nil } return nil, nil } @@ -128,7 +107,7 @@ case interfaces.SecurityAppArmor: old := []byte("###PLUG_SECURITY_TAGS###") new := plugAppLabelExpr(plug) - snippet := bytes.Replace(mirConnectedSlotAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(mirConnectedSlotAppArmor), old, new, -1) return snippet, nil } return nil, nil diff -Nru snapd-2.22.6+16.10/interfaces/builtin/mir_test.go snapd-2.23.1+16.10/interfaces/builtin/mir_test.go --- snapd-2.22.6+16.10/interfaces/builtin/mir_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/mir_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -62,13 +62,13 @@ snippet, err := s.iface.PermanentSlotSnippet(s.slot, system) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) if system != interfaces.SecuritySecComp { snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, system) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) } } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/modem_manager.go snapd-2.23.1+16.10/interfaces/builtin/modem_manager.go --- snapd-2.22.6+16.10/interfaces/builtin/modem_manager.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/modem_manager.go 2017-03-08 13:28:17.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -26,10 +26,9 @@ "github.com/snapcore/snapd/release" ) -var modemManagerPermanentSlotAppArmor = []byte(` -# Description: Allow operating as the ModemManager service. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const modemManagerPermanentSlotAppArmor = ` +# Description: Allow operating as the ModemManager service. This gives +# privileged access to the system. # To check present devices /run/udev/data/* r, @@ -80,9 +79,9 @@ path=/org/freedesktop/ModemManager1{,/**} interface=org.freedesktop.DBus.* peer=(label=unconfined), -`) +` -var modemManagerConnectedSlotAppArmor = []byte(` +const modemManagerConnectedSlotAppArmor = ` # Allow connected clients to interact with the service # Allow traffic to/from our path and interface with any method @@ -98,12 +97,11 @@ path=/org/freedesktop/ModemManager1{,/**} interface=org.freedesktop.DBus.* peer=(label=###PLUG_SECURITY_TAGS###), -`) +` -var modemManagerConnectedPlugAppArmor = []byte(` -# Description: Allow using ModemManager service. Reserved because this gives -# privileged access to the ModemManager service. -# Usage: reserved +const modemManagerConnectedPlugAppArmor = ` +# Description: Allow using ModemManager service. This gives privileged access +# to the ModemManager service. #include @@ -118,9 +116,9 @@ path=/org/freedesktop/ModemManager1{,/**} interface=org.freedesktop.DBus.* peer=(label=###SLOT_SECURITY_TAGS###), -`) +` -var modemManagerConnectedPlugAppArmorClassic = []byte(` +const modemManagerConnectedPlugAppArmorClassic = ` # Allow access to the unconfined ModemManager service on classic. dbus (receive, send) bus=system @@ -132,67 +130,35 @@ path=/org/freedesktop/ModemManager1{,/**} interface=org.freedesktop.DBus.* peer=(label=unconfined), -`) +` + +const modemManagerPermanentSlotSecComp = ` +# Description: Allow operating as the ModemManager service. This gives +# privileged access to the system. -var modemManagerPermanentSlotSecComp = []byte(` -# Description: Allow operating as the ModemManager service. Reserved because this -# gives privileged access to the system. -# Usage: reserved # TODO: add ioctl argument filters when seccomp arg filtering is implemented accept accept4 bind -connect -getpeername -getsockname -getsockopt listen -recv -recvfrom -recvmmsg -recvmsg -send -sendmmsg -sendmsg -sendto -setsockopt shutdown -socketpair -socket -`) - -var modemManagerConnectedPlugSecComp = []byte(` -# Description: Allow using ModemManager service. Reserved because this gives -# privileged access to the ModemManager service. -# Usage: reserved - -# Can communicate with DBus system service -connect -getsockname -recv -recvmsg -recvfrom -send -sendto -sendmsg -socket -`) +` -var modemManagerPermanentSlotDBus = []byte(` +const modemManagerPermanentSlotDBus = ` -`) +` -var modemManagerConnectedPlugDBus = []byte(` +const modemManagerConnectedPlugDBus = ` -`) +` -var modemManagerPermanentSlotUdev = []byte(` +const modemManagerPermanentSlotUdev = ` # Concatenation of all ModemManager udev rules # do not edit this file, it will be overwritten on update @@ -1181,7 +1147,7 @@ KERNEL=="cdc-wdm*", SUBSYSTEM=="usbmisc", ENV{ID_MM_CANDIDATE}="1" LABEL="mm_candidate_end" -`) +` type ModemManagerInterface struct{} @@ -1206,8 +1172,6 @@ snippet = append(snippet, modemManagerConnectedPlugAppArmorClassic...) } return snippet, nil - case interfaces.SecuritySecComp: - return modemManagerConnectedPlugSecComp, nil } return nil, nil } @@ -1215,13 +1179,13 @@ func (iface *ModemManagerInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return modemManagerPermanentSlotAppArmor, nil + return []byte(modemManagerPermanentSlotAppArmor), nil case interfaces.SecuritySecComp: - return modemManagerPermanentSlotSecComp, nil + return []byte(modemManagerPermanentSlotSecComp), nil case interfaces.SecurityUDev: - return modemManagerPermanentSlotUdev, nil + return []byte(modemManagerPermanentSlotUdev), nil case interfaces.SecurityDBus: - return modemManagerPermanentSlotDBus, nil + return []byte(modemManagerPermanentSlotDBus), nil } return nil, nil } @@ -1231,7 +1195,7 @@ case interfaces.SecurityAppArmor: old := []byte("###PLUG_SECURITY_TAGS###") new := plugAppLabelExpr(plug) - snippet := bytes.Replace(modemManagerConnectedSlotAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(modemManagerConnectedSlotAppArmor), old, new, -1) return snippet, nil } return nil, nil diff -Nru snapd-2.22.6+16.10/interfaces/builtin/modem_manager_test.go snapd-2.23.1+16.10/interfaces/builtin/modem_manager_test.go --- snapd-2.22.6+16.10/interfaces/builtin/modem_manager_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/modem_manager_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -135,22 +135,25 @@ } func (s *ModemManagerInterfaceSuite) TestUsedSecuritySystems(c *C) { - systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor, - interfaces.SecuritySecComp} - for _, system := range systems { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - snippet, err = s.iface.PermanentSlotSnippet(s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - } - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus) + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + + snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityDBus) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) + + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecuritySecComp) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityUDev) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/mount_observe.go snapd-2.23.1+16.10/interfaces/builtin/mount_observe.go --- snapd-2.22.6+16.10/interfaces/builtin/mount_observe.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/mount_observe.go 2017-03-02 06:37:54.000000000 +0000 @@ -25,10 +25,9 @@ // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/mount-observe const mountObserveConnectedPlugAppArmor = ` -# Description: Can query system mount information. This is restricted because -# it gives privileged read access to mount arguments and should only be used -# with trusted apps. -# Usage: reserved +# Description: Can query system mount and disk quota information. This is +# restricted because it gives privileged read access to mount arguments and +# should only be used with trusted apps. /{,usr/}bin/df ixr, @@ -45,11 +44,26 @@ /etc/fstab r, ` +const mountObserveConnectedPlugSecComp = ` +# Description: Can query system mount and disk quota information. This is +# restricted because it gives privileged read access to mount arguments and +# should only be used with trusted apps. + +# FIXME: restore quotactl with parameter filtering once snap-confine can read +# this syntax. See LP:#1662489 for context. +#quotactl Q_GETQUOTA - - - +#quotactl Q_GETINFO - - - +#quotactl Q_GETFMT - - - +#quotactl Q_XGETQUOTA - - - +#quotactl Q_XGETQSTAT - - - +` + // NewMountObserveInterface returns a new "mount-observe" interface. func NewMountObserveInterface() interfaces.Interface { return &commonInterface{ name: "mount-observe", connectedPlugAppArmor: mountObserveConnectedPlugAppArmor, + connectedPlugSecComp: mountObserveConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/mpris.go snapd-2.23.1+16.10/interfaces/builtin/mpris.go --- snapd-2.22.6+16.10/interfaces/builtin/mpris.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/mpris.go 2017-03-06 13:33:50.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -28,9 +28,8 @@ "github.com/snapcore/snapd/release" ) -var mprisPermanentSlotAppArmor = []byte(` +const mprisPermanentSlotAppArmor = ` # Description: Allow operating as an MPRIS player. -# Usage: common # DBus accesses #include @@ -74,9 +73,9 @@ bus=session path=/org/mpris/MediaPlayer2 peer=(label=@{profile_name}), -`) +` -var mprisConnectedSlotAppArmor = []byte(` +const mprisConnectedSlotAppArmor = ` # Allow connected clients to interact with the player dbus (receive) bus=session @@ -93,9 +92,9 @@ interface="org.mpris.MediaPlayer2{,.*}" path=/org/mpris/MediaPlayer2 peer=(label=###PLUG_SECURITY_TAGS###), -`) +` -var mprisConnectedSlotAppArmorClassic = []byte(` +const mprisConnectedSlotAppArmorClassic = ` # Allow unconfined clients to interact with the player on classic dbus (receive) bus=session @@ -105,11 +104,10 @@ bus=session interface=org.freedesktop.DBus.Introspectable peer=(label=unconfined), -`) +` -var mprisConnectedPlugAppArmor = []byte(` +const mprisConnectedPlugAppArmor = ` # Description: Allow connecting to an MPRIS player. -# Usage: common #include @@ -137,21 +135,7 @@ bus=session path=/org/mpris/MediaPlayer2 peer=(label=###SLOT_SECURITY_TAGS###), -`) - -var mprisPermanentSlotSecComp = []byte(` -getsockname -recvmsg -sendmsg -sendto -`) - -var mprisConnectedPlugSecComp = []byte(` -getsockname -recvmsg -sendmsg -sendto -`) +` type MprisInterface struct{} @@ -168,10 +152,8 @@ case interfaces.SecurityAppArmor: old := []byte("###SLOT_SECURITY_TAGS###") new := slotAppLabelExpr(slot) - snippet := bytes.Replace(mprisConnectedPlugAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(mprisConnectedPlugAppArmor), old, new, -1) return snippet, nil - case interfaces.SecuritySecComp: - return mprisConnectedPlugSecComp, nil } return nil, nil } @@ -186,15 +168,13 @@ old := []byte("###MPRIS_NAME###") new := []byte(name) - snippet := bytes.Replace(mprisPermanentSlotAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(mprisPermanentSlotAppArmor), old, new, -1) // on classic, allow unconfined remotes to control the player // (eg, indicator-sound) if release.OnClassic { snippet = append(snippet, mprisConnectedSlotAppArmorClassic...) } return snippet, nil - case interfaces.SecuritySecComp: - return mprisPermanentSlotSecComp, nil } return nil, nil } @@ -204,7 +184,7 @@ case interfaces.SecurityAppArmor: old := []byte("###PLUG_SECURITY_TAGS###") new := plugAppLabelExpr(plug) - snippet := bytes.Replace(mprisConnectedSlotAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(mprisConnectedSlotAppArmor), old, new, -1) return snippet, nil } return nil, nil diff -Nru snapd-2.22.6+16.10/interfaces/builtin/mpris_test.go snapd-2.23.1+16.10/interfaces/builtin/mpris_test.go --- snapd-2.22.6+16.10/interfaces/builtin/mpris_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/mpris_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -181,14 +181,6 @@ c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.{app1,app2}"),`) } -func (s *MprisInterfaceSuite) TestConnectedPlugSecComp(c *C) { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - - c.Check(string(snippet), testutil.Contains, "getsockname\n") -} - // The label uses short form when exactly one app is bound to the mpris slot func (s *MprisInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) { app := &snap.AppInfo{Name: "app"} @@ -320,26 +312,11 @@ c.Check(string(snippet), testutil.Contains, "# Allow unconfined clients to interact with the player on classic\n") } -func (s *MprisInterfaceSuite) TestPermanentSlotSecComp(c *C) { - snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecuritySecComp) +func (s *MprisInterfaceSuite) TestUsedSecuritySystems(c *C) { + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - - c.Check(string(snippet), testutil.Contains, "getsockname\n") -} - -func (s *MprisInterfaceSuite) TestUsedSecuritySystems(c *C) { - systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor, - interfaces.SecuritySecComp} - for _, system := range systems { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - snippet, err = s.iface.PermanentSlotSnippet(s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - } - snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/network_bind.go snapd-2.23.1+16.10/interfaces/builtin/network_bind.go --- snapd-2.22.6+16.10/interfaces/builtin/network_bind.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/network_bind.go 2017-03-02 06:37:54.000000000 +0000 @@ -26,7 +26,6 @@ // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/network-bind const networkBindConnectedPlugAppArmor = ` # Description: Can access the network as a server. -# Usage: common #include #include @@ -62,34 +61,11 @@ // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/network-bind const networkBindConnectedPlugSecComp = ` # Description: Can access the network as a server. -# Usage: common accept accept4 bind -connect -getpeername -getsockname -getsockopt listen -recv -recvfrom -recvmmsg -recvmsg -send -sendmmsg -sendmsg -sendto -setsockopt shutdown - -# LP: #1446748 - limit this to AF_INET/AF_INET6 -socket - -# This is an older interface and single entry point that can be used instead -# of socket(), bind(), connect(), etc individually. While we could allow it, -# we wouldn't be able to properly arg filter socketcall for AF_INET/AF_INET6 -# when LP: #1446748 is implemented. -socketcall ` // NewNetworkBindInterface returns a new "network-bind" interface. diff -Nru snapd-2.22.6+16.10/interfaces/builtin/network_control.go snapd-2.23.1+16.10/interfaces/builtin/network_control.go --- snapd-2.22.6+16.10/interfaces/builtin/network_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/network_control.go 2017-03-06 13:33:50.000000000 +0000 @@ -61,6 +61,10 @@ @{PROC}/sys/net/netfilter/** rw, @{PROC}/sys/net/nf_conntrack_max rw, +# read netfilter module parameters +/sys/module/nf_*/ r, +/sys/module/nf_*/parameters/{,*} r, + # networking tools /{,usr/}{,s}bin/arp ixr, /{,usr/}{,s}bin/arpd ixr, @@ -123,6 +127,9 @@ # route /etc/networks r, +/etc/ethers r, + +/etc/rpc r, # TUN/TAP /dev/net/tun rw, @@ -184,10 +191,6 @@ # network configuration files into /etc in that namespace. See man ip-netns(8) # for details. bind -sendmsg -sendto -recvfrom -recvmsg mount umount diff -Nru snapd-2.22.6+16.10/interfaces/builtin/network.go snapd-2.23.1+16.10/interfaces/builtin/network.go --- snapd-2.22.6+16.10/interfaces/builtin/network.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/network.go 2017-03-02 06:37:54.000000000 +0000 @@ -24,7 +24,6 @@ // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/network const networkConnectedPlugAppArmor = ` # Description: Can access the network as a client. -# Usage: common #include #include @@ -35,31 +34,8 @@ // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/network const networkConnectedPlugSecComp = ` # Description: Can access the network as a client. -# Usage: common bind -connect -getpeername -getsockname -getsockopt -recv -recvfrom -recvmmsg -recvmsg -send -sendmmsg -sendmsg -sendto -setsockopt shutdown - -# LP: #1446748 - limit this to AF_UNIX/AF_LOCAL and perhaps AF_NETLINK -socket - -# This is an older interface and single entry point that can be used instead -# of socket(), bind(), connect(), etc individually. While we could allow it, -# we wouldn't be able to properly arg filter socketcall for AF_INET/AF_INET6 -# when LP: #1446748 is implemented. -socketcall ` // NewNetworkInterface returns a new "network" interface. diff -Nru snapd-2.22.6+16.10/interfaces/builtin/network_manager.go snapd-2.23.1+16.10/interfaces/builtin/network_manager.go --- snapd-2.22.6+16.10/interfaces/builtin/network_manager.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/network_manager.go 2017-03-08 13:28:17.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -26,10 +26,9 @@ "github.com/snapcore/snapd/release" ) -var networkManagerPermanentSlotAppArmor = []byte(` -# Description: Allow operating as the NetworkManager service. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const networkManagerPermanentSlotAppArmor = ` +# Description: Allow operating as the NetworkManager service. This gives +# privileged access to the system. capability net_admin, capability net_bind_service, @@ -109,7 +108,7 @@ path=/org/freedesktop/DBus interface=org.freedesktop.DBus member={Request,Release}Name - peer=(name=org.freedesktop.DBus), + peer=(name=org.freedesktop.DBus, label=unconfined), dbus (receive, send) bus=system @@ -130,17 +129,20 @@ bus=system name="org.freedesktop.NetworkManager", -# Allow traffic to/from our path and interface with any method +# Allow traffic to/from our path and interface with any method for unconfined +# clients to talk to our service. dbus (receive, send) bus=system path=/org/freedesktop/NetworkManager{,/**} - interface=org.freedesktop.NetworkManager*, + interface=org.freedesktop.NetworkManager* + peer=(label=unconfined), # Allow traffic to/from org.freedesktop.DBus for NetworkManager service dbus (receive, send) bus=system path=/org/freedesktop/NetworkManager{,/**} - interface=org.freedesktop.DBus.*, + interface=org.freedesktop.DBus.* + peer=(label=unconfined), # Allow access to hostname system service dbus (receive, send) @@ -168,6 +170,12 @@ member=PrepareForSleep interface=org.freedesktop.login1.Manager peer=(label=unconfined), +dbus (receive) + bus=system + path=/org/freedesktop/login1 + interface=org.freedesktop.login1.Manager + member=Session{New,Removed} + peer=(label=unconfined), # Allow access to wpa-supplicant for managing WiFi networks dbus (receive, send) @@ -180,12 +188,21 @@ path=/fi/w1/wpa_supplicant1{,/**} interface=org.freedesktop.DBus.* peer=(label=unconfined), -`) +` + +const networkManagerConnectedSlotAppArmor = ` +# Allow connected clients to interact with the service -var networkManagerConnectedPlugAppArmor = []byte(` -# Description: Allow using NetworkManager service. Reserved because this gives -# privileged access to the NetworkManager service. -# Usage: reserved +# Allow traffic to/from our DBus path +dbus (receive, send) + bus=system + path=/org/freedesktop/NetworkManager{,/**} + peer=(label=###PLUG_SECURITY_TAGS###), +` + +const networkManagerConnectedPlugAppArmor = ` +# Description: Allow using NetworkManager service. This gives privileged access +# to the NetworkManager service. #include @@ -194,69 +211,20 @@ bus=system path=/org/freedesktop/NetworkManager{,/**} peer=(label=###SLOT_SECURITY_TAGS###), -`) +` -var networkManagerPermanentSlotSecComp = []byte(` -# Description: Allow operating as the NetworkManager service. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const networkManagerPermanentSlotSecComp = ` +# Description: Allow operating as the NetworkManager service. This gives +# privileged access to the system. accept accept4 bind -connect -getpeername -getsockname -getsockopt listen -recv -recvfrom -recvmmsg -recvmsg -send -sendmmsg -sendmsg -sendto -setsockopt sethostname shutdown -socketpair -socket -# Needed for keyfile settings plugin to allow adding settings -# for different users. This is currently at runtime only used -# to make new created network settings files only editable by -# root:root. The existence of this chown call is only that its -# used for some tests where a different user:group combination -# will be supplied. -# FIXME: adjust after seccomp argument filtering lands so that -# we only allow chown and its variant to be called for root:root -# and nothign else (LP: #1446748) -chown -chown32 -fchown -fchown32 -fchownat -lchown -lchown32 -`) - -var networkManagerConnectedPlugSecComp = []byte(` -# Description: Allow using NetworkManager service. Reserved because this gives -# privileged access to the NetworkManager service. -# Usage: reserved - -# Can communicate with DBus system service -connect -getsockname -recv -recvmsg -recvfrom -send -sendto -sendmsg -socket -`) +` -var networkManagerPermanentSlotDBus = []byte(` +const networkManagerPermanentSlotDBus = ` @@ -398,7 +366,7 @@ 1024 2048 -`) +` type NetworkManagerInterface struct{} @@ -424,10 +392,8 @@ } else { new = slotAppLabelExpr(slot) } - snippet := bytes.Replace(networkManagerConnectedPlugAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(networkManagerConnectedPlugAppArmor), old, new, -1) return snippet, nil - case interfaces.SecuritySecComp: - return networkManagerConnectedPlugSecComp, nil } return nil, nil } @@ -435,16 +401,23 @@ func (iface *NetworkManagerInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return networkManagerPermanentSlotAppArmor, nil + return []byte(networkManagerPermanentSlotAppArmor), nil case interfaces.SecuritySecComp: - return networkManagerPermanentSlotSecComp, nil + return []byte(networkManagerPermanentSlotSecComp), nil case interfaces.SecurityDBus: - return networkManagerPermanentSlotDBus, nil + return []byte(networkManagerPermanentSlotDBus), nil } return nil, nil } func (iface *NetworkManagerInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityAppArmor: + old := []byte("###PLUG_SECURITY_TAGS###") + new := plugAppLabelExpr(plug) + snippet := bytes.Replace([]byte(networkManagerConnectedSlotAppArmor), old, new, -1) + return snippet, nil + } return nil, nil } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/network_manager_test.go snapd-2.23.1+16.10/interfaces/builtin/network_manager_test.go --- snapd-2.22.6+16.10/interfaces/builtin/network_manager_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/network_manager_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -128,21 +128,28 @@ c.Assert(string(snippet), testutil.Contains, "peer=(label=unconfined),") } +func (s *NetworkManagerInterfaceSuite) TestConnectedSlotSnippetAppArmor(c *C) { + snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + + c.Check(string(snippet), testutil.Contains, "peer=(label=\"snap.network-manager.*\")") +} + func (s *NetworkManagerInterfaceSuite) TestUsedSecuritySystems(c *C) { - systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor, - interfaces.SecuritySecComp} - for _, system := range systems { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - snippet, err = s.iface.PermanentSlotSnippet(s.slot, system) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - } - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus) + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityDBus) c.Assert(err, IsNil) c.Assert(snippet, IsNil) snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityDBus) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecuritySecComp) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/network_observe.go snapd-2.23.1+16.10/interfaces/builtin/network_observe.go --- snapd-2.22.6+16.10/interfaces/builtin/network_observe.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/network_observe.go 2017-03-06 13:33:50.000000000 +0000 @@ -28,7 +28,6 @@ # Description: Can query network status information. This is restricted because # it gives privileged read-only access to networking information and should # only be used with trusted apps. -# Usage: reserved # network-monitor can't allow this otherwise we are basically # network-management, but don't explicitly deny since someone might try to use @@ -85,6 +84,9 @@ # route /etc/networks r, +/etc/ethers r, + +/etc/rpc r, # network devices /sys/devices/**/net/** r, @@ -95,7 +97,6 @@ # Description: Can query network status information. This is restricted because # it gives privileged read-only access to networking information and should # only be used with trusted apps. -# Usage: reserved # for ping and ping6 capset diff -Nru snapd-2.22.6+16.10/interfaces/builtin/network_setup_control.go snapd-2.23.1+16.10/interfaces/builtin/network_setup_control.go --- snapd-2.22.6+16.10/interfaces/builtin/network_setup_control.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/network_setup_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,40 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "github.com/snapcore/snapd/interfaces" +) + +const networkSetupControlConnectedPlugAppArmor = ` +# Description: Can read/write netplan configuration files + +/etc/netplan/{,**} rw, +/etc/network/{,**} rw, +` + +// NewNetworkSetupControlInterface returns a new "network-setup-control" interface. +func NewNetworkSetupControlInterface() interfaces.Interface { + return &commonInterface{ + name: "network-setup-control", + connectedPlugAppArmor: networkSetupControlConnectedPlugAppArmor, + reservedForOS: true, + } +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/network_setup_control_test.go snapd-2.23.1+16.10/interfaces/builtin/network_setup_control_test.go --- snapd-2.22.6+16.10/interfaces/builtin/network_setup_control_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/network_setup_control_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,86 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" +) + +type NetworkSetupControlInterfaceSuite struct { + iface interfaces.Interface + slot *interfaces.Slot + plug *interfaces.Plug +} + +var _ = Suite(&NetworkSetupControlInterfaceSuite{ + iface: builtin.NewNetworkSetupControlInterface(), + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, + Name: "network-setup-control", + Interface: "network-setup-control", + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "other"}, + Name: "network-setup-control", + Interface: "network-setup-control", + }, + }, +}) + +func (s *NetworkSetupControlInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "network-setup-control") +} + +func (s *NetworkSetupControlInterfaceSuite) TestSanitizeSlot(c *C) { + err := s.iface.SanitizeSlot(s.slot) + c.Assert(err, IsNil) + err = s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "some-snap"}, + Name: "network-setup-control", + Interface: "network-setup-control", + }}) + c.Assert(err, ErrorMatches, "network-setup-control slots are reserved for the operating system snap") +} + +func (s *NetworkSetupControlInterfaceSuite) TestSanitizePlug(c *C) { + err := s.iface.SanitizePlug(s.plug) + c.Assert(err, IsNil) +} + +func (s *NetworkSetupControlInterfaceSuite) TestSanitizeIncorrectInterface(c *C) { + c.Assert(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) }, + PanicMatches, `slot is not of interface "network-setup-control"`) + c.Assert(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) }, + PanicMatches, `plug is not of interface "network-setup-control"`) +} + +func (s *NetworkSetupControlInterfaceSuite) TestUsedSecuritySystems(c *C) { + // connected plugs have a non-nil security snippet for apparmor + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/network_setup_observe.go snapd-2.23.1+16.10/interfaces/builtin/network_setup_observe.go --- snapd-2.22.6+16.10/interfaces/builtin/network_setup_observe.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/network_setup_observe.go 2017-03-02 06:37:54.000000000 +0000 @@ -25,7 +25,6 @@ const networkSetupObserveConnectedPlugAppArmor = ` # Description: Can read netplan configuration files -# Usage: reserved /etc/netplan/{,**} r, /etc/network/{,**} r, diff -Nru snapd-2.22.6+16.10/interfaces/builtin/ofono.go snapd-2.23.1+16.10/interfaces/builtin/ofono.go --- snapd-2.22.6+16.10/interfaces/builtin/ofono.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/ofono.go 2017-03-08 13:28:17.000000000 +0000 @@ -27,8 +27,8 @@ ) const ofonoPermanentSlotAppArmor = ` -# Description: Allow operating as the ofono service. Reserved because this -# gives privileged access to the system. +# Description: Allow operating as the ofono service. This gives privileged +# access to the system. # to create ppp network interfaces capability net_admin, @@ -109,8 +109,8 @@ ` const ofonoConnectedPlugAppArmor = ` -# Description: Allow using Ofono service. Reserved because this gives -# privileged access to the Ofono service. +# Description: Allow using Ofono service. This gives privileged access to the +# Ofono service. #include @@ -132,39 +132,17 @@ ` const ofonoPermanentSlotSecComp = ` -# Description: Allow operating as the ofono service. Reserved because this -# gives privileged access to the system. +# Description: Allow operating as the ofono service. This gives privileged +# access to the system. # Communicate with DBus, netlink, rild accept accept4 bind -getsockopt listen -recv -recvfrom -recvmmsg -recvmsg -send -sendmmsg -sendmsg -sendto shutdown ` -const ofonoConnectedPlugSecComp = ` -# Description: Allow using ofono service. Reserved because this gives -# privileged access to the ofono service. - -# Can communicate with DBus system service -recv -recvmsg -recvfrom -send -sendto -sendmsg -` - const ofonoPermanentSlotDBus = ` @@ -275,8 +253,6 @@ snippet = append(snippet, ofonoConnectedPlugAppArmorClassic...) } return snippet, nil - case interfaces.SecuritySecComp: - return []byte(ofonoConnectedPlugSecComp), nil } return nil, nil } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/ofono_test.go snapd-2.23.1+16.10/interfaces/builtin/ofono_test.go --- snapd-2.22.6+16.10/interfaces/builtin/ofono_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/ofono_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -140,14 +140,6 @@ c.Assert(string(snippet), Not(testutil.Contains), "peer=(label=unconfined),") } -func (s *OfonoInterfaceSuite) TestConnectedPlugSnippetSecComp(c *C) { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - - c.Check(string(snippet), testutil.Contains, "send\n") -} - func (s *OfonoInterfaceSuite) TestConnectedSlotSnippetAppArmor(c *C) { snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/opengl.go snapd-2.23.1+16.10/interfaces/builtin/opengl.go --- snapd-2.22.6+16.10/interfaces/builtin/opengl.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/opengl.go 2017-03-02 06:37:54.000000000 +0000 @@ -25,7 +25,6 @@ const openglConnectedPlugAppArmor = ` # Description: Can access opengl. -# Usage: reserved # specific gl libs /var/lib/snapd/lib/gl/ r, @@ -57,19 +56,11 @@ /run/udev/data/c226:[0-9]* r, # 226 drm ` -const openglConnectedPlugSecComp = ` -# Description: Can access opengl. -# Usage: reserved - -getsockopt -` - // NewOpenglInterface returns a new "opengl" interface. func NewOpenglInterface() interfaces.Interface { return &commonInterface{ name: "opengl", connectedPlugAppArmor: openglConnectedPlugAppArmor, - connectedPlugSecComp: openglConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/openvswitch.go snapd-2.23.1+16.10/interfaces/builtin/openvswitch.go --- snapd-2.22.6+16.10/interfaces/builtin/openvswitch.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/openvswitch.go 2017-03-02 06:37:54.000000000 +0000 @@ -25,22 +25,10 @@ /run/openvswitch/db.sock rw, ` -const openvswitchConnectedPlugSecComp = ` -connect -recv -recvmsg -send -sendto -sendmsg -socket -socketpair -` - func NewOpenvSwitchInterface() interfaces.Interface { return &commonInterface{ name: "openvswitch", connectedPlugAppArmor: openvswitchConnectedPlugAppArmor, - connectedPlugSecComp: openvswitchConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/ppp.go snapd-2.23.1+16.10/interfaces/builtin/ppp.go --- snapd-2.22.6+16.10/interfaces/builtin/ppp.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/ppp.go 2017-03-08 13:28:17.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -23,10 +23,9 @@ "github.com/snapcore/snapd/interfaces" ) -var pppConnectedPlugAppArmor = []byte(` -# Description: Allow operating ppp daemon. Reserved because this gives -# privileged access to the ppp daemon. -# Usage: reserved +const pppConnectedPlugAppArmor = ` +# Description: Allow operating ppp daemon. This gives privileged access to the +# ppp daemon. # Needed for modem connections using PPP /usr/sbin/pppd ix, @@ -41,7 +40,7 @@ @{PROC}/@{pid}/loginuid r, capability setgid, capability setuid, -`) +` // ppp_generic creates /dev/ppp. Other ppp modules will be automatically loaded // by the kernel on different ioctl calls for this device. Note also that @@ -63,7 +62,7 @@ func (iface *PppInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return pppConnectedPlugAppArmor, nil + return []byte(pppConnectedPlugAppArmor), nil case interfaces.SecurityKMod: return pppConnectedPlugKmod, nil } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/process_control.go snapd-2.23.1+16.10/interfaces/builtin/process_control.go --- snapd-2.22.6+16.10/interfaces/builtin/process_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/process_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -27,9 +27,9 @@ # Description: This interface allows for controlling other processes via # signals and nice. This is reserved because it grants privileged access to # all processes under root or processes running under the same UID otherwise. -# Usage: reserved -/{,usr/}bin/nice ixr, +# /{,usr/}bin/nice is already in default policy, so just allow renice here +/{,usr/}bin/renice ixr, capability sys_resource, capability sys_nice, @@ -41,8 +41,9 @@ # Description: This interface allows for controlling other processes via # signals and nice. This is reserved because it grants privileged access to # all processes under root or processes running under the same UID otherwise. -# Usage: reserved +# Allow setting the nice value/priority for any process +nice setpriority sched_setaffinity sched_setparam diff -Nru snapd-2.22.6+16.10/interfaces/builtin/pulseaudio.go snapd-2.23.1+16.10/interfaces/builtin/pulseaudio.go --- snapd-2.22.6+16.10/interfaces/builtin/pulseaudio.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/pulseaudio.go 2017-03-08 13:28:17.000000000 +0000 @@ -48,15 +48,7 @@ ` const pulseaudioConnectedPlugSecComp = ` -getsockopt -setsockopt -connect -sendto shmctl -getsockname -getpeername -sendmsg -recvmsg ` const pulseaudioPermanentSlotAppArmor = ` @@ -106,18 +98,11 @@ # The following are needed for UNIX sockets personality setpriority -setsockopt -getsockname bind listen -sendto -recvfrom +accept accept4 shmctl -getsockname -getpeername -sendmsg -recvmsg # Needed to set root as group for different state dirs # pulseaudio creates on startup. setgroups diff -Nru snapd-2.22.6+16.10/interfaces/builtin/raw_usb.go snapd-2.23.1+16.10/interfaces/builtin/raw_usb.go --- snapd-2.22.6+16.10/interfaces/builtin/raw_usb.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/raw_usb.go 2017-03-02 06:37:54.000000000 +0000 @@ -25,8 +25,7 @@ const rawusbConnectedPlugAppArmor = ` # Description: Allow raw access to all connected USB devices. -# Reserved because this gives privileged access to the system. -# Usage: reserved +# This gives privileged access to the system. /dev/bus/usb/[0-9][0-9][0-9]/[0-9][0-9][0-9] rw, # Allow detection of usb devices. Leaks plugged in USB device info diff -Nru snapd-2.22.6+16.10/interfaces/builtin/screen_inhibit_control.go snapd-2.23.1+16.10/interfaces/builtin/screen_inhibit_control.go --- snapd-2.22.6+16.10/interfaces/builtin/screen_inhibit_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/screen_inhibit_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -55,21 +55,16 @@ bus=session path=/Screensaver interface=org.freedesktop.ScreenSaver - member=org.freedesktop.ScreenSaver.{Inhibit,UnInhibit} + member=org.freedesktop.ScreenSaver.{Inhibit,UnInhibit,SimulateUserActivity} peer=(label=unconfined), -` -const screenInhibitControlConnectedPlugSecComp = ` -# Description: Can inhibit and uninhibit screen savers in desktop sessions. -# dbus -connect -getsockname -recvfrom -recvmsg -send -sendto -sendmsg -socket +# gnome, kde and cinnamon screensaver +dbus (send) + bus=session + path=/{,ScreenSaver} + interface=org.{gnome.ScreenSaver,kde.screensaver,cinnamon.ScreenSaver} + member=SimulateUserActivity + peer=(label=unconfined), ` // NewScreenInhibitControlInterface returns a new "screen-inhibit-control" interface. @@ -77,7 +72,6 @@ return &commonInterface{ name: "screen-inhibit-control", connectedPlugAppArmor: screenInhibitControlConnectedPlugAppArmor, - connectedPlugSecComp: screenInhibitControlConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/serial_port.go snapd-2.23.1+16.10/interfaces/builtin/serial_port.go --- snapd-2.22.6+16.10/interfaces/builtin/serial_port.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/serial_port.go 2017-03-02 06:37:54.000000000 +0000 @@ -43,7 +43,13 @@ // Pattern to match allowed serial device nodes, path attributes will be // compared to this for validity when not using udev identification -var serialDeviceNodePattern = regexp.MustCompile("^/dev/tty[A-Z]{1,3}[0-9]{1,3}$") +// Known device node patterns we need to support +// - ttyUSBX (UART over USB devices) +// - ttyACMX (ACM modem devices ) +// - ttyXRUSBx (Exar Corp. USB UART devices) +// - ttySX (UART serial ports) +// - ttyOX (UART serial ports on ARM) +var serialDeviceNodePattern = regexp.MustCompile("^/dev/tty(USB|ACM|XRUSB|S|O)[0-9]+$") // Pattern that is considered valid for the udev symlink to the serial device, // path attributes will be compared to this for validity when usb vid and pid @@ -148,9 +154,10 @@ switch securitySystem { case interfaces.SecurityAppArmor: if iface.hasUsbAttrs(slot) { - // This apparmor rule must match serialDeviceNodePattern + // This apparmor rule is an approximation of serialDeviceNodePattern + // (AARE is different than regex, so we must approximate). // UDev tagging and device cgroups will restrict down to the specific device - return []byte("/dev/tty[A-Z]{,[A-Z],[A-Z][A-Z]}[0-9]{,[0-9],[0-9][0-9]} rw,\n"), nil + return []byte("/dev/tty[A-Z]*[0-9] rw,\n"), nil } // Path to fixed device node (no udev tagging) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/serial_port_test.go snapd-2.23.1+16.10/interfaces/builtin/serial_port_test.go --- snapd-2.22.6+16.10/interfaces/builtin/serial_port_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/serial_port_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -37,10 +37,19 @@ testSlot2 *interfaces.Slot testSlot3 *interfaces.Slot testSlot4 *interfaces.Slot + testSlot5 *interfaces.Slot + testSlot6 *interfaces.Slot missingPathSlot *interfaces.Slot badPathSlot1 *interfaces.Slot badPathSlot2 *interfaces.Slot badPathSlot3 *interfaces.Slot + badPathSlot4 *interfaces.Slot + badPathSlot5 *interfaces.Slot + badPathSlot6 *interfaces.Slot + badPathSlot7 *interfaces.Slot + badPathSlot8 *interfaces.Slot + badPathSlot9 *interfaces.Slot + badPathSlot10 *interfaces.Slot badInterfaceSlot *interfaces.Slot // Gadget Snap @@ -69,33 +78,69 @@ path: /dev/ttyS0 test-port-2: interface: serial-port - path: /dev/ttyAMA2 + path: /dev/ttyUSB927 test-port-3: interface: serial-port - path: /dev/ttyUSB927 + path: /dev/ttyS42 test-port-4: interface: serial-port - path: /dev/ttyS42 + path: /dev/ttyO0 + test-port-5: + interface: serial-port + path: /dev/ttyACM0 + test-port-6: + interface: serial-port + path: /dev/ttyXRUSB0 missing-path: serial-port bad-path-1: interface: serial-port path: path bad-path-2: interface: serial-port - path: /dev/tty0 + path: /dev/tty bad-path-3: interface: serial-port - path: /dev/ttyUSB9271 + path: /dev/tty0 + bad-path-4: + interface: serial-port + path: /dev/tty63 + bad-path-5: + interface: serial-port + path: /dev/ttyUSB + bad-path-6: + interface: serial-port + path: /dev/usb + bad-path-7: + interface: serial-port + path: /dev/ttyprintk + bad-path-8: + interface: serial-port + path: /dev/ttyO + bad-path-9: + interface: serial-port + path: /dev/ttyS + bad-path-10: + interface: serial-port + path: /dev/ttyillegal0 bad-interface: other-interface `, nil) s.testSlot1 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-1"]} s.testSlot2 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-2"]} s.testSlot3 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-3"]} s.testSlot4 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-4"]} + s.testSlot5 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-5"]} + s.testSlot6 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["test-port-6"]} s.missingPathSlot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["missing-path"]} s.badPathSlot1 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-1"]} s.badPathSlot2 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-2"]} s.badPathSlot3 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-3"]} + s.badPathSlot4 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-4"]} + s.badPathSlot5 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-5"]} + s.badPathSlot6 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-6"]} + s.badPathSlot7 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-7"]} + s.badPathSlot8 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-8"]} + s.badPathSlot9 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-9"]} + s.badPathSlot10 = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-path-10"]} s.badInterfaceSlot = &interfaces.Slot{SlotInfo: osSnapInfo.Slots["bad-interface"]} gadgetSnapInfo := snaptest.MockInfo(c, ` @@ -159,7 +204,7 @@ } func (s *SerialPortInterfaceSuite) TestSanitizeCoreSnapSlots(c *C) { - for _, slot := range []*interfaces.Slot{s.testSlot1, s.testSlot2, s.testSlot3, s.testSlot4} { + for _, slot := range []*interfaces.Slot{s.testSlot1, s.testSlot2, s.testSlot3, s.testSlot4, s.testSlot5, s.testSlot6} { err := s.iface.SanitizeSlot(slot) c.Assert(err, IsNil) } @@ -171,7 +216,7 @@ c.Assert(err, ErrorMatches, `serial-port slot must have a path attribute`) // Slots with incorrect value of the "path" attribute are rejected. - for _, slot := range []*interfaces.Slot{s.badPathSlot1, s.badPathSlot2, s.badPathSlot3} { + for _, slot := range []*interfaces.Slot{s.badPathSlot1, s.badPathSlot2, s.badPathSlot3, s.badPathSlot4, s.badPathSlot5, s.badPathSlot6, s.badPathSlot7, s.badPathSlot8, s.badPathSlot9, s.badPathSlot10} { err := s.iface.SanitizeSlot(slot) c.Assert(err, ErrorMatches, "serial-port path attribute must be a valid device node") } @@ -248,15 +293,45 @@ c.Assert(err, IsNil) c.Assert(snippet, DeepEquals, expectedSnippet1, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet1, snippet)) - expectedSnippet2 := []byte(`/dev/tty[A-Z]{,[A-Z],[A-Z][A-Z]}[0-9]{,[0-9],[0-9][0-9]} rw, + expectedSnippet2 := []byte(`/dev/ttyUSB927 rw, `) - snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityAppArmor) + snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testSlot2, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, DeepEquals, expectedSnippet2, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet2, snippet)) - expectedSnippet3 := []byte(`/dev/tty[A-Z]{,[A-Z],[A-Z][A-Z]}[0-9]{,[0-9],[0-9][0-9]} rw, + expectedSnippet3 := []byte(`/dev/ttyS42 rw, `) - snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort2, s.testUdev2, interfaces.SecurityAppArmor) + snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testSlot3, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, DeepEquals, expectedSnippet3, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet3, snippet)) + + expectedSnippet4 := []byte(`/dev/ttyO0 rw, +`) + snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testSlot4, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, DeepEquals, expectedSnippet4, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet4, snippet)) + + expectedSnippet5 := []byte(`/dev/ttyACM0 rw, +`) + snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testSlot5, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, DeepEquals, expectedSnippet5, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet5, snippet)) + + expectedSnippet6 := []byte(`/dev/ttyXRUSB0 rw, +`) + snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testSlot6, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, DeepEquals, expectedSnippet6, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet6, snippet)) + + expectedSnippet7 := []byte(`/dev/tty[A-Z]*[0-9] rw, +`) + snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort1, s.testUdev1, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, DeepEquals, expectedSnippet7, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet7, snippet)) + + expectedSnippet8 := []byte(`/dev/tty[A-Z]*[0-9] rw, +`) + snippet, err = s.iface.ConnectedPlugSnippet(s.testPlugPort2, s.testUdev2, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, DeepEquals, expectedSnippet8, Commentf("\nexpected:\n%s\nfound:\n%s", expectedSnippet8, snippet)) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/shutdown.go snapd-2.23.1+16.10/interfaces/builtin/shutdown.go --- snapd-2.22.6+16.10/interfaces/builtin/shutdown.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/shutdown.go 2017-03-02 06:37:54.000000000 +0000 @@ -25,7 +25,6 @@ const shutdownConnectedPlugAppArmor = ` # Description: Can reboot, power-off and halt the system. -# Usage: reserved #include @@ -35,16 +34,13 @@ interface=org.freedesktop.systemd1.Manager member={Reboot,PowerOff,Halt} peer=(label=unconfined), -` -const shutdownConnectedPlugSecComp = ` -# Description: Can reboot, power-off and halt the system. -# Following things are needed for dbus connectivity -recvfrom -recvmsg -send -sendto -sendmsg +dbus (send) + bus=system + path=/org/freedesktop/login1 + interface=org.freedesktop.login1.Manager + member={PowerOff,Reboot,Suspend,Hibernate,HybridSleep,CanPowerOff,CanReboot,CanSuspend,CanHibernate,CanHybridSleep,ScheduleShutdown,CancelScheduledShutdown} + peer=(label=unconfined), ` // NewShutdownInterface returns a new "shutdown" interface. @@ -52,7 +48,6 @@ return &commonInterface{ name: "shutdown", connectedPlugAppArmor: shutdownConnectedPlugAppArmor, - connectedPlugSecComp: shutdownConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/shutdown_test.go snapd-2.23.1+16.10/interfaces/builtin/shutdown_test.go --- snapd-2.22.6+16.10/interfaces/builtin/shutdown_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/shutdown_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -84,18 +84,10 @@ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - // connected plugs have a non-nil security snippet for seccomp - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) } func (s *ShutdownInterfaceSuite) TestConnectedPlugSnippet(c *C) { snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(string(snippet), testutil.Contains, `org.freedesktop.systemd1`) - - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(string(snippet), testutil.Contains, `recvfrom`) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/snapd_control.go snapd-2.23.1+16.10/interfaces/builtin/snapd_control.go --- snapd-2.22.6+16.10/interfaces/builtin/snapd_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/snapd_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -26,34 +26,15 @@ // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/snapd-control const snapdControlConnectedPlugAppArmor = ` # Description: Can manage snaps via snapd. -# Usage: reserved /run/snapd.socket rw, ` -// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/snapd-control -const snapdControlConnectedPlugSecComp = ` -# Description: Can use snapd. -# Usage: reserved - -# Can communicate with snapd abstract socket -connect -getsockname -recv -recvmsg -send -sendto -sendmsg -socket -socketpair -` - // NewSnapdControlInterface returns a new "snapd-control" interface. func NewSnapdControlInterface() interfaces.Interface { return &commonInterface{ name: "snapd-control", connectedPlugAppArmor: snapdControlConnectedPlugAppArmor, - connectedPlugSecComp: snapdControlConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/system_observe.go snapd-2.23.1+16.10/interfaces/builtin/system_observe.go --- snapd-2.22.6+16.10/interfaces/builtin/system_observe.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/system_observe.go 2017-03-02 06:37:54.000000000 +0000 @@ -28,7 +28,6 @@ # Description: Can query system status information. This is restricted because # it gives privileged read access to all processes on the system and should # only be used with trusted apps. -# Usage: reserved # Needed by 'ps' @{PROC}/tty/drivers r, @@ -70,12 +69,10 @@ peer=(label=unconfined), ` -// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/system-observe const systemObserveConnectedPlugSecComp = ` # Description: Can query system status information. This is restricted because # it gives privileged read access to all processes on the system and should # only be used with trusted apps. -# Usage: reserved # ptrace can be used to break out of the seccomp sandbox, but ps requests # 'ptrace (trace)' from apparmor. 'ps' does not need the ptrace syscall though, @@ -83,16 +80,6 @@ # Note: may uncomment once ubuntu-core-launcher understands @deny rules and # if/when we conditionally deny this in the future. #@deny ptrace - -# for connecting to /org/freedesktop/hostname1 over DBus -connect -getsockname -recvfrom -recvmsg -send -sendto -sendmsg -socket ` // NewSystemObserveInterface returns a new "system-observe" interface. diff -Nru snapd-2.22.6+16.10/interfaces/builtin/system_trace.go snapd-2.23.1+16.10/interfaces/builtin/system_trace.go --- snapd-2.22.6+16.10/interfaces/builtin/system_trace.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/system_trace.go 2017-03-02 06:37:54.000000000 +0000 @@ -27,7 +27,6 @@ # Description: Can use kernel tracing facilities. This is restricted because it # gives privileged access to all processes on the system and should only be # used with trusted apps. -# Usage: reserved # For the bpf() syscall and manipulating bpf map types capability sys_admin, @@ -51,7 +50,6 @@ # Description: Can use kernel tracing facilities. This is restricted because it # gives privileged access to all processes on the system and should only be # used with trusted apps. -# Usage: reserved bpf perf_event_open diff -Nru snapd-2.22.6+16.10/interfaces/builtin/thumbnailer.go snapd-2.23.1+16.10/interfaces/builtin/thumbnailer.go --- snapd-2.22.6+16.10/interfaces/builtin/thumbnailer.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/thumbnailer.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,142 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "bytes" + "fmt" + + "github.com/snapcore/snapd/interfaces" +) + +const thumbnailerPermanentSlotAppArmor = ` +# Description: Allow use of aa_query_label API. This +# discloses the AppArmor policy for all processes. + +/sys/module/apparmor/parameters/enabled r, +@{PROC}/@{pid}/mounts r, +/sys/kernel/security/apparmor/.access rw, + +# Description: Allow owning the Thumbnailer bus name on the session bus + +#include + +dbus (send) + bus=session + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member={RequestName,ReleaseName,GetConnectionCredentials} + peer=(name=org.freedesktop.DBus, label=unconfined), + +dbus (bind) + bus=session + name=com.canonical.Thumbnailer, +` + +const thumbnailerConnectedSlotAppArmor = ` +# Description: Allow access to plug's data directory. + +@{INSTALL_DIR}/###PLUG_SNAP_NAME###/** r, +owner @{HOME}/snap/###PLUG_SNAP_NAME###/** r, +/var/snap/###PLUG_SNAP_NAME###/** r, + +# Description: allow client snaps to access the thumbnailer service. +dbus (receive, send) + bus=session + interface=com.canonical.Thumbnailer + path=/com/canonical/Thumbnailer + peer=(label=###PLUG_SECURITY_TAGS###), +` + +const thumbnailerConnectedPlugAppArmor = ` +# Description: allow access to the thumbnailer D-Bus service. + +#include + +dbus (receive, send) + bus=session + interface=com.canonical.Thumbnailer + path=/com/canonical/Thumbnailer + peer=(label=###SLOT_SECURITY_TAGS###), +` + +type ThumbnailerInterface struct{} + +func (iface *ThumbnailerInterface) Name() string { + return "thumbnailer" +} + +func (iface *ThumbnailerInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return nil, nil +} + +func (iface *ThumbnailerInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityAppArmor: + snippet := []byte(thumbnailerConnectedPlugAppArmor) + old := []byte("###SLOT_SECURITY_TAGS###") + new := slotAppLabelExpr(slot) + snippet = bytes.Replace(snippet, old, new, -1) + return snippet, nil + } + return nil, nil +} + +func (iface *ThumbnailerInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityAppArmor: + return []byte(thumbnailerPermanentSlotAppArmor), nil + } + return nil, nil +} + +func (iface *ThumbnailerInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityAppArmor: + snippet := []byte(thumbnailerConnectedSlotAppArmor) + old := []byte("###PLUG_SNAP_NAME###") + new := []byte(plug.Snap.Name()) + snippet = bytes.Replace(snippet, old, new, -1) + + old = []byte("###PLUG_SECURITY_TAGS###") + new = plugAppLabelExpr(plug) + snippet = bytes.Replace(snippet, old, new, -1) + return snippet, nil + } + return nil, nil +} + +func (iface *ThumbnailerInterface) SanitizePlug(plug *interfaces.Plug) error { + if iface.Name() != plug.Interface { + panic(fmt.Sprintf("plug is not of interface %q", iface.Name())) + } + return nil +} + +func (iface *ThumbnailerInterface) SanitizeSlot(slot *interfaces.Slot) error { + if iface.Name() != slot.Interface { + panic(fmt.Sprintf("slot is not of interface %q", iface.Name())) + } + return nil +} + +func (iface *ThumbnailerInterface) AutoConnect(plug *interfaces.Plug, slot *interfaces.Slot) bool { + return true +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/thumbnailer_test.go snapd-2.23.1+16.10/interfaces/builtin/thumbnailer_test.go --- snapd-2.22.6+16.10/interfaces/builtin/thumbnailer_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/thumbnailer_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,84 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type ThumbnailerInterfaceSuite struct { + iface interfaces.Interface + slot *interfaces.Slot + plug *interfaces.Plug +} + +var _ = Suite(&ThumbnailerInterfaceSuite{ + iface: &builtin.ThumbnailerInterface{}, + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "server", Type: snap.TypeOS}, + Name: "thumbnailer", + Interface: "thumbnailer", + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "client"}, + Name: "thumbnailer", + Interface: "thumbnailer", + }, + }, +}) + +func (s *ThumbnailerInterfaceSuite) TestName(c *C) { + c.Check(s.iface.Name(), Equals, "thumbnailer") +} + +func (s *ThumbnailerInterfaceSuite) TestSanitizeIncorrectInterface(c *C) { + c.Check(func() { s.iface.SanitizeSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{Interface: "other"}}) }, + PanicMatches, `slot is not of interface "thumbnailer"`) + c.Check(func() { s.iface.SanitizePlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{Interface: "other"}}) }, + PanicMatches, `plug is not of interface "thumbnailer"`) +} + +func (s *ThumbnailerInterfaceSuite) TestUsedSecuritySystems(c *C) { + // connected slots have a non-nil security snippet for apparmor + snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + // slots have a permanent non-nil security snippet for apparmor + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) +} + +func (s *ThumbnailerInterfaceSuite) TestSlotGrantedAccessToPlugFiles(c *C) { + snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + + c.Check(string(snippet), testutil.Contains, `@{INSTALL_DIR}/client/**`) + c.Check(string(snippet), testutil.Contains, `@{HOME}/snap/client/**`) + c.Check(string(snippet), testutil.Contains, `/var/snap/client/**`) +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/time_control.go snapd-2.23.1+16.10/interfaces/builtin/time_control.go --- snapd-2.22.6+16.10/interfaces/builtin/time_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/time_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -31,7 +31,6 @@ # Can read all properties of /org/freedesktop/timedate1 D-Bus object; see # https://www.freedesktop.org/wiki/Software/systemd/timedated/; This also # gives full access to the RTC device nodes and relevant parts of sysfs. -# Usage: reserved #include @@ -83,17 +82,6 @@ # device nodes. /sbin/hwclock ixr, ` -const timeControlConnectedPlugSecComp = ` -# dbus -connect -getsockname -recvmsg -recvfrom -send -sendto -sendmsg -socket -` // The type for the rtc interface type TimeControlInterface struct{} @@ -142,9 +130,6 @@ case interfaces.SecurityAppArmor: return []byte(timeControlConnectedPlugAppArmor), nil - case interfaces.SecuritySecComp: - return []byte(timeControlConnectedPlugSecComp), nil - case interfaces.SecurityUDev: var tagSnippet bytes.Buffer const udevRule = `KERNEL=="/dev/rtc0", TAG+="%s"` diff -Nru snapd-2.22.6+16.10/interfaces/builtin/time_control_test.go snapd-2.23.1+16.10/interfaces/builtin/time_control_test.go --- snapd-2.22.6+16.10/interfaces/builtin/time_control_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/time_control_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -94,10 +94,6 @@ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - // connected plugs have a non-nil security snippet for seccomp - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) // connected plugs have a non-nil security snippet for udev snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityUDev) c.Assert(err, IsNil) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/timeserver_control.go snapd-2.23.1+16.10/interfaces/builtin/timeserver_control.go --- snapd-2.22.6+16.10/interfaces/builtin/timeserver_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/timeserver_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -29,7 +29,6 @@ # Can enable system clock NTP synchronization via timedated D-Bus interface, # Can read all properties of /org/freedesktop/timedate1 D-Bus object; see # https://www.freedesktop.org/wiki/Software/systemd/timedated/ -# Usage: reserved #include @@ -68,24 +67,12 @@ member=PropertiesChanged peer=(label=unconfined), ` -const timeserverControlConnectedPlugSecComp = ` -# dbus -connect -getsockname -recvmsg -recvfrom -send -sendto -sendmsg -socket -` // NewTimeserverControlInterface returns a new "timeserver-control" interface. func NewTimeserverControlInterface() interfaces.Interface { return &commonInterface{ name: "timeserver-control", connectedPlugAppArmor: timeserverControlConnectedPlugAppArmor, - connectedPlugSecComp: timeserverControlConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/timezone_control.go snapd-2.23.1+16.10/interfaces/builtin/timezone_control.go --- snapd-2.22.6+16.10/interfaces/builtin/timezone_control.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/timezone_control.go 2017-03-02 06:37:54.000000000 +0000 @@ -29,7 +29,6 @@ # Can change timezone via timedated D-Bus interface, # Can read all properties of /org/freedesktop/timedate1 D-Bus object, see: # https://www.freedesktop.org/wiki/Software/systemd/timedated/ -# Usage: reserved #include @@ -69,24 +68,11 @@ peer=(label=unconfined), ` -const timezoneControlConnectedPlugSecComp = ` -# dbus -connect -getsockname -recvmsg -recvfrom -send -sendto -sendmsg -socket -` - // NewTimezoneControlInterface returns a new "timezone-control" interface. func NewTimezoneControlInterface() interfaces.Interface { return &commonInterface{ name: "timezone-control", connectedPlugAppArmor: timezoneControlConnectedPlugAppArmor, - connectedPlugSecComp: timezoneControlConnectedPlugSecComp, reservedForOS: true, } } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/tpm.go snapd-2.23.1+16.10/interfaces/builtin/tpm.go --- snapd-2.22.6+16.10/interfaces/builtin/tpm.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/tpm.go 2017-03-02 06:37:54.000000000 +0000 @@ -23,7 +23,6 @@ const tpmConnectedPlugAppArmor = ` # Description: for those who need to talk to the system TPM chip over /dev/tpm0 -# Usage: reserved /dev/tpm0 rw, ` diff -Nru snapd-2.22.6+16.10/interfaces/builtin/ubuntu_download_manager.go snapd-2.23.1+16.10/interfaces/builtin/ubuntu_download_manager.go --- snapd-2.22.6+16.10/interfaces/builtin/ubuntu_download_manager.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/ubuntu_download_manager.go 2017-03-06 13:33:50.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -28,7 +28,7 @@ /* The methods: allowGSMDownload, createMmsDownload, exit and setDefaultThrottle are deliberately left out of this profile due to their privileged nature. */ -var downloadConnectedPlugAppArmor = []byte(` +const downloadConnectedPlugAppArmor = ` # Description: Can access the download manager. #include @@ -103,9 +103,9 @@ interface=com.canonical.applications.DownloadManager member=isGSMDownloadAllowed peer=(label=###SLOT_SECURITY_TAGS###), -`) +` -var downloadPermanentSlotAppArmor = []byte(` +const downloadPermanentSlotAppArmor = ` # Description: Allow operating as a download manager. # DBus accesses @@ -137,9 +137,9 @@ interface=org.freedesktop.DBus member="GetConnectionAppArmorSecurityContext" peer=(name=org.freedesktop.DBus, label=unconfined), -`) +` -var downloadConnectedSlotAppArmor = []byte(` +const downloadConnectedSlotAppArmor = ` # Allow connected clients to interact with the download manager dbus (receive) bus=session @@ -182,31 +182,7 @@ # Allow writing to app download directories owner @{HOME}/snap/###PLUG_NAME###/common/Downloads/ rw, owner @{HOME}/snap/###PLUG_NAME###/common/Downloads/** rwk, -`) - -var downloadConnectedPlugSecComp = []byte(` -# Description: Can access download manager. - -# dbus -connect -recvmsg -send -sendto -sendmsg -socket -`) - -var downloadPermanentSlotSecComp = []byte(` -# Description: Can act as a download manager. - -# dbus -connect -recvmsg -send -sendto -sendmsg -socket -`) +` type UbuntuDownloadManagerInterface struct{} @@ -227,10 +203,8 @@ case interfaces.SecurityAppArmor: old := []byte("###SLOT_SECURITY_TAGS###") new := slotAppLabelExpr(slot) - snippet := bytes.Replace(downloadConnectedPlugAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(downloadConnectedPlugAppArmor), old, new, -1) return snippet, nil - case interfaces.SecuritySecComp: - return downloadConnectedPlugSecComp, nil } return nil, nil } @@ -238,9 +212,7 @@ func (iface *UbuntuDownloadManagerInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { switch securitySystem { case interfaces.SecurityAppArmor: - return downloadPermanentSlotAppArmor, nil - case interfaces.SecuritySecComp: - return downloadPermanentSlotSecComp, nil + return []byte(downloadPermanentSlotAppArmor), nil } return nil, nil } @@ -250,7 +222,7 @@ case interfaces.SecurityAppArmor: old := []byte("###PLUG_SECURITY_TAGS###") new := plugAppLabelExpr(plug) - snippet := bytes.Replace(downloadConnectedSlotAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(downloadConnectedSlotAppArmor), old, new, -1) old = []byte("###PLUG_NAME###") new = []byte(plug.Snap.Name()) snippet = bytes.Replace(snippet, old, new, -1) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/ubuntu_download_manager_test.go snapd-2.23.1+16.10/interfaces/builtin/ubuntu_download_manager_test.go --- snapd-2.22.6+16.10/interfaces/builtin/ubuntu_download_manager_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/ubuntu_download_manager_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -83,8 +83,4 @@ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - // connected plugs have a non-nil security snippet for seccomp - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/udisks2.go snapd-2.23.1+16.10/interfaces/builtin/udisks2.go --- snapd-2.22.6+16.10/interfaces/builtin/udisks2.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/udisks2.go 2017-03-08 13:28:17.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -26,9 +26,8 @@ ) const udisks2PermanentSlotAppArmor = ` -# Description: Allow operating as the udisks2. Reserved because this -# gives privileged access to the system. -# Usage: reserved +# Description: Allow operating as the udisks2. This gives privileged access to +# the system. # DBus accesses #include @@ -88,12 +87,14 @@ # give raw read access to the system disks and therefore the entire system. /dev/sd* r, /dev/mmcblk* r, + +# Needed for probing raw devices +capability sys_rawio, ` -var udisks2ConnectedSlotAppArmor = []byte(` -# Allow connected clients to interact with the service. Reserved because this -# gives privileged access to the system. -# Usage: reserved +const udisks2ConnectedSlotAppArmor = ` +# Allow connected clients to interact with the service. This gives privileged +# access to the system. dbus (send) bus=system @@ -114,12 +115,11 @@ path=/org/freedesktop/UDisks2/** interface=org.freedesktop.UDisks2.* peer=(label=###PLUG_SECURITY_TAGS###), -`) +` -var udisks2ConnectedPlugAppArmor = []byte(` -# Description: Allow using udisks service. Reserved because this gives -# privileged access to the service. -# Usage: reserved +const udisks2ConnectedPlugAppArmor = ` +# Description: Allow using udisks service. This gives privileged access to the +# service. #include @@ -142,7 +142,7 @@ path=/org/freedesktop/UDisks2/** interface=org.freedesktop.UDisks2.* peer=(label=###SLOT_SECURITY_TAGS###), -`) +` const udisks2PermanentSlotSecComp = ` bind @@ -152,30 +152,12 @@ fchownat lchown lchown32 -getsockname -setsockopt mount -recv -recvfrom -recvmsg -send -sendmsg -sendto shmctl umount umount2 ` -const udisks2ConnectedPlugSecComp = ` -getsockname -recv -recvfrom -recvmsg -send -sendmsg -sendto -` - const udisks2PermanentSlotDBus = ` @@ -362,12 +344,10 @@ case interfaces.SecurityAppArmor: old := []byte("###SLOT_SECURITY_TAGS###") new := slotAppLabelExpr(slot) - snippet := bytes.Replace(udisks2ConnectedPlugAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(udisks2ConnectedPlugAppArmor), old, new, -1) return snippet, nil case interfaces.SecurityDBus: return []byte(udisks2ConnectedPlugDBus), nil - case interfaces.SecuritySecComp: - return []byte(udisks2ConnectedPlugSecComp), nil } return nil, nil } @@ -391,7 +371,7 @@ case interfaces.SecurityAppArmor: old := []byte("###PLUG_SECURITY_TAGS###") new := plugAppLabelExpr(plug) - snippet := bytes.Replace(udisks2ConnectedSlotAppArmor, old, new, -1) + snippet := bytes.Replace([]byte(udisks2ConnectedSlotAppArmor), old, new, -1) return snippet, nil } return nil, nil diff -Nru snapd-2.22.6+16.10/interfaces/builtin/udisks2_test.go snapd-2.23.1+16.10/interfaces/builtin/udisks2_test.go --- snapd-2.22.6+16.10/interfaces/builtin/udisks2_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/udisks2_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -185,7 +185,7 @@ func (s *UDisks2InterfaceSuite) TestUsedSecuritySystems(c *C) { systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor, - interfaces.SecuritySecComp, interfaces.SecurityDBus} + interfaces.SecurityDBus} for _, system := range systems { snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) c.Assert(err, IsNil) @@ -200,4 +200,7 @@ snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityUDev) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecuritySecComp) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/unity7.go snapd-2.23.1+16.10/interfaces/builtin/unity7.go --- snapd-2.22.6+16.10/interfaces/builtin/unity7.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/unity7.go 2017-03-06 13:33:50.000000000 +0000 @@ -28,7 +28,6 @@ # Description: Can access Unity7. Restricted because Unity 7 runs on X and # requires access to various DBus services and this environment does not prevent # eavesdropping or apps interfering with one another. -# Usage: reserved #include #include @@ -62,6 +61,12 @@ /usr/share/thumbnailer/icons/** r, /usr/share/themes/** r, +# The snapcraft desktop part may look for schema files in various locations, so +# allow reading system installed schemas. +/usr/share/glib*/schemas/{,*} r, +/usr/share/gnome/glib*/schemas/{,*} r, +/usr/share/ubuntu/glib*/schemas/{,*} r, + # Snappy's 'xdg-open' talks to the snapd-xdg-open service which currently works # only in environments supporting dbus-send (eg, X11). In the future once # snappy's xdg-open supports all snaps images, this access may move to another @@ -291,7 +296,7 @@ member="{AboutTo*,Event*}" peer=(label=unconfined), -# notifications +# app-indicators dbus (send) bus=session path=/StatusNotifierWatcher @@ -328,9 +333,16 @@ bus=session path=/{StatusNotifierItem,org/ayatana/NotificationItem/*} interface=org.kde.StatusNotifierItem - member="New{AttentionIcon,Icon,OverlayIcon,Status,Title,ToolTip}" + member="New{AttentionIcon,Icon,IconThemePath,OverlayIcon,Status,Title,ToolTip}" peer=(name=org.freedesktop.DBus, label=unconfined), +dbus (receive) + bus=session + path=/{StatusNotifierItem,org/ayatana/NotificationItem/*} + interface=org.kde.StatusNotifierItem + member={Activate,ContextMenu,Scroll,SecondaryActivate,XAyatanaSecondaryActivate} + peer=(label=unconfined), + dbus (send) bus=session path=/{StatusNotifierItem/menu,org/ayatana/NotificationItem/*/Menu} @@ -345,6 +357,7 @@ member={Get*,AboutTo*,Event*} peer=(label=unconfined), +# notifications dbus (send) bus=session path=/org/freedesktop/Notifications @@ -427,10 +440,24 @@ member="{GetAll,GetLayout}" peer=(label=unconfined), +# Allow requesting interest in receiving media key events. This tells Gnome +# settings that our application should be notified when key events we are +# interested in are pressed. +dbus (send) + bus=session + interface=org.gnome.SettingsDaemon.MediaKeys + path=/org/gnome/SettingsDaemon/MediaKeys + peer=(label=unconfined), +dbus (send) + bus=session + interface=org.freedesktop.DBus.Properties + path=/org/gnome/SettingsDaemon/MediaKeys + member="Get{,All}" + peer=(label=unconfined), # Lttng tracing is very noisy and should not be allowed by confined apps. Can # safely deny. LP: #1260491 -deny /{,var/}{dev,run}/shm/lttng-ust-* r, +deny /{dev,run,var/run}/shm/lttng-ust-* r, ` // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/unity7 @@ -440,20 +467,7 @@ # eavesdropping or apps interfering with one another. # X -getpeername -recvfrom -recvmsg shutdown -getsockopt - -# dbus -connect -getsockname -recvmsg -send -sendto -sendmsg -socket ` // NewUnity7Interface returns a new "unity7" interface. diff -Nru snapd-2.22.6+16.10/interfaces/builtin/unity8_calendar.go snapd-2.23.1+16.10/interfaces/builtin/unity8_calendar.go --- snapd-2.22.6+16.10/interfaces/builtin/unity8_calendar.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/unity8_calendar.go 2017-03-02 06:37:54.000000000 +0000 @@ -24,8 +24,8 @@ ) const unity8CalendarPermanentSlotAppArmor = ` -# Description: Allow operating as the EDS service. Reserved because this -# gives privileged access to the system. +# Description: Allow operating as the EDS service. This gives privileged access +# to the system. # DBus accesses dbus (bind) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/unity8_calendar_test.go snapd-2.23.1+16.10/interfaces/builtin/unity8_calendar_test.go --- snapd-2.22.6+16.10/interfaces/builtin/unity8_calendar_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/unity8_calendar_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -74,10 +74,6 @@ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - // connected plugs have a non-nil security snippet for seccomp - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) } // The label glob when all apps are bound to the calendar slot @@ -163,14 +159,6 @@ c.Assert(string(snippet), Not(testutil.Contains), "peer=(label=unconfined),") } -func (s *Unity8CalendarInterfaceSuite) TestConnectedPlugSnippetSecComp(c *C) { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - - c.Check(string(snippet), testutil.Contains, "send\n") -} - func (s *Unity8CalendarInterfaceSuite) TestConnectedSlotSnippetAppArmor(c *C) { snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/unity8_contacts.go snapd-2.23.1+16.10/interfaces/builtin/unity8_contacts.go --- snapd-2.22.6+16.10/interfaces/builtin/unity8_contacts.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/unity8_contacts.go 2017-03-02 06:37:54.000000000 +0000 @@ -24,8 +24,8 @@ ) const unity8ContactsPermanentSlotAppArmor = ` -# Description: Allow operating as the EDS service. Reserved because this -# gives privileged access to the system. +# Description: Allow operating as the EDS service. This gives privileged access +# to the system. # Allow binding the service to the requested connection name dbus (bind) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/unity8_contacts_test.go snapd-2.23.1+16.10/interfaces/builtin/unity8_contacts_test.go --- snapd-2.22.6+16.10/interfaces/builtin/unity8_contacts_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/unity8_contacts_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -74,10 +74,6 @@ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - // connected plugs have a non-nil security snippet for seccomp - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) } // The label glob when all apps are bound to the contacts slot @@ -163,14 +159,6 @@ c.Assert(string(snippet), Not(testutil.Contains), "peer=(label=unconfined),") } -func (s *Unity8ContactsInterfaceSuite) TestConnectedPlugSnippetSecComp(c *C) { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - - c.Check(string(snippet), testutil.Contains, "send\n") -} - func (s *Unity8ContactsInterfaceSuite) TestConnectedSlotSnippetAppArmor(c *C) { snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/unity8.go snapd-2.23.1+16.10/interfaces/builtin/unity8.go --- snapd-2.22.6+16.10/interfaces/builtin/unity8.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/unity8.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,71 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "fmt" + + "github.com/snapcore/snapd/interfaces" +) + +type Unity8Interface struct{} + +func (iface *Unity8Interface) Name() string { + return "unity8" +} + +func (iface *Unity8Interface) String() string { + return iface.Name() +} + +func (iface *Unity8Interface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return nil, nil +} + +func (iface *Unity8Interface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return nil, nil +} + +func (iface *Unity8Interface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return nil, nil +} + +func (iface *Unity8Interface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return nil, nil +} + +func (iface *Unity8Interface) SanitizePlug(plug *interfaces.Plug) error { + if iface.Name() != plug.Interface { + panic(fmt.Sprintf("slot is not of interface %q", iface)) + } + return nil +} + +func (iface *Unity8Interface) SanitizeSlot(slot *interfaces.Slot) error { + if iface.Name() != slot.Interface { + panic(fmt.Sprintf("slot is not of interface %q", iface)) + } + return nil +} + +func (iface *Unity8Interface) AutoConnect(*interfaces.Plug, *interfaces.Slot) bool { + // allow what declarations allowed + return true +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/unity8_pim_common.go snapd-2.23.1+16.10/interfaces/builtin/unity8_pim_common.go --- snapd-2.22.6+16.10/interfaces/builtin/unity8_pim_common.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/unity8_pim_common.go 2017-03-08 13:28:17.000000000 +0000 @@ -28,8 +28,8 @@ ) const unity8PimCommonPermanentSlotAppArmor = ` -# Description: Allow operating as the EDS service. Reserved because this -# gives privileged access to the system. +# Description: Allow operating as the EDS service. This gives privileged access +# to the system. # DBus accesses #include @@ -83,35 +83,15 @@ ` const unity8PimCommonPermanentSlotSecComp = ` -# Description: Allow operating as the EDS service. Reserved because this -# gives privileged access to the system. +# Description: Allow operating as the EDS service. This gives privileged access +# to the system. accept accept4 bind listen -recv -recvfrom -recvmmsg -recvmsg -send -sendmmsg -sendmsg -sendto shutdown ` -const unity8PimCommonConnectedPlugSecComp = ` -# Description: Allow using EDS service. Reserved because this gives -# privileged access to the eds service. - -# Can communicate with DBus system service -recv -recvmsg -send -sendto -sendmsg -` - type unity8PimCommonInterface struct { name string permanentSlotAppArmor string @@ -144,8 +124,6 @@ } return snippet, nil - case interfaces.SecuritySecComp: - return []byte(unity8PimCommonConnectedPlugSecComp), nil default: return nil, nil } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/unity8_test.go snapd-2.23.1+16.10/interfaces/builtin/unity8_test.go --- snapd-2.22.6+16.10/interfaces/builtin/unity8_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/unity8_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,56 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" +) + +type unity8InterfaceSuite struct { + iface interfaces.Interface + slot *interfaces.Slot + plug *interfaces.Plug +} + +var _ = Suite(&unity8InterfaceSuite{ + iface: &builtin.Unity8Interface{}, + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "unity8-session"}, + Name: "unity8-session", + Interface: "unity8", + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "unity8-session"}, + Name: "unity8-app", + Interface: "unity8", + }, + }, +}) + +func (s *unity8InterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "unity8") +} diff -Nru snapd-2.22.6+16.10/interfaces/builtin/upower_observe.go snapd-2.23.1+16.10/interfaces/builtin/upower_observe.go --- snapd-2.22.6+16.10/interfaces/builtin/upower_observe.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/upower_observe.go 2017-03-08 13:28:17.000000000 +0000 @@ -104,10 +104,6 @@ const upowerObservePermanentSlotSeccomp = ` bind -recvmsg -sendmsg -sendto -recvfrom ` const upowerObservePermanentSlotDBus = ` @@ -194,20 +190,6 @@ peer=(label=###SLOT_SECURITY_TAGS###), ` -const upowerObserveConnectedPlugSecComp = ` -# Description: Can query UPower for power devices, history and statistics. - -# dbus -connect -getsockname -recvfrom -recvmsg -send -sendto -sendmsg -socket -` - type UpowerObserveInterface struct{} func (iface *UpowerObserveInterface) Name() string { @@ -229,8 +211,6 @@ } snippet := bytes.Replace([]byte(upowerObserveConnectedPlugAppArmor), old, new, -1) return snippet, nil - case interfaces.SecuritySecComp: - return []byte(upowerObserveConnectedPlugSecComp), nil } return nil, nil } diff -Nru snapd-2.22.6+16.10/interfaces/builtin/upower_observe_test.go snapd-2.23.1+16.10/interfaces/builtin/upower_observe_test.go --- snapd-2.22.6+16.10/interfaces/builtin/upower_observe_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/upower_observe_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -85,10 +85,6 @@ snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) c.Assert(snippet, Not(IsNil)) - // connected plugs have a non-nil security snippet for seccomp - snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) } // The label glob when all apps are bound to the ofono slot @@ -174,13 +170,6 @@ c.Assert(string(snippet), Not(testutil.Contains), "peer=(label=unconfined),") } -func (s *UPowerObserveInterfaceSuite) TestConnectedPlugSnippetSecComp(c *C) { - snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) - c.Assert(err, IsNil) - c.Assert(snippet, Not(IsNil)) - c.Check(string(snippet), testutil.Contains, "send\n") -} - func (s *UPowerObserveInterfaceSuite) TestPermanentSlotSnippetAppArmor(c *C) { snippet, err := s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityAppArmor) c.Assert(err, IsNil) diff -Nru snapd-2.22.6+16.10/interfaces/builtin/x11.go snapd-2.23.1+16.10/interfaces/builtin/x11.go --- snapd-2.22.6+16.10/interfaces/builtin/x11.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/x11.go 2017-03-02 06:37:54.000000000 +0000 @@ -27,7 +27,6 @@ const x11ConnectedPlugAppArmor = ` # Description: Can access the X server. Restricted because X does not prevent # eavesdropping or apps interfering with one another. -# Usage: reserved #include #include @@ -40,14 +39,7 @@ const x11ConnectedPlugSecComp = ` # Description: Can access the X server. Restricted because X does not prevent # eavesdropping or apps interfering with one another. -# Usage: reserved -getpeername -getsockname -getsockopt -recvfrom -recvmsg -sendmsg shutdown ` diff -Nru snapd-2.22.6+16.10/interfaces/builtin/x11_test.go snapd-2.23.1+16.10/interfaces/builtin/x11_test.go --- snapd-2.22.6+16.10/interfaces/builtin/x11_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/builtin/x11_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -90,9 +90,9 @@ c.Assert(snippet, Not(IsNil)) } -// The getsockname system call is allowed +// The shutdown system call is allowed func (s *X11InterfaceSuite) TestLP1574526(c *C) { snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecuritySecComp) c.Assert(err, IsNil) - c.Check(string(snippet), testutil.Contains, "getsockname\n") + c.Check(string(snippet), testutil.Contains, "shutdown\n") } diff -Nru snapd-2.22.6+16.10/interfaces/ifacetest/testiface.go snapd-2.23.1+16.10/interfaces/ifacetest/testiface.go --- snapd-2.22.6+16.10/interfaces/ifacetest/testiface.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/ifacetest/testiface.go 2017-03-06 13:33:50.000000000 +0000 @@ -23,6 +23,11 @@ "fmt" "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/kmod" + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/interfaces/seccomp" + "github.com/snapcore/snapd/interfaces/udev" ) // TestInterface is a interface for various kind of tests. @@ -51,6 +56,40 @@ TestConnectedSlotCallback func(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error TestPermanentPlugCallback func(spec *Specification, plug *interfaces.Plug) error TestPermanentSlotCallback func(spec *Specification, slot *interfaces.Slot) error + + // Support for interacting with the mount backend. + + MountConnectedPlugCallback func(spec *mount.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + MountConnectedSlotCallback func(spec *mount.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + MountPermanentPlugCallback func(spec *mount.Specification, plug *interfaces.Plug) error + MountPermanentSlotCallback func(spec *mount.Specification, slot *interfaces.Slot) error + + // Support for interacting with the udev backend. + + UdevConnectedPlugCallback func(spec *udev.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + UdevConnectedSlotCallback func(spec *udev.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + UdevPermanentPlugCallback func(spec *udev.Specification, plug *interfaces.Plug) error + UdevPermanentSlotCallback func(spec *udev.Specification, slot *interfaces.Slot) error + + // Support for interacting with the apparmor backend. + + AppArmorConnectedPlugCallback func(spec *apparmor.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + AppArmorConnectedSlotCallback func(spec *apparmor.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + AppArmorPermanentPlugCallback func(spec *apparmor.Specification, plug *interfaces.Plug) error + AppArmorPermanentSlotCallback func(spec *apparmor.Specification, slot *interfaces.Slot) error + + // Support for interacting with the kmod backend. + KModConnectedPlugCallback func(spec *kmod.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + KModConnectedSlotCallback func(spec *kmod.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + KModPermanentPlugCallback func(spec *kmod.Specification, plug *interfaces.Plug) error + KModPermanentSlotCallback func(spec *kmod.Specification, slot *interfaces.Slot) error + + // Support for interacting with the seccomp backend. + + SecCompConnectedPlugCallback func(spec *seccomp.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + SecCompConnectedSlotCallback func(spec *seccomp.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + SecCompPermanentPlugCallback func(spec *seccomp.Specification, plug *interfaces.Plug) error + SecCompPermanentSlotCallback func(spec *seccomp.Specification, slot *interfaces.Slot) error } // String() returns the same value as Name(). @@ -160,3 +199,154 @@ } return nil } + +// Support for interacting with the mount backend. + +func (t *TestInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.MountConnectedPlugCallback != nil { + return t.MountConnectedPlugCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) MountConnectedSlot(spec *mount.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.MountConnectedSlotCallback != nil { + return t.MountConnectedSlotCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) MountPermanentPlug(spec *mount.Specification, plug *interfaces.Plug) error { + if t.MountPermanentPlugCallback != nil { + return t.MountPermanentPlugCallback(spec, plug) + } + return nil +} + +func (t *TestInterface) MountPermanentSlot(spec *mount.Specification, slot *interfaces.Slot) error { + if t.MountPermanentSlotCallback != nil { + return t.MountPermanentSlotCallback(spec, slot) + } + return nil +} + +// Support for interacting with the udev backend. + +func (t *TestInterface) UdevConnectedPlug(spec *udev.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.UdevConnectedPlugCallback != nil { + return t.UdevConnectedPlugCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) UdevPermanentPlug(spec *udev.Specification, plug *interfaces.Plug) error { + if t.UdevPermanentPlugCallback != nil { + return t.UdevPermanentPlugCallback(spec, plug) + } + return nil +} + +func (t *TestInterface) UdevPermanentSlot(spec *udev.Specification, slot *interfaces.Slot) error { + if t.UdevPermanentSlotCallback != nil { + return t.UdevPermanentSlotCallback(spec, slot) + } + return nil +} + +func (t *TestInterface) UdevConnectedSlot(spec *udev.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.UdevConnectedSlotCallback != nil { + return t.UdevConnectedSlotCallback(spec, plug, slot) + } + return nil +} + +// Support for interacting with the apparmor backend. + +func (t *TestInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.AppArmorConnectedPlugCallback != nil { + return t.AppArmorConnectedPlugCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) AppArmorPermanentSlot(spec *apparmor.Specification, slot *interfaces.Slot) error { + if t.AppArmorPermanentSlotCallback != nil { + return t.AppArmorPermanentSlotCallback(spec, slot) + } + return nil +} + +func (t *TestInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.AppArmorConnectedSlotCallback != nil { + return t.AppArmorConnectedSlotCallback(spec, plug, slot) + + } + return nil +} + +func (t *TestInterface) AppArmorPermanentPlug(spec *apparmor.Specification, plug *interfaces.Plug) error { + if t.AppArmorPermanentPlugCallback != nil { + return t.AppArmorPermanentPlugCallback(spec, plug) + } + return nil +} + +// Support for interacting with the seccomp backend. + +func (t *TestInterface) SecCompConnectedPlug(spec *seccomp.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.SecCompConnectedPlugCallback != nil { + return t.SecCompConnectedPlugCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) SecCompConnectedSlot(spec *seccomp.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.SecCompConnectedSlotCallback != nil { + return t.SecCompConnectedSlotCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) SecCompPermanentSlot(spec *seccomp.Specification, slot *interfaces.Slot) error { + if t.SecCompPermanentSlotCallback != nil { + return t.SecCompPermanentSlotCallback(spec, slot) + } + return nil +} + +func (t *TestInterface) SecCompPermanentPlug(spec *seccomp.Specification, plug *interfaces.Plug) error { + if t.SecCompPermanentPlugCallback != nil { + return t.SecCompPermanentPlugCallback(spec, plug) + } + return nil +} + +// Support for interacting with the kmod backend. + +func (t *TestInterface) KModConnectedPlug(spec *kmod.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.KModConnectedPlugCallback != nil { + return t.KModConnectedPlugCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) KModConnectedSlot(spec *kmod.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + if t.KModConnectedSlotCallback != nil { + return t.KModConnectedSlotCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) KModPermanentPlug(spec *kmod.Specification, plug *interfaces.Plug) error { + if t.KModPermanentPlugCallback != nil { + return t.KModPermanentPlugCallback(spec, plug) + } + return nil +} + +func (t *TestInterface) KModPermanentSlot(spec *kmod.Specification, slot *interfaces.Slot) error { + if t.KModPermanentSlotCallback != nil { + return t.KModPermanentSlotCallback(spec, slot) + } + return nil +} diff -Nru snapd-2.22.6+16.10/interfaces/kmod/spec.go snapd-2.23.1+16.10/interfaces/kmod/spec.go --- snapd-2.22.6+16.10/interfaces/kmod/spec.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/kmod/spec.go 2017-03-08 13:28:17.000000000 +0000 @@ -0,0 +1,102 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package kmod + +import ( + "strings" + + "github.com/snapcore/snapd/interfaces" +) + +// Specification assists in collecting kernel modules associated with an interface. +// +// Unlike the Backend itself (which is stateless and non-persistent) this type +// holds internal state that is used by the kmod backend during the interface +// setup process. +type Specification struct { + modules map[string]bool +} + +// AddModule adds a kernel module, trimming spaces and ignoring duplicated modules. +func (spec *Specification) AddModule(module string) error { + m := strings.TrimSpace(module) + if len(m) > 0 { + if spec.modules == nil { + spec.modules = make(map[string]bool) + } + spec.modules[m] = true + } + return nil +} + +// Modules returns a copy of the kernel module names added. +func (spec *Specification) Modules() map[string]bool { + result := make(map[string]bool, len(spec.modules)) + for k, v := range spec.modules { + result[k] = v + } + return result +} + +// Implementation of methods required by interfaces.Specification + +// AddConnectedPlug records kmod-specific side-effects of having a connected plug. +func (spec *Specification) AddConnectedPlug(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + KModConnectedPlug(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + return iface.KModConnectedPlug(spec, plug, slot) + } + return nil +} + +// AddConnectedSlot records mount-specific side-effects of having a connected slot. +func (spec *Specification) AddConnectedSlot(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + KModConnectedSlot(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + return iface.KModConnectedSlot(spec, plug, slot) + } + return nil +} + +// AddPermanentPlug records mount-specific side-effects of having a plug. +func (spec *Specification) AddPermanentPlug(iface interfaces.Interface, plug *interfaces.Plug) error { + type definer interface { + KModPermanentPlug(spec *Specification, plug *interfaces.Plug) error + } + if iface, ok := iface.(definer); ok { + return iface.KModPermanentPlug(spec, plug) + } + return nil +} + +// AddPermanentSlot records mount-specific side-effects of having a slot. +func (spec *Specification) AddPermanentSlot(iface interfaces.Interface, slot *interfaces.Slot) error { + type definer interface { + KModPermanentSlot(spec *Specification, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + return iface.KModPermanentSlot(spec, slot) + } + return nil +} diff -Nru snapd-2.22.6+16.10/interfaces/kmod/spec_test.go snapd-2.23.1+16.10/interfaces/kmod/spec_test.go --- snapd-2.22.6+16.10/interfaces/kmod/spec_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/kmod/spec_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,129 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package kmod_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/interfaces/kmod" + "github.com/snapcore/snapd/snap" +) + +type specSuite struct { + iface1, iface2 *ifacetest.TestInterface + spec *kmod.Specification + plug *interfaces.Plug + slot *interfaces.Slot +} + +var _ = Suite(&specSuite{ + iface1: &ifacetest.TestInterface{ + InterfaceName: "test", + KModConnectedPlugCallback: func(spec *kmod.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddModule("module1") + }, + KModConnectedSlotCallback: func(spec *kmod.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddModule("module2") + }, + KModPermanentPlugCallback: func(spec *kmod.Specification, plug *interfaces.Plug) error { + return spec.AddModule("module3") + }, + KModPermanentSlotCallback: func(spec *kmod.Specification, slot *interfaces.Slot) error { + return spec.AddModule("module4") + }, + }, + iface2: &ifacetest.TestInterface{ + InterfaceName: "test-two", + KModConnectedPlugCallback: func(spec *kmod.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddModule("module1") + }, + KModConnectedSlotCallback: func(spec *kmod.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddModule("module2") + }, + KModPermanentPlugCallback: func(spec *kmod.Specification, plug *interfaces.Plug) error { + return spec.AddModule("module5") + }, + KModPermanentSlotCallback: func(spec *kmod.Specification, slot *interfaces.Slot) error { + return spec.AddModule("module6") + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "snap"}, + Name: "name", + Interface: "test", + }, + }, + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "snap"}, + Name: "name", + Interface: "test", + }, + }, +}) + +func (s *specSuite) SetUpTest(c *C) { + s.spec = &kmod.Specification{} +} + +// AddModule is not broken +func (s *specSuite) TestSmoke(c *C) { + m1 := "module1" + m2 := "module2" + c.Assert(s.spec.AddModule(m1), IsNil) + c.Assert(s.spec.AddModule(m2), IsNil) + c.Assert(s.spec.Modules(), DeepEquals, map[string]bool{ + "module1": true, "module2": true}) +} + +// AddModule ignores duplicated modules +func (s *specSuite) TestDeduplication(c *C) { + mod := "module1" + c.Assert(s.spec.AddModule(mod), IsNil) + c.Assert(s.spec.AddModule(mod), IsNil) + c.Assert(s.spec.Modules(), DeepEquals, map[string]bool{"module1": true}) + + var r interfaces.Specification = s.spec + c.Assert(r.AddConnectedPlug(s.iface1, s.plug, s.slot), IsNil) + c.Assert(r.AddConnectedSlot(s.iface1, s.plug, s.slot), IsNil) + c.Assert(r.AddPermanentPlug(s.iface1, s.plug), IsNil) + c.Assert(r.AddPermanentSlot(s.iface1, s.slot), IsNil) + + c.Assert(r.AddConnectedPlug(s.iface2, s.plug, s.slot), IsNil) + c.Assert(r.AddConnectedSlot(s.iface2, s.plug, s.slot), IsNil) + c.Assert(r.AddPermanentPlug(s.iface2, s.plug), IsNil) + c.Assert(r.AddPermanentSlot(s.iface2, s.slot), IsNil) + c.Assert(s.spec.Modules(), DeepEquals, map[string]bool{ + "module1": true, "module2": true, "module3": true, "module4": true, "module5": true, "module6": true}) +} + +// The kmod.Specification can be used through the interfaces.Specification interface +func (s *specSuite) TestSpecificationIface(c *C) { + var r interfaces.Specification = s.spec + c.Assert(r.AddConnectedPlug(s.iface1, s.plug, s.slot), IsNil) + c.Assert(r.AddConnectedSlot(s.iface1, s.plug, s.slot), IsNil) + c.Assert(r.AddPermanentPlug(s.iface1, s.plug), IsNil) + c.Assert(r.AddPermanentSlot(s.iface1, s.slot), IsNil) + c.Assert(s.spec.Modules(), DeepEquals, map[string]bool{ + "module1": true, "module2": true, "module3": true, "module4": true}) +} diff -Nru snapd-2.22.6+16.10/interfaces/mount/backend.go snapd-2.23.1+16.10/interfaces/mount/backend.go --- snapd-2.22.6+16.10/interfaces/mount/backend.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/mount/backend.go 2017-03-06 13:33:50.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -50,24 +50,20 @@ // Setup creates mount mount profile files specific to a given snap. func (b *Backend) Setup(snapInfo *snap.Info, confinement interfaces.ConfinementOptions, repo *interfaces.Repository) error { + // Record all changes to the mount system for this snap. snapName := snapInfo.Name() - // Get the snippets that apply to this snap - snippets, err := repo.SecuritySnippetsForSnap(snapInfo.Name(), interfaces.SecurityMount) + spec, err := repo.SnapSpecification(b.Name(), snapName) if err != nil { return fmt.Errorf("cannot obtain mount security snippets for snap %q: %s", snapName, err) } - // Get the files that this snap should have - content, err := b.combineSnippets(snapInfo, snippets) - if err != nil { - return fmt.Errorf("cannot obtain expected mount configuration files for snap %q: %s", snapName, err) - } - glob := fmt.Sprintf("%s.fstab", interfaces.SecurityTagGlob(snapName)) + content := deriveContent(spec.(*Specification), snapInfo) + // synchronize the content with the filesystem + glob := fmt.Sprintf("snap.%s.*fstab", snapName) dir := dirs.SnapMountPolicyDir if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("cannot create directory for mount configuration files %q: %s", dir, err) } - _, _, err = osutil.EnsureDirState(dir, glob, content) - if err != nil { + if _, _, err := osutil.EnsureDirState(dir, glob, content); err != nil { return fmt.Errorf("cannot synchronize mount configuration files for snap %q: %s", snapName, err) } return nil @@ -77,7 +73,7 @@ // // This method should be called after removing a snap. func (b *Backend) Remove(snapName string) error { - glob := fmt.Sprintf("%s.fstab", interfaces.SecurityTagGlob(snapName)) + glob := fmt.Sprintf("snap.%s.*fstab", snapName) _, _, err := osutil.EnsureDirState(dirs.SnapMountPolicyDir, glob, nil) if err != nil { return fmt.Errorf("cannot synchronize mount configuration files for snap %q: %s", snapName, err) @@ -85,50 +81,34 @@ return nil } -// combineSnippets combines security snippets collected from all the interfaces -// affecting a given snap into a content map applicable to EnsureDirState. -func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) { - for _, appInfo := range snapInfo.Apps { - securityTag := appInfo.SecurityTag() - appSnippets := snippets[securityTag] - if len(appSnippets) == 0 { - continue - } - if content == nil { - content = make(map[string]*osutil.FileState) - } - - addContent(securityTag, appSnippets, content) +// deriveContent computes .fstab tables based on requests made to the specification. +func deriveContent(spec *Specification, snapInfo *snap.Info) map[string]*osutil.FileState { + // No entries? Nothing to do! + if len(spec.mountEntries) == 0 { + return nil } - - for _, hookInfo := range snapInfo.Hooks { - securityTag := hookInfo.SecurityTag() - hookSnippets := snippets[securityTag] - if len(hookSnippets) == 0 { - continue - } - if content == nil { - content = make(map[string]*osutil.FileState) - } - - addContent(securityTag, hookSnippets, content) - } - return content, nil -} - -func addContent(securityTag string, executableSnippets [][]byte, content map[string]*osutil.FileState) { + // Compute the contents of the fstab file. It should contain all the mount + // rules collected by the backend controller. var buffer bytes.Buffer - for _, snippet := range executableSnippets { - buffer.Write(snippet) - buffer.WriteRune('\n') + for _, entry := range spec.mountEntries { + fmt.Fprintf(&buffer, "%s\n", entry) } - - content[fmt.Sprintf("%s.fstab", securityTag)] = &osutil.FileState{ - Content: buffer.Bytes(), - Mode: 0644, + fstate := &osutil.FileState{Content: buffer.Bytes(), Mode: 0644} + content := make(map[string]*osutil.FileState) + // Add the new per-snap fstab file. This file will be read by snap-confine. + content[fmt.Sprintf("snap.%s.fstab", snapInfo.Name())] = fstate + // Add legacy per-app/per-hook fstab files. Those are identical but + // snap-confine doesn't yet load it from a per-snap location. This can be + // safely removed once snap-confine is updated. + for _, appInfo := range snapInfo.Apps { + content[fmt.Sprintf("%s.fstab", appInfo.SecurityTag())] = fstate + } + for _, hookInfo := range snapInfo.Hooks { + content[fmt.Sprintf("%s.fstab", hookInfo.SecurityTag())] = fstate } + return content } func (b *Backend) NewSpecification() interfaces.Specification { - panic(fmt.Errorf("%s is not using specifications yet", b.Name())) + return &Specification{} } diff -Nru snapd-2.22.6+16.10/interfaces/mount/backend_test.go snapd-2.23.1+16.10/interfaces/mount/backend_test.go --- snapd-2.22.6+16.10/interfaces/mount/backend_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/mount/backend_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -53,6 +53,8 @@ s.Backend = &mount.Backend{} s.BackendSuite.SetUpTest(c) + c.Assert(s.Repo.AddBackend(s.Backend), IsNil) + err := os.MkdirAll(dirs.SnapMountPolicyDir, 0700) c.Assert(err, IsNil) @@ -79,16 +81,28 @@ err = ioutil.WriteFile(hookCanaryToGo, []byte("ni! ni! ni!"), 0644) c.Assert(err, IsNil) - canaryToStay := filepath.Join(dirs.SnapMountPolicyDir, "snap.i-stay.really.fstab") - err = ioutil.WriteFile(canaryToStay, []byte("stay!"), 0644) + snapCanaryToGo := filepath.Join(dirs.SnapMountPolicyDir, "snap.hello-world.fstab") + err = ioutil.WriteFile(snapCanaryToGo, []byte("ni! ni! ni!"), 0644) + c.Assert(err, IsNil) + + appCanaryToStay := filepath.Join(dirs.SnapMountPolicyDir, "snap.i-stay.really.fstab") + err = ioutil.WriteFile(appCanaryToStay, []byte("stay!"), 0644) + c.Assert(err, IsNil) + + snapCanaryToStay := filepath.Join(dirs.SnapMountPolicyDir, "snap.i-stay.fstab") + err = ioutil.WriteFile(snapCanaryToStay, []byte("stay!"), 0644) c.Assert(err, IsNil) err = s.Backend.Remove("hello-world") c.Assert(err, IsNil) + c.Assert(osutil.FileExists(snapCanaryToGo), Equals, false) c.Assert(osutil.FileExists(appCanaryToGo), Equals, false) c.Assert(osutil.FileExists(hookCanaryToGo), Equals, false) - content, err := ioutil.ReadFile(canaryToStay) + content, err := ioutil.ReadFile(appCanaryToStay) + c.Assert(err, IsNil) + c.Assert(string(content), Equals, "stay!") + content, err = ioutil.ReadFile(snapCanaryToStay) c.Assert(err, IsNil) c.Assert(string(content), Equals, "stay!") } @@ -100,47 +114,47 @@ app2: hooks: configure: - plugs: [iface-plug, iface2-plug] + plugs: [iface-plug] plugs: iface-plug: interface: iface - iface2-plug: - interface: iface2 slots: iface-slot: - interface: iface - iface2-slot: interface: iface2 ` func (s *backendSuite) TestSetupSetsupSimple(c *C) { - fsEntryIF1 := "/src-1 /dst-1 none bind,ro 0 0" - fsEntryIF2 := "/src-2 /dst-2 none bind,ro 0 0" + fsEntry1 := mount.Entry{Name: "/src-1", Dir: "/dst-1", Type: "none", Options: []string{"bind", "ro"}, DumpFrequency: 0, CheckPassNumber: 0} + fsEntry2 := mount.Entry{Name: "/src-2", Dir: "/dst-2", Type: "none", Options: []string{"bind", "ro"}, DumpFrequency: 0, CheckPassNumber: 0} - s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { - return []byte(fsEntryIF1), nil - } - s.Iface.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) { - return []byte(fsEntryIF1), nil - } - s.iface2.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { - return []byte(fsEntryIF2), nil - } - s.iface2.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) { - return []byte(fsEntryIF2), nil + // Give the plug a permanent effect + s.Iface.MountPermanentPlugCallback = func(spec *mount.Specification, plug *interfaces.Plug) error { + return spec.AddMountEntry(fsEntry1) + } + // Give the slot a permanent effect + s.iface2.MountPermanentSlotCallback = func(spec *mount.Specification, slot *interfaces.Slot) error { + return spec.AddMountEntry(fsEntry2) } // confinement options are irrelevant to this security backend s.InstallSnap(c, interfaces.ConfinementOptions{}, mockSnapYaml, 0) - // ensure both security snippets for iface/iface2 are combined - expected := strings.Split(fmt.Sprintf("%s\n%s\n", fsEntryIF1, fsEntryIF2), "\n") + // ensure both security effects from iface/iface2 are combined + // (because mount profiles are global in the whole snap) + expected := strings.Split(fmt.Sprintf("%s\n%s\n", fsEntry1, fsEntry2), "\n") sort.Strings(expected) - // and we have them both for both apps and the hook + // and that we have the modern fstab file (global for snap) + fn := filepath.Join(dirs.SnapMountPolicyDir, "snap.snap-name.fstab") + content, err := ioutil.ReadFile(fn) + c.Assert(err, IsNil, Commentf("Expected mount profile for the whole snap")) + got := strings.Split(string(content), "\n") + sort.Strings(got) + c.Check(got, DeepEquals, expected) + // and that we have the legacy, per app/hook files as well. for _, binary := range []string{"app1", "app2", "hook.configure"} { - fn1 := filepath.Join(dirs.SnapMountPolicyDir, fmt.Sprintf("snap.snap-name.%s.fstab", binary)) - content, err := ioutil.ReadFile(fn1) - c.Assert(err, IsNil, Commentf("Expected mount file for %q", binary)) + fn := filepath.Join(dirs.SnapMountPolicyDir, fmt.Sprintf("snap.snap-name.%s.fstab", binary)) + content, err := ioutil.ReadFile(fn) + c.Assert(err, IsNil, Commentf("Expected mount profile for %q", binary)) got := strings.Split(string(content), "\n") sort.Strings(got) c.Check(got, DeepEquals, expected) @@ -148,11 +162,8 @@ } func (s *backendSuite) TestSetupSetsupWithoutDir(c *C) { - s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { - return []byte("xxx"), nil - } - s.Iface.PermanentPlugSnippetCallback = func(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) { - return []byte("xxx"), nil + s.Iface.MountPermanentPlugCallback = func(spec *mount.Specification, plug *interfaces.Plug) error { + return spec.AddMountEntry(mount.Entry{}) } // Ensure that backend.Setup() creates the required dir on demand diff -Nru snapd-2.22.6+16.10/interfaces/mount/entry.go snapd-2.23.1+16.10/interfaces/mount/entry.go --- snapd-2.22.6+16.10/interfaces/mount/entry.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/mount/entry.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,86 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package mount + +import ( + "fmt" + "strings" +) + +// Entry describes an /etc/fstab-like mount entry. +// +// Fields are named after names in struct returned by getmntent(3). +// +// struct mntent { +// char *mnt_fsname; /* name of mounted filesystem */ +// char *mnt_dir; /* filesystem path prefix */ +// char *mnt_type; /* mount type (see Mntent.h) */ +// char *mnt_opts; /* mount options (see Mntent.h) */ +// int mnt_freq; /* dump frequency in days */ +// int mnt_passno; /* pass number on parallel fsck */ +// }; +type Entry struct { + Name string + Dir string + Type string + Options []string + + DumpFrequency int + CheckPassNumber int +} + +// escape replaces whitespace characters so that getmntent can parse it correctly. +// +// According to the manual page, the following characters need to be escaped. +// space => (\040) +// tab => (\011) +// newline => (\012) +// backslash => (\134) +func escape(s string) string { + return whitespaceReplacer.Replace(s) +} + +var whitespaceReplacer = strings.NewReplacer( + " ", `\040`, "\t", `\011`, "\n", `\012`, "\\", `\134`) + +func (e Entry) String() string { + // Name represents name of the device in a mount entry. + name := "none" + if e.Name != "" { + name = escape(e.Name) + } + // Dir represents mount directory in a mount entry. + dir := "none" + if e.Dir != "" { + dir = escape(e.Dir) + } + // Type represents file system type in a mount entry. + fsType := "none" + if e.Type != "" { + fsType = escape(e.Type) + } + // Options represents mount options in a mount entry. + options := "defaults" + if len(e.Options) != 0 { + options = escape(strings.Join(e.Options, ",")) + } + return fmt.Sprintf("%s %s %s %s %d %d", + name, dir, fsType, options, e.DumpFrequency, e.CheckPassNumber) +} diff -Nru snapd-2.22.6+16.10/interfaces/mount/entry_test.go snapd-2.23.1+16.10/interfaces/mount/entry_test.go --- snapd-2.22.6+16.10/interfaces/mount/entry_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/mount/entry_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,56 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package mount_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces/mount" +) + +type entrySuite struct{} + +var _ = Suite(&entrySuite{}) + +func (s *entrySuite) TestString(c *C) { + ent0 := mount.Entry{} + c.Assert(ent0.String(), Equals, "none none none defaults 0 0") + ent1 := mount.Entry{ + Name: "/var/snap/foo/common", + Dir: "/var/snap/bar/common", + Options: []string{"bind"}, + } + c.Assert(ent1.String(), Equals, + "/var/snap/foo/common /var/snap/bar/common none bind 0 0") + ent2 := mount.Entry{ + Name: "/dev/sda5", + Dir: "/media/foo", + Type: "ext4", + Options: []string{"rw,noatime"}, + } + c.Assert(ent2.String(), Equals, "/dev/sda5 /media/foo ext4 rw,noatime 0 0") + ent3 := mount.Entry{ + Name: "/dev/sda5", + Dir: "/media/My Files", + Type: "ext4", + Options: []string{"rw,noatime"}, + } + c.Assert(ent3.String(), Equals, `/dev/sda5 /media/My\040Files ext4 rw,noatime 0 0`) +} diff -Nru snapd-2.22.6+16.10/interfaces/mount/spec.go snapd-2.23.1+16.10/interfaces/mount/spec.go --- snapd-2.22.6+16.10/interfaces/mount/spec.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/mount/spec.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,92 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package mount + +import ( + "github.com/snapcore/snapd/interfaces" +) + +// Specification assists in collecting mount entries associated with an interface. +// +// Unlike the Backend itself (which is stateless and non-persistent) this type +// holds internal state that is used by the mount backend during the interface +// setup process. +type Specification struct { + mountEntries []Entry +} + +// AddMountEntry adds a new mount entry. +func (spec *Specification) AddMountEntry(e Entry) error { + spec.mountEntries = append(spec.mountEntries, e) + return nil +} + +// MountEntries returns a copy of the added mount entries. +func (spec *Specification) MountEntries() []Entry { + result := make([]Entry, len(spec.mountEntries)) + copy(result, spec.mountEntries) + return result +} + +// Implementation of methods required by interfaces.Specification + +// ConnectedPlug records mount-specific side-effects of having a connected plug. +func (spec *Specification) AddConnectedPlug(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + MountConnectedPlug(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + return iface.MountConnectedPlug(spec, plug, slot) + } + return nil +} + +// ConnectedSlot records mount-specific side-effects of having a connected slot. +func (spec *Specification) AddConnectedSlot(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + MountConnectedSlot(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + return iface.MountConnectedSlot(spec, plug, slot) + } + return nil +} + +// PermanentPlug records mount-specific side-effects of having a plug. +func (spec *Specification) AddPermanentPlug(iface interfaces.Interface, plug *interfaces.Plug) error { + type definer interface { + MountPermanentPlug(spec *Specification, plug *interfaces.Plug) error + } + if iface, ok := iface.(definer); ok { + return iface.MountPermanentPlug(spec, plug) + } + return nil +} + +// PermanentSlot records mount-specific side-effects of having a slot. +func (spec *Specification) AddPermanentSlot(iface interfaces.Interface, slot *interfaces.Slot) error { + type definer interface { + MountPermanentSlot(spec *Specification, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + return iface.MountPermanentSlot(spec, slot) + } + return nil +} diff -Nru snapd-2.22.6+16.10/interfaces/mount/spec_test.go snapd-2.23.1+16.10/interfaces/mount/spec_test.go --- snapd-2.22.6+16.10/interfaces/mount/spec_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/mount/spec_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,93 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package mount_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/snap" +) + +type specSuite struct { + iface *ifacetest.TestInterface + spec *mount.Specification + plug *interfaces.Plug + slot *interfaces.Slot +} + +var _ = Suite(&specSuite{ + iface: &ifacetest.TestInterface{ + InterfaceName: "test", + MountConnectedPlugCallback: func(spec *mount.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddMountEntry(mount.Entry{Name: "connected-plug"}) + }, + MountConnectedSlotCallback: func(spec *mount.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddMountEntry(mount.Entry{Name: "connected-slot"}) + }, + MountPermanentPlugCallback: func(spec *mount.Specification, plug *interfaces.Plug) error { + return spec.AddMountEntry(mount.Entry{Name: "permanent-plug"}) + }, + MountPermanentSlotCallback: func(spec *mount.Specification, slot *interfaces.Slot) error { + return spec.AddMountEntry(mount.Entry{Name: "permanent-slot"}) + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "snap"}, + Name: "name", + Interface: "test", + }, + }, + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "snap"}, + Name: "name", + Interface: "test", + }, + }, +}) + +func (s *specSuite) SetUpTest(c *C) { + s.spec = &mount.Specification{} +} + +// AddMountEntry is not broken +func (s *specSuite) TestSmoke(c *C) { + ent0 := mount.Entry{Name: "fs1"} + ent1 := mount.Entry{Name: "fs2"} + c.Assert(s.spec.AddMountEntry(ent0), IsNil) + c.Assert(s.spec.AddMountEntry(ent1), IsNil) + c.Assert(s.spec.MountEntries(), DeepEquals, []mount.Entry{ent0, ent1}) +} + +// The mount.Specification can be used through the interfaces.Specification interface +func (s *specSuite) TestSpecificationIface(c *C) { + var r interfaces.Specification = s.spec + c.Assert(r.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddConnectedSlot(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddPermanentPlug(s.iface, s.plug), IsNil) + c.Assert(r.AddPermanentSlot(s.iface, s.slot), IsNil) + c.Assert(s.spec.MountEntries(), DeepEquals, []mount.Entry{ + {Name: "connected-plug"}, {Name: "connected-slot"}, + {Name: "permanent-plug"}, {Name: "permanent-slot"}}) +} diff -Nru snapd-2.22.6+16.10/interfaces/policy/export_test.go snapd-2.23.1+16.10/interfaces/policy/export_test.go --- snapd-2.22.6+16.10/interfaces/policy/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/policy/export_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,24 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package policy + +var ( + NestedGet = nestedGet +) diff -Nru snapd-2.22.6+16.10/interfaces/policy/helpers.go snapd-2.23.1+16.10/interfaces/policy/helpers.go --- snapd-2.22.6+16.10/interfaces/policy/helpers.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/policy/helpers.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -81,10 +81,10 @@ } func checkPlugConnectionConstraints1(connc *ConnectCandidate, cstrs *asserts.PlugConnectionConstraints) error { - if err := cstrs.PlugAttributes.Check(connc.plugAttrs()); err != nil { + if err := cstrs.PlugAttributes.Check(connc.plugAttrs(), connc); err != nil { return err } - if err := cstrs.SlotAttributes.Check(connc.slotAttrs()); err != nil { + if err := cstrs.SlotAttributes.Check(connc.slotAttrs(), connc); err != nil { return err } if err := checkSnapType(connc.slotSnapType(), cstrs.SlotSnapTypes); err != nil { @@ -121,10 +121,10 @@ } func checkSlotConnectionConstraints1(connc *ConnectCandidate, cstrs *asserts.SlotConnectionConstraints) error { - if err := cstrs.PlugAttributes.Check(connc.plugAttrs()); err != nil { + if err := cstrs.PlugAttributes.Check(connc.plugAttrs(), connc); err != nil { return err } - if err := cstrs.SlotAttributes.Check(connc.slotAttrs()); err != nil { + if err := cstrs.SlotAttributes.Check(connc.slotAttrs(), connc); err != nil { return err } if err := checkSnapType(connc.plugSnapType(), cstrs.PlugSnapTypes); err != nil { @@ -161,7 +161,8 @@ } func checkSlotInstallationConstraints1(slot *snap.SlotInfo, cstrs *asserts.SlotInstallationConstraints) error { - if err := cstrs.SlotAttributes.Check(slot.Attrs); err != nil { + // TODO: allow evaluated attr constraints here too? + if err := cstrs.SlotAttributes.Check(slot.Attrs, nil); err != nil { return err } if err := checkSnapType(slot.Snap.Type, cstrs.SlotSnapTypes); err != nil { @@ -189,7 +190,8 @@ } func checkPlugInstallationConstraints1(plug *snap.PlugInfo, cstrs *asserts.PlugInstallationConstraints) error { - if err := cstrs.PlugAttributes.Check(plug.Attrs); err != nil { + // TODO: allow evaluated attr constraints here too? + if err := cstrs.PlugAttributes.Check(plug.Attrs, nil); err != nil { return err } if err := checkSnapType(plug.Snap.Type, cstrs.PlugSnapTypes); err != nil { diff -Nru snapd-2.22.6+16.10/interfaces/policy/helpers_test.go snapd-2.23.1+16.10/interfaces/policy/helpers_test.go --- snapd-2.22.6+16.10/interfaces/policy/helpers_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/policy/helpers_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,54 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package policy_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces/policy" +) + +type helpersSuite struct{} + +var _ = Suite(&helpersSuite{}) + +func (s *helpersSuite) TestNestedGet(c *C) { + _, err := policy.NestedGet("slot", nil, "a") + c.Check(err, ErrorMatches, `slot attribute "a" not found`) + + _, err = policy.NestedGet("plug", map[string]interface{}{ + "a": "123", + }, "a.b") + c.Check(err, ErrorMatches, `plug attribute "a\.b" not found`) + + v, err := policy.NestedGet("slot", map[string]interface{}{ + "a": "123", + }, "a") + c.Check(err, IsNil) + c.Check(v, Equals, "123") + + v, err = policy.NestedGet("slot", map[string]interface{}{ + "a": map[string]interface{}{ + "b": []interface{}{"1", "2", "3"}, + }, + }, "a.b") + c.Check(err, IsNil) + c.Check(v, DeepEquals, []interface{}{"1", "2", "3"}) +} diff -Nru snapd-2.22.6+16.10/interfaces/policy/policy.go snapd-2.23.1+16.10/interfaces/policy/policy.go --- snapd-2.22.6+16.10/interfaces/policy/policy.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/policy/policy.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -24,6 +24,7 @@ import ( "fmt" + "strings" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/snap" @@ -133,6 +134,31 @@ return connc.Slot.Attrs } +func nestedGet(which string, attrs map[string]interface{}, path string) (interface{}, error) { + notFound := fmt.Errorf("%s attribute %q not found", which, path) + comps := strings.Split(path, ".") + var v interface{} = attrs + for _, comp := range comps { + m, ok := v.(map[string]interface{}) + if !ok { + return nil, notFound + } + v, ok = m[comp] + if !ok { + return nil, notFound + } + } + return v, nil +} + +func (connc *ConnectCandidate) PlugAttr(arg string) (interface{}, error) { + return nestedGet("plug", connc.Plug.Attrs, arg) +} + +func (connc *ConnectCandidate) SlotAttr(arg string) (interface{}, error) { + return nestedGet("slot", connc.Slot.Attrs, arg) +} + func (connc *ConnectCandidate) plugSnapType() snap.Type { return connc.Plug.Snap.Type } diff -Nru snapd-2.22.6+16.10/interfaces/policy/policy_test.go snapd-2.23.1+16.10/interfaces/policy/policy_test.go --- snapd-2.22.6+16.10/interfaces/policy/policy_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/policy/policy_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -71,6 +71,14 @@ allow-connection: slot-publisher-id: - $PLUG_PUBLISHER_ID + plug-plug-attr: + allow-connection: + slot-attributes: + c: $PLUG(c) + plug-slot-attr: + allow-connection: + plug-attributes: + c: $SLOT(c) plug-or: allow-connection: - @@ -171,6 +179,19 @@ allow-connection: plug-publisher-id: - $SLOT_PUBLISHER_ID + slot-slot-attr: + allow-connection: + plug-attributes: + a: + b: $SLOT(a.b) + slot-plug-attr: + allow-connection: + slot-attributes: + c: $PLUG(c) + slot-plug-missing: + allow-connection: + plug-attributes: + x: $MISSING slot-or: allow-connection: - @@ -330,6 +351,39 @@ same-plug-publisher-id: + slot-slot-attr-mismatch: + interface: slot-slot-attr + a: + b: [] + + slot-slot-attr-match: + interface: slot-slot-attr + a: + b: ["x", "y"] + + slot-plug-attr-mismatch: + interface: slot-plug-attr + c: "Z" + + slot-plug-attr-match: + interface: slot-plug-attr + c: "C" + + slot-plug-missing-mismatch: + interface: slot-plug-missing + x: 1 + z: 2 + + slot-plug-missing-match: + interface: slot-plug-missing + z: 2 + + plug-plug-attr: + c: "C" + + plug-slot-attr: + c: "C" + plug-or-p1-s1: interface: plug-or p: P1 @@ -456,6 +510,31 @@ same-slot-publisher-id: + slot-slot-attr: + a: + b: ["x", "y"] + + slot-plug-attr: + c: "C" + + slot-plug-missing: + + plug-plug-attr-mismatch: + interface: plug-plug-attr + c: "Z" + + plug-plug-attr-match: + interface: plug-plug-attr + c: "C" + + plug-slot-attr-mismatch: + interface: plug-slot-attr + c: "Z" + + plug-slot-attr-match: + interface: plug-slot-attr + c: "C" + plug-or-p1-s1: interface: plug-or s: S1 @@ -601,6 +680,7 @@ precise-plug-snap-id: checked-plug-publisher-id: same-slot-publisher-id: + slot-slot-attr: slots: precise-slot-snap-id: checked-slot-publisher-id: @@ -1421,3 +1501,101 @@ } } } + +func (s *policySuite) TestSlotDollarSlotAttrConnection(c *C) { + // no corresponding attr + cand := policy.ConnectCandidate{ + Plug: s.randomSnap.Plugs["slot-slot-attr"], + Slot: s.slotSnap.Slots["slot-slot-attr"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), ErrorMatches, "connection not allowed.*") + + // different attr values + cand = policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["slot-slot-attr-mismatch"], + Slot: s.slotSnap.Slots["slot-slot-attr"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), ErrorMatches, "connection not allowed.*") + + // plug attr == slot attr + cand = policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["slot-slot-attr-match"], + Slot: s.slotSnap.Slots["slot-slot-attr"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), IsNil) +} + +func (s *policySuite) TestSlotDollarPlugAttrConnection(c *C) { + // different attr values + cand := policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["slot-plug-attr-mismatch"], + Slot: s.slotSnap.Slots["slot-plug-attr"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), ErrorMatches, "connection not allowed.*") + + // plug attr == slot attr + cand = policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["slot-plug-attr-match"], + Slot: s.slotSnap.Slots["slot-plug-attr"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), IsNil) +} + +func (s *policySuite) TestPlugDollarPlugAttrConnection(c *C) { + // different attr values + cand := policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["plug-plug-attr"], + Slot: s.slotSnap.Slots["plug-plug-attr-mismatch"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), ErrorMatches, "connection not allowed.*") + + // plug attr == slot attr + cand = policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["plug-plug-attr"], + Slot: s.slotSnap.Slots["plug-plug-attr-match"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), IsNil) +} + +func (s *policySuite) TestPlugDollarSlotAttrConnection(c *C) { + // different attr values + cand := policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["plug-slot-attr"], + Slot: s.slotSnap.Slots["plug-slot-attr-mismatch"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), ErrorMatches, "connection not allowed.*") + + // plug attr == slot attr + cand = policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["plug-slot-attr"], + Slot: s.slotSnap.Slots["plug-slot-attr-match"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), IsNil) +} + +func (s *policySuite) TestDollarMissingConnection(c *C) { + // not missing + cand := policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["slot-plug-missing-mismatch"], + Slot: s.slotSnap.Slots["slot-plug-missing"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), ErrorMatches, "connection not allowed.*") + + // missing + cand = policy.ConnectCandidate{ + Plug: s.plugSnap.Plugs["slot-plug-missing-match"], + Slot: s.slotSnap.Slots["slot-plug-missing"], + BaseDeclaration: s.baseDecl, + } + c.Check(cand.Check(), IsNil) +} diff -Nru snapd-2.22.6+16.10/interfaces/seccomp/backend.go snapd-2.23.1+16.10/interfaces/seccomp/backend.go --- snapd-2.22.6+16.10/interfaces/seccomp/backend.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/seccomp/backend.go 2017-03-08 13:28:17.000000000 +0000 @@ -36,6 +36,7 @@ "bytes" "fmt" "os" + "sort" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" @@ -123,7 +124,9 @@ } buffer.Write(defaultTemplate) - for _, snippet := range snippets[securityTag] { + snippetsForTag := snippets[securityTag] + sort.Sort(byByteContent(snippetsForTag)) + for _, snippet := range snippetsForTag { buffer.Write(snippet) buffer.WriteRune('\n') } @@ -137,3 +140,11 @@ func (b *Backend) NewSpecification() interfaces.Specification { panic(fmt.Errorf("%s is not using specifications yet", b.Name())) } + +type byByteContent [][]byte + +func (x byByteContent) Len() int { return len(x) } +func (x byByteContent) Swap(a, b int) { x[a], x[b] = x[b], x[a] } +func (x byByteContent) Less(a, b int) bool { + return bytes.Compare(x[a], x[b]) < 0 +} diff -Nru snapd-2.22.6+16.10/interfaces/seccomp/backend_test.go snapd-2.23.1+16.10/interfaces/seccomp/backend_test.go --- snapd-2.22.6+16.10/interfaces/seccomp/backend_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/seccomp/backend_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -167,7 +167,7 @@ for _, line := range []string{ // NOTE: a few randomly picked lines from the real profile. Comments // and empty lines are avoided as those can be discarded in the future. - "deny init_module\n", + "# - create_module, init_module, finit_module, delete_module (kernel modules)\n", "open\n", "getuid\n", } { @@ -227,3 +227,38 @@ s.RemoveSnap(c, snapInfo) } } + +const snapYaml = ` +name: foo +version: 1 +developer: acme +apps: + foo: + slots: [iface, iface2] +` + +// Ensure that combined snippets are sorted +func (s *backendSuite) TestCombineSnippetsOrdering(c *C) { + // NOTE: replace the real template with a shorter variant + restore := seccomp.MockTemplate([]byte("default\n")) + defer restore() + + iface2 := &ifacetest.TestInterface{InterfaceName: "iface2"} + s.Repo.AddInterface(iface2) + + s.Iface.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return []byte("zzz"), nil + } + iface2.PermanentSlotSnippetCallback = func(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + return []byte("aaa"), nil + } + + s.InstallSnap(c, interfaces.ConfinementOptions{}, snapYaml, 0) + profile := filepath.Join(dirs.SnapSeccompDir, "snap.foo.foo") + data, err := ioutil.ReadFile(profile) + c.Assert(err, IsNil) + c.Check(string(data), Equals, "default\naaa\nzzz\n") + stat, err := os.Stat(profile) + c.Assert(err, IsNil) + c.Check(stat.Mode(), Equals, os.FileMode(0644)) +} diff -Nru snapd-2.22.6+16.10/interfaces/seccomp/spec.go snapd-2.23.1+16.10/interfaces/seccomp/spec.go --- snapd-2.22.6+16.10/interfaces/seccomp/spec.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/seccomp/spec.go 2017-03-08 13:28:17.000000000 +0000 @@ -0,0 +1,113 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package seccomp + +import "github.com/snapcore/snapd/interfaces" + +// Specification keeps all the seccomp snippets. +type Specification struct { + // Snippets are indexed by security tag. + snippets map[string][][]byte + securityTags []string +} + +// AddSnippet adds a new seccomp snippet. +func (spec *Specification) AddSnippet(snippet []byte) error { + if len(spec.securityTags) == 0 { + return nil + } + if spec.snippets == nil { + spec.snippets = make(map[string][][]byte) + } + for _, tag := range spec.securityTags { + spec.snippets[tag] = append(spec.snippets[tag], snippet) + } + + return nil +} + +// Snippets returns a deep copy of all the added snippets. +func (spec *Specification) Snippets() map[string][][]byte { + result := make(map[string][][]byte, len(spec.snippets)) + for k, v := range spec.snippets { + vCopy := make([][]byte, 0, len(v)) + for _, vElem := range v { + vElemCopy := make([]byte, len(vElem)) + copy(vElemCopy, vElem) + vCopy = append(vCopy, vElemCopy) + } + result[k] = vCopy + } + return result +} + +// Implementation of methods required by interfaces.Specification + +// AddConnectedPlug records seccomp-specific side-effects of having a connected plug. +func (spec *Specification) AddConnectedPlug(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + SecCompConnectedPlug(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + spec.securityTags = plug.SecurityTags() + defer func() { spec.securityTags = nil }() + return iface.SecCompConnectedPlug(spec, plug, slot) + } + return nil +} + +// AddConnectedSlot records seccomp-specific side-effects of having a connected slot. +func (spec *Specification) AddConnectedSlot(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + SecCompConnectedSlot(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + spec.securityTags = slot.SecurityTags() + defer func() { spec.securityTags = nil }() + return iface.SecCompConnectedSlot(spec, plug, slot) + } + return nil +} + +// AddPermanentPlug records seccomp-specific side-effects of having a plug. +func (spec *Specification) AddPermanentPlug(iface interfaces.Interface, plug *interfaces.Plug) error { + type definer interface { + SecCompPermanentPlug(spec *Specification, plug *interfaces.Plug) error + } + if iface, ok := iface.(definer); ok { + spec.securityTags = plug.SecurityTags() + defer func() { spec.securityTags = nil }() + return iface.SecCompPermanentPlug(spec, plug) + } + return nil +} + +// AddPermanentSlot records seccomp-specific side-effects of having a slot. +func (spec *Specification) AddPermanentSlot(iface interfaces.Interface, slot *interfaces.Slot) error { + type definer interface { + SecCompPermanentSlot(spec *Specification, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + spec.securityTags = slot.SecurityTags() + defer func() { spec.securityTags = nil }() + return iface.SecCompPermanentSlot(spec, slot) + } + return nil +} diff -Nru snapd-2.22.6+16.10/interfaces/seccomp/spec_test.go snapd-2.23.1+16.10/interfaces/seccomp/spec_test.go --- snapd-2.22.6+16.10/interfaces/seccomp/spec_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/seccomp/spec_test.go 2017-03-08 13:28:17.000000000 +0000 @@ -0,0 +1,97 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package seccomp_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/interfaces/seccomp" + "github.com/snapcore/snapd/snap" +) + +type specSuite struct { + iface *ifacetest.TestInterface + spec *seccomp.Specification + plug *interfaces.Plug + slot *interfaces.Slot +} + +var _ = Suite(&specSuite{ + iface: &ifacetest.TestInterface{ + InterfaceName: "test", + SecCompConnectedPlugCallback: func(spec *seccomp.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddSnippet([]byte("connected-plug")) + }, + SecCompConnectedSlotCallback: func(spec *seccomp.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddSnippet([]byte("connected-slot")) + }, + SecCompPermanentPlugCallback: func(spec *seccomp.Specification, plug *interfaces.Plug) error { + return spec.AddSnippet([]byte("permanent-plug")) + }, + SecCompPermanentSlotCallback: func(spec *seccomp.Specification, slot *interfaces.Slot) error { + return spec.AddSnippet([]byte("permanent-slot")) + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "snap1"}, + Name: "name", + Interface: "test", + Apps: map[string]*snap.AppInfo{ + "app1": { + Snap: &snap.Info{ + SuggestedName: "snap1", + }, + Name: "app1"}}, + }, + }, + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "snap2"}, + Name: "name", + Interface: "test", + Apps: map[string]*snap.AppInfo{ + "app2": { + Snap: &snap.Info{ + SuggestedName: "snap2", + }, + Name: "app2"}}, + }, + }, +}) + +func (s *specSuite) SetUpTest(c *C) { + s.spec = &seccomp.Specification{} +} + +// The spec.Specification can be used through the interfaces.Specification interface +func (s *specSuite) TestSpecificationIface(c *C) { + var r interfaces.Specification = s.spec + c.Assert(r.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddConnectedSlot(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddPermanentPlug(s.iface, s.plug), IsNil) + c.Assert(r.AddPermanentSlot(s.iface, s.slot), IsNil) + c.Assert(s.spec.Snippets(), DeepEquals, map[string][][]byte{ + "snap.snap1.app1": {[]byte("connected-plug"), []byte("permanent-plug")}, + "snap.snap2.app2": {[]byte("connected-slot"), []byte("permanent-slot")}, + }) +} diff -Nru snapd-2.22.6+16.10/interfaces/seccomp/template.go snapd-2.23.1+16.10/interfaces/seccomp/template.go --- snapd-2.22.6+16.10/interfaces/seccomp/template.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/seccomp/template.go 2017-03-02 06:37:54.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2017 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -20,53 +20,25 @@ package seccomp // defaultTemplate contains default seccomp template. -// // It can be overridden for testing using MockTemplate(). -// -// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/templates/ubuntu-core/16.04/default var defaultTemplate = []byte(` # Description: Allows access to app-specific directories and basic runtime -# Usage: common # +# The default seccomp policy is default deny with a whitelist of allowed +# syscalls. The default policy is intended to be safe for any application to +# use and should be evaluated in conjunction with other security backends (eg +# AppArmor). For example, a few particularly problematic syscalls that are left +# out of the default policy are (non-exhaustive): +# - kexec_load +# - create_module, init_module, finit_module, delete_module (kernel modules) +# - name_to_handle_at (history of vulnerabilities) +# - open_by_handle_at (history of vulnerabilities) +# - ptrace (can be used to break out of sandbox with <4.8 kernels) +# - add_key, keyctl, request_key (kernel keyring) -# Dangerous syscalls that we don't ever want to allow. -# Note: may uncomment once ubuntu-core-launcher understands @deny rules and -# if/when we conditionally deny these in the future. - -# kexec -#@deny kexec_load - -# kernel modules -#@deny create_module -#@deny init_module -#@deny finit_module -#@deny delete_module - -# these have a history of vulnerabilities, are not widely used, and -# open_by_handle_at has been used to break out of docker containers by brute -# forcing the handle value: http://stealth.openwall.net/xSports/shocker.c -#@deny name_to_handle_at -#@deny open_by_handle_at - -# Explicitly deny ptrace since it can be abused to break out of the seccomp -# sandbox -#@deny ptrace - -# Explicitly deny capability mknod so apps can't create devices -#@deny mknod -#@deny mknodat - -# Explicitly deny (u)mount so apps can't change mounts in their namespace -#@deny mount -#@deny umount -#@deny umount2 - -# Explicitly deny kernel keyring access -#@deny add_key -#@deny keyctl -#@deny request_key - -# end dangerous syscalls +# +# Allowed accesses +# access faccessat @@ -109,6 +81,10 @@ clock_nanosleep clone close + +# needed by ls -l +connect + creat dup dup2 @@ -185,7 +161,11 @@ inotify_init1 inotify_rm_watch -# Needed by shell +# TIOCSTI allows for faking input (man tty_ioctl) +# TODO: this should be scaled back even more +#ioctl - !TIOCSTI +# FIXME: replace this with the filter of TIOCSTI once snap-confine can read this syntax +# See LP:#1662489 for context. ioctl io_cancel @@ -252,10 +232,16 @@ nanosleep -# LP: #1446748 - deny until we have syscall arg filtering. Alternatively, set -# RLIMIT_NICE hard limit for apps, launch them under an appropriate nice value -# and allow this call -#nice +# Allow using nice() with default or lower priority +# FIXME: https://github.com/seccomp/libseccomp/issues/69 which means we +# currently have to use <=19. When that bug is fixed, use >=0 +nice <=19 +# Allow using setpriority to set the priority of the calling process to default +# or lower priority (eg, 'nice -n 9 ') +# default or lower priority. +# FIXME: https://github.com/seccomp/libseccomp/issues/69 which means we +# currently have to use <=19. When that bug is fixed, use >=0 +setpriority PRIO_PROCESS 0 <=19 # LP: #1446748 - support syscall arg filtering for mode_t with O_CREAT open @@ -282,6 +268,13 @@ readdir readlink readlinkat + +# allow reading from sockets +recv +recvfrom +recvmsg +recvmmsg + remap_file_pages removexattr @@ -338,6 +331,13 @@ semget semop semtimedop + +# allow sending to sockets +send +sendto +sendmsg +sendmmsg + sendfile sendfile64 @@ -398,9 +398,10 @@ sigtimedwait sigwaitinfo -# needed by ls -l +# AppArmor mediates AF_UNIX/AF_LOCAL via 'unix' rules and all other AF_* +# domains via 'network' rules. We won't allow bare 'network' AppArmor rules, so +# we can allow 'socket' for any domain and let AppArmor handle the rest. socket -connect # needed by snapctl getsockopt diff -Nru snapd-2.22.6+16.10/interfaces/udev/spec.go snapd-2.23.1+16.10/interfaces/udev/spec.go --- snapd-2.22.6+16.10/interfaces/udev/spec.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/udev/spec.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,94 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package udev + +import ( + "github.com/snapcore/snapd/interfaces" +) + +// Specification assists in collecting udev snippets associated with an interface. +type Specification struct { + // Snippets are stored in a map for de-duplication + snippets map[string]bool +} + +// AddSnippet adds a new udev snippet. +func (spec *Specification) AddSnippet(snippet []byte) error { + if spec.snippets == nil { + spec.snippets = make(map[string]bool) + } + spec.snippets[string(snippet)] = true + return nil +} + +// Snippets returns a copy of all the snippets added so far. +func (spec *Specification) Snippets() map[string]bool { + result := make(map[string]bool, len(spec.snippets)) + for k, v := range spec.snippets { + result[k] = v + } + return result +} + +// Implementation of methods required by interfaces.Specification + +// AddConnectedPlug records udev-specific side-effects of having a connected plug. +func (spec *Specification) AddConnectedPlug(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + UdevConnectedPlug(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + return iface.UdevConnectedPlug(spec, plug, slot) + } + return nil +} + +// AddConnectedSlot records mount-specific side-effects of having a connected slot. +func (spec *Specification) AddConnectedSlot(iface interfaces.Interface, plug *interfaces.Plug, slot *interfaces.Slot) error { + type definer interface { + UdevConnectedSlot(spec *Specification, plug *interfaces.Plug, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + return iface.UdevConnectedSlot(spec, plug, slot) + } + return nil +} + +// AddPermanentPlug records mount-specific side-effects of having a plug. +func (spec *Specification) AddPermanentPlug(iface interfaces.Interface, plug *interfaces.Plug) error { + type definer interface { + UdevPermanentPlug(spec *Specification, plug *interfaces.Plug) error + } + if iface, ok := iface.(definer); ok { + return iface.UdevPermanentPlug(spec, plug) + } + return nil +} + +// AddPermanentSlot records mount-specific side-effects of having a slot. +func (spec *Specification) AddPermanentSlot(iface interfaces.Interface, slot *interfaces.Slot) error { + type definer interface { + UdevPermanentSlot(spec *Specification, slot *interfaces.Slot) error + } + if iface, ok := iface.(definer); ok { + return iface.UdevPermanentSlot(spec, slot) + } + return nil +} diff -Nru snapd-2.22.6+16.10/interfaces/udev/spec_test.go snapd-2.23.1+16.10/interfaces/udev/spec_test.go --- snapd-2.22.6+16.10/interfaces/udev/spec_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/interfaces/udev/spec_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package udev_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/interfaces/udev" + "github.com/snapcore/snapd/snap" +) + +type specSuite struct { + iface *ifacetest.TestInterface + spec *udev.Specification + plug *interfaces.Plug + slot *interfaces.Slot +} + +var _ = Suite(&specSuite{ + iface: &ifacetest.TestInterface{ + InterfaceName: "test", + UdevConnectedPlugCallback: func(spec *udev.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddSnippet([]byte("connected-plug")) + }, + UdevConnectedSlotCallback: func(spec *udev.Specification, plug *interfaces.Plug, slot *interfaces.Slot) error { + return spec.AddSnippet([]byte("connected-slot")) + }, + UdevPermanentPlugCallback: func(spec *udev.Specification, plug *interfaces.Plug) error { + return spec.AddSnippet([]byte("permanent-plug")) + }, + UdevPermanentSlotCallback: func(spec *udev.Specification, slot *interfaces.Slot) error { + return spec.AddSnippet([]byte("permanent-slot")) + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "snap1"}, + Name: "name", + Interface: "test", + }, + }, + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "snap2"}, + Name: "name", + Interface: "test", + }, + }, +}) + +func (s *specSuite) SetUpTest(c *C) { + s.spec = &udev.Specification{} +} + +func (s *specSuite) TestAddSnippte(c *C) { + c.Assert(s.spec.AddSnippet([]byte("foo")), IsNil) + c.Assert(s.spec.Snippets(), DeepEquals, map[string]bool{ + "foo": true}) +} + +// The spec.Specification can be used through the interfaces.Specification interface +func (s *specSuite) TestSpecificationIface(c *C) { + var r interfaces.Specification = s.spec + c.Assert(r.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddConnectedSlot(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddPermanentPlug(s.iface, s.plug), IsNil) + c.Assert(r.AddPermanentSlot(s.iface, s.slot), IsNil) + c.Assert(s.spec.Snippets(), DeepEquals, map[string]bool{ + "connected-plug": true, "permanent-plug": true, + "connected-slot": true, "permanent-slot": true, + }) +} diff -Nru snapd-2.22.6+16.10/mkversion.sh snapd-2.23.1+16.10/mkversion.sh --- snapd-2.22.6+16.10/mkversion.sh 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/mkversion.sh 2017-03-02 06:37:54.000000000 +0000 @@ -26,9 +26,18 @@ GO_GENERATE_BUILDDIR="$(pwd)/.." fi -if which git >/dev/null; then - v="$(git describe --dirty --always | sed -e 's/-/+git/;y/-/./' )" - o=git +# If the version is passed in as an argument to mkversion.sh, let's use that. +if [ ! -z "$1" ]; then + v="$1" + o=shell +fi + +if [ -z "$v" ]; then + # Let's try to derive the version from git.. + if which git >/dev/null; then + v="$(git describe --dirty --always | sed -e 's/-/+git/;y/-/./' )" + o=git + fi fi if [ -z "$v" ]; then diff -Nru snapd-2.22.6+16.10/osutil/env.go snapd-2.23.1+16.10/osutil/env.go --- snapd-2.22.6+16.10/osutil/env.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/osutil/env.go 2017-03-02 06:37:54.000000000 +0000 @@ -20,8 +20,10 @@ package osutil import ( + "fmt" "os" "strconv" + "strings" ) // GetenvBool returns whether the given key may be considered "set" in the @@ -50,3 +52,39 @@ return b } + +// SubstituteEnv takes a list of environment strings like: +// - K1=BAR +// - K2=$K1 +// - K3=${K2} +// and substitutes them top-down from the given environment +// and from the os environment. +// +// Input strings that do not have the form "k=v" will be dropped +// from the output. +// +// The result will be a list of environment strings in the same +// order as the input. +func SubstituteEnv(env []string) []string { + envMap := map[string]string{} + out := make([]string, 0, len(env)) + + for _, s := range env { + l := strings.SplitN(s, "=", 2) + if len(l) < 2 { + continue + } + k := l[0] + v := l[1] + v = os.Expand(v, func(k string) string { + if s, ok := envMap[k]; ok { + return s + } + return os.ExpandEnv(v) + }) + out = append(out, fmt.Sprintf("%s=%s", k, v)) + envMap[k] = v + } + + return out +} diff -Nru snapd-2.22.6+16.10/osutil/env_test.go snapd-2.23.1+16.10/osutil/env_test.go --- snapd-2.22.6+16.10/osutil/env_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/osutil/env_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -20,7 +20,9 @@ package osutil_test import ( + "fmt" "os" + "strings" "gopkg.in/check.v1" @@ -82,3 +84,27 @@ c.Check(osutil.GetenvBool(key, true), check.Equals, true, check.Commentf(s)) } } + +func (s *envSuite) TestSubstitueEnv(c *check.C) { + for _, t := range []struct { + env string + + expected string + }{ + // trivial + {"K1=V1,K2=V2", "K1=V1,K2=V2"}, + // simple (order is preserved) + {"K=V,K2=$K", "K=V,K2=V"}, + // simple from environment + {"K=$PATH", fmt.Sprintf("K=%s", os.Getenv("PATH"))}, + // multi-level + {"A=1,B=$A/2,C=$B/3,D=$C/4", "A=1,B=1/2,C=1/2/3,D=1/2/3/4"}, + // parsing is top down + {"A=$A", "A="}, + {"A=$B,B=$A", "A=,B="}, + {"A=$B,B=$C,C=$A", "A=,B=,C="}, + } { + env := osutil.SubstituteEnv(strings.Split(t.env, ",")) + c.Check(strings.Join(env, ","), check.DeepEquals, t.expected, check.Commentf("invalid result for %q, got %q expected %q", t.env, env, t.expected)) + } +} diff -Nru snapd-2.22.6+16.10/osutil/exec.go snapd-2.23.1+16.10/osutil/exec.go --- snapd-2.22.6+16.10/osutil/exec.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/osutil/exec.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,136 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package osutil + +import ( + "bufio" + "bytes" + "debug/elf" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/dirs" +) + +func parseCoreLdSoConf(confPath string) []string { + root := filepath.Join(dirs.SnapMountDir, "/core/current") + + f, err := os.Open(filepath.Join(root, confPath)) + if err != nil { + return nil + } + defer f.Close() + + var out []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + switch { + case strings.HasPrefix(line, "#"): + // nothing + case strings.TrimSpace(line) == "": + // nothing + case strings.HasPrefix(line, "include "): + l := strings.SplitN(line, "include ", 2) + files, err := filepath.Glob(filepath.Join(root, l[1])) + if err != nil { + return nil + } + for _, f := range files { + out = append(out, parseCoreLdSoConf(f[len(root):])...) + } + default: + out = append(out, filepath.Join(root, line)) + } + + } + if err := scanner.Err(); err != nil { + return nil + } + + return out +} + +func elfInterp(cmd string) (string, error) { + el, err := elf.Open(cmd) + if err != nil { + return "", err + } + defer el.Close() + + for _, prog := range el.Progs { + if prog.Type == elf.PT_INTERP { + r := prog.Open() + interp, err := ioutil.ReadAll(r) + if err != nil { + return "", nil + } + + return string(bytes.Trim(interp, "\x00")), nil + } + } + + return "", fmt.Errorf("cannot find PT_INTERP header") +} + +// CommandFromCore runs a command from the core snap using the proper +// interpreter and library paths. +// +// At the moment it can only run ELF files, expects a standard ld.so +// interpreter, and can't handle RPATH. +func CommandFromCore(name string, cmdArgs ...string) (*exec.Cmd, error) { + root := filepath.Join(dirs.SnapMountDir, "/core/current") + + cmdPath := filepath.Join(root, name) + interp, err := elfInterp(cmdPath) + if err != nil { + return nil, err + } + coreLdSo := filepath.Join(root, interp) + // we cannot use EvalSymlink here because we need to resolve + // relative and absolute symlinks differently. A absolute + // symlink is relative to root of the core snap. + seen := map[string]bool{} + for IsSymlink(coreLdSo) { + link, err := os.Readlink(coreLdSo) + if err != nil { + return nil, err + } + if filepath.IsAbs(link) { + coreLdSo = filepath.Join(root, link) + } else { + coreLdSo = filepath.Join(filepath.Dir(coreLdSo), link) + } + if seen[coreLdSo] { + return nil, fmt.Errorf("cannot run command from core: symlink cycle found") + } + seen[coreLdSo] = true + } + + ldLibraryPathForCore := parseCoreLdSoConf("/etc/ld.so.conf") + + ldSoArgs := []string{"--library-path", strings.Join(ldLibraryPathForCore, ":"), cmdPath} + allArgs := append(ldSoArgs, cmdArgs...) + return exec.Command(coreLdSo, allArgs...), nil +} diff -Nru snapd-2.22.6+16.10/osutil/exec_test.go snapd-2.23.1+16.10/osutil/exec_test.go --- snapd-2.22.6+16.10/osutil/exec_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/osutil/exec_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,109 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package osutil_test + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +type commandFromCoreSuite struct{} + +var _ = Suite(&commandFromCoreSuite{}) + +func (s *commandFromCoreSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) +} + +func (s *commandFromCoreSuite) TearDownTest(c *C) { + dirs.SetRootDir("") +} + +func (s *commandFromCoreSuite) makeMockLdSoConf(c *C) { + ldSoConf := filepath.Join(dirs.SnapMountDir, "/core/current/etc/ld.so.conf") + ldSoConfD := ldSoConf + ".d" + + err := os.MkdirAll(filepath.Dir(ldSoConf), 0755) + c.Assert(err, IsNil) + err = os.MkdirAll(ldSoConfD, 0755) + c.Assert(err, IsNil) + + err = ioutil.WriteFile(ldSoConf, []byte("include /etc/ld.so.conf.d/*.conf"), 0644) + c.Assert(err, IsNil) + + ldSoConf1 := filepath.Join(ldSoConfD, "x86_64-linux-gnu.conf") + + err = ioutil.WriteFile(ldSoConf1, []byte(` +# Multiarch support +/lib/x86_64-linux-gnu +/usr/lib/x86_64-linux-gnu`), 0644) + c.Assert(err, IsNil) +} + +func (s *commandFromCoreSuite) TestCommandFromCore(c *C) { + s.makeMockLdSoConf(c) + root := filepath.Join(dirs.SnapMountDir, "/core/current") + + os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755) + osutil.CopyFile("/bin/true", filepath.Join(root, "/usr/bin/xdelta3"), 0) + cmd, err := osutil.CommandFromCore("/usr/bin/xdelta3", "--some-xdelta-arg") + c.Assert(err, IsNil) + + out, err := exec.Command("/bin/sh", "-c", "readelf -l /bin/true |grep interpreter:|cut -f2 -d:|cut -f1 -d]").Output() + c.Assert(err, IsNil) + interp := strings.TrimSpace(string(out)) + + c.Check(cmd.Args, DeepEquals, []string{ + filepath.Join(root, interp), + "--library-path", + fmt.Sprintf("%s/lib/x86_64-linux-gnu:%s/usr/lib/x86_64-linux-gnu", root, root), + filepath.Join(dirs.SnapMountDir, "/core/current/usr/bin/xdelta3"), + "--some-xdelta-arg", + }) +} + +func (s *commandFromCoreSuite) TestCommandFromCoreSymlinkCycle(c *C) { + s.makeMockLdSoConf(c) + root := filepath.Join(dirs.SnapMountDir, "/core/current") + + os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755) + osutil.CopyFile("/bin/true", filepath.Join(root, "/usr/bin/xdelta3"), 0) + + out, err := exec.Command("/bin/sh", "-c", "readelf -l /bin/true |grep interpreter:|cut -f2 -d:|cut -f1 -d]").Output() + c.Assert(err, IsNil) + interp := strings.TrimSpace(string(out)) + + coreInterp := filepath.Join(root, interp) + c.Assert(os.MkdirAll(filepath.Dir(coreInterp), 0755), IsNil) + c.Assert(os.Symlink(filepath.Base(coreInterp), coreInterp), IsNil) + + _, err = osutil.CommandFromCore("/usr/bin/xdelta3", "--some-xdelta-arg") + c.Assert(err, ErrorMatches, "cannot run command from core: symlink cycle found") + +} diff -Nru snapd-2.22.6+16.10/osutil/stat.go snapd-2.23.1+16.10/osutil/stat.go --- snapd-2.22.6+16.10/osutil/stat.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/osutil/stat.go 2017-03-02 06:37:54.000000000 +0000 @@ -21,6 +21,7 @@ import ( "os" + "os/exec" ) // FileExists return true if given path can be stat()ed by us. Note that @@ -55,3 +56,10 @@ return (fileInfo.Mode() & os.ModeSymlink) != 0 } + +// ExecutableExists returns whether there an exists an executable with the given name somewhere on $PATH. +func ExecutableExists(name string) bool { + _, err := exec.LookPath(name) + + return err == nil +} diff -Nru snapd-2.22.6+16.10/osutil/stat_test.go snapd-2.23.1+16.10/osutil/stat_test.go --- snapd-2.22.6+16.10/osutil/stat_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/osutil/stat_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -74,3 +74,18 @@ func (ts *StatTestSuite) TestIsSymlinkNoSymlink(c *C) { c.Assert(IsSymlink(c.MkDir()), Equals, false) } + +func (ts *StatTestSuite) TestExecutableExists(c *C) { + oldPath := os.Getenv("PATH") + defer os.Setenv("PATH", oldPath) + d := c.MkDir() + os.Setenv("PATH", d) + c.Check(ExecutableExists("xyzzy"), Equals, false) + + fname := filepath.Join(d, "xyzzy") + c.Assert(ioutil.WriteFile(fname, []byte{}, 0644), IsNil) + c.Check(ExecutableExists("xyzzy"), Equals, false) + + c.Assert(os.Chmod(fname, 0755), IsNil) + c.Check(ExecutableExists("xyzzy"), Equals, true) +} diff -Nru snapd-2.22.6+16.10/overlord/assertstate/assertmgr.go snapd-2.23.1+16.10/overlord/assertstate/assertmgr.go --- snapd-2.22.6+16.10/overlord/assertstate/assertmgr.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/assertstate/assertmgr.go 2017-03-02 06:37:54.000000000 +0000 @@ -475,6 +475,8 @@ func init() { // hook validation of refreshes into snapstate logic snapstate.ValidateRefreshes = ValidateRefreshes + // hook auto refresh of assertions into snapstate + snapstate.AutoRefreshAssertions = AutoRefreshAssertions } // BaseDeclaration returns the base-declaration assertion with policies governing all snaps. @@ -538,3 +540,8 @@ // hook retrieving auto-aliases into snapstate logic snapstate.AutoAliases = AutoAliases } + +// AutoRefreshAssertions tries to refresh all assertions +func AutoRefreshAssertions(s *state.State, userID int) error { + return RefreshSnapDeclarations(s, userID) +} diff -Nru snapd-2.22.6+16.10/overlord/assertstate/assertmgr_test.go snapd-2.23.1+16.10/overlord/assertstate/assertmgr_test.go --- snapd-2.22.6+16.10/overlord/assertstate/assertmgr_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/assertstate/assertmgr_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -272,6 +272,9 @@ } func (s *assertMgrSuite) TestBatchAddUnsupported(c *C) { + restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 111) + defer restore() + batch := assertstate.NewBatch() var a asserts.Assertion @@ -293,7 +296,7 @@ })() err := batch.Add(a) - c.Check(err, ErrorMatches, `proposed "snap-declaration" assertion has format 999 but 1 is latest supported`) + c.Check(err, ErrorMatches, `proposed "snap-declaration" assertion has format 999 but 111 is latest supported`) } func fakeSnap(rev int) []byte { diff -Nru snapd-2.22.6+16.10/overlord/configstate/config/helpers.go snapd-2.23.1+16.10/overlord/configstate/config/helpers.go --- snapd-2.22.6+16.10/overlord/configstate/config/helpers.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/configstate/config/helpers.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,118 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package config + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" +) + +var validKey = regexp.MustCompile("^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$") + +func ParseKey(key string) (subkeys []string, err error) { + subkeys = strings.Split(key, ".") + for _, subkey := range subkeys { + if !validKey.MatchString(subkey) { + return nil, fmt.Errorf("invalid option name: %q", subkey) + } + } + return subkeys, nil +} + +func PatchConfig(snapName string, subkeys []string, pos int, config interface{}, value *json.RawMessage) (interface{}, error) { + + switch config := config.(type) { + case nil: + // Missing update map. Create and nest final value under it. + configm := make(map[string]interface{}) + _, err := PatchConfig(snapName, subkeys, pos, configm, value) + if err != nil { + return nil, err + } + return configm, nil + + case *json.RawMessage: + // Raw replaces pristine on commit. Unpack, update, and repack. + var configm map[string]interface{} + err := json.Unmarshal([]byte(*config), &configm) + if err != nil { + return nil, fmt.Errorf("snap %q option %q is not a map", snapName, strings.Join(subkeys[:pos], ".")) + } + _, err = PatchConfig(snapName, subkeys, pos, configm, value) + if err != nil { + return nil, err + } + return jsonRaw(configm), nil + + case map[string]interface{}: + // Update map to apply against pristine on commit. + if pos+1 == len(subkeys) { + config[subkeys[pos]] = value + return config, nil + } else { + result, err := PatchConfig(snapName, subkeys, pos+1, config[subkeys[pos]], value) + if err != nil { + return nil, err + } + config[subkeys[pos]] = result + return config, nil + } + } + panic(fmt.Errorf("internal error: unexpected configuration type %T", config)) +} + +// Get unmarshals into result the value of the provided snap's configuration key. +// If the key does not exist, an error of type *NoOptionError is returned. +// The provided key may be formed as a dotted key path through nested maps. +// For example, the "a.b.c" key describes the {a: {b: {c: value}}} map. +func GetFromChange(snapName string, subkeys []string, pos int, config map[string]interface{}, result interface{}) error { + value, ok := config[subkeys[pos]] + if !ok { + return &NoOptionError{SnapName: snapName, Key: strings.Join(subkeys[:pos+1], ".")} + } + + if pos+1 == len(subkeys) { + raw, ok := value.(*json.RawMessage) + if !ok { + raw = jsonRaw(value) + } + err := json.Unmarshal([]byte(*raw), result) + if err != nil { + key := strings.Join(subkeys, ".") + return fmt.Errorf("internal error: cannot unmarshal snap %q option %q into %T: %s, json: %s", snapName, key, result, err, *raw) + } + return nil + } + + configm, ok := value.(map[string]interface{}) + if !ok { + raw, ok := value.(*json.RawMessage) + if !ok { + raw = jsonRaw(value) + } + err := json.Unmarshal([]byte(*raw), &configm) + if err != nil { + return fmt.Errorf("snap %q option %q is not a map", snapName, strings.Join(subkeys[:pos+1], ".")) + } + } + return GetFromChange(snapName, subkeys, pos+1, configm, result) +} diff -Nru snapd-2.22.6+16.10/overlord/configstate/config/transaction.go snapd-2.23.1+16.10/overlord/configstate/config/transaction.go --- snapd-2.22.6+16.10/overlord/configstate/config/transaction.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/configstate/config/transaction.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,247 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package config + +import ( + "encoding/json" + "fmt" + "strings" + "sync" + + "github.com/snapcore/snapd/overlord/state" +) + +// Transaction holds a copy of the configuration originally present in the +// provided state which can be queried and mutated in isolation from +// concurrent logic. All changes performed into it are persisted back into +// the state at once when Commit is called. +// +// Transactions are safe to access and modify concurrently. +type Transaction struct { + mu sync.Mutex + state *state.State + pristine map[string]map[string]*json.RawMessage // snap => key => value + changes map[string]map[string]interface{} +} + +// NewTransaction creates a new configuration transaction initialized with the given state. +// +// The provided state must be locked by the caller. +func NewTransaction(st *state.State) *Transaction { + transaction := &Transaction{state: st} + transaction.changes = make(map[string]map[string]interface{}) + + // Record the current state of the map containing the config of every snap + // in the system. We'll use it for this transaction. + err := st.Get("config", &transaction.pristine) + if err == state.ErrNoState { + transaction.pristine = make(map[string]map[string]*json.RawMessage) + } else if err != nil { + panic(fmt.Errorf("internal error: cannot unmarshal configuration: %v", err)) + } + return transaction +} + +// Set sets the provided snap's configuration key to the given value. +// The provided key may be formed as a dotted key path through nested maps. +// For example, the "a.b.c" key describes the {a: {b: {c: value}}} map. +// When the key is provided in that form, intermediate maps are mutated +// rather than replaced, and created when necessary. +// +// The provided value must marshal properly by encoding/json. +// Changes are not persisted until Commit is called. +func (t *Transaction) Set(snapName, key string, value interface{}) error { + t.mu.Lock() + defer t.mu.Unlock() + + config, ok := t.changes[snapName] + if !ok { + config = make(map[string]interface{}) + } + + data, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("cannot marshal snap %q option %q: %s", snapName, key, err) + } + raw := json.RawMessage(data) + + subkeys, err := ParseKey(key) + if err != nil { + return err + } + + // Check whether it's trying to traverse a non-map from pristine. This + // would go unperceived by the configuration patching below. + if len(subkeys) > 1 { + var result interface{} + err = getFromPristine(snapName, subkeys, 0, t.pristine[snapName], &result) + if err != nil && !IsNoOption(err) { + return err + } + } + _, err = PatchConfig(snapName, subkeys, 0, config, &raw) + if err != nil { + return err + } + + t.changes[snapName] = config + return nil +} + +// Get unmarshals into result the cached value of the provided snap's configuration key. +// If the key does not exist, an error of type *NoOptionError is returned. +// The provided key may be formed as a dotted key path through nested maps. +// For example, the "a.b.c" key describes the {a: {b: {c: value}}} map. +// +// Transactions do not see updates from the current state or from other transactions. +func (t *Transaction) Get(snapName, key string, result interface{}) error { + t.mu.Lock() + defer t.mu.Unlock() + + subkeys, err := ParseKey(key) + if err != nil { + return err + } + + err = GetFromChange(snapName, subkeys, 0, t.changes[snapName], result) + if IsNoOption(err) { + err = getFromPristine(snapName, subkeys, 0, t.pristine[snapName], result) + } + return err +} + +// GetMaybe unmarshals into result the cached value of the provided snap's configuration key. +// If the key does not exist, no error is returned. +// +// Transactions do not see updates from the current state or from other transactions. +func (t *Transaction) GetMaybe(snapName, key string, result interface{}) error { + err := t.Get(snapName, key, result) + if err != nil && !IsNoOption(err) { + return err + } + return nil +} + +func getFromPristine(snapName string, subkeys []string, pos int, config map[string]*json.RawMessage, result interface{}) error { + raw, ok := config[subkeys[pos]] + if !ok { + return &NoOptionError{SnapName: snapName, Key: strings.Join(subkeys[:pos+1], ".")} + } + + if pos+1 == len(subkeys) { + err := json.Unmarshal([]byte(*raw), result) + if err != nil { + key := strings.Join(subkeys, ".") + return fmt.Errorf("internal error: cannot unmarshal snap %q option %q into %T: %s, json: %s", snapName, key, result, err, *raw) + } + return nil + } + + var configm map[string]*json.RawMessage + err := json.Unmarshal([]byte(*raw), &configm) + if err != nil { + return fmt.Errorf("snap %q option %q is not a map", snapName, strings.Join(subkeys[:pos+1], ".")) + } + return getFromPristine(snapName, subkeys, pos+1, configm, result) +} + +// Commit applies to the state the configuration changes made in the transaction +// and updates the observed configuration to the result of the operation. +// +// The state associated with the transaction must be locked by the caller. +func (t *Transaction) Commit() { + t.mu.Lock() + defer t.mu.Unlock() + + if len(t.changes) == 0 { + return + } + + // Update our copy of the config with the most recent one from the state. + err := t.state.Get("config", &t.pristine) + if err == state.ErrNoState { + t.pristine = make(map[string]map[string]*json.RawMessage) + } else if err != nil { + panic(fmt.Errorf("internal error: cannot unmarshal configuration: %v", err)) + } + + // Iterate through the write cache and save each item. + for snapName, snapChanges := range t.changes { + config, ok := t.pristine[snapName] + if !ok { + config = make(map[string]*json.RawMessage) + } + for k, v := range snapChanges { + config[k] = commitChange(config[k], v) + } + t.pristine[snapName] = config + } + + t.state.Set("config", t.pristine) + + // The cache has been flushed, reset it. + t.changes = make(map[string]map[string]interface{}) +} + +func jsonRaw(v interface{}) *json.RawMessage { + data, err := json.Marshal(v) + if err != nil { + panic(fmt.Errorf("internal error: cannot marshal configuration: %v", err)) + } + raw := json.RawMessage(data) + return &raw +} + +func commitChange(pristine *json.RawMessage, change interface{}) *json.RawMessage { + switch change := change.(type) { + case *json.RawMessage: + return change + case map[string]interface{}: + if pristine == nil { + return jsonRaw(change) + } + var pristinem map[string]*json.RawMessage + if err := json.Unmarshal([]byte(*pristine), &pristinem); err != nil { + // Not a map. Overwrite with the change. + return jsonRaw(change) + } + for k, v := range change { + pristinem[k] = commitChange(pristinem[k], v) + } + return jsonRaw(pristinem) + } + panic(fmt.Errorf("internal error: unexpected configuration type %T", change)) +} + +// IsNoOption returns whether the provided error is a *NoOptionError. +func IsNoOption(err error) bool { + _, ok := err.(*NoOptionError) + return ok +} + +// NoOptionError indicates that a config option is not set. +type NoOptionError struct { + SnapName string + Key string +} + +func (e *NoOptionError) Error() string { + return fmt.Sprintf("snap %q has no %q configuration option", e.SnapName, e.Key) +} diff -Nru snapd-2.22.6+16.10/overlord/configstate/config/transaction_test.go snapd-2.23.1+16.10/overlord/configstate/config/transaction_test.go --- snapd-2.22.6+16.10/overlord/configstate/config/transaction_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/configstate/config/transaction_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -0,0 +1,284 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package config_test + +import ( + "encoding/json" + "fmt" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/state" + "strings" +) + +func TestT(t *testing.T) { TestingT(t) } + +type transactionSuite struct { + state *state.State + transaction *config.Transaction +} + +var _ = Suite(&transactionSuite{}) + +func (s *transactionSuite) SetUpTest(c *C) { + s.state = state.New(nil) + s.state.Lock() + defer s.state.Unlock() + s.transaction = config.NewTransaction(s.state) +} + +type setGetOp string + +func (op setGetOp) kind() string { + return strings.Fields(string(op))[0] +} + +func (op setGetOp) args() map[string]interface{} { + m := make(map[string]interface{}) + args := strings.Fields(string(op)) + for _, pair := range args[1:] { + if pair == "=>" { + break + } + kv := strings.SplitN(pair, "=", 2) + var v interface{} + err := json.Unmarshal([]byte(kv[1]), &v) + if err != nil { + v = kv[1] + } + m[kv[0]] = v + } + return m +} + +func (op setGetOp) error() string { + if i := strings.Index(string(op), " => "); i >= 0 { + return string(op[i+4:]) + } + return "" +} + +func (op setGetOp) fails() bool { + return op.error() != "" +} + +var setGetTests = [][]setGetOp{{ + // Basics. + `set one=1 two=2`, + `setunder three=3`, + `get one=1 two=2 three=-`, + `getunder one=- two=- three=3`, + `commit`, + `getunder one=1 two=2 three=3`, + `get one=1 two=2 three=3`, + `set two=22 four=4`, + `get one=1 two=22 three=3 four=4`, + `getunder one=1 two=2 three=3 four=-`, + `commit`, + `getunder one=1 two=22 three=3 four=4`, +}, { + // Trivial full doc. + `set doc={"one":1,"two":2}`, + `get doc={"one":1,"two":2}`, +}, { + // Nested mutations. + `set one.two.three=3`, + `set one.five=5`, + `setunder one={"two":{"four":4}}`, + `get one={"two":{"three":3},"five":5}`, + `get one.two={"three":3}`, + `get one.two.three=3`, + `get one.five=5`, + `commit`, + `getunder one={"two":{"three":3,"four":4},"five":5}`, + `get one={"two":{"three":3,"four":4},"five":5}`, + `get one.two={"three":3,"four":4}`, + `get one.two.three=3`, + `get one.two.four=4`, + `get one.five=5`, +}, { + // Replacement with nested mutation. + `set one={"two":{"three":3}}`, + `set one.five=5`, + `get one={"two":{"three":3},"five":5}`, + `get one.two={"three":3}`, + `get one.two.three=3`, + `get one.five=5`, + `setunder one={"two":{"four":4},"six":6}`, + `commit`, + `getunder one={"two":{"three":3},"five":5}`, +}, { + // Cannot go through known scalar implicitly. + `set one.two=2`, + `set one.two.three=3 => snap "core" option "one\.two" is not a map`, + `get one.two.three=3 => snap "core" option "one\.two" is not a map`, + `get one={"two":2}`, + `commit`, + `set one.two.three=3 => snap "core" option "one\.two" is not a map`, + `get one.two.three=3 => snap "core" option "one\.two" is not a map`, + `get one={"two":2}`, + `getunder one={"two":2}`, +}, { + // Unknown scalars may be overwritten though. + `setunder one={"two":2}`, + `set one.two.three=3`, + `commit`, + `getunder one={"two":{"three":3}}`, +}, { + // Invalid option names. + `set BAD=1 => invalid option name: "BAD"`, + `set 42=1 => invalid option name: "42"`, + `set .bad=1 => invalid option name: ""`, + `set bad.=1 => invalid option name: ""`, + `set bad..bad=1 => invalid option name: ""`, + `set one.bad--bad.two=1 => invalid option name: "bad--bad"`, + `set one.-bad.two=1 => invalid option name: "-bad"`, + `set one.bad-.two=1 => invalid option name: "bad-"`, +}} + +func (s *transactionSuite) TestSetGet(c *C) { + s.state.Lock() + defer s.state.Unlock() + + for _, test := range setGetTests { + c.Logf("-----") + s.state.Set("config", map[string]interface{}{}) + t := config.NewTransaction(s.state) + snap := "core" + for _, op := range test { + c.Logf("%s", op) + switch op.kind() { + case "set": + for k, v := range op.args() { + err := t.Set(snap, k, v) + if op.fails() { + c.Assert(err, ErrorMatches, op.error()) + } else { + c.Assert(err, IsNil) + } + } + + case "get": + for k, expected := range op.args() { + var obtained interface{} + err := t.Get(snap, k, &obtained) + if op.fails() { + c.Assert(err, ErrorMatches, op.error()) + var nothing interface{} + c.Assert(t.GetMaybe(snap, k, ¬hing), ErrorMatches, op.error()) + c.Assert(nothing, IsNil) + continue + } + if expected == "-" { + if !config.IsNoOption(err) { + c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) + } + c.Assert(err, ErrorMatches, fmt.Sprintf("snap %q has no %q configuration option", snap, k)) + var nothing interface{} + c.Assert(t.GetMaybe(snap, k, ¬hing), IsNil) + c.Assert(nothing, IsNil) + continue + } + c.Assert(err, IsNil) + c.Assert(obtained, DeepEquals, expected) + + obtained = nil + c.Assert(t.GetMaybe(snap, k, &obtained), IsNil) + c.Assert(obtained, DeepEquals, expected) + } + + case "commit": + t.Commit() + + case "setunder": + var config map[string]map[string]interface{} + s.state.Get("config", &config) + if config == nil { + config = make(map[string]map[string]interface{}) + } + if config[snap] == nil { + config[snap] = make(map[string]interface{}) + } + for k, v := range op.args() { + if v == "-" { + delete(config[snap], k) + if len(config[snap]) == 0 { + delete(config[snap], snap) + } + } else { + config[snap][k] = v + } + } + s.state.Set("config", config) + + case "getunder": + var config map[string]map[string]interface{} + s.state.Get("config", &config) + for k, expected := range op.args() { + obtained, ok := config[snap][k] + if expected == "-" { + if ok { + c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) + } + continue + } + c.Assert(obtained, DeepEquals, expected) + } + + default: + panic("unknown test op kind: " + op.kind()) + } + } + } +} + +type brokenType struct { + on string +} + +func (b *brokenType) UnmarshalJSON(data []byte) error { + if b.on == string(data) { + return fmt.Errorf("BAM!") + } + return nil +} + +func (s *transactionSuite) TestGetUnmarshalError(c *C) { + s.state.Lock() + defer s.state.Unlock() + c.Check(s.transaction.Set("test-snap", "foo", "good"), IsNil) + s.transaction.Commit() + + tr := config.NewTransaction(s.state) + c.Check(tr.Set("test-snap", "foo", "break"), IsNil) + + // Pristine state is good, value in the transaction breaks. + broken := brokenType{`"break"`} + err := tr.Get("test-snap", "foo", &broken) + c.Assert(err, ErrorMatches, ".*BAM!.*") + + // Pristine state breaks, nothing in the transaction. + tr.Commit() + err = tr.Get("test-snap", "foo", &broken) + c.Assert(err, ErrorMatches, ".*BAM!.*") +} diff -Nru snapd-2.22.6+16.10/overlord/configstate/handler.go snapd-2.23.1+16.10/overlord/configstate/handler.go --- snapd-2.22.6+16.10/overlord/configstate/handler.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/configstate/handler.go 2017-03-02 06:37:54.000000000 +0000 @@ -19,7 +19,10 @@ package configstate -import "github.com/snapcore/snapd/overlord/hookstate" +import ( + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/hookstate" +) // configureHandler is the handler for the configure hook. type configureHandler struct { @@ -32,23 +35,23 @@ // ContextTransaction retrieves the transaction cached within the context (and // creates one if it hasn't already been cached). -func ContextTransaction(context *hookstate.Context) *Transaction { +func ContextTransaction(context *hookstate.Context) *config.Transaction { // Check for one already cached - transaction, ok := context.Cached(cachedTransaction{}).(*Transaction) + tr, ok := context.Cached(cachedTransaction{}).(*config.Transaction) if ok { - return transaction + return tr } // It wasn't already cached, so create and cache a new one - transaction = NewTransaction(context.State()) + tr = config.NewTransaction(context.State()) context.OnDone(func() error { - transaction.Commit() + tr.Commit() return nil }) - context.Cache(cachedTransaction{}, transaction) - return transaction + context.Cache(cachedTransaction{}, tr) + return tr } func newConfigureHandler(context *hookstate.Context) hookstate.Handler { @@ -60,14 +63,14 @@ h.context.Lock() defer h.context.Unlock() - transaction := ContextTransaction(h.context) + tr := ContextTransaction(h.context) // Initialize the transaction if there's a patch provided in the // context. var patch map[string]interface{} if err := h.context.Get("patch", &patch); err == nil { for key, value := range patch { - transaction.Set(h.context.SnapName(), key, value) + tr.Set(h.context.SnapName(), key, value) } } diff -Nru snapd-2.22.6+16.10/overlord/configstate/handler_test.go snapd-2.23.1+16.10/overlord/configstate/handler_test.go --- snapd-2.22.6+16.10/overlord/configstate/handler_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/configstate/handler_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -20,6 +20,8 @@ package configstate_test import ( + "testing" + . "gopkg.in/check.v1" "github.com/snapcore/snapd/overlord/configstate" @@ -29,6 +31,8 @@ "github.com/snapcore/snapd/snap" ) +func TestConfigState(t *testing.T) { TestingT(t) } + type configureHandlerSuite struct { context *hookstate.Context handler hookstate.Handler @@ -62,10 +66,10 @@ c.Check(s.handler.Before(), IsNil) s.context.Lock() - transaction := configstate.ContextTransaction(s.context) + tr := configstate.ContextTransaction(s.context) s.context.Unlock() var value string - c.Check(transaction.Get("test-snap", "foo", &value), IsNil) + c.Check(tr.Get("test-snap", "foo", &value), IsNil) c.Check(value, Equals, "bar") } diff -Nru snapd-2.22.6+16.10/overlord/configstate/transaction.go snapd-2.23.1+16.10/overlord/configstate/transaction.go --- snapd-2.22.6+16.10/overlord/configstate/transaction.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/configstate/transaction.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,335 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2016 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package configstate - -import ( - "encoding/json" - "fmt" - "regexp" - "strings" - "sync" - - "github.com/snapcore/snapd/overlord/state" -) - -// Transaction holds a copy of the configuration originally present in the -// provided state which can be queried and mutated in isolation from -// concurrent logic. All changes performed into it are persisted back into -// the state at once when Commit is called. -// -// Transactions are safe to access and modify concurrently. -type Transaction struct { - mu sync.Mutex - state *state.State - pristine map[string]map[string]*json.RawMessage // snap => key => value - changes map[string]map[string]interface{} -} - -// NewTransaction creates a new configuration transaction initialized with the given state. -// -// The provided state must be locked by the caller. -func NewTransaction(st *state.State) *Transaction { - transaction := &Transaction{state: st} - transaction.changes = make(map[string]map[string]interface{}) - - // Record the current state of the map containing the config of every snap - // in the system. We'll use it for this transaction. - err := st.Get("config", &transaction.pristine) - if err == state.ErrNoState { - transaction.pristine = make(map[string]map[string]*json.RawMessage) - } else if err != nil { - panic(fmt.Errorf("internal error: cannot unmarshal configuration: %v", err)) - } - return transaction -} - -var validKey = regexp.MustCompile("^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$") - -func parseKey(key string) (subkeys []string, err error) { - subkeys = strings.Split(key, ".") - for _, subkey := range subkeys { - if !validKey.MatchString(subkey) { - return nil, fmt.Errorf("invalid option name: %q", subkey) - } - } - return subkeys, nil -} - -// Set sets the provided snap's configuration key to the given value. -// The provided key may be formed as a dotted key path through nested maps. -// For example, the "a.b.c" key describes the {a: {b: {c: value}}} map. -// When the key is provided in that form, intermediate maps are mutated -// rather than replaced, and created when necessary. -// -// The provided value must marshal properly by encoding/json. -// Changes are not persisted until Commit is called. -func (t *Transaction) Set(snapName, key string, value interface{}) error { - t.mu.Lock() - defer t.mu.Unlock() - - config, ok := t.changes[snapName] - if !ok { - config = make(map[string]interface{}) - } - - data, err := json.Marshal(value) - if err != nil { - return fmt.Errorf("cannot marshal snap %q option %q: %s", snapName, key, err) - } - raw := json.RawMessage(data) - - subkeys, err := parseKey(key) - if err != nil { - return err - } - - // Check whether it's trying to traverse a non-map from pristine. This - // would go unperceived by the configuration patching below. - if len(subkeys) > 1 { - var result interface{} - err = getFromPristine(snapName, subkeys, 0, t.pristine[snapName], &result) - if err != nil && !IsNoOption(err) { - return err - } - } - _, err = patchConfig(snapName, subkeys, 0, config, &raw) - if err != nil { - return err - } - - t.changes[snapName] = config - return nil -} - -// Get unmarshals into result the cached value of the provided snap's configuration key. -// If the key does not exist, an error of type *NoOptionError is returned. -// The provided key may be formed as a dotted key path through nested maps. -// For example, the "a.b.c" key describes the {a: {b: {c: value}}} map. -// -// Transactions do not see updates from the current state or from other transactions. -func (t *Transaction) Get(snapName, key string, result interface{}) error { - t.mu.Lock() - defer t.mu.Unlock() - - subkeys, err := parseKey(key) - if err != nil { - return err - } - - err = getFromChange(snapName, subkeys, 0, t.changes[snapName], result) - if IsNoOption(err) { - err = getFromPristine(snapName, subkeys, 0, t.pristine[snapName], result) - } - return err -} - -// GetMaybe unmarshals into result the cached value of the provided snap's configuration key. -// If the key does not exist, no error is returned. -// -// Transactions do not see updates from the current state or from other transactions. -func (t *Transaction) GetMaybe(snapName, key string, result interface{}) error { - err := t.Get(snapName, key, result) - if err != nil && !IsNoOption(err) { - return err - } - return nil -} - -func getFromPristine(snapName string, subkeys []string, pos int, config map[string]*json.RawMessage, result interface{}) error { - raw, ok := config[subkeys[pos]] - if !ok { - return &NoOptionError{SnapName: snapName, Key: strings.Join(subkeys[:pos+1], ".")} - } - - if pos+1 == len(subkeys) { - err := json.Unmarshal([]byte(*raw), result) - if err != nil { - key := strings.Join(subkeys, ".") - return fmt.Errorf("internal error: cannot unmarshal snap %q option %q into %T: %s, json: %s", snapName, key, result, err, *raw) - } - return nil - } - - var configm map[string]*json.RawMessage - err := json.Unmarshal([]byte(*raw), &configm) - if err != nil { - return fmt.Errorf("snap %q option %q is not a map", snapName, strings.Join(subkeys[:pos+1], ".")) - } - return getFromPristine(snapName, subkeys, pos+1, configm, result) -} - -func getFromChange(snapName string, subkeys []string, pos int, config map[string]interface{}, result interface{}) error { - value, ok := config[subkeys[pos]] - if !ok { - return &NoOptionError{SnapName: snapName, Key: strings.Join(subkeys[:pos+1], ".")} - } - - if pos+1 == len(subkeys) { - raw, ok := value.(*json.RawMessage) - if !ok { - raw = jsonRaw(value) - } - err := json.Unmarshal([]byte(*raw), result) - if err != nil { - key := strings.Join(subkeys, ".") - return fmt.Errorf("internal error: cannot unmarshal snap %q option %q into %T: %s, json: %s", snapName, key, result, err, *raw) - } - return nil - } - - configm, ok := value.(map[string]interface{}) - if !ok { - raw, ok := value.(*json.RawMessage) - if !ok { - raw = jsonRaw(value) - } - err := json.Unmarshal([]byte(*raw), &configm) - if err != nil { - return fmt.Errorf("snap %q option %q is not a map", snapName, strings.Join(subkeys[:pos+1], ".")) - } - } - return getFromChange(snapName, subkeys, pos+1, configm, result) -} - -func patchConfig(snapName string, subkeys []string, pos int, config interface{}, value *json.RawMessage) (interface{}, error) { - - switch config := config.(type) { - case nil: - // Missing update map. Create and nest final value under it. - configm := make(map[string]interface{}) - _, err := patchConfig(snapName, subkeys, pos, configm, value) - if err != nil { - return nil, err - } - return configm, nil - - case *json.RawMessage: - // Raw replaces pristine on commit. Unpack, update, and repack. - var configm map[string]interface{} - err := json.Unmarshal([]byte(*config), &configm) - if err != nil { - return nil, fmt.Errorf("snap %q option %q is not a map", snapName, strings.Join(subkeys[:pos], ".")) - } - _, err = patchConfig(snapName, subkeys, pos, configm, value) - if err != nil { - return nil, err - } - return jsonRaw(configm), nil - - case map[string]interface{}: - // Update map to apply against pristine on commit. - if pos+1 == len(subkeys) { - config[subkeys[pos]] = value - return config, nil - } else { - result, err := patchConfig(snapName, subkeys, pos+1, config[subkeys[pos]], value) - if err != nil { - return nil, err - } - config[subkeys[pos]] = result - return config, nil - } - } - panic(fmt.Errorf("internal error: unexpected configuration type %T", config)) -} - -// Commit applies to the state the configuration changes made in the transaction -// and updates the observed configuration to the result of the operation. -// -// The state associated with the transaction must be locked by the caller. -func (t *Transaction) Commit() { - t.mu.Lock() - defer t.mu.Unlock() - - if len(t.changes) == 0 { - return - } - - // Update our copy of the config with the most recent one from the state. - err := t.state.Get("config", &t.pristine) - if err == state.ErrNoState { - t.pristine = make(map[string]map[string]*json.RawMessage) - } else if err != nil { - panic(fmt.Errorf("internal error: cannot unmarshal configuration: %v", err)) - } - - // Iterate through the write cache and save each item. - for snapName, snapChanges := range t.changes { - config, ok := t.pristine[snapName] - if !ok { - config = make(map[string]*json.RawMessage) - } - for k, v := range snapChanges { - config[k] = commitChange(config[k], v) - } - t.pristine[snapName] = config - } - - t.state.Set("config", t.pristine) - - // The cache has been flushed, reset it. - t.changes = make(map[string]map[string]interface{}) -} - -func jsonRaw(v interface{}) *json.RawMessage { - data, err := json.Marshal(v) - if err != nil { - panic(fmt.Errorf("internal error: cannot marshal configuration: %v", err)) - } - raw := json.RawMessage(data) - return &raw -} - -func commitChange(pristine *json.RawMessage, change interface{}) *json.RawMessage { - switch change := change.(type) { - case *json.RawMessage: - return change - case map[string]interface{}: - if pristine == nil { - return jsonRaw(change) - } - var pristinem map[string]*json.RawMessage - if err := json.Unmarshal([]byte(*pristine), &pristinem); err != nil { - // Not a map. Overwrite with the change. - return jsonRaw(change) - } - for k, v := range change { - pristinem[k] = commitChange(pristinem[k], v) - } - return jsonRaw(pristinem) - } - panic(fmt.Errorf("internal error: unexpected configuration type %T", change)) -} - -// IsNoOption returns whether the provided error is a *NoOptionError. -func IsNoOption(err error) bool { - _, ok := err.(*NoOptionError) - return ok -} - -// NoOptionError indicates that a config option is not set. -type NoOptionError struct { - SnapName string - Key string -} - -func (e *NoOptionError) Error() string { - return fmt.Sprintf("snap %q has no %q configuration option", e.SnapName, e.Key) -} diff -Nru snapd-2.22.6+16.10/overlord/configstate/transaction_test.go snapd-2.23.1+16.10/overlord/configstate/transaction_test.go --- snapd-2.22.6+16.10/overlord/configstate/transaction_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/configstate/transaction_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,284 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2016 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package configstate_test - -import ( - "encoding/json" - "fmt" - "testing" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/overlord/configstate" - "github.com/snapcore/snapd/overlord/state" - "strings" -) - -func TestConfigState(t *testing.T) { TestingT(t) } - -type transactionSuite struct { - state *state.State - transaction *configstate.Transaction -} - -var _ = Suite(&transactionSuite{}) - -func (s *transactionSuite) SetUpTest(c *C) { - s.state = state.New(nil) - s.state.Lock() - defer s.state.Unlock() - s.transaction = configstate.NewTransaction(s.state) -} - -type setGetOp string - -func (op setGetOp) kind() string { - return strings.Fields(string(op))[0] -} - -func (op setGetOp) args() map[string]interface{} { - m := make(map[string]interface{}) - args := strings.Fields(string(op)) - for _, pair := range args[1:] { - if pair == "=>" { - break - } - kv := strings.SplitN(pair, "=", 2) - var v interface{} - err := json.Unmarshal([]byte(kv[1]), &v) - if err != nil { - v = kv[1] - } - m[kv[0]] = v - } - return m -} - -func (op setGetOp) error() string { - if i := strings.Index(string(op), " => "); i >= 0 { - return string(op[i+4:]) - } - return "" -} - -func (op setGetOp) fails() bool { - return op.error() != "" -} - -var setGetTests = [][]setGetOp{{ - // Basics. - `set one=1 two=2`, - `setunder three=3`, - `get one=1 two=2 three=-`, - `getunder one=- two=- three=3`, - `commit`, - `getunder one=1 two=2 three=3`, - `get one=1 two=2 three=3`, - `set two=22 four=4`, - `get one=1 two=22 three=3 four=4`, - `getunder one=1 two=2 three=3 four=-`, - `commit`, - `getunder one=1 two=22 three=3 four=4`, -}, { - // Trivial full doc. - `set doc={"one":1,"two":2}`, - `get doc={"one":1,"two":2}`, -}, { - // Nested mutations. - `set one.two.three=3`, - `set one.five=5`, - `setunder one={"two":{"four":4}}`, - `get one={"two":{"three":3},"five":5}`, - `get one.two={"three":3}`, - `get one.two.three=3`, - `get one.five=5`, - `commit`, - `getunder one={"two":{"three":3,"four":4},"five":5}`, - `get one={"two":{"three":3,"four":4},"five":5}`, - `get one.two={"three":3,"four":4}`, - `get one.two.three=3`, - `get one.two.four=4`, - `get one.five=5`, -}, { - // Replacement with nested mutation. - `set one={"two":{"three":3}}`, - `set one.five=5`, - `get one={"two":{"three":3},"five":5}`, - `get one.two={"three":3}`, - `get one.two.three=3`, - `get one.five=5`, - `setunder one={"two":{"four":4},"six":6}`, - `commit`, - `getunder one={"two":{"three":3},"five":5}`, -}, { - // Cannot go through known scalar implicitly. - `set one.two=2`, - `set one.two.three=3 => snap "core" option "one\.two" is not a map`, - `get one.two.three=3 => snap "core" option "one\.two" is not a map`, - `get one={"two":2}`, - `commit`, - `set one.two.three=3 => snap "core" option "one\.two" is not a map`, - `get one.two.three=3 => snap "core" option "one\.two" is not a map`, - `get one={"two":2}`, - `getunder one={"two":2}`, -}, { - // Unknown scalars may be overwritten though. - `setunder one={"two":2}`, - `set one.two.three=3`, - `commit`, - `getunder one={"two":{"three":3}}`, -}, { - // Invalid option names. - `set BAD=1 => invalid option name: "BAD"`, - `set 42=1 => invalid option name: "42"`, - `set .bad=1 => invalid option name: ""`, - `set bad.=1 => invalid option name: ""`, - `set bad..bad=1 => invalid option name: ""`, - `set one.bad--bad.two=1 => invalid option name: "bad--bad"`, - `set one.-bad.two=1 => invalid option name: "-bad"`, - `set one.bad-.two=1 => invalid option name: "bad-"`, -}} - -func (s *transactionSuite) TestSetGet(c *C) { - s.state.Lock() - defer s.state.Unlock() - - for _, test := range setGetTests { - c.Logf("-----") - s.state.Set("config", map[string]interface{}{}) - t := configstate.NewTransaction(s.state) - snap := "core" - for _, op := range test { - c.Logf("%s", op) - switch op.kind() { - case "set": - for k, v := range op.args() { - err := t.Set(snap, k, v) - if op.fails() { - c.Assert(err, ErrorMatches, op.error()) - } else { - c.Assert(err, IsNil) - } - } - - case "get": - for k, expected := range op.args() { - var obtained interface{} - err := t.Get(snap, k, &obtained) - if op.fails() { - c.Assert(err, ErrorMatches, op.error()) - var nothing interface{} - c.Assert(t.GetMaybe(snap, k, ¬hing), ErrorMatches, op.error()) - c.Assert(nothing, IsNil) - continue - } - if expected == "-" { - if !configstate.IsNoOption(err) { - c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) - } - c.Assert(err, ErrorMatches, fmt.Sprintf("snap %q has no %q configuration option", snap, k)) - var nothing interface{} - c.Assert(t.GetMaybe(snap, k, ¬hing), IsNil) - c.Assert(nothing, IsNil) - continue - } - c.Assert(err, IsNil) - c.Assert(obtained, DeepEquals, expected) - - obtained = nil - c.Assert(t.GetMaybe(snap, k, &obtained), IsNil) - c.Assert(obtained, DeepEquals, expected) - } - - case "commit": - t.Commit() - - case "setunder": - var config map[string]map[string]interface{} - s.state.Get("config", &config) - if config == nil { - config = make(map[string]map[string]interface{}) - } - if config[snap] == nil { - config[snap] = make(map[string]interface{}) - } - for k, v := range op.args() { - if v == "-" { - delete(config[snap], k) - if len(config[snap]) == 0 { - delete(config[snap], snap) - } - } else { - config[snap][k] = v - } - } - s.state.Set("config", config) - - case "getunder": - var config map[string]map[string]interface{} - s.state.Get("config", &config) - for k, expected := range op.args() { - obtained, ok := config[snap][k] - if expected == "-" { - if ok { - c.Fatalf("Expected %q key to not exist, but it has value %v", k, obtained) - } - continue - } - c.Assert(obtained, DeepEquals, expected) - } - - default: - panic("unknown test op kind: " + op.kind()) - } - } - } -} - -type brokenType struct { - on string -} - -func (b *brokenType) UnmarshalJSON(data []byte) error { - if b.on == string(data) { - return fmt.Errorf("BAM!") - } - return nil -} - -func (s *transactionSuite) TestGetUnmarshalError(c *C) { - s.state.Lock() - defer s.state.Unlock() - c.Check(s.transaction.Set("test-snap", "foo", "good"), IsNil) - s.transaction.Commit() - - transaction := configstate.NewTransaction(s.state) - c.Check(transaction.Set("test-snap", "foo", "break"), IsNil) - - // Pristine state is good, value in the transaction breaks. - broken := brokenType{`"break"`} - err := transaction.Get("test-snap", "foo", &broken) - c.Assert(err, ErrorMatches, ".*BAM!.*") - - // Pristine state breaks, nothing in the transaction. - transaction.Commit() - err = transaction.Get("test-snap", "foo", &broken) - c.Assert(err, ErrorMatches, ".*BAM!.*") -} diff -Nru snapd-2.22.6+16.10/overlord/devicestate/devicemgr.go snapd-2.23.1+16.10/overlord/devicestate/devicemgr.go --- snapd-2.22.6+16.10/overlord/devicestate/devicemgr.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/devicestate/devicemgr.go 2017-03-08 13:28:14.000000000 +0000 @@ -30,8 +30,6 @@ "fmt" "net/http" "net/url" - "os" - "path/filepath" "regexp" "strings" "time" @@ -42,10 +40,11 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/i18n/dumb" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" - "github.com/snapcore/snapd/overlord/configstate" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" @@ -60,7 +59,12 @@ state *state.State keypairMgr asserts.KeypairManager runner *state.TaskRunner - bootOkRan bool + + bootOkRan bool + bootRevisionsUpdated bool + + lastBecomeOperationalAttempt time.Time + becomeOperationalBackoff time.Duration } // Manager returns a new device manager. @@ -112,6 +116,41 @@ return false } +// helpers to keep count of attempts to get a serial, useful to decide +// to give up holding off trying to auto-refresh + +type ensureOperationalAttemptsKey struct{} + +func incEnsureOperationalAttempts(st *state.State) { + cur, _ := st.Cached(ensureOperationalAttemptsKey{}).(int) + st.Cache(ensureOperationalAttemptsKey{}, cur+1) +} + +func ensureOperationalAttempts(st *state.State) int { + cur, _ := st.Cached(ensureOperationalAttemptsKey{}).(int) + return cur +} + +// ensureOperationalShouldBackoff returns whether we should abstain from +// further become-operational tentatives while its backoff interval is +// not expired. +func (m *DeviceManager) ensureOperationalShouldBackoff(now time.Time) bool { + if !m.lastBecomeOperationalAttempt.IsZero() && m.lastBecomeOperationalAttempt.Add(m.becomeOperationalBackoff).After(now) { + return true + } + if m.becomeOperationalBackoff == 0 { + m.becomeOperationalBackoff = 5 * time.Minute + } else { + newBackoff := m.becomeOperationalBackoff * 2 + if newBackoff > (12 * time.Hour) { + newBackoff = 24 * time.Hour + } + m.becomeOperationalBackoff = newBackoff + } + m.lastBecomeOperationalAttempt = now + return false +} + func (m *DeviceManager) ensureOperational() error { m.state.Lock() defer m.state.Unlock() @@ -127,13 +166,12 @@ } if device.Brand == "" || device.Model == "" { - // need first-boot, loading of model assertion info - if release.OnClassic { - // TODO: are we going to have model assertions on classic or need will need to cheat here? - return nil - } - // cannot proceed yet, once first boot is done these will be set - // and we can pick up from there + // cannot proceed until seeding has loaded the model + // assertion and set the brand and model, that is + // optional on classic + // TODO: later we can check if + // "seeded" was set and we still don't have a brand/model + // and use a fallback assertion return nil } @@ -141,11 +179,9 @@ return nil } - if serialRequestURL == "" { - // cannot do anything actually - return nil - } - + // TODO: make presence of gadget optional on classic? that is + // sensible only for devices that the store can give directly + // serials to and when we will have a general fallback gadgetInfo, err := snapstate.GadgetInfo(m.state) if err == state.ErrNoState { // no gadget installed yet, cannot proceed @@ -155,6 +191,13 @@ return err } + // have some backoff between full retries + if m.ensureOperationalShouldBackoff(time.Now()) { + return nil + } + // increment attempt count + incEnsureOperationalAttempts(m.state) + // XXX: some of these will need to be split and use hooks // retries might need to embrace more than one "task" then, // need to be careful @@ -195,17 +238,6 @@ m.state.Lock() defer m.state.Unlock() - // FIXME: enable on classic? - // - // Disable seed.yaml on classic for now. In the long run we want - // classic to have a seed parsing as well so that we can install - // snaps in a classic environment (LP: #1609903). However right - // now it is under heavy development so until the dust - // settles we disable it. - if release.OnClassic { - return nil - } - var seeded bool err := m.state.Get("seeded", &seeded) if err != nil && err != state.ErrNoState { @@ -219,12 +251,6 @@ return nil } - coreInfo, err := snapstate.CoreInfo(m.state) - if err == nil && coreInfo.Name() == "ubuntu-core" { - // already seeded... recover - return m.alreadyFirstbooted() - } - tsAll, err := populateStateFromSeed(m.state) if err != nil { return err @@ -243,52 +269,6 @@ return nil } -// alreadyFirstbooted recovers already first booted devices with the old method appropriately -func (m *DeviceManager) alreadyFirstbooted() error { - device, err := auth.Device(m.state) - if err != nil { - return err - } - // recover key-id - if device.Brand != "" && device.Model != "" { - serials, err := assertstate.DB(m.state).FindMany(asserts.SerialType, map[string]string{ - "brand-id": device.Brand, - "model": device.Model, - }) - if err != nil && err != asserts.ErrNotFound { - return err - } - - if len(serials) == 1 { - // we can recover the key id from the assertion - serial := serials[0].(*asserts.Serial) - keyID := serial.DeviceKey().ID() - device.KeyID = keyID - device.Serial = serial.Serial() - err := auth.SetDevice(m.state, device) - if err != nil { - return err - } - // best effort to cleanup abandoned keys - pat := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "*") - keyFns, err := filepath.Glob(pat) - if err != nil { - panic(fmt.Sprintf("invalid glob for device keys: %v", err)) - } - for _, keyFn := range keyFns { - if filepath.Base(keyFn) == keyID { - continue - } - os.Remove(keyFn) - } - } - - } - - m.state.Set("seeded", true) - return nil -} - func (m *DeviceManager) ensureBootOk() error { m.state.Lock() defer m.state.Unlock() @@ -308,7 +288,14 @@ m.bootOkRan = true } - return snapstate.UpdateBootRevisions(m.state) + if !m.bootRevisionsUpdated { + if err := snapstate.UpdateBootRevisions(m.state); err != nil { + return err + } + m.bootRevisionsUpdated = true + } + + return nil } type ensureError struct { @@ -447,6 +434,33 @@ return &state.Retry{After: retryInterval} } +type serverError struct { + Message string `json:"message"` + Errors []*serverError `json:"error_list"` +} + +func retryBadStatus(t *state.Task, reason string, resp *http.Response) error { + if resp.StatusCode > http.StatusInternalServerError { + // likely temporary + return retryErr(t, "%s: unexpected status %d", reason, resp.StatusCode) + } + if resp.Header.Get("Content-Type") == "application/json" { + var srvErr serverError + dec := json.NewDecoder(resp.Body) + err := dec.Decode(&srvErr) + if err == nil { + msg := srvErr.Message + if msg == "" && len(srvErr.Errors) > 0 { + msg = srvErr.Errors[0].Message + } + if msg != "" { + return fmt.Errorf("%s: %s", reason, msg) + } + } + } + return fmt.Errorf("%s: unexpected status %d", reason, resp.StatusCode) +} + func prepareSerialRequest(t *state.Task, privKey asserts.PrivateKey, device *auth.DeviceState, client *http.Client, cfg *serialRequestConfig) (string, error) { st := t.State() st.Unlock() @@ -465,7 +479,7 @@ } defer resp.Body.Close() if resp.StatusCode != 200 { - return "", retryErr(t, "cannot retrieve request-id for making a request for a serial: unexpected status %d", resp.StatusCode) + return "", retryBadStatus(t, "cannot retrieve request-id for making a request for a serial", resp) } dec := json.NewDecoder(resp.Body) @@ -525,7 +539,7 @@ case 202: return nil, errPoll default: - return nil, retryErr(t, "cannot deliver device serial request: unexpected status %d", resp.StatusCode) + return nil, retryBadStatus(t, "cannot deliver device serial request", resp) } // decode body with serial assertion @@ -624,7 +638,7 @@ } gadgetName := gadgetInfo.Name() - tr := configstate.NewTransaction(t.State()) + tr := config.NewTransaction(t.State()) var svcURL string err = tr.GetMaybe(gadgetName, "device-service.url", &svcURL) if err != nil { @@ -788,6 +802,44 @@ return nil } +// canAutoRefresh is a helper that checks if the device is able to +// auto-refresh +func canAutoRefresh(st *state.State) (bool, error) { + // we need to be seeded first + var seeded bool + st.Get("seeded", &seeded) + if !seeded { + return false, nil + } + + _, err := Model(st) + if err == state.ErrNoState { + // no model, no need to wait for a serial + // can happen only on classic + return true, nil + } + if err != nil { + return false, err + } + + // either we have a serial or we try anyway if we attempted + // for a while to get a serial, this would allow us to at + // least upgrade core if that can help + if ensureOperationalAttempts(st) >= 3 { + return true, nil + } + + _, err = Serial(st) + if err == state.ErrNoState { + return false, nil + } + if err != nil { + return false, err + } + + return true, nil +} + var repeatRequestSerial string // implementing auth.DeviceAssertions @@ -902,6 +954,10 @@ currentInfo = snapstate.GadgetInfo getName = (*asserts.Model).Gadget case snap.TypeKernel: + if release.OnClassic { + return fmt.Errorf("cannot install a kernel snap on classic") + } + kind = "kernel" currentInfo = snapstate.KernelInfo getName = (*asserts.Model).Kernel @@ -910,9 +966,25 @@ return nil } - if release.OnClassic { - // for the time being - return fmt.Errorf("cannot install a %s snap on classic", kind) + model, err := Model(st) + if err == state.ErrNoState { + return fmt.Errorf("cannot install %s without model assertion", kind) + } + if err != nil { + return err + } + + if snapInfo.SnapID != "" { + snapDecl, err := assertstate.SnapDeclaration(st, snapInfo.SnapID) + if err != nil { + return fmt.Errorf("internal error: cannot find snap declaration for %q: %v", snapInfo.Name(), err) + } + publisher := snapDecl.PublisherID() + if publisher != "canonical" && publisher != model.BrandID() { + return fmt.Errorf("cannot install %s %q published by %q for model by %q", kind, snapInfo.Name(), publisher, model.BrandID()) + } + } else { + logger.Noticef("installing unasserted %s %q", kind, snapInfo.Name()) } currentSnap, err := currentInfo(st) @@ -925,15 +997,11 @@ } // first installation of a gadget/kernel - model, err := Model(st) - if err == state.ErrNoState { - return fmt.Errorf("cannot install %s without model assertion", kind) - } - if err != nil { - return err + expectedName := getName(model) + if expectedName == "" { // can happen only on classic + return fmt.Errorf("cannot install %s snap on classic if not requested by the model", kind) } - expectedName := getName(model) if snapInfo.Name() != expectedName { return fmt.Errorf("cannot install %s %q, model assertion requests %q", kind, snapInfo.Name(), expectedName) } @@ -943,4 +1011,5 @@ func init() { snapstate.AddCheckSnapCallback(checkGadgetOrKernel) + snapstate.CanAutoRefresh = canAutoRefresh } diff -Nru snapd-2.22.6+16.10/overlord/devicestate/devicemgr_test.go snapd-2.23.1+16.10/overlord/devicestate/devicemgr_test.go --- snapd-2.22.6+16.10/overlord/devicestate/devicemgr_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/devicestate/devicemgr_test.go 2017-03-08 13:28:14.000000000 +0000 @@ -21,13 +21,11 @@ import ( "encoding/json" - "errors" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" - "path/filepath" "sync" "testing" "time" @@ -66,6 +64,11 @@ db *asserts.Database storeSigning *assertstest.StoreStack + brandSigning *assertstest.SigningDB + + reqID string + + restoreOnClassic func() } var _ = Suite(&deviceMgrSuite{}) @@ -127,11 +130,16 @@ func (s *deviceMgrSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) + s.restoreOnClassic = release.MockOnClassic(false) + rootPrivKey, _ := assertstest.GenerateKey(1024) storePrivKey, _ := assertstest.GenerateKey(752) s.storeSigning = assertstest.NewStoreStack("canonical", rootPrivKey, storePrivKey) s.state = state.New(nil) + brandPrivKey, _ := assertstest.GenerateKey(752) + s.brandSigning = assertstest.NewSigningDB("my-brand", brandPrivKey) + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ Backstore: asserts.NewMemoryBackstore(), Trusted: s.storeSigning.Trusted, @@ -167,7 +175,7 @@ assertstate.ReplaceDB(s.state, nil) s.state.Unlock() dirs.SetRootDir("") - release.OnClassic = true + s.restoreOnClassic() } func (s *deviceMgrSuite) settle() { @@ -179,7 +187,13 @@ } } -func (s *deviceMgrSuite) mockServer(c *C, reqID string) *httptest.Server { +// seeding avoids triggering a real full seeding, it simulates having it in process instead +func (s *deviceMgrSuite) seeding() { + chg := s.state.NewChange("seed", "Seed system") + chg.SetStatus(state.DoingStatus) +} + +func (s *deviceMgrSuite) mockServer(c *C) *httptest.Server { expectedUserAgent := httputil.UserAgent() var mu sync.Mutex @@ -189,7 +203,7 @@ case "/identity/api/v1/request-id": w.WriteHeader(http.StatusOK) c.Check(r.Header.Get("User-Agent"), Equals, expectedUserAgent) - io.WriteString(w, fmt.Sprintf(`{"request-id": "%s"}`, reqID)) + io.WriteString(w, fmt.Sprintf(`{"request-id": "%s"}`, s.reqID)) case "/identity/api/v1/serial": c.Check(r.Header.Get("X-Extra-Header"), Equals, "extra") @@ -212,6 +226,15 @@ c.Assert(err, IsNil) c.Check(serialReq.BrandID(), Equals, "canonical") c.Check(serialReq.Model(), Equals, "pc") + reqID := serialReq.RequestID() + if reqID == "REQID-BADREQ" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{ + "error_list": [{"message": "bad serial-request"}] +}`)) + return + } if reqID == "REQID-POLL" && serialNum != 10002 { w.WriteHeader(http.StatusAccepted) return @@ -269,7 +292,8 @@ r1 := devicestate.MockKeyLength(752) defer r1() - mockServer := s.mockServer(c, "REQID-1") + s.reqID = "REQID-1" + mockServer := s.mockServer(c) defer mockServer.Close() mockRequestIDURL := mockServer.URL + "/identity/api/v1/request-id" @@ -295,6 +319,9 @@ Model: "pc", }) + // avoid full seeding + s.seeding() + // runs the whole device registration process s.state.Unlock() s.settle() @@ -336,7 +363,8 @@ func (s *deviceMgrSuite) TestDoRequestSerialIdempotentAfterAddSerial(c *C) { privKey, _ := assertstest.GenerateKey(1024) - mockServer := s.mockServer(c, "REQID-1") + s.reqID = "REQID-1" + mockServer := s.mockServer(c) defer mockServer.Close() mockRequestIDURL := mockServer.URL + "/identity/api/v1/request-id" @@ -371,6 +399,9 @@ chg := s.state.NewChange("become-operational", "...") chg.AddTask(t) + // avoid full seeding + s.seeding() + s.state.Unlock() s.mgr.Ensure() s.mgr.Wait() @@ -401,7 +432,8 @@ func (s *deviceMgrSuite) TestDoRequestSerialIdempotentAfterGotSerial(c *C) { privKey, _ := assertstest.GenerateKey(1024) - mockServer := s.mockServer(c, "REQID-1") + s.reqID = "REQID-1" + mockServer := s.mockServer(c) defer mockServer.Close() mockRequestIDURL := mockServer.URL + "/identity/api/v1/request-id" @@ -436,6 +468,9 @@ chg := s.state.NewChange("become-operational", "...") chg.AddTask(t) + // avoid full seeding + s.seeding() + s.state.Unlock() s.mgr.Ensure() s.mgr.Wait() @@ -467,7 +502,8 @@ r1 := devicestate.MockKeyLength(752) defer r1() - mockServer := s.mockServer(c, "REQID-POLL") + s.reqID = "REQID-POLL" + mockServer := s.mockServer(c) defer mockServer.Close() mockRequestIDURL := mockServer.URL + "/identity/api/v1/request-id" @@ -497,6 +533,9 @@ Model: "pc", }) + // avoid full seeding + s.seeding() + // runs the whole device registration process with polling s.state.Unlock() s.settle() @@ -539,7 +578,8 @@ r1 := devicestate.MockKeyLength(752) defer r1() - mockServer := s.mockServer(c, "REQID-1") + s.reqID = "REQID-1" + mockServer := s.mockServer(c) defer mockServer.Close() r2 := hookstate.MockRunHook(func(ctx *hookstate.Context, _ *tomb.Tomb) ([]byte, error) { @@ -582,12 +622,14 @@ hooks: prepare-device: `, "") - auth.SetDevice(s.state, &auth.DeviceState{ Brand: "canonical", Model: "pc", }) + // avoid full seeding + s.seeding() + // runs the whole device registration process s.state.Unlock() s.settle() @@ -634,6 +676,117 @@ c.Check(device.KeyID, Equals, privKey.PublicKey().ID()) } +func (s *deviceMgrSuite) TestFullDeviceRegistrationErrorBackoff(c *C) { + r1 := devicestate.MockKeyLength(752) + defer r1() + + s.reqID = "REQID-BADREQ" + mockServer := s.mockServer(c) + defer mockServer.Close() + + mockRequestIDURL := mockServer.URL + "/identity/api/v1/request-id" + r2 := devicestate.MockRequestIDURL(mockRequestIDURL) + defer r2() + + mockSerialRequestURL := mockServer.URL + "/identity/api/v1/devices" + r3 := devicestate.MockSerialRequestURL(mockSerialRequestURL) + defer r3() + + // setup state as will be done by first-boot + s.state.Lock() + defer s.state.Unlock() + + // sanity + c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 0) + + s.setupGadget(c, ` +name: gadget +type: gadget +version: gadget +`, "") + + auth.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + }) + + // avoid full seeding + s.seeding() + + // try the whole device registration process + s.state.Unlock() + s.settle() + s.state.Lock() + + var becomeOperational *state.Change + for _, chg := range s.state.Changes() { + if chg.Kind() == "become-operational" { + becomeOperational = chg + break + } + } + c.Assert(becomeOperational, NotNil) + firstTryID := becomeOperational.ID() + + c.Check(becomeOperational.Status().Ready(), Equals, true) + c.Check(becomeOperational.Err(), ErrorMatches, `(?s).*cannot deliver device serial request: bad serial-request.*`) + + device, err := auth.Device(s.state) + c.Assert(err, IsNil) + c.Check(device.KeyID, Not(Equals), "") + keyID := device.KeyID + + c.Check(s.mgr.EnsureOperationalShouldBackoff(time.Now()), Equals, true) + c.Check(s.mgr.EnsureOperationalShouldBackoff(time.Now().Add(6*time.Minute)), Equals, false) + c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 1) + + // try again the whole device registration process + s.reqID = "REQID-1" + s.mgr.SetLastBecomeOperationalAttempt(time.Now().Add(-15 * time.Minute)) + s.state.Unlock() + s.settle() + s.state.Lock() + + becomeOperational = nil + for _, chg := range s.state.Changes() { + if chg.Kind() == "become-operational" && chg.ID() != firstTryID { + becomeOperational = chg + break + } + } + c.Assert(becomeOperational, NotNil) + + c.Check(becomeOperational.Status().Ready(), Equals, true) + c.Check(becomeOperational.Err(), IsNil) + + c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 2) + + device, err = auth.Device(s.state) + c.Assert(err, IsNil) + c.Check(device.KeyID, Equals, keyID) + c.Check(device.Serial, Equals, "10000") +} + +func (s *deviceMgrSuite) TestEnsureBecomeOperationalShouldBackoff(c *C) { + t0 := time.Now() + c.Check(s.mgr.EnsureOperationalShouldBackoff(t0), Equals, false) + c.Check(s.mgr.BecomeOperationalBackoff(), Equals, 5*time.Minute) + + backoffs := []time.Duration{5, 10, 20, 40, 80, 160, 320, 640, 1440, 1440} + t1 := t0 + for _, m := range backoffs { + c.Check(s.mgr.EnsureOperationalShouldBackoff(t1.Add(time.Duration(m-1)*time.Minute)), Equals, true) + + t1 = t1.Add(time.Duration(m+1) * time.Minute) + c.Check(s.mgr.EnsureOperationalShouldBackoff(t1), Equals, false) + m *= 2 + if m > (12 * 60) { + m = 24 * 60 + } + c.Check(s.mgr.BecomeOperationalBackoff(), Equals, m*time.Minute) + } +} + func (s *deviceMgrSuite) TestDeviceAssertionsModelAndSerial(c *C) { // nothing in the state s.state.Lock() @@ -705,22 +858,9 @@ c.Check(err, Equals, state.ErrNoState) // have a serial assertion - devKey, _ := assertstest.GenerateKey(752) - encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey()) - c.Assert(err, IsNil) - serial, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{ - "brand-id": "canonical", - "model": "pc", - "serial": "8989", - "device-key": string(encDevKey), - "device-key-sha3-384": devKey.PublicKey().ID(), - "timestamp": time.Now().Format(time.RFC3339), - }, nil, "") - c.Assert(err, IsNil) s.state.Lock() - err = assertstate.Add(s.state, serial) + s.makeSerialAssertionInState(c, "canonical", "pc", "8989") s.state.Unlock() - c.Assert(err, IsNil) _, err = s.mgr.Model() c.Assert(err, IsNil) @@ -782,8 +922,6 @@ } func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlAlreadySeeded(c *C) { - release.OnClassic = false - s.state.Lock() s.state.Set("seeded", true) s.state.Unlock() @@ -801,8 +939,6 @@ } func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlChangeInFlight(c *C) { - release.OnClassic = false - s.state.Lock() chg := s.state.NewChange("seed", "just for testing") chg.AddTask(s.state.NewTask("test-task", "the change needs a task")) @@ -820,7 +956,7 @@ c.Assert(called, Equals, false) } -func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlSkippedOnClassic(c *C) { +func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlAlsoOnClassic(c *C) { release.OnClassic = true called := false @@ -832,12 +968,10 @@ err := s.mgr.EnsureSeedYaml() c.Assert(err, IsNil) - c.Assert(called, Equals, false) + c.Assert(called, Equals, true) } func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlHappy(c *C) { - release.OnClassic = false - restore := devicestate.MockPopulateStateFromSeed(func(*state.State) (ts []*state.TaskSet, err error) { t := s.state.NewTask("test-task", "a random task") ts = append(ts, state.NewTaskSet(t)) @@ -854,104 +988,6 @@ c.Check(s.state.Changes(), HasLen, 1) } -func (s *deviceMgrSuite) TestDeviceManagerEnsureSeedYamlRecover(c *C) { - release.OnClassic = false - - restore := devicestate.MockPopulateStateFromSeed(func(*state.State) (ts []*state.TaskSet, err error) { - return nil, errors.New("should not be called") - }) - defer restore() - - s.state.Lock() - defer s.state.Unlock() - - s.setupCore(c, "ubuntu-core", ` -name: ubuntu-core -type: os -version: ubuntu-core -`, "") - - // have a model assertion - model, err := s.storeSigning.Sign(asserts.ModelType, map[string]interface{}{ - "series": "16", - "brand-id": "canonical", - "model": "pc", - "gadget": "pc", - "kernel": "kernel", - "architecture": "amd64", - "timestamp": time.Now().Format(time.RFC3339), - }, nil, "") - c.Assert(err, IsNil) - err = assertstate.Add(s.state, model) - c.Assert(err, IsNil) - - // have a serial assertion - devKey, _ := assertstest.GenerateKey(752) - encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey()) - keyID := devKey.PublicKey().ID() - c.Assert(err, IsNil) - serial, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{ - "brand-id": "canonical", - "model": "pc", - "serial": "8989", - "device-key": string(encDevKey), - "device-key-sha3-384": keyID, - "timestamp": time.Now().Format(time.RFC3339), - }, nil, "") - c.Assert(err, IsNil) - err = assertstate.Add(s.state, serial) - c.Assert(err, IsNil) - - // forgotten key id and serial - auth.SetDevice(s.state, &auth.DeviceState{ - Brand: "canonical", - Model: "pc", - }) - // put key on disk - err = s.mgr.KeypairManager().Put(devKey) - c.Assert(err, IsNil) - // extra unused stuff - junk1 := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "junkjunk1") - err = ioutil.WriteFile(junk1, nil, 0644) - c.Assert(err, IsNil) - junk2 := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "junkjunk2") - err = ioutil.WriteFile(junk2, nil, 0644) - c.Assert(err, IsNil) - // double check - pat := filepath.Join(dirs.SnapDeviceDir, "private-keys-v1", "*") - onDisk, err := filepath.Glob(pat) - c.Assert(err, IsNil) - c.Check(onDisk, HasLen, 3) - - s.state.Unlock() - err = s.mgr.EnsureSeedYaml() - s.state.Lock() - c.Assert(err, IsNil) - - c.Check(s.state.Changes(), HasLen, 0) - - var seeded bool - err = s.state.Get("seeded", &seeded) - c.Assert(err, IsNil) - c.Check(seeded, Equals, true) - - device, err := auth.Device(s.state) - c.Assert(err, IsNil) - c.Check(device, DeepEquals, &auth.DeviceState{ - Brand: "canonical", - Model: "pc", - KeyID: keyID, - Serial: "8989", - }) - // key is still there - _, err = s.mgr.KeypairManager().Get(keyID) - c.Assert(err, IsNil) - onDisk, err = filepath.Glob(pat) - c.Assert(err, IsNil) - // junk was removed - c.Check(onDisk, HasLen, 1) -} - func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkSkippedOnClassic(c *C) { release.OnClassic = true @@ -960,8 +996,6 @@ } func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkBootloaderHappy(c *C) { - release.OnClassic = false - bootloader := boottest.NewMockBootloader("mock", c.MkDir()) partition.ForceBootloader(bootloader) defer partition.ForceBootloader(nil) @@ -991,8 +1025,6 @@ } func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkUpdateBootRevisionsHappy(c *C) { - release.OnClassic = false - bootloader := boottest.NewMockBootloader("mock", c.MkDir()) partition.ForceBootloader(bootloader) defer partition.ForceBootloader(nil) @@ -1006,6 +1038,14 @@ s.state.Lock() defer s.state.Unlock() + siKernel1 := &snap.SideInfo{RealName: "kernel", Revision: snap.R(1)} + snapstate.Set(s.state, "kernel", &snapstate.SnapState{ + SnapType: "kernel", + Active: true, + Sequence: []*snap.SideInfo{siKernel1}, + Current: siKernel1.Revision, + }) + siCore1 := &snap.SideInfo{RealName: "core", Revision: snap.R(1)} siCore2 := &snap.SideInfo{RealName: "core", Revision: snap.R(2)} snapstate.Set(s.state, "core", &snapstate.SnapState{ @@ -1025,8 +1065,6 @@ } func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkNotRunAgain(c *C) { - release.OnClassic = false - bootloader := boottest.NewMockBootloader("mock", c.MkDir()) bootloader.SetBootVars(map[string]string{ "snap_mode": "trying", @@ -1043,8 +1081,6 @@ } func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkError(c *C) { - release.OnClassic = false - s.state.Lock() // seeded s.state.Set("seeded", true) @@ -1067,24 +1103,57 @@ c.Assert(err, ErrorMatches, "devicemgr: bootloader err") } +func (s *deviceMgrSuite) setupBrands(c *C) { + brandAcct := assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{ + "account-id": "my-brand", + }, "") + err := assertstate.Add(s.state, brandAcct) + c.Assert(err, IsNil) + otherAcct := assertstest.NewAccount(s.storeSigning, "other-brand", map[string]interface{}{ + "account-id": "other-brand", + }, "") + err = assertstate.Add(s.state, otherAcct) + c.Assert(err, IsNil) + + brandPubKey, err := s.brandSigning.PublicKey("") + c.Assert(err, IsNil) + brandAccKey := assertstest.NewAccountKey(s.storeSigning, brandAcct, nil, brandPubKey, "") + err = assertstate.Add(s.state, brandAccKey) + c.Assert(err, IsNil) +} + +func (s *deviceMgrSuite) setupSnapDecl(c *C, name, snapID, publisherID string) { + brandGadgetDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-name": name, + "snap-id": snapID, + "publisher-id": publisherID, + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = assertstate.Add(s.state, brandGadgetDecl) + c.Assert(err, IsNil) +} + func (s *deviceMgrSuite) TestCheckGadget(c *C) { - release.OnClassic = false s.state.Lock() defer s.state.Unlock() // nothing is setup gadgetInfo := snaptest.MockInfo(c, `type: gadget -name: gadget`, nil) +name: other-gadget`, nil) err := devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{}) c.Check(err, ErrorMatches, `cannot install gadget without model assertion`) // setup model assertion - model, err := s.storeSigning.Sign(asserts.ModelType, map[string]interface{}{ + s.setupBrands(c) + + model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{ "series": "16", - "brand-id": "canonical", - "model": "pc", - "gadget": "pc", - "kernel": "kernel", + "brand-id": "my-brand", + "model": "my-model", + "gadget": "gadget", + "kernel": "krnl", "architecture": "amd64", "timestamp": time.Now().Format(time.RFC3339), }, nil, "") @@ -1092,38 +1161,186 @@ err = assertstate.Add(s.state, model) c.Assert(err, IsNil) err = auth.SetDevice(s.state, &auth.DeviceState{ - Brand: "canonical", - Model: "pc", + Brand: "my-brand", + Model: "my-model", }) c.Assert(err, IsNil) err = devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{}) - c.Check(err, ErrorMatches, `cannot install gadget "gadget", model assertion requests "pc"`) + c.Check(err, ErrorMatches, `cannot install gadget "other-gadget", model assertion requests "gadget"`) + + // brand gadget + s.setupSnapDecl(c, "gadget", "brand-gadget-id", "my-brand") + brandGadgetInfo := snaptest.MockInfo(c, ` +type: gadget +name: gadget +`, nil) + brandGadgetInfo.SnapID = "brand-gadget-id" + + // canonical gadget + s.setupSnapDecl(c, "gadget", "canonical-gadget-id", "canonical") + canonicalGadgetInfo := snaptest.MockInfo(c, ` +type: gadget +name: gadget +`, nil) + canonicalGadgetInfo.SnapID = "canonical-gadget-id" + + // other gadget + s.setupSnapDecl(c, "gadget", "other-gadget-id", "other-brand") + otherGadgetInfo := snaptest.MockInfo(c, ` +type: gadget +name: gadget +`, nil) + otherGadgetInfo.SnapID = "other-gadget-id" + + // install brand gadget ok + err = devicestate.CheckGadgetOrKernel(s.state, brandGadgetInfo, nil, snapstate.Flags{}) + c.Check(err, IsNil) + + // install canonical gadget ok + err = devicestate.CheckGadgetOrKernel(s.state, canonicalGadgetInfo, nil, snapstate.Flags{}) + c.Check(err, IsNil) - // install pc gadget - pcGadgetInfo := snaptest.MockInfo(c, `type: gadget -name: pc`, nil) - err = devicestate.CheckGadgetOrKernel(s.state, pcGadgetInfo, nil, snapstate.Flags{}) + // install other gadget fails + err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, snapstate.Flags{}) + c.Check(err, ErrorMatches, `cannot install gadget "gadget" published by "other-brand" for model by "my-brand"`) + + // unasserted installation of other works + otherGadgetInfo.SnapID = "" + err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, snapstate.Flags{}) c.Check(err, IsNil) } +func (s *deviceMgrSuite) TestCheckGadgetOnClassic(c *C) { + release.OnClassic = true + + s.state.Lock() + defer s.state.Unlock() + + gadgetInfo := snaptest.MockInfo(c, `type: gadget +name: other-gadget`, nil) + + // setup model assertion + s.setupBrands(c) + + model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "brand-id": "my-brand", + "model": "my-model", + "classic": "true", + "gadget": "gadget", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = assertstate.Add(s.state, model) + c.Assert(err, IsNil) + err = auth.SetDevice(s.state, &auth.DeviceState{ + Brand: "my-brand", + Model: "my-model", + }) + c.Assert(err, IsNil) + + err = devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{}) + c.Check(err, ErrorMatches, `cannot install gadget "other-gadget", model assertion requests "gadget"`) + + // brand gadget + s.setupSnapDecl(c, "gadget", "brand-gadget-id", "my-brand") + brandGadgetInfo := snaptest.MockInfo(c, ` +type: gadget +name: gadget +`, nil) + brandGadgetInfo.SnapID = "brand-gadget-id" + + // canonical gadget + s.setupSnapDecl(c, "gadget", "canonical-gadget-id", "canonical") + canonicalGadgetInfo := snaptest.MockInfo(c, ` +type: gadget +name: gadget +`, nil) + canonicalGadgetInfo.SnapID = "canonical-gadget-id" + + // other gadget + s.setupSnapDecl(c, "gadget", "other-gadget-id", "other-brand") + otherGadgetInfo := snaptest.MockInfo(c, ` +type: gadget +name: gadget +`, nil) + otherGadgetInfo.SnapID = "other-gadget-id" + + // install brand gadget ok + err = devicestate.CheckGadgetOrKernel(s.state, brandGadgetInfo, nil, snapstate.Flags{}) + c.Check(err, IsNil) + + // install canonical gadget ok + err = devicestate.CheckGadgetOrKernel(s.state, canonicalGadgetInfo, nil, snapstate.Flags{}) + c.Check(err, IsNil) + + // install other gadget fails + err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, snapstate.Flags{}) + c.Check(err, ErrorMatches, `cannot install gadget "gadget" published by "other-brand" for model by "my-brand"`) + + // unasserted installation of other works + otherGadgetInfo.SnapID = "" + err = devicestate.CheckGadgetOrKernel(s.state, otherGadgetInfo, nil, snapstate.Flags{}) + c.Check(err, IsNil) +} + +func (s *deviceMgrSuite) TestCheckGadgetOnClassicGadgetNotSpecified(c *C) { + release.OnClassic = true + + s.state.Lock() + defer s.state.Unlock() + + gadgetInfo := snaptest.MockInfo(c, `type: gadget +name: gadget`, nil) + + // setup model assertion + s.setupBrands(c) + + model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "brand-id": "my-brand", + "model": "my-model", + "classic": "true", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = assertstate.Add(s.state, model) + c.Assert(err, IsNil) + err = auth.SetDevice(s.state, &auth.DeviceState{ + Brand: "my-brand", + Model: "my-model", + }) + c.Assert(err, IsNil) + + err = devicestate.CheckGadgetOrKernel(s.state, gadgetInfo, nil, snapstate.Flags{}) + c.Check(err, ErrorMatches, `cannot install gadget snap on classic if not requested by the model`) +} + func (s *deviceMgrSuite) TestCheckKernel(c *C) { - release.OnClassic = false s.state.Lock() defer s.state.Unlock() - // nothing is setup kernelInfo := snaptest.MockInfo(c, `type: kernel name: lnrk`, nil) + // not on classic + release.OnClassic = true err := devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, snapstate.Flags{}) + c.Check(err, ErrorMatches, `cannot install a kernel snap on classic`) + release.OnClassic = false + + // nothing is setup + err = devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, snapstate.Flags{}) c.Check(err, ErrorMatches, `cannot install kernel without model assertion`) // setup model assertion - model, err := s.storeSigning.Sign(asserts.ModelType, map[string]interface{}{ + s.setupBrands(c) + + model, err := s.brandSigning.Sign(asserts.ModelType, map[string]interface{}{ "series": "16", - "brand-id": "canonical", - "model": "pc", - "gadget": "pc", + "brand-id": "my-brand", + "model": "my-model", + "gadget": "gadget", "kernel": "krnl", "architecture": "amd64", "timestamp": time.Now().Format(time.RFC3339), @@ -1132,17 +1349,204 @@ err = assertstate.Add(s.state, model) c.Assert(err, IsNil) err = auth.SetDevice(s.state, &auth.DeviceState{ - Brand: "canonical", - Model: "pc", + Brand: "my-brand", + Model: "my-model", }) c.Assert(err, IsNil) err = devicestate.CheckGadgetOrKernel(s.state, kernelInfo, nil, snapstate.Flags{}) c.Check(err, ErrorMatches, `cannot install kernel "lnrk", model assertion requests "krnl"`) - // install krnl kernel - krnlKernelInfo := snaptest.MockInfo(c, `type: kernel -name: krnl`, nil) - err = devicestate.CheckGadgetOrKernel(s.state, krnlKernelInfo, nil, snapstate.Flags{}) + // brand kernel + s.setupSnapDecl(c, "krnl", "brand-krnl-id", "my-brand") + brandKrnlInfo := snaptest.MockInfo(c, ` +type: kernel +name: krnl +`, nil) + brandKrnlInfo.SnapID = "brand-krnl-id" + + // canonical kernel + s.setupSnapDecl(c, "krnl", "canonical-krnl-id", "canonical") + canonicalKrnlInfo := snaptest.MockInfo(c, ` +type: kernel +name: krnl +`, nil) + canonicalKrnlInfo.SnapID = "canonical-krnl-id" + + // other kernel + s.setupSnapDecl(c, "krnl", "other-krnl-id", "other-brand") + otherKrnlInfo := snaptest.MockInfo(c, ` +type: kernel +name: krnl +`, nil) + otherKrnlInfo.SnapID = "other-krnl-id" + + // install brand kernel ok + err = devicestate.CheckGadgetOrKernel(s.state, brandKrnlInfo, nil, snapstate.Flags{}) c.Check(err, IsNil) + + // install canonical kernel ok + err = devicestate.CheckGadgetOrKernel(s.state, canonicalKrnlInfo, nil, snapstate.Flags{}) + c.Check(err, IsNil) + + // install other kernel fails + err = devicestate.CheckGadgetOrKernel(s.state, otherKrnlInfo, nil, snapstate.Flags{}) + c.Check(err, ErrorMatches, `cannot install kernel "krnl" published by "other-brand" for model by "my-brand"`) + + // unasserted installation of other works + otherKrnlInfo.SnapID = "" + err = devicestate.CheckGadgetOrKernel(s.state, otherKrnlInfo, nil, snapstate.Flags{}) + c.Check(err, IsNil) +} + +func (s *deviceMgrSuite) makeModelAssertionInState(c *C, brandID, model string, extras map[string]string) { + headers := map[string]interface{}{ + "series": "16", + "brand-id": brandID, + "model": model, + "timestamp": time.Now().Format(time.RFC3339), + } + for k, v := range extras { + headers[k] = v + } + modelAs, err := s.storeSigning.Sign(asserts.ModelType, headers, nil, "") + c.Assert(err, IsNil) + err = assertstate.Add(s.state, modelAs) + c.Assert(err, IsNil) +} + +func (s *deviceMgrSuite) makeSerialAssertionInState(c *C, brandID, model, serialN string) { + devKey, _ := assertstest.GenerateKey(752) + encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey()) + c.Assert(err, IsNil) + serial, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{ + "brand-id": brandID, + "model": model, + "serial": serialN, + "device-key": string(encDevKey), + "device-key-sha3-384": devKey.PublicKey().ID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = assertstate.Add(s.state, serial) + c.Assert(err, IsNil) +} + +func (s *deviceMgrSuite) TestCanAutoRefreshOnCore(c *C) { + s.state.Lock() + defer s.state.Unlock() + + canAutoRefresh := func() bool { + ok, err := devicestate.CanAutoRefresh(s.state) + c.Assert(err, IsNil) + return ok + } + + // not seeded, no model, no serial -> no auto-refresh + s.state.Set("seeded", false) + c.Check(canAutoRefresh(), Equals, false) + + // seeded, model, no serial -> no auto-refresh + s.state.Set("seeded", true) + auth.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + }) + s.makeModelAssertionInState(c, "canonical", "pc", map[string]string{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + }) + c.Check(canAutoRefresh(), Equals, false) + + // seeded, model, serial -> auto-refresh + auth.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + Serial: "8989", + }) + s.makeSerialAssertionInState(c, "canonical", "pc", "8989") + c.Check(canAutoRefresh(), Equals, true) + + // not seeded, model, serial -> no auto-refresh + s.state.Set("seeded", false) + c.Check(canAutoRefresh(), Equals, false) +} + +func (s *deviceMgrSuite) TestCanAutoRefreshNoSerialFallback(c *C) { + s.state.Lock() + defer s.state.Unlock() + + canAutoRefresh := func() bool { + ok, err := devicestate.CanAutoRefresh(s.state) + c.Assert(err, IsNil) + return ok + } + + // seeded, model, no serial, two attempts at getting serial + // -> no auto-refresh + devicestate.IncEnsureOperationalAttempts(s.state) + devicestate.IncEnsureOperationalAttempts(s.state) + s.state.Set("seeded", true) + auth.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + }) + s.makeModelAssertionInState(c, "canonical", "pc", map[string]string{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + }) + c.Check(canAutoRefresh(), Equals, false) + + // third attempt ongoing, or done + // fallback, try auto-refresh + devicestate.IncEnsureOperationalAttempts(s.state) + // sanity + c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 3) + c.Check(canAutoRefresh(), Equals, true) +} + +func (s *deviceMgrSuite) TestCanAutoRefreshOnClassic(c *C) { + release.OnClassic = true + + s.state.Lock() + defer s.state.Unlock() + + canAutoRefresh := func() bool { + ok, err := devicestate.CanAutoRefresh(s.state) + c.Assert(err, IsNil) + return ok + } + + // not seeded, no model, no serial -> no auto-refresh + s.state.Set("seeded", false) + c.Check(canAutoRefresh(), Equals, false) + + // seeded, no model -> auto-refresh + s.state.Set("seeded", true) + c.Check(canAutoRefresh(), Equals, true) + + // seeded, model, no serial -> no auto-refresh + auth.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + }) + s.makeModelAssertionInState(c, "canonical", "pc", map[string]string{ + "classic": "true", + }) + c.Check(canAutoRefresh(), Equals, false) + + // seeded, model, serial -> auto-refresh + auth.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + Serial: "8989", + }) + s.makeSerialAssertionInState(c, "canonical", "pc", "8989") + c.Check(canAutoRefresh(), Equals, true) + + // not seeded, model, serial -> no auto-refresh + s.state.Set("seeded", false) + c.Check(canAutoRefresh(), Equals, false) } diff -Nru snapd-2.22.6+16.10/overlord/devicestate/export_test.go snapd-2.23.1+16.10/overlord/devicestate/export_test.go --- snapd-2.22.6+16.10/overlord/devicestate/export_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/devicestate/export_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -62,6 +62,18 @@ return m.keypairMgr } +func (m *DeviceManager) EnsureOperationalShouldBackoff(now time.Time) bool { + return m.ensureOperationalShouldBackoff(now) +} + +func (m *DeviceManager) BecomeOperationalBackoff() time.Duration { + return m.becomeOperationalBackoff +} + +func (m *DeviceManager) SetLastBecomeOperationalAttempt(t time.Time) { + m.lastBecomeOperationalAttempt = t +} + func MockRepeatRequestSerial(label string) (restore func()) { old := repeatRequestSerial repeatRequestSerial = label @@ -95,4 +107,8 @@ var ( ImportAssertionsFromSeed = importAssertionsFromSeed CheckGadgetOrKernel = checkGadgetOrKernel + CanAutoRefresh = canAutoRefresh + + IncEnsureOperationalAttempts = incEnsureOperationalAttempts + EnsureOperationalAttempts = ensureOperationalAttempts ) diff -Nru snapd-2.22.6+16.10/overlord/devicestate/firstboot.go snapd-2.23.1+16.10/overlord/devicestate/firstboot.go --- snapd-2.22.6+16.10/overlord/devicestate/firstboot.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/devicestate/firstboot.go 2017-03-02 06:37:54.000000000 +0000 @@ -20,6 +20,7 @@ package devicestate import ( + "errors" "fmt" "io/ioutil" "os" @@ -29,13 +30,17 @@ "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n/dumb" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" ) +var errNothingToDo = errors.New("nothing to do") + func populateStateFromSeedImpl(st *state.State) ([]*state.TaskSet, error) { // check that the state is empty var seeded bool @@ -47,13 +52,24 @@ return nil, fmt.Errorf("cannot populate state: already seeded") } + markSeeded := st.NewTask("mark-seeded", i18n.G("Mark system seeded")) + // ack all initial assertions model, err := importAssertionsFromSeed(st) + if err == errNothingToDo { + return []*state.TaskSet{state.NewTaskSet(markSeeded)}, nil + } if err != nil { return nil, err } - seed, err := snap.ReadSeedYaml(filepath.Join(dirs.SnapSeedDir, "seed.yaml")) + seedYamlFile := filepath.Join(dirs.SnapSeedDir, "seed.yaml") + if release.OnClassic && !osutil.FileExists(seedYamlFile) { + // on classic it is ok to not seed any snaps + return []*state.TaskSet{state.NewTaskSet(markSeeded)}, nil + } + + seed, err := snap.ReadSeedYaml(seedYamlFile) if err != nil { return nil, err } @@ -92,6 +108,7 @@ } sideInfo = *si sideInfo.Private = sn.Private + sideInfo.Contact = sn.Contact } ts, err := snapstate.InstallPath(st, &sideInfo, path, sn.Channel, flags) @@ -106,11 +123,10 @@ tsAll = append(tsAll, ts) } if len(tsAll) == 0 { - return nil, nil + return nil, fmt.Errorf("cannot proceed, no snaps to seed") } ts := tsAll[len(tsAll)-1] - markSeeded := st.NewTask("mark-seeded", i18n.G("Mark system seeded")) markSeeded.WaitAll(ts) tsAll = append(tsAll, state.NewTaskSet(markSeeded)) @@ -135,6 +151,10 @@ // set device,model from the model assertion assertSeedDir := filepath.Join(dirs.SnapSeedDir, "assertions") dc, err := ioutil.ReadDir(assertSeedDir) + if release.OnClassic && os.IsNotExist(err) { + // on classic seeding is optional + return nil, errNothingToDo + } if err != nil { return nil, fmt.Errorf("cannot read assert seed dir: %s", err) } @@ -172,6 +192,17 @@ } modelAssertion := a.(*asserts.Model) + classicModel := modelAssertion.Classic() + if release.OnClassic != classicModel { + var msg string + if classicModel { + msg = "cannot seed an all-snaps system with a classic model" + } else { + msg = "cannot seed a classic system with an all-snaps model" + } + return nil, fmt.Errorf(msg) + } + // set device,model from the model assertion device.Brand = modelAssertion.BrandID() device.Model = modelAssertion.Model() diff -Nru snapd-2.22.6+16.10/overlord/devicestate/firstboot_test.go snapd-2.23.1+16.10/overlord/devicestate/firstboot_test.go --- snapd-2.22.6+16.10/overlord/devicestate/firstboot_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/devicestate/firstboot_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -25,6 +25,7 @@ "os" "path/filepath" "strconv" + "strings" "time" . "gopkg.in/check.v1" @@ -40,6 +41,7 @@ "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" @@ -56,6 +58,8 @@ brandSigning *assertstest.SigningDB overlord *overlord.Overlord + + restoreOnClassic func() } var _ = Suite(&FirstBootTestSuite{}) @@ -63,6 +67,7 @@ func (s *FirstBootTestSuite) SetUpTest(c *C) { tempdir := c.MkDir() dirs.SetRootDir(tempdir) + s.restoreOnClassic = release.MockOnClassic(false) // mock the world! err := os.MkdirAll(filepath.Join(dirs.SnapSeedDir, "snaps"), 0755) @@ -93,12 +98,67 @@ } func (s *FirstBootTestSuite) TearDownTest(c *C) { - dirs.SetRootDir("/") os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS") s.systemctl.Restore() s.mockUdevAdm.Restore() s.restore() + s.restoreOnClassic() + dirs.SetRootDir("/") +} + +func (s *FirstBootTestSuite) TestPopulateFromSeedOnClassicNoop(c *C) { + release.OnClassic = true + + st := s.overlord.State() + st.Lock() + defer st.Unlock() + + err := os.Remove(filepath.Join(dirs.SnapSeedDir, "assertions")) + c.Assert(err, IsNil) + + tsAll, err := devicestate.PopulateStateFromSeedImpl(st) + c.Assert(err, IsNil) + // only mark seeded + c.Check(tsAll, HasLen, 1) + tasks := tsAll[0].Tasks() + c.Check(tasks, HasLen, 1) + c.Check(tasks[0].Kind(), Equals, "mark-seeded") +} + +func (s *FirstBootTestSuite) TestPopulateFromSeedOnClassicNoSeedYaml(c *C) { + release.OnClassic = true + + ovld, err := overlord.New() + c.Assert(err, IsNil) + st := ovld.State() + + // add a bunch of assert files + assertsChain := s.makeModelAssertionChain(c, "my-model-classic") + for i, as := range assertsChain { + fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i)) + err := ioutil.WriteFile(fn, asserts.Encode(as), 0644) + c.Assert(err, IsNil) + } + + err = os.Remove(filepath.Join(dirs.SnapSeedDir, "seed.yaml")) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + tsAll, err := devicestate.PopulateStateFromSeedImpl(st) + c.Assert(err, IsNil) + // only mark seeded + c.Check(tsAll, HasLen, 1) + tasks := tsAll[0].Tasks() + c.Check(tasks, HasLen, 1) + c.Check(tasks[0].Kind(), Equals, "mark-seeded") + + ds, err := auth.Device(st) + c.Assert(err, IsNil) + c.Check(ds.Brand, Equals, "my-brand") + c.Check(ds.Model, Equals, "my-model-classic") } func (s *FirstBootTestSuite) TestPopulateFromSeedErrorsOnState(c *C) { @@ -164,7 +224,7 @@ c.Assert(err, IsNil) // add a model assertion and its chain - assertsChain := s.makeModelAssertionChain(c, "foo") + assertsChain := s.makeModelAssertionChain(c, "my-model", "foo") for i, as := range assertsChain { fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i)) err := ioutil.WriteFile(fn, asserts.Encode(as), 0644) @@ -177,6 +237,7 @@ - name: foo file: %s devmode: true + contact: mailto:some.guy@example.com - name: local unasserted: true file: %s @@ -200,7 +261,8 @@ c.Check(markSeededTask.WaitTasks(), testutil.Contains, otherTask) // now run the change and check the result - chg := st.NewChange("run-it", "run the populate from seed changes") + // use the expected kind otherwise settle with start another one + chg := st.NewChange("seed", "run the populate from seed changes") for _, ts := range tsAll { chg.AddAll(ts) } @@ -229,6 +291,7 @@ c.Assert(err, IsNil) c.Assert(info.SnapID, Equals, "snapidsnapid") c.Assert(info.Revision, Equals, snap.R(128)) + c.Assert(info.Contact, Equals, "mailto:some.guy@example.com") pubAcct, err := assertstate.Publisher(st, info.SnapID) c.Assert(err, IsNil) c.Check(pubAcct.AccountID(), Equals, "developerid") @@ -343,7 +406,7 @@ writeAssertionsToFile("bar.asserts", []asserts.Assertion{devAcct, snapDeclBar, snapRevBar}) // add a model assertion and its chain - assertsChain := s.makeModelAssertionChain(c) + assertsChain := s.makeModelAssertionChain(c, "my-model") writeAssertionsToFile("model.asserts", assertsChain) // create a seed.yaml @@ -364,7 +427,8 @@ tsAll, err := devicestate.PopulateStateFromSeedImpl(st) c.Assert(err, IsNil) - chg := st.NewChange("run-it", "run the populate from seed changes") + // use the expected kind otherwise settle with start another one + chg := st.NewChange("seed", "run the populate from seed changes") for _, ts := range tsAll { chg.AddAll(ts) } @@ -417,9 +481,13 @@ "architecture": "amd64", "store": "canonical", "gadget": "pc", - "kernel": "pc-kernel", "timestamp": time.Now().Format(time.RFC3339), } + if strings.HasSuffix(modelStr, "-classic") { + headers["classic"] = "true" + } else { + headers["kernel"] = "pc-kernel" + } if len(reqSnaps) != 0 { reqs := make([]interface{}, len(reqSnaps)) for i, req := range reqSnaps { @@ -432,7 +500,7 @@ return model.(*asserts.Model) } -func (s *FirstBootTestSuite) makeModelAssertionChain(c *C, reqSnaps ...string) []asserts.Assertion { +func (s *FirstBootTestSuite) makeModelAssertionChain(c *C, modName string, reqSnaps ...string) []asserts.Assertion { assertChain := []asserts.Assertion{} brandAcct := assertstest.NewAccount(s.storeSigning, "my-brand", map[string]interface{}{ @@ -444,7 +512,7 @@ brandAccKey := assertstest.NewAccountKey(s.storeSigning, brandAcct, nil, s.brandPrivKey.PublicKey(), "") assertChain = append(assertChain, brandAccKey) - model := s.makeModelAssertion(c, "my-model", reqSnaps...) + model := s.makeModelAssertion(c, modName, reqSnaps...) assertChain = append(assertChain, model) storeAccountKey := s.storeSigning.StoreAccountKey("") @@ -452,13 +520,57 @@ return assertChain } +func (s *FirstBootTestSuite) TestImportAssertionsFromSeedClassicModelMismatch(c *C) { + release.OnClassic = true + + ovld, err := overlord.New() + c.Assert(err, IsNil) + st := ovld.State() + + // add a bunch of assert files + assertsChain := s.makeModelAssertionChain(c, "my-model") + for i, as := range assertsChain { + fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i)) + err := ioutil.WriteFile(fn, asserts.Encode(as), 0644) + c.Assert(err, IsNil) + } + + // import them + st.Lock() + defer st.Unlock() + + _, err = devicestate.ImportAssertionsFromSeed(st) + c.Assert(err, ErrorMatches, "cannot seed a classic system with an all-snaps model") +} + +func (s *FirstBootTestSuite) TestImportAssertionsFromSeedAllSnapsModelMismatch(c *C) { + ovld, err := overlord.New() + c.Assert(err, IsNil) + st := ovld.State() + + // add a bunch of assert files + assertsChain := s.makeModelAssertionChain(c, "my-model-classic") + for i, as := range assertsChain { + fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i)) + err := ioutil.WriteFile(fn, asserts.Encode(as), 0644) + c.Assert(err, IsNil) + } + + // import them + st.Lock() + defer st.Unlock() + + _, err = devicestate.ImportAssertionsFromSeed(st) + c.Assert(err, ErrorMatches, "cannot seed an all-snaps system with a classic model") +} + func (s *FirstBootTestSuite) TestImportAssertionsFromSeedHappy(c *C) { ovld, err := overlord.New() c.Assert(err, IsNil) st := ovld.State() // add a bunch of assert files - assertsChain := s.makeModelAssertionChain(c) + assertsChain := s.makeModelAssertionChain(c, "my-model") for i, as := range assertsChain { fn := filepath.Join(dirs.SnapSeedDir, "assertions", strconv.Itoa(i)) err := ioutil.WriteFile(fn, asserts.Encode(as), 0644) @@ -499,7 +611,7 @@ defer st.Unlock() // write out only the model assertion - assertsChain := s.makeModelAssertionChain(c) + assertsChain := s.makeModelAssertionChain(c, "my-model") for _, as := range assertsChain { if as.Type() == asserts.ModelType { fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model") @@ -542,7 +654,7 @@ st.Lock() defer st.Unlock() - assertsChain := s.makeModelAssertionChain(c) + assertsChain := s.makeModelAssertionChain(c, "my-model") for _, as := range assertsChain { if as.Type() != asserts.ModelType { fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model") diff -Nru snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/export_test.go snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/export_test.go --- snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/export_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/export_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -21,6 +21,9 @@ import "fmt" +var AttributesTask = attributesTask +var CopyAttributes = copyAttributes + func AddMockCommand(name string) *MockCommand { mockCommand := NewMockCommand() addCommand(name, "", "", func() command { return mockCommand }) diff -Nru snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/get.go snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/get.go --- snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/get.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/get.go 2017-03-02 06:37:54.000000000 +0000 @@ -22,23 +22,33 @@ import ( "encoding/json" "fmt" + "strings" "github.com/snapcore/snapd/i18n/dumb" + "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/overlord/configstate" + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/hookstate" + "github.com/snapcore/snapd/overlord/state" ) type getCommand struct { baseCommand + // these two options are mutually exclusive + ForceSlotSide bool `long:"slot" description:"return attribute values from the slot side of the connection"` + ForcePlugSide bool `long:"plug" description:"return attribute values from the plug side of the connection"` + Positional struct { - Keys []string `positional-arg-name:"" description:"option keys" required:"yes"` - } `positional-args:"yes" required:"yes"` + PlugOrSlotSpec string `positional-args:"true" positional-arg-name:":"` + Keys []string `positional-arg-name:"" description:"option keys"` + } `positional-args:"yes"` Document bool `short:"d" description:"always return document, even with single key"` Typed bool `short:"t" description:"strict typing with nulls and quoted strings"` } -var shortGetHelp = i18n.G("Prints configuration options") +var shortGetHelp = i18n.G("The get command prints configuration and interface connection settings.") var longGetHelp = i18n.G(` The get command prints configuration options for the current snap. @@ -57,36 +67,35 @@ $ snapctl get author.name frank + +Values of interface connection settings may be printed with: + + $ snapctl get :myplug usb-vendor + $ snapctl get :myslot path + +This will return the named setting from the local interface endpoint, whether a plug +or a slot. Returning the setting from the connected snap's endpoint is also possible +by explicitly requesting that via the --plug and --slot command line options: + + $ snapctl get :myplug --slot usb-vendor + +This requests the "usb-vendor" setting from the slot that is connected to "myplug". `) func init() { - addCommand("get", shortGetHelp, longGetHelp, func() command { return &getCommand{} }) + addCommand("get", shortGetHelp, longGetHelp, func() command { + return &getCommand{} + }) } -func (c *getCommand) Execute(args []string) error { - context := c.context() - if context == nil { - return fmt.Errorf("cannot get without a context") - } - - if c.Typed && c.Document { - return fmt.Errorf("cannot use -d and -t together") - } - +func (c *getCommand) printValues(getByKey func(string) (interface{}, bool, error)) error { patch := make(map[string]interface{}) - context.Lock() - transaction := configstate.ContextTransaction(context) - context.Unlock() - for _, key := range c.Positional.Keys { - var value interface{} - err := transaction.Get(c.context().SnapName(), key, &value) + value, output, err := getByKey(key) if err == nil { - patch[key] = value - } else if configstate.IsNoOption(err) { - if !c.Typed { - value = "" - } + if output { + patch[key] = value + } // else skip this value } else { return err } @@ -120,3 +129,177 @@ return nil } + +func (c *getCommand) Execute(args []string) error { + if c.Positional.PlugOrSlotSpec == "" && len(c.Positional.Keys) == 0 { + return fmt.Errorf(i18n.G("get which option?")) + } + + context := c.context() + if context == nil { + return fmt.Errorf("cannot get without a context") + } + + if c.Typed && c.Document { + return fmt.Errorf("cannot use -d and -t together") + } + + if strings.Contains(c.Positional.PlugOrSlotSpec, ":") { + parts := strings.SplitN(c.Positional.PlugOrSlotSpec, ":", 2) + snap, name := parts[0], parts[1] + if name == "" { + return fmt.Errorf("plug or slot name not provided") + } + if snap != "" { + return fmt.Errorf(`"snapctl get %s" not supported, use "snapctl get :%s" instead`, c.Positional.PlugOrSlotSpec, parts[1]) + } + + return c.getInterfaceSetting(context, name) + } + + // PlugOrSlotSpec is actually a configuration key. + c.Positional.Keys = append([]string{c.Positional.PlugOrSlotSpec}, c.Positional.Keys[0:]...) + c.Positional.PlugOrSlotSpec = "" + + return c.getConfigSetting(context) +} + +func (c *getCommand) getConfigSetting(context *hookstate.Context) error { + if c.ForcePlugSide || c.ForceSlotSide { + return fmt.Errorf("cannot use --plug or --slot without : argument") + } + + context.Lock() + transaction := configstate.ContextTransaction(context) + context.Unlock() + + return c.printValues(func(key string) (interface{}, bool, error) { + var value interface{} + err := transaction.Get(c.context().SnapName(), key, &value) + if err == nil { + return value, true, nil + } + if config.IsNoOption(err) { + if !c.Typed { + value = "" + } + return value, false, nil + } + return value, false, err + }) +} + +type ifaceHookType int + +const ( + preparePlugHook ifaceHookType = iota + prepareSlotHook + connectPlugHook + connectSlotHook + unknownHook +) + +func interfaceHookType(hookName string) (ifaceHookType, error) { + if strings.HasPrefix(hookName, "prepare-plug-") { + return preparePlugHook, nil + } else if strings.HasPrefix(hookName, "connect-plug-") { + return connectPlugHook, nil + } else if strings.HasPrefix(hookName, "prepare-slot-") { + return prepareSlotHook, nil + } else if strings.HasPrefix(hookName, "connect-slot-") { + return connectSlotHook, nil + } + return unknownHook, fmt.Errorf("unknown hook type") +} + +func validatePlugOrSlot(attrsTask *state.Task, plugSide bool, plugOrSlot string) error { + // check if the requested plug or slot is correct for given hook. + attrsTask.State().Lock() + defer attrsTask.State().Unlock() + + var name string + var err error + if plugSide { + var plugRef interfaces.PlugRef + if err = attrsTask.Get("plug", &plugRef); err == nil { + name = plugRef.Name + } + } else { + var slotRef interfaces.SlotRef + if err = attrsTask.Get("slot", &slotRef); err == nil { + name = slotRef.Name + } + } + if err != nil { + return fmt.Errorf(i18n.G("internal error: cannot find plug or slot data in the appropriate task")) + } + if name != plugOrSlot { + return fmt.Errorf(i18n.G("unknown plug or slot %q"), plugOrSlot) + } + return nil +} + +func attributesTask(context *hookstate.Context) (*state.Task, error) { + var attrsTaskID string + context.Lock() + defer context.Unlock() + + if err := context.Get("attrs-task", &attrsTaskID); err != nil { + return nil, err + } + + st := context.State() + + attrsTask := st.Task(attrsTaskID) + if attrsTask == nil { + return nil, fmt.Errorf(i18n.G("internal error: cannot find attrs task")) + } + + return attrsTask, nil +} + +func (c *getCommand) getInterfaceSetting(context *hookstate.Context, plugOrSlot string) error { + // Make sure get : is only supported during the execution of interface hooks + hookType, err := interfaceHookType(context.HookName()) + if err != nil { + return fmt.Errorf(i18n.G("interface attributes can only be read during the execution of interface hooks")) + } + + var attrsTask *state.Task + attrsTask, err = attributesTask(context) + if err != nil { + return err + } + + if c.ForcePlugSide && c.ForceSlotSide { + return fmt.Errorf("cannot use --plug and --slot together") + } + + isPlugSide := (hookType == preparePlugHook || hookType == connectPlugHook) + if err = validatePlugOrSlot(attrsTask, isPlugSide, plugOrSlot); err != nil { + return err + } + + var which string + if c.ForcePlugSide || (isPlugSide && !c.ForceSlotSide) { + which = "plug-attrs" + } else { + which = "slot-attrs" + } + + st := context.State() + st.Lock() + defer st.Unlock() + + attributes := make(map[string]interface{}) + if err = attrsTask.Get(which, &attributes); err != nil { + return fmt.Errorf(i18n.G("internal error: cannot get %s from appropriate task"), which) + } + + return c.printValues(func(key string) (interface{}, bool, error) { + if value, ok := attributes[key]; ok { + return value, true, nil + } + return nil, false, fmt.Errorf(i18n.G("unknown attribute %q"), key) + }) +} diff -Nru snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/get_test.go snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/get_test.go --- snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/get_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/get_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -20,15 +20,17 @@ package ctlcmd_test import ( - "github.com/snapcore/snapd/overlord/configstate" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" "github.com/snapcore/snapd/overlord/hookstate/hooktest" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" - . "gopkg.in/check.v1" "strings" + + . "gopkg.in/check.v1" ) type getSuite struct { @@ -36,8 +38,16 @@ mockHandler *hooktest.MockHandler } +type getAttrSuite struct { + mockPlugHookContext *hookstate.Context + mockSlotHookContext *hookstate.Context + mockHandler *hooktest.MockHandler +} + var _ = Suite(&getSuite{}) +var _ = Suite(&getAttrSuite{}) + func (s *getSuite) SetUpTest(c *C) { s.mockHandler = hooktest.NewMockHandler() @@ -53,17 +63,29 @@ c.Assert(err, IsNil) // Initialize configuration - transaction := configstate.NewTransaction(state) - transaction.Set("test-snap", "initial-key", "initial-value") - transaction.Commit() + tr := config.NewTransaction(state) + tr.Set("test-snap", "initial-key", "initial-value") + tr.Commit() } var getTests = []struct { args, stdout, error string }{{ + args: "get", + error: ".*get which option.*", +}, { + args: "get --plug key", + error: "cannot use --plug or --slot without : argument", +}, { + args: "get --slot key", + error: "cannot use --plug or --slot without : argument", +}, { args: "get --foo", error: ".*unknown flag.*foo.*", }, { + args: "get :foo bar", + error: ".*interface attributes can only be read during the execution of interface hooks.*", +}, { args: "get test-key1", stdout: "test-value1\n", }, { @@ -106,10 +128,10 @@ c.Check(err, IsNil) // Initialize configuration - t := configstate.NewTransaction(state) - t.Set("test-snap", "test-key1", "test-value1") - t.Set("test-snap", "test-key2", 2) - t.Commit() + tr := config.NewTransaction(state) + tr.Set("test-snap", "test-key1", "test-value1") + tr.Set("test-snap", "test-key2", 2) + tr.Commit() state.Unlock() @@ -128,3 +150,149 @@ _, _, err := ctlcmd.Run(nil, []string{"get", "foo"}) c.Check(err, ErrorMatches, ".*cannot get without a context.*") } + +func (s *getAttrSuite) SetUpTest(c *C) { + s.mockHandler = hooktest.NewMockHandler() + + state := state.New(nil) + state.Lock() + ch := state.NewChange("mychange", "mychange") + + attrsTask := state.NewTask("connect-task", "my connect task") + attrsTask.Set("plug", &interfaces.PlugRef{Snap: "a", Name: "aplug"}) + attrsTask.Set("slot", &interfaces.SlotRef{Snap: "b", Name: "bslot"}) + plugAttrs := make(map[string]interface{}) + slotAttrs := make(map[string]interface{}) + plugAttrs["aattr"] = "foo" + plugAttrs["baz"] = []string{"a", "b"} + slotAttrs["battr"] = "bar" + attrsTask.Set("plug-attrs", plugAttrs) + attrsTask.Set("slot-attrs", slotAttrs) + ch.AddTask(attrsTask) + state.Unlock() + + var err error + + // setup plug hook task + state.Lock() + plugHookTask := state.NewTask("run-hook", "my test task") + state.Unlock() + plugTaskSetup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "connect-plug-aplug"} + s.mockPlugHookContext, err = hookstate.NewContext(plugHookTask, plugTaskSetup, s.mockHandler) + c.Assert(err, IsNil) + + s.mockPlugHookContext.Lock() + s.mockPlugHookContext.Set("attrs-task", attrsTask.ID()) + s.mockPlugHookContext.Unlock() + state.Lock() + ch.AddTask(plugHookTask) + state.Unlock() + + // setup slot hook task + state.Lock() + slotHookTask := state.NewTask("run-hook", "my test task") + state.Unlock() + slotTaskSetup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "connect-slot-aplug"} + s.mockSlotHookContext, err = hookstate.NewContext(slotHookTask, slotTaskSetup, s.mockHandler) + c.Assert(err, IsNil) + + s.mockSlotHookContext.Lock() + s.mockSlotHookContext.Set("attrs-task", attrsTask.ID()) + s.mockSlotHookContext.Unlock() + + state.Lock() + defer state.Unlock() + ch.AddTask(slotHookTask) +} + +func (s *getAttrSuite) TestGetPlugAttributesInPlugHook(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"get", ":aplug", "aattr"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "foo\n") + c.Check(string(stderr), Equals, "") + + stdout, stderr, err = ctlcmd.Run(s.mockPlugHookContext, []string{"get", "-d", ":aplug", "baz"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "{\n\t\"baz\": [\n\t\t\"a\",\n\t\t\"b\"\n\t]\n}\n") + c.Check(string(stderr), Equals, "") + + // The --plug parameter doesn't do anything if used on plug side + stdout, stderr, err = ctlcmd.Run(s.mockPlugHookContext, []string{"get", "--plug", ":aplug", "aattr"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "foo\n") + c.Check(string(stderr), Equals, "") +} + +func (s *getAttrSuite) TestGetSlotAttributesInSlotHook(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockSlotHookContext, []string{"get", ":bslot", "battr"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "bar\n") + c.Check(string(stderr), Equals, "") + + // The --slot parameter doesn't do anything if used on slot side + stdout, stderr, err = ctlcmd.Run(s.mockSlotHookContext, []string{"get", "--slot", ":bslot", "battr"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "bar\n") + c.Check(string(stderr), Equals, "") +} + +func (s *getAttrSuite) TestGetSlotAttributeInPlugHook(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"get", "--slot", ":aplug", "battr"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "bar\n") + c.Check(string(stderr), Equals, "") +} + +func (s *getAttrSuite) TestGetPlugAttributeInSlotHook(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockSlotHookContext, []string{"get", "--plug", ":bslot", "aattr"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "foo\n") + c.Check(string(stderr), Equals, "") +} + +func (s *getAttrSuite) TestUnknownPlugAttribute(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"get", ":aplug", "x"}) + c.Check(err, NotNil) + c.Check(err.Error(), Equals, `unknown attribute "x"`) + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") +} + +func (s *getAttrSuite) TestUnknownSlotAttribute(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockSlotHookContext, []string{"get", ":bslot", "x"}) + c.Check(err, NotNil) + c.Check(err.Error(), Equals, `unknown attribute "x"`) + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") +} + +func (s *getAttrSuite) TestUsingPlugNameInSlotHookFails(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockSlotHookContext, []string{"get", ":aplug", "x"}) + c.Check(err, NotNil) + c.Check(err.Error(), Equals, `unknown plug or slot "aplug"`) + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") +} + +func (s *getAttrSuite) TestUsingSlotNameInPlugHookFails(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"get", ":bslot", "x"}) + c.Check(err, NotNil) + c.Check(err.Error(), Equals, `unknown plug or slot "bslot"`) + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") +} + +func (s *getAttrSuite) TestForcePlugOrSlotMutuallyExclusive(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockSlotHookContext, []string{"get", "--slot", "--plug", ":aplug", "x"}) + c.Check(err, NotNil) + c.Check(err.Error(), Equals, `cannot use --plug and --slot together`) + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") +} + +func (s *getAttrSuite) TestPlugOrSlotEmpty(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"get", ":", "foo"}) + c.Check(err.Error(), Equals, "plug or slot name not provided") + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") +} diff -Nru snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/set.go snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/set.go --- snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/set.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/set.go 2017-03-06 13:33:50.000000000 +0000 @@ -26,14 +26,16 @@ "github.com/snapcore/snapd/i18n/dumb" "github.com/snapcore/snapd/overlord/configstate" + "github.com/snapcore/snapd/overlord/hookstate" ) type setCommand struct { baseCommand Positional struct { - ConfValues []string `positional-arg-name:"key=value" required:"1"` - } `positional-args:"yes" required:"yes"` + PlugOrSlotSpec string `positional-arg-name:":"` + ConfValues []string `positional-arg-name:"key=value"` + } `positional-args:"yes"` } var shortSetHelp = i18n.G("Changes configuration options") @@ -48,6 +50,11 @@ Nested values may be modified via a dotted path: $ snapctl set author.name=frank + +Plug and slot attributes may be set in the respective prepare and connect hooks by +naming the respective plug or slot: + + $ snapctl set :myplug path=/dev/ttyS0 `) func init() { @@ -55,13 +62,37 @@ } func (s *setCommand) Execute(args []string) error { + if s.Positional.PlugOrSlotSpec == "" && len(s.Positional.ConfValues) == 0 { + return fmt.Errorf(i18n.G("set which option?")) + } + context := s.context() if context == nil { return fmt.Errorf("cannot set without a context") } + // treat PlugOrSlotSpec argument as key=value if it contans '=' or doesn't contain ':' - this is to support + // values such as "device-service.url=192.168.0.1:5555" and error out on invalid key=value if only "key" is given. + if strings.Contains(s.Positional.PlugOrSlotSpec, "=") || !strings.Contains(s.Positional.PlugOrSlotSpec, ":") { + s.Positional.ConfValues = append([]string{s.Positional.PlugOrSlotSpec}, s.Positional.ConfValues[0:]...) + s.Positional.PlugOrSlotSpec = "" + return s.setConfigSetting(context) + } + + parts := strings.SplitN(s.Positional.PlugOrSlotSpec, ":", 2) + snap, name := parts[0], parts[1] + if name == "" { + return fmt.Errorf("plug or slot name not provided") + } + if snap != "" { + return fmt.Errorf(`"snapctl set %s" not supported, use "snapctl set :%s" instead`, s.Positional.PlugOrSlotSpec, parts[1]) + } + return s.setInterfaceSetting(context, name) +} + +func (s *setCommand) setConfigSetting(context *hookstate.Context) error { context.Lock() - transaction := configstate.ContextTransaction(context) + tr := configstate.ContextTransaction(context) context.Unlock() for _, patchValue := range s.Positional.ConfValues { @@ -77,8 +108,103 @@ value = parts[1] } - transaction.Set(s.context().SnapName(), key, value) + tr.Set(s.context().SnapName(), key, value) } return nil } + +func (s *setCommand) setInterfaceSetting(context *hookstate.Context, plugOrSlot string) error { + // Make sure set : is only supported during the execution of prepare-[plug|slot] hooks + hookType, _ := interfaceHookType(context.HookName()) + if hookType != preparePlugHook && hookType != prepareSlotHook { + return fmt.Errorf(i18n.G("interface attributes can only be set during the execution of prepare hooks")) + } + + attrsTask, err := attributesTask(context) + if err != nil { + return err + } + + // check if the requested plug or slot is correct for this hook. + if err := validatePlugOrSlot(attrsTask, hookType == preparePlugHook, plugOrSlot); err != nil { + return err + } + + var which string + if hookType == preparePlugHook { + which = "plug-attrs" + } else { + which = "slot-attrs" + } + + st := context.State() + st.Lock() + defer st.Unlock() + + attributes := make(map[string]interface{}) + if err := attrsTask.Get(which, &attributes); err != nil { + return fmt.Errorf(i18n.G("internal error: cannot get %s from appropriate task"), which) + } + + for _, attrValue := range s.Positional.ConfValues { + parts := strings.SplitN(attrValue, "=", 2) + if len(parts) != 2 { + return fmt.Errorf(i18n.G("invalid parameter: %q (want key=value)"), attrValue) + } + + var value interface{} + err := json.Unmarshal([]byte(parts[1]), &value) + if err != nil { + // Not valid JSON, save the string as-is + value = parts[1] + } + attributes[parts[0]] = value + } + + attrsTask.Set(which, attributes) + return nil +} + +func copyAttributes(value map[string]interface{}) (map[string]interface{}, error) { + cpy, err := copyRecursive(value) + if err != nil { + return nil, err + } + return cpy.(map[string]interface{}), err +} + +func copyRecursive(value interface{}) (interface{}, error) { + switch v := value.(type) { + case string: + return v, nil + case bool: + return v, nil + case int: + return int64(v), nil + case int64: + return v, nil + case []interface{}: + arr := make([]interface{}, len(v)) + for i, el := range v { + tmp, err := copyRecursive(el) + if err != nil { + return nil, err + } + arr[i] = tmp + } + return arr, nil + case map[string]interface{}: + mp := make(map[string]interface{}, len(v)) + for key, item := range v { + tmp, err := copyRecursive(item) + if err != nil { + return nil, err + } + mp[key] = tmp + } + return mp, nil + default: + return nil, fmt.Errorf("unsupported attribute type '%T', value '%v'", value, value) + } +} diff -Nru snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/set_test.go snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/set_test.go --- snapd-2.22.6+16.10/overlord/hookstate/ctlcmd/set_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/hookstate/ctlcmd/set_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -20,7 +20,10 @@ package ctlcmd_test import ( - "github.com/snapcore/snapd/overlord/configstate" + "reflect" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" "github.com/snapcore/snapd/overlord/hookstate/hooktest" @@ -35,7 +38,14 @@ mockHandler *hooktest.MockHandler } +type setAttrSuite struct { + mockPlugHookContext *hookstate.Context + mockSlotHookContext *hookstate.Context + mockHandler *hooktest.MockHandler +} + var _ = Suite(&setSuite{}) +var _ = Suite(&setAttrSuite{}) func (s *setSuite) SetUpTest(c *C) { s.mockHandler = hooktest.NewMockHandler() @@ -53,8 +63,12 @@ } func (s *setSuite) TestInvalidArguments(c *C) { - _, _, err := ctlcmd.Run(s.mockContext, []string{"set", "foo", "bar"}) + _, _, err := ctlcmd.Run(s.mockContext, []string{"set"}) + c.Check(err, ErrorMatches, "set which option.*") + _, _, err = ctlcmd.Run(s.mockContext, []string{"set", "foo", "bar"}) c.Check(err, ErrorMatches, ".*invalid parameter.*want key=value.*") + _, _, err = ctlcmd.Run(s.mockContext, []string{"set", ":foo", "bar=baz"}) + c.Check(err, ErrorMatches, ".*interface attributes can only be set during the execution of prepare hooks.*") } func (s *setSuite) TestCommand(c *C) { @@ -65,11 +79,11 @@ // Verify that the previous set doesn't modify the global state s.mockContext.State().Lock() - transaction := configstate.NewTransaction(s.mockContext.State()) + tr := config.NewTransaction(s.mockContext.State()) s.mockContext.State().Unlock() var value string - c.Check(transaction.Get("test-snap", "foo", &value), ErrorMatches, ".*snap.*has no.*configuration.*") - c.Check(transaction.Get("test-snap", "baz", &value), ErrorMatches, ".*snap.*has no.*configuration.*") + c.Check(tr.Get("test-snap", "foo", &value), ErrorMatches, ".*snap.*has no.*configuration.*") + c.Check(tr.Get("test-snap", "baz", &value), ErrorMatches, ".*snap.*has no.*configuration.*") // Notify the context that we're done. This should save the config. s.mockContext.Lock() @@ -77,20 +91,38 @@ c.Check(s.mockContext.Done(), IsNil) // Verify that the global config has been updated. - transaction = configstate.NewTransaction(s.mockContext.State()) - c.Check(transaction.Get("test-snap", "foo", &value), IsNil) + tr = config.NewTransaction(s.mockContext.State()) + c.Check(tr.Get("test-snap", "foo", &value), IsNil) c.Check(value, Equals, "bar") - c.Check(transaction.Get("test-snap", "baz", &value), IsNil) + c.Check(tr.Get("test-snap", "baz", &value), IsNil) c.Check(value, Equals, "qux") } +func (s *setSuite) TestSetConfigOptionWithColon(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "device-service.url=192.168.0.1:5555"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") + + // Notify the context that we're done. This should save the config. + s.mockContext.Lock() + defer s.mockContext.Unlock() + c.Check(s.mockContext.Done(), IsNil) + + // Verify that the global config has been updated. + var value string + tr := config.NewTransaction(s.mockContext.State()) + c.Check(tr.Get("test-snap", "device-service.url", &value), IsNil) + c.Check(value, Equals, "192.168.0.1:5555") +} + func (s *setSuite) TestCommandSavesDeltasOnly(c *C) { // Setup an initial configuration s.mockContext.State().Lock() - transaction := configstate.NewTransaction(s.mockContext.State()) - transaction.Set("test-snap", "test-key1", "test-value1") - transaction.Set("test-snap", "test-key2", "test-value2") - transaction.Commit() + tr := config.NewTransaction(s.mockContext.State()) + tr.Set("test-snap", "test-key1", "test-value1") + tr.Set("test-snap", "test-key2", "test-value2") + tr.Commit() s.mockContext.State().Unlock() stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key2=test-value3"}) @@ -104,11 +136,11 @@ c.Check(s.mockContext.Done(), IsNil) // Verify that the global config has been updated, but only test-key2 - transaction = configstate.NewTransaction(s.mockContext.State()) + tr = config.NewTransaction(s.mockContext.State()) var value string - c.Check(transaction.Get("test-snap", "test-key1", &value), IsNil) + c.Check(tr.Get("test-snap", "test-key1", &value), IsNil) c.Check(value, Equals, "test-value1") - c.Check(transaction.Get("test-snap", "test-key2", &value), IsNil) + c.Check(tr.Get("test-snap", "test-key2", &value), IsNil) c.Check(value, Equals, "test-value3") } @@ -116,3 +148,145 @@ _, _, err := ctlcmd.Run(nil, []string{"set", "foo=bar"}) c.Check(err, ErrorMatches, ".*cannot set without a context.*") } + +func (s *setAttrSuite) SetUpTest(c *C) { + s.mockHandler = hooktest.NewMockHandler() + state := state.New(nil) + state.Lock() + ch := state.NewChange("mychange", "mychange") + + attrsTask := state.NewTask("connect-task", "my connect task") + attrsTask.Set("plug", &interfaces.PlugRef{Snap: "a", Name: "aplug"}) + attrsTask.Set("slot", &interfaces.SlotRef{Snap: "b", Name: "bslot"}) + attrs := make(map[string]interface{}) + attrsTask.Set("plug-attrs", attrs) + attrsTask.Set("slot-attrs", attrs) + ch.AddTask(attrsTask) + state.Unlock() + + var err error + + // setup plug hook task + state.Lock() + plugHookTask := state.NewTask("run-hook", "my test task") + state.Unlock() + plugTaskSetup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "prepare-plug-aplug"} + s.mockPlugHookContext, err = hookstate.NewContext(plugHookTask, plugTaskSetup, s.mockHandler) + c.Assert(err, IsNil) + + s.mockPlugHookContext.Lock() + s.mockPlugHookContext.Set("attrs-task", attrsTask.ID()) + s.mockPlugHookContext.Unlock() + state.Lock() + ch.AddTask(plugHookTask) + state.Unlock() + + // setup slot hook task + state.Lock() + slotHookTask := state.NewTask("run-hook", "my test task") + state.Unlock() + slotTaskSetup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "prepare-slot-aplug"} + s.mockSlotHookContext, err = hookstate.NewContext(slotHookTask, slotTaskSetup, s.mockHandler) + c.Assert(err, IsNil) + + s.mockSlotHookContext.Lock() + s.mockSlotHookContext.Set("attrs-task", attrsTask.ID()) + s.mockSlotHookContext.Unlock() + + state.Lock() + defer state.Unlock() + ch.AddTask(slotHookTask) +} + +func (s *setAttrSuite) TestSetPlugAttributesInPlugHook(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"set", ":aplug", "foo=bar"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") + + attrsTask, err := ctlcmd.AttributesTask(s.mockPlugHookContext) + c.Assert(err, IsNil) + st := s.mockPlugHookContext.State() + st.Lock() + defer st.Unlock() + attrs := make(map[string]interface{}) + err = attrsTask.Get("plug-attrs", &attrs) + c.Assert(err, IsNil) + c.Check(attrs["foo"], Equals, "bar") +} + +func (s *setAttrSuite) TestSetSlotAttributesInSlotHook(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockSlotHookContext, []string{"set", ":bslot", "foo=bar"}) + c.Check(err, IsNil) + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") + + attrsTask, err := ctlcmd.AttributesTask(s.mockSlotHookContext) + c.Assert(err, IsNil) + st := s.mockSlotHookContext.State() + st.Lock() + defer st.Unlock() + attrs := make(map[string]interface{}) + err = attrsTask.Get("slot-attrs", &attrs) + c.Assert(err, IsNil) + c.Check(attrs["foo"], Equals, "bar") +} + +func (s *setAttrSuite) TestPlugOrSlotEmpty(c *C) { + stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"set", ":", "foo=bar"}) + c.Check(err.Error(), Equals, "plug or slot name not provided") + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") +} + +func (s *setAttrSuite) TestSetCommandFailsOutsideOfValidContext(c *C) { + var err error + var mockContext *hookstate.Context + + state := state.New(nil) + state.Lock() + defer state.Unlock() + + task := state.NewTask("test-task", "my test task") + setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "not-a-connect-hook"} + mockContext, err = hookstate.NewContext(task, setup, s.mockHandler) + c.Assert(err, IsNil) + + stdout, stderr, err := ctlcmd.Run(mockContext, []string{"set", ":aplug", "foo=bar"}) + c.Check(err, NotNil) + c.Check(err.Error(), Equals, `interface attributes can only be set during the execution of prepare hooks`) + c.Check(string(stdout), Equals, "") + c.Check(string(stderr), Equals, "") +} + +func (s *setAttrSuite) TestCopyAttributes(c *C) { + orig := map[string]interface{}{ + "a": "A", + "b": true, + "c": int(100), + "d": []interface{}{"x", "y", true}, + "e": map[string]interface{}{ + "e1": "E1", + }, + } + + cpy, err := ctlcmd.CopyAttributes(orig) + c.Assert(err, IsNil) + // verify that int is converted into int64 + c.Check(reflect.TypeOf(cpy["c"]).Kind(), Equals, reflect.Int64) + c.Check(reflect.TypeOf(orig["c"]).Kind(), Equals, reflect.Int) + // change the type of orig's value to int64 to make DeepEquals happy in the test + orig["c"] = int64(100) + c.Check(cpy, DeepEquals, orig) + + cpy["d"].([]interface{})[0] = 999 + c.Check(orig["d"].([]interface{})[0], Equals, "x") + cpy["e"].(map[string]interface{})["e1"] = "x" + c.Check(orig["e"].(map[string]interface{})["e1"], Equals, "E1") + + type unsupported struct{} + var x unsupported + _, err = ctlcmd.CopyAttributes(map[string]interface{}{"x": x}) + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, "unsupported attribute type 'ctlcmd_test.unsupported', value '{}'") +} diff -Nru snapd-2.22.6+16.10/overlord/hookstate/export_test.go snapd-2.23.1+16.10/overlord/hookstate/export_test.go --- snapd-2.22.6+16.10/overlord/hookstate/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/hookstate/export_test.go 2017-03-08 13:28:21.000000000 +0000 @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package hookstate + +func MockReadlink(f func(string) (string, error)) func() { + oldReadlink := osReadlink + osReadlink = f + return func() { + osReadlink = oldReadlink + } +} diff -Nru snapd-2.22.6+16.10/overlord/hookstate/hookmgr.go snapd-2.23.1+16.10/overlord/hookstate/hookmgr.go --- snapd-2.22.6+16.10/overlord/hookstate/hookmgr.go 2017-01-20 09:44:59.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/hookstate/hookmgr.go 2017-03-08 13:28:21.000000000 +0000 @@ -26,11 +26,14 @@ "fmt" "os" "os/exec" + "path/filepath" "regexp" + "strings" "sync" "gopkg.in/tomb.v2" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" @@ -218,7 +221,7 @@ return handlerErr } - return err + return fmt.Errorf("run hook %q: %v", hooksup.Hook, err) } } @@ -250,8 +253,30 @@ } } +var osReadlink = os.Readlink + +// snapCmd returns the "snap" command to run. If snapd is re-execed +// it will be the snap command from the core snap, otherwise it will +// be the system "snap" command (c.f. LP: #1668738). +func snapCmd() string { + // sensible default, assume PATH is correct + snapCmd := "snap" + + exe, err := osReadlink("/proc/self/exe") + if err != nil { + return snapCmd + } + if !strings.HasPrefix(exe, dirs.SnapMountDir) { + return snapCmd + } + + // snap is running from the core snap, we know the relative + // location of "snap" from "snapd" + return filepath.Join(filepath.Dir(exe), "../../bin/snap") +} + func runHookAndWait(snapName string, revision snap.Revision, hookName, hookContext string, tomb *tomb.Tomb) ([]byte, error) { - command := exec.Command("snap", "run", "--hook", hookName, "-r", revision.String(), snapName) + command := exec.Command(snapCmd(), "run", "--hook", hookName, "-r", revision.String(), snapName) // Make sure the hook has its context defined so it can communicate via the // REST API. diff -Nru snapd-2.22.6+16.10/overlord/hookstate/hookmgr_test.go snapd-2.23.1+16.10/overlord/hookstate/hookmgr_test.go --- snapd-2.22.6+16.10/overlord/hookstate/hookmgr_test.go 2017-02-08 12:14:30.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/hookstate/hookmgr_test.go 2017-03-08 13:28:21.000000000 +0000 @@ -21,6 +21,8 @@ import ( "encoding/json" + "os" + "path/filepath" "regexp" "testing" @@ -431,3 +433,28 @@ c.Check(found, Equals, true, Commentf("Expected to find regex %q in task log: %v", pattern, task.Log())) } + +func (s *hookManagerSuite) TestHookTaskRunsRightSnapCmd(c *C) { + coreSnapCmdPath := filepath.Join(dirs.SnapMountDir, "core/12/usr/bin/snap") + err := os.MkdirAll(filepath.Dir(coreSnapCmdPath), 0755) + c.Assert(err, IsNil) + s.command = testutil.MockCommand(c, coreSnapCmdPath, "") + + r := hookstate.MockReadlink(func(p string) (string, error) { + c.Assert(p, Equals, "/proc/self/exe") + return filepath.Join(dirs.SnapMountDir, "core/12/usr/lib/snapd/snapd"), nil + }) + defer r() + + s.manager.Ensure() + s.manager.Wait() + + s.state.Lock() + defer s.state.Unlock() + + c.Assert(s.context, NotNil, Commentf("Expected handler generator to be called with a valid context")) + c.Check(s.command.Calls(), DeepEquals, [][]string{{ + "snap", "run", "--hook", "configure", "-r", "1", "test-snap", + }}) + +} diff -Nru snapd-2.22.6+16.10/overlord/ifacestate/handlers.go snapd-2.23.1+16.10/overlord/ifacestate/handlers.go --- snapd-2.22.6+16.10/overlord/ifacestate/handlers.go 2017-02-22 22:31:58.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/ifacestate/handlers.go 2017-03-08 13:28:14.000000000 +0000 @@ -22,6 +22,7 @@ import ( "fmt" "sort" + "time" "gopkg.in/tomb.v2" @@ -31,6 +32,7 @@ "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" ) @@ -63,7 +65,7 @@ } snap.AddImplicitSlots(affectedSnapInfo) opts := confinementOptions(snapst.Flags) - if err := setupSnapSecurity(task, affectedSnapInfo, opts, m.repo); err != nil { + if err := m.setupSnapSecurity(task, affectedSnapInfo, opts); err != nil { return err } } @@ -85,6 +87,36 @@ return err } + // TODO: this whole bit seems maybe that it should belong (largely) to a snapstate helper + var corePhase2 bool + if err := task.Get("core-phase-2", &corePhase2); err != nil && err != state.ErrNoState { + return err + } + if corePhase2 { + if snapInfo.Type != snap.TypeOS { + // not core, nothing to do + return nil + } + if task.State().Restarting() { + // don't continue until we are in the restarted snapd + task.Logf("Waiting for restart...") + return &state.Retry{} + } + // if not on classic check there was no rollback + if !release.OnClassic { + name, rev, err := snapstate.CurrentBootNameAndRevision(snap.TypeOS) + if err == snapstate.ErrBootNameAndRevisionAgain { + return &state.Retry{After: 5 * time.Second} + } + if err != nil { + return err + } + if snapsup.Name() != name || snapInfo.Revision != rev { + return fmt.Errorf("cannot finish core installation, there was a rollback across reboot") + } + } + } + opts := confinementOptions(snapsup.Flags) return m.setupProfilesForSnap(task, tomb, snapInfo, opts) } @@ -128,7 +160,7 @@ if err != nil { return err } - if err := setupSnapSecurity(task, snapInfo, opts, m.repo); err != nil { + if err := m.setupSnapSecurity(task, snapInfo, opts); err != nil { return err } affectedSet := make(map[string]bool) @@ -183,7 +215,7 @@ } // Remove security artefacts of the snap. - if err := removeSnapSecurity(task, snapName); err != nil { + if err := m.removeSnapSecurity(task, snapName); err != nil { return err } @@ -195,6 +227,15 @@ st.Lock() defer st.Unlock() + var corePhase2 bool + if err := task.Get("core-phase-2", &corePhase2); err != nil && err != state.ErrNoState { + return err + } + if corePhase2 { + // let the first setup-profiles deal with this + return nil + } + snapsup, err := snapstate.TaskSnapSetup(task) if err != nil { return err @@ -371,11 +412,11 @@ } slotOpts := confinementOptions(slotSnapst.Flags) - if err := setupSnapSecurity(task, slot.Snap, slotOpts, m.repo); err != nil { + if err := m.setupSnapSecurity(task, slot.Snap, slotOpts); err != nil { return err } plugOpts := confinementOptions(plugSnapst.Flags) - if err := setupSnapSecurity(task, plug.Snap, plugOpts, m.repo); err != nil { + if err := m.setupSnapSecurity(task, plug.Snap, plugOpts); err != nil { return err } @@ -431,7 +472,7 @@ return err } opts := confinementOptions(snapst.Flags) - if err := setupSnapSecurity(task, snapInfo, opts, m.repo); err != nil { + if err := m.setupSnapSecurity(task, snapInfo, opts); err != nil { return err } } diff -Nru snapd-2.22.6+16.10/overlord/ifacestate/helpers.go snapd-2.23.1+16.10/overlord/ifacestate/helpers.go --- snapd-2.22.6+16.10/overlord/ifacestate/helpers.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/ifacestate/helpers.go 2017-03-06 13:33:50.000000000 +0000 @@ -34,11 +34,14 @@ "github.com/snapcore/snapd/snap" ) -func (m *InterfaceManager) initialize(extra []interfaces.Interface) error { +func (m *InterfaceManager) initialize(extraInterfaces []interfaces.Interface, extraBackends []interfaces.SecurityBackend) error { m.state.Lock() defer m.state.Unlock() - if err := m.addInterfaces(extra); err != nil { + if err := m.addInterfaces(extraInterfaces); err != nil { + return err + } + if err := m.addBackends(extraBackends); err != nil { return err } if err := m.addSnaps(); err != nil { @@ -47,6 +50,9 @@ if err := m.reloadConnections(""); err != nil { return err } + if err := m.regenerateAllSecurityProfiles(); err != nil { + return err + } return nil } @@ -64,6 +70,20 @@ return nil } +func (m *InterfaceManager) addBackends(extra []interfaces.SecurityBackend) error { + for _, backend := range backends.All { + if err := m.repo.AddBackend(backend); err != nil { + return err + } + } + for _, backend := range extra { + if err := m.repo.AddBackend(backend); err != nil { + return err + } + } + return nil +} + func (m *InterfaceManager) addSnaps() error { snaps, err := snapstate.ActiveInfos(m.state) if err != nil { @@ -78,6 +98,61 @@ return nil } +// regenerateAllSecurityProfiles will regenerate the security profiles +// for apparmor and seccomp. This is needed because: +// - for seccomp we may have "terms" on disk that the current snap-confine +// does not understand (e.g. in a rollback scenario). a refresh ensures +// we have a profile that matches what snap-confine understand +// - for apparmor the kernel 4.4.0-65.86 has an incompatible apparmor +// change that breaks existing profiles for installed snaps. With a +// refresh those get fixed. +func (m *InterfaceManager) regenerateAllSecurityProfiles() error { + // Get all the security backends + securityBackends := m.repo.Backends() + + // Get all the snap infos + snaps, err := snapstate.ActiveInfos(m.state) + if err != nil { + return err + } + // Add implicit slots to all snaps + for _, snapInfo := range snaps { + snap.AddImplicitSlots(snapInfo) + } + + // For each snap: + for _, snapInfo := range snaps { + snapName := snapInfo.Name() + // Get the state of the snap so we can compute the confinement option + var snapst snapstate.SnapState + if err := snapstate.Get(m.state, snapName, &snapst); err != nil { + logger.Noticef("cannot get state of snap %q: %s", snapName, err) + } + + // Compute confinement options + opts := confinementOptions(snapst.Flags) + + // For each backend: + for _, backend := range securityBackends { + // The issue this is attempting to fix is only + // affecting seccomp/apparmor so limit the work just to + // this backend. + shouldRefresh := (backend.Name() == interfaces.SecuritySecComp || backend.Name() == interfaces.SecurityAppArmor) + if !shouldRefresh { + continue + } + // Refresh security of this snap and backend + if err := backend.Setup(snapInfo, opts, m.repo); err != nil { + // Let's log this but carry on + logger.Noticef("cannot regenerate %s profile for snap %q: %s", + backend.Name(), snapName, err) + } + } + } + + return nil +} + // reloadConnections reloads connections stored in the state in the repository. // Using non-empty snapName the operation can be scoped to connections // affecting a given snap. @@ -101,13 +176,13 @@ return nil } -func setupSnapSecurity(task *state.Task, snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error { +func (m *InterfaceManager) setupSnapSecurity(task *state.Task, snapInfo *snap.Info, opts interfaces.ConfinementOptions) error { st := task.State() snapName := snapInfo.Name() - for _, backend := range backends.All { + for _, backend := range m.repo.Backends() { st.Unlock() - err := backend.Setup(snapInfo, opts, repo) + err := backend.Setup(snapInfo, opts, m.repo) st.Lock() if err != nil { task.Errorf("cannot setup %s for snap %q: %s", backend.Name(), snapName, err) @@ -117,9 +192,9 @@ return nil } -func removeSnapSecurity(task *state.Task, snapName string) error { +func (m *InterfaceManager) removeSnapSecurity(task *state.Task, snapName string) error { st := task.State() - for _, backend := range backends.All { + for _, backend := range m.repo.Backends() { st.Unlock() err := backend.Remove(snapName) st.Lock() diff -Nru snapd-2.22.6+16.10/overlord/ifacestate/hooks.go snapd-2.23.1+16.10/overlord/ifacestate/hooks.go --- snapd-2.22.6+16.10/overlord/ifacestate/hooks.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/ifacestate/hooks.go 2017-03-02 06:37:54.000000000 +0000 @@ -25,9 +25,13 @@ "github.com/snapcore/snapd/overlord/hookstate" ) -type prepareHandler struct{} +type prepareHandler struct { + context *hookstate.Context +} -type connectHandler struct{} +type connectHandler struct { + context *hookstate.Context +} func (h *prepareHandler) Before() error { return nil @@ -56,11 +60,11 @@ // setupHooks sets hooks of InterfaceManager up func setupHooks(hookMgr *hookstate.HookManager) { prepareGenerator := func(context *hookstate.Context) hookstate.Handler { - return &prepareHandler{} + return &prepareHandler{context: context} } connectGenerator := func(context *hookstate.Context) hookstate.Handler { - return &connectHandler{} + return &connectHandler{context: context} } hookMgr.Register(regexp.MustCompile("^prepare-plug-[-a-z0-9]+$"), prepareGenerator) diff -Nru snapd-2.22.6+16.10/overlord/ifacestate/ifacemgr.go snapd-2.23.1+16.10/overlord/ifacestate/ifacemgr.go --- snapd-2.22.6+16.10/overlord/ifacestate/ifacemgr.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/ifacestate/ifacemgr.go 2017-03-02 06:37:54.000000000 +0000 @@ -29,8 +29,8 @@ "github.com/snapcore/snapd/interfaces/backends" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/snapstate" - "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" ) // InterfaceManager is responsible for the maintenance of interfaces in @@ -44,7 +44,7 @@ // Manager returns a new InterfaceManager. // Extra interfaces can be provided for testing. -func Manager(s *state.State, hookManager *hookstate.HookManager, extra []interfaces.Interface) (*InterfaceManager, error) { +func Manager(s *state.State, hookManager *hookstate.HookManager, extraInterfaces []interfaces.Interface, extraBackends []interfaces.SecurityBackend) (*InterfaceManager, error) { // NOTE: hookManager is nil only when testing. if hookManager != nil { setupHooks(hookManager) @@ -56,7 +56,7 @@ runner: runner, repo: interfaces.NewRepository(), } - if err := m.initialize(extra); err != nil { + if err := m.initialize(extraInterfaces, extraBackends); err != nil { return nil, err } @@ -77,6 +77,42 @@ return m, nil } +func setInitialConnectAttributes(ts *state.Task, plugSnap string, plugName string, slotSnap string, slotName string) error { + // Set initial interface attributes for the plug and slot snaps in connect task. + var snapst snapstate.SnapState + var err error + + st := ts.State() + if err = snapstate.Get(st, plugSnap, &snapst); err != nil { + return err + } + snapInfo, err := snapst.CurrentInfo() + if err != nil { + return err + } + if plug, ok := snapInfo.Plugs[plugName]; ok { + ts.Set("plug-attrs", plug.Attrs) + } else { + return fmt.Errorf("snap %q has no plug named %q", plugSnap, plugName) + } + + if err = snapstate.Get(st, slotSnap, &snapst); err != nil { + return err + } + snapInfo, err = snapst.CurrentInfo() + if err != nil { + return err + } + snap.AddImplicitSlots(snapInfo) + if slot, ok := snapInfo.Slots[slotName]; ok { + ts.Set("slot-attrs", slot.Attrs) + } else { + return fmt.Errorf("snap %q has no slot named %q", slotSnap, slotName) + } + + return nil +} + // Connect returns a set of tasks for connecting an interface. // func Connect(st *state.State, plugSnap, plugName, slotSnap, slotName string) (*state.TaskSet, error) { @@ -90,49 +126,72 @@ // TODO: Store the intent-to-connect in the state so that we automatically // try to reconnect on reboot (reconnection can fail or can connect with // different parameters so we cannot store the actual connection details). + + // Create a series of tasks: + // - prepare-plug- hook + // - prepare-slot- hook + // - connect task + // - connect-slot- hook + // - connect-plug- hook + // The tasks run in sequence (are serialized by WaitFor). + // The prepare- hooks collect attributes via snapctl set. + // 'snapctl set' can only modify own attributes (plug's attributes in the *-plug-* hook and + // slot's attributes in the *-slot-* hook). + // 'snapctl get' can read both slot's and plug's attributes. + summary := fmt.Sprintf(i18n.G("Connect %s:%s to %s:%s"), + plugSnap, plugName, slotSnap, slotName) + connectInterface := st.NewTask("connect", summary) + + initialContext := make(map[string]interface{}) + initialContext["attrs-task"] = connectInterface.ID() + plugHookSetup := &hookstate.HookSetup{ Snap: plugSnap, Hook: "prepare-plug-" + plugName, Optional: true, } - summary := fmt.Sprintf(i18n.G("Prepare connection of plug %s:%s"), plugSnap, plugName) - preparePlugConnection := hookstate.HookTask(st, summary, plugHookSetup, nil) + + summary = fmt.Sprintf(i18n.G("Run hook %s of snap %q"), plugHookSetup.Hook, plugHookSetup.Snap) + preparePlugConnection := hookstate.HookTask(st, summary, plugHookSetup, initialContext) slotHookSetup := &hookstate.HookSetup{ Snap: slotSnap, Hook: "prepare-slot-" + slotName, Optional: true, } - summary = fmt.Sprintf(i18n.G("Prepare connection of slot %s:%s"), slotSnap, slotName) - prepareSlotConnection := hookstate.HookTask(st, summary, slotHookSetup, nil) + + summary = fmt.Sprintf(i18n.G("Run hook %s of snap %q"), slotHookSetup.Hook, slotHookSetup.Snap) + prepareSlotConnection := hookstate.HookTask(st, summary, slotHookSetup, initialContext) prepareSlotConnection.WaitFor(preparePlugConnection) - summary = fmt.Sprintf(i18n.G("Connect %s:%s to %s:%s"), - plugSnap, plugName, slotSnap, slotName) - connectInterface := st.NewTask("connect", summary) connectInterface.Set("slot", interfaces.SlotRef{Snap: slotSnap, Name: slotName}) connectInterface.Set("plug", interfaces.PlugRef{Snap: plugSnap, Name: plugName}) + if err := setInitialConnectAttributes(connectInterface, plugSnap, plugName, slotSnap, slotName); err != nil { + return nil, err + } connectInterface.WaitFor(prepareSlotConnection) - confirmSlotHookSetup := &hookstate.HookSetup{ + connectSlotHookSetup := &hookstate.HookSetup{ Snap: slotSnap, Hook: "connect-slot-" + slotName, Optional: true, } - summary = fmt.Sprintf(i18n.G("Confirm connection of slot %s:%s"), slotSnap, slotName) - confirmSlotConnection := hookstate.HookTask(st, summary, confirmSlotHookSetup, nil) - confirmSlotConnection.WaitFor(connectInterface) - confirmPlugHookSetup := &hookstate.HookSetup{ + summary = fmt.Sprintf(i18n.G("Run hook %s of snap %q"), connectSlotHookSetup.Hook, connectSlotHookSetup.Snap) + connectSlotConnection := hookstate.HookTask(st, summary, connectSlotHookSetup, initialContext) + connectSlotConnection.WaitFor(connectInterface) + + connectPlugHookSetup := &hookstate.HookSetup{ Snap: plugSnap, Hook: "connect-plug-" + plugName, Optional: true, } - summary = fmt.Sprintf(i18n.G("Confirm connection of plug %s:%s"), plugSnap, plugName) - confirmPlugConnection := hookstate.HookTask(st, summary, confirmPlugHookSetup, nil) - confirmPlugConnection.WaitFor(confirmSlotConnection) - return state.NewTaskSet(preparePlugConnection, prepareSlotConnection, connectInterface, confirmSlotConnection, confirmPlugConnection), nil + summary = fmt.Sprintf(i18n.G("Run hook %s of snap %q"), connectPlugHookSetup.Hook, connectPlugHookSetup.Snap) + connectPlugConnection := hookstate.HookTask(st, summary, connectPlugHookSetup, initialContext) + connectPlugConnection.WaitFor(connectSlotConnection) + + return state.NewTaskSet(preparePlugConnection, prepareSlotConnection, connectInterface, connectSlotConnection, connectPlugConnection), nil } // Disconnect returns a set of tasks for disconnecting an interface. diff -Nru snapd-2.22.6+16.10/overlord/ifacestate/ifacemgr_test.go snapd-2.23.1+16.10/overlord/ifacestate/ifacemgr_test.go --- snapd-2.22.6+16.10/overlord/ifacestate/ifacemgr_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/ifacestate/ifacemgr_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -55,6 +55,7 @@ privateMgr *ifacestate.InterfaceManager privateHookMgr *hookstate.HookManager extraIfaces []interfaces.Interface + extraBackends []interfaces.SecurityBackend secBackend *ifacetest.TestSecurityBackend restoreBackends func() mockSnapCmd *testutil.MockCmd @@ -87,7 +88,11 @@ s.privateHookMgr = nil s.privateMgr = nil s.extraIfaces = nil + s.extraBackends = nil s.secBackend = &ifacetest.TestSecurityBackend{} + // TODO: transition this so that we don't load real backends and instead + // just load the test backend here and this is nicely integrated with + // extraBackends above. s.restoreBackends = ifacestate.MockSecurityBackends([]interfaces.SecurityBackend{s.secBackend}) } @@ -103,9 +108,9 @@ func (s *interfaceManagerSuite) manager(c *C) *ifacestate.InterfaceManager { if s.privateMgr == nil { - mgr, err := ifacestate.Manager(s.state, s.hookManager(c), s.extraIfaces) - mgr.AddForeignTaskHandlers() + mgr, err := ifacestate.Manager(s.state, s.hookManager(c), s.extraIfaces, s.extraBackends) c.Assert(err, IsNil) + mgr.AddForeignTaskHandlers() s.privateMgr = mgr } return s.privateMgr @@ -136,6 +141,11 @@ } func (s *interfaceManagerSuite) TestConnectTask(c *C) { + s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + _ = s.manager(c) + s.state.Lock() defer s.state.Unlock() @@ -169,6 +179,14 @@ c.Assert(err, IsNil) c.Assert(slot.Snap, Equals, "producer") c.Assert(slot.Name, Equals, "slot") + // verify initial attributes are present in connect task + var attrs map[string]interface{} + err = task.Get("plug-attrs", &attrs) + c.Assert(err, IsNil) + c.Assert(attrs["attr1"], Equals, "value1") + err = task.Get("slot-attrs", &attrs) + c.Assert(err, IsNil) + c.Assert(attrs["attr2"], Equals, "value2") i++ task = ts.Tasks()[i] c.Check(task.Kind(), Equals, "run-hook") @@ -306,25 +324,9 @@ _ = s.manager(c) s.state.Lock() - change := s.state.NewChange("kind", "summary") - ts, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "whatslot") - c.Assert(err, IsNil) - ts.Tasks()[0].Set("snap-setup", &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "consumer", - }, - }) - - change.AddAll(ts) - s.state.Unlock() - - s.settle(c) - - s.state.Lock() - defer s.state.Unlock() - - c.Check(change.Err(), ErrorMatches, `cannot perform the following tasks:\n- Connect consumer:plug to producer:whatslot \(snap "producer" has no "whatslot" slot\)`) - c.Check(change.Status(), Equals, state.ErrorStatus) + _ = s.state.NewChange("kind", "summary") + _, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "whatslot") + c.Assert(err, ErrorMatches, `snap "producer" has no slot named "whatslot"`) } func (s *interfaceManagerSuite) TestConnectTaskNoSuchPlug(c *C) { @@ -334,26 +336,9 @@ _ = s.manager(c) s.state.Lock() - change := s.state.NewChange("kind", "summary") - ts, err := ifacestate.Connect(s.state, "consumer", "whatplug", "producer", "slot") - c.Assert(err, IsNil) - c.Assert(ts.Tasks(), HasLen, 5) - ts.Tasks()[2].Set("snap-setup", &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "consumer", - }, - }) - - change.AddAll(ts) - s.state.Unlock() - - s.settle(c) - - s.state.Lock() - defer s.state.Unlock() - - c.Check(change.Err(), ErrorMatches, `cannot perform the following tasks:\n- Connect consumer:whatplug to producer:slot \(snap "consumer" has no "whatplug" plug\).*`) - c.Check(change.Status(), Equals, state.ErrorStatus) + _ = s.state.NewChange("kind", "summary") + _, err := ifacestate.Connect(s.state, "consumer", "whatplug", "producer", "slot") + c.Assert(err, ErrorMatches, `snap "consumer" has no plug named "whatplug"`) } func (s *interfaceManagerSuite) TestConnectTaskCheckNotAllowed(c *C) { @@ -705,6 +690,7 @@ plugs: plug: interface: test + attr1: value1 otherplug: interface: test2 ` @@ -715,6 +701,7 @@ slots: slot: interface: test + attr2: value2 ` // The setup-profiles task will not auto-connect an plug that was previously diff -Nru snapd-2.22.6+16.10/overlord/managers_test.go snapd-2.23.1+16.10/overlord/managers_test.go --- snapd-2.22.6+16.10/overlord/managers_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/managers_test.go 2017-03-08 13:28:14.000000000 +0000 @@ -104,6 +104,7 @@ c.Assert(err, IsNil) os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1") + snapstate.CanAutoRefresh = nil // create a fake systemd environment os.MkdirAll(filepath.Join(dirs.SnapServicesDir, "multi-user.target.wants"), 0755) @@ -116,7 +117,7 @@ ms.udev = testutil.MockCommand(c, "udevadm", "") ms.umount = testutil.MockCommand(c, "umount", "") ms.snapDiscardNs = testutil.MockCommand(c, "snap-discard-ns", "") - dirs.LibExecDir = ms.snapDiscardNs.BinDir() + dirs.DistroLibExecDir = ms.snapDiscardNs.BinDir() ms.storeSigning = assertstest.NewStoreStack("can0nical", rootPrivKey, storePrivKey) ms.restoreTrusted = sysdb.InjectTrusted(ms.storeSigning.Trusted) @@ -800,7 +801,7 @@ // core & kernel -func (ms *mgrsSuite) TestInstallCoreSnapUpdatesBootloader(c *C) { +func (ms *mgrsSuite) TestInstallCoreSnapUpdatesBootloaderAndSplitsAcrossRestart(c *C) { bootloader := boottest.NewMockBootloader("mock", c.MkDir()) partition.ForceBootloader(bootloader) defer partition.ForceBootloader(nil) @@ -829,12 +830,28 @@ st.Lock() c.Assert(err, IsNil) - c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err())) + // final steps will are post poned until we are in the restarted snapd + c.Assert(st.Restarting(), Equals, true) + c.Assert(chg.Status(), Equals, state.DoingStatus, Commentf("install-snap change failed with: %v", chg.Err())) + // this is already set c.Assert(bootloader.BootVars, DeepEquals, map[string]string{ "snap_try_core": "core_x1.snap", "snap_mode": "try", }) + + // simulate successful restart happened + state.MockRestarting(st, false) + bootloader.BootVars["snap_mode"] = "" + bootloader.BootVars["snap_core"] = "core_x1.snap" + + st.Unlock() + err = ms.o.Settle() + st.Lock() + c.Assert(err, IsNil) + + c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err())) + } func (ms *mgrsSuite) TestInstallKernelSnapUpdatesBootloader(c *C) { diff -Nru snapd-2.22.6+16.10/overlord/overlord.go snapd-2.23.1+16.10/overlord/overlord.go --- snapd-2.22.6+16.10/overlord/overlord.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/overlord.go 2017-03-02 06:37:54.000000000 +0000 @@ -116,7 +116,7 @@ o.assertMgr = assertMgr o.stateEng.AddManager(o.assertMgr) - ifaceMgr, err := ifacestate.Manager(s, hookMgr, nil) + ifaceMgr, err := ifacestate.Manager(s, hookMgr, nil, nil) if err != nil { return nil, err } diff -Nru snapd-2.22.6+16.10/overlord/overlord_test.go snapd-2.23.1+16.10/overlord/overlord_test.go --- snapd-2.22.6+16.10/overlord/overlord_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/overlord_test.go 2017-03-02 06:37:58.000000000 +0000 @@ -51,6 +51,7 @@ tmpdir := c.MkDir() dirs.SetRootDir(tmpdir) dirs.SnapStateFile = filepath.Join(tmpdir, "test.json") + snapstate.CanAutoRefresh = nil } func (ovs *overlordSuite) TearDownTest(c *C) { @@ -173,10 +174,19 @@ func (wm *witnessManager) Wait() { } +// markSeeded flags the state under the overlord as seeded to avoid running the seeding code in these tests +func markSeeded(o *overlord.Overlord) { + st := o.State() + st.Lock() + st.Set("seeded", true) + st.Unlock() +} + func (ovs *overlordSuite) TestTrivialRunAndStop(c *C) { o, err := overlord.New() c.Assert(err, IsNil) + markSeeded(o) o.Loop() err = o.Stop() @@ -196,6 +206,7 @@ } o.Engine().AddManager(witness) + markSeeded(o) o.Loop() defer o.Stop() @@ -231,6 +242,7 @@ se := o.Engine() se.AddManager(witness) + markSeeded(o) o.Loop() defer o.Stop() @@ -261,6 +273,7 @@ se := o.Engine() se.AddManager(witness) + markSeeded(o) o.Loop() defer o.Stop() @@ -293,6 +306,7 @@ se := o.Engine() se.AddManager(witness) + markSeeded(o) o.Loop() defer o.Stop() @@ -324,6 +338,7 @@ se := o.Engine() se.AddManager(witness) + markSeeded(o) o.Loop() defer o.Stop() @@ -357,6 +372,7 @@ chg2.SetStatus(state.DoneStatus) st.Unlock() + markSeeded(o) o.Loop() time.Sleep(150 * time.Millisecond) err = o.Stop() @@ -391,6 +407,7 @@ c.Check(st.Changes(), HasLen, 2) st.Unlock() + markSeeded(o) // start the loop that runs the prune ticker o.Loop() @@ -508,6 +525,7 @@ s.Unlock() + markSeeded(o) o.Settle() s.Lock() @@ -542,6 +560,7 @@ s.Unlock() + markSeeded(o) o.Settle() s.Lock() @@ -584,6 +603,7 @@ chg.AddTask(t) s.Unlock() + markSeeded(o) o.Settle() s.Lock() diff -Nru snapd-2.22.6+16.10/overlord/snapstate/backend/link.go snapd-2.23.1+16.10/overlord/snapstate/backend/link.go --- snapd-2.22.6+16.10/overlord/snapstate/backend/link.go 2016-12-12 15:13:29.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/backend/link.go 2017-03-06 13:33:50.000000000 +0000 @@ -58,6 +58,10 @@ // LinkSnap makes the snap available by generating wrappers and setting the current symlinks. func (b Backend) LinkSnap(info *snap.Info) error { + if info.Revision.Unset() { + return fmt.Errorf("cannot link snap %q with unset revision", info.Name()) + } + if err := generateWrappers(info); err != nil { return err } diff -Nru snapd-2.22.6+16.10/overlord/snapstate/backend/link_test.go snapd-2.23.1+16.10/overlord/snapstate/backend/link_test.go --- snapd-2.22.6+16.10/overlord/snapstate/backend/link_test.go 2017-02-08 12:14:30.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/backend/link_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -210,3 +210,11 @@ c.Check(osutil.FileExists(currentActiveSymlink), Equals, false) c.Check(osutil.FileExists(currentDataSymlink), Equals, false) } + +func (s *linkSuite) TestLinkFailsForUnsetRevision(c *C) { + info := &snap.Info{ + SuggestedName: "foo", + } + err := s.be.LinkSnap(info) + c.Assert(err, ErrorMatches, `cannot link snap "foo" with unset revision`) +} diff -Nru snapd-2.22.6+16.10/overlord/snapstate/backend/ns.go snapd-2.23.1+16.10/overlord/snapstate/backend/ns.go --- snapd-2.22.6+16.10/overlord/snapstate/backend/ns.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/backend/ns.go 2017-03-02 06:37:54.000000000 +0000 @@ -34,17 +34,32 @@ return filepath.Join(dirs.SnapRunNsDir, fmt.Sprintf("%s.mnt", snapName)) } -func (b Backend) DiscardSnapNamespace(snapName string) error { +// Run an internal tool on a given snap namespace, if one exists. +func (b Backend) runNamespaceTool(toolName, snapName string) ([]byte, error) { mntFile := mountNsPath(snapName) - // If there's a .mnt file that was created by snap-confine we should ask - // snap-confine to discard it appropriately. if osutil.FileExists(mntFile) { - snapDiscardNs := filepath.Join(dirs.LibExecDir, "snap-discard-ns") - cmd := exec.Command(snapDiscardNs, snapName) + toolPath := filepath.Join(dirs.DistroLibExecDir, toolName) + cmd := exec.Command(toolPath, snapName) output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("cannot discard preserved namespaces of snap %q: %s", snapName, osutil.OutputErr(output, err)) - } + return output, err + } + return nil, nil +} + +// Discard the mount namespace of a given snap. +func (b Backend) DiscardSnapNamespace(snapName string) error { + output, err := b.runNamespaceTool("snap-discard-ns", snapName) + if err != nil { + return fmt.Errorf("cannot discard preserved namespace of snap %q: %s", snapName, osutil.OutputErr(output, err)) + } + return nil +} + +// Update the mount namespace of a given snap. +func (b Backend) UpdateSnapNamespace(snapName string) error { + output, err := b.runNamespaceTool("snap-update-ns", snapName) + if err != nil { + return fmt.Errorf("cannot update preserved namespace of snap %q: %s", snapName, osutil.OutputErr(output, err)) } return nil } diff -Nru snapd-2.22.6+16.10/overlord/snapstate/backend/ns_test.go snapd-2.23.1+16.10/overlord/snapstate/backend/ns_test.go --- snapd-2.22.6+16.10/overlord/snapstate/backend/ns_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/backend/ns_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -33,9 +33,9 @@ ) type nsSuite struct { - be backend.Backend - nullProgress progress.NullProgress - oldLibExecDir string + be backend.Backend + nullProgress progress.NullProgress + oldDistroLibExecDir string } var _ = Suite(&nsSuite{}) @@ -43,71 +43,106 @@ func (s *nsSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) // Mock enough bits so that we can observe calls to snap-discard-ns - s.oldLibExecDir = dirs.LibExecDir + s.oldDistroLibExecDir = dirs.DistroLibExecDir } func (s *nsSuite) TearDownTest(c *C) { dirs.SetRootDir("") - dirs.LibExecDir = s.oldLibExecDir + dirs.DistroLibExecDir = s.oldDistroLibExecDir } -func (s *nsSuite) TestDiscardNamespaceMntFilePresent(c *C) { - // Mock the snap-discard-ns command - cmd := testutil.MockCommand(c, "snap-discard-ns", "") - dirs.LibExecDir = cmd.BinDir() - defer cmd.Restore() - - // the presence of the .mnt file is the trigger so create it now - c.Assert(os.MkdirAll(dirs.SnapRunNsDir, 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapRunNsDir, "snap-name.mnt"), nil, 0644), IsNil) - - err := s.be.DiscardSnapNamespace("snap-name") - c.Assert(err, IsNil) - c.Check(cmd.Calls(), DeepEquals, [][]string{{"snap-discard-ns", "snap-name"}}) -} - -func (s *nsSuite) TestDiscardNamespaceMntFileAbsent(c *C) { - // Mock the snap-discard-ns command - cmd := testutil.MockCommand(c, "snap-discard-ns", "") - dirs.LibExecDir = cmd.BinDir() - defer cmd.Restore() - - // don't create the .mnt file that triggers the discard operation - - // ask the backend to discard the namespace - err := s.be.DiscardSnapNamespace("snap-name") - c.Assert(err, IsNil) - c.Check(cmd.Calls(), IsNil) -} - -func (s *nsSuite) TestDiscardNamespaceFailure(c *C) { - // Mock the snap-discard-ns command, make it fail - cmd := testutil.MockCommand(c, "snap-discard-ns", "echo failure; exit 1;") - dirs.LibExecDir = cmd.BinDir() - defer cmd.Restore() - - // the presence of the .mnt file is the trigger so create it now - c.Assert(os.MkdirAll(dirs.SnapRunNsDir, 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapRunNsDir, "snap-name.mnt"), nil, 0644), IsNil) - - // ask the backend to discard the namespace - err := s.be.DiscardSnapNamespace("snap-name") - c.Assert(err, ErrorMatches, `cannot discard preserved namespaces of snap "snap-name": failure`) - c.Check(cmd.Calls(), DeepEquals, [][]string{{"snap-discard-ns", "snap-name"}}) -} - -func (s *nsSuite) TestDiscardNamespaceSilentFailure(c *C) { - // Mock the snap-discard-ns command, make it fail - cmd := testutil.MockCommand(c, "snap-discard-ns", "exit 1") - dirs.LibExecDir = cmd.BinDir() - defer cmd.Restore() - - // the presence of the .mnt file is the trigger so create it now - c.Assert(os.MkdirAll(dirs.SnapRunNsDir, 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapRunNsDir, "snap-name.mnt"), nil, 0644), IsNil) - - // ask the backend to discard the namespace - err := s.be.DiscardSnapNamespace("snap-name") - c.Assert(err, ErrorMatches, `cannot discard preserved namespaces of snap "snap-name": exit status 1`) - c.Check(cmd.Calls(), DeepEquals, [][]string{{"snap-discard-ns", "snap-name"}}) +func (s *nsSuite) TestDiscardNamespaceMnt(c *C) { + for _, t := range []struct { + cmd string + mnt bool + errStr string + res [][]string + }{ + // The mnt file present so we use snap-discard-ns; + // The command doesn't fail and there's no error. + {cmd: "", mnt: true, errStr: "", res: [][]string{{"snap-discard-ns", "snap-name"}}}, + // The mnt file is not present so we don't do anything. + {cmd: "", mnt: false, errStr: "", res: nil}, + // The mnt file is present so we use snap-discard-ns; + // The command fails and we forward the error along with the output. + { + cmd: "echo failure; exit 1;", + mnt: true, + errStr: `cannot discard preserved namespace of snap "snap-name": failure`, + res: [][]string{{"snap-discard-ns", "snap-name"}}}, + // The mnt file is present so we use snap-discard-ns; + // The command fails silently and we forward this fact using a generic message. + { + cmd: "exit 1;", + mnt: true, + errStr: `cannot discard preserved namespace of snap "snap-name": exit status 1`, + res: [][]string{{"snap-discard-ns", "snap-name"}}}, + } { + cmd := testutil.MockCommand(c, "snap-discard-ns", t.cmd) + dirs.DistroLibExecDir = cmd.BinDir() + defer cmd.Restore() + + if t.mnt { + c.Assert(os.MkdirAll(dirs.SnapRunNsDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapRunNsDir, "snap-name.mnt"), nil, 0644), IsNil) + } else { + c.Assert(os.RemoveAll(dirs.SnapRunNsDir), IsNil) + } + + err := s.be.DiscardSnapNamespace("snap-name") + if t.errStr != "" { + c.Check(err, ErrorMatches, t.errStr) + } else { + c.Check(err, IsNil) + c.Check(cmd.Calls(), DeepEquals, t.res) + } + } +} + +func (s *nsSuite) TestUpdateNamespaceMnt(c *C) { + for _, t := range []struct { + cmd string + mnt bool + errStr string + res [][]string + }{ + // The mnt file present so we use snap-update-ns; + // The command doesn't fail and there's no error. + {cmd: "", mnt: true, errStr: "", res: [][]string{{"snap-update-ns", "snap-name"}}}, + // The mnt file is not present so we don't do anything. + {cmd: "", mnt: false, errStr: "", res: nil}, + // The mnt file is present so we use snap-update-ns; + // The command fails and we forward the error along with the output. + { + cmd: "echo failure; exit 1;", + mnt: true, + errStr: `cannot update preserved namespace of snap "snap-name": failure`, + res: [][]string{{"snap-update-ns", "snap-name"}}}, + // The mnt file is present so we use snap-update-ns; + // The command fails silently and we forward this fact using a generic message. + { + cmd: "exit 1;", + mnt: true, + errStr: `cannot update preserved namespace of snap "snap-name": exit status 1`, + res: [][]string{{"snap-update-ns", "snap-name"}}}, + } { + cmd := testutil.MockCommand(c, "snap-update-ns", t.cmd) + dirs.DistroLibExecDir = cmd.BinDir() + defer cmd.Restore() + + if t.mnt { + c.Assert(os.MkdirAll(dirs.SnapRunNsDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapRunNsDir, "snap-name.mnt"), nil, 0644), IsNil) + } else { + c.Assert(os.RemoveAll(dirs.SnapRunNsDir), IsNil) + } + + err := s.be.UpdateSnapNamespace("snap-name") + if t.errStr != "" { + c.Check(err, ErrorMatches, t.errStr) + } else { + c.Check(err, IsNil) + c.Check(cmd.Calls(), DeepEquals, t.res) + } + } } diff -Nru snapd-2.22.6+16.10/overlord/snapstate/backend_test.go snapd-2.23.1+16.10/overlord/snapstate/backend_test.go --- snapd-2.22.6+16.10/overlord/snapstate/backend_test.go 2017-02-22 08:24:38.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/backend_test.go 2017-03-08 13:28:14.000000000 +0000 @@ -22,7 +22,6 @@ import ( "errors" "fmt" - "strings" "golang.org/x/net/context" @@ -39,11 +38,12 @@ type fakeOp struct { op string - name string - revno snap.Revision - sinfo snap.SideInfo - stype snap.Type - cand store.RefreshCandidate + name string + channel string + revno snap.Revision + sinfo snap.SideInfo + stype snap.Type + cand store.RefreshCandidate old string @@ -120,10 +120,15 @@ confinement = snap.ClassicConfinement } + typ := snap.TypeApp + if spec.Name == "some-core" { + typ = snap.TypeOS + } + info := &snap.Info{ Architectures: []string{"all"}, SideInfo: snap.SideInfo{ - RealName: strings.Split(spec.Name, ".")[0], + RealName: spec.Name, Channel: spec.Channel, SnapID: "snapIDsnapidsnapidsnapidsnapidsn", Revision: spec.Revision, @@ -133,6 +138,7 @@ DownloadURL: "https://some-server.com/some/path.snap", }, Confinement: confinement, + Type: typ, } f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-snap", name: spec.Name, revno: spec.Revision}) diff -Nru snapd-2.22.6+16.10/overlord/snapstate/booted.go snapd-2.23.1+16.10/overlord/snapstate/booted.go --- snapd-2.22.6+16.10/overlord/snapstate/booted.go 2017-01-20 09:44:59.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/booted.go 2017-03-08 13:28:14.000000000 +0000 @@ -20,6 +20,7 @@ package snapstate import ( + "errors" "fmt" "strings" @@ -59,6 +60,15 @@ return nil } + // nothing to check if there's no kernel + _, err := KernelInfo(st) + if err == state.ErrNoState { + return nil + } + if err != nil { + return fmt.Errorf(errorPrefix+"%s", err) + } + bootloader, err := partition.FindBootloader() if err != nil { return fmt.Errorf(errorPrefix+"%s", err) @@ -105,3 +115,55 @@ return nil } + +var ErrBootNameAndRevisionAgain = errors.New("boot revision not yet established") + +// CurrentBootNameAndRevision returns the currently set name and +// revision for boot for the given type of snap, which can be core or +// kernel. Returns ErrBootNameAndRevisionAgain if the value are not +// temporarily established. +func CurrentBootNameAndRevision(typ snap.Type) (name string, revision snap.Revision, err error) { + var kind string + var bootVar string + + switch typ { + case snap.TypeKernel: + kind = "kernel" + bootVar = "snap_kernel" + case snap.TypeOS: + kind = "core" + bootVar = "snap_core" + default: + return "", snap.Revision{}, fmt.Errorf("cannot find boot revision for anything but core and kernel") + } + + errorPrefix := fmt.Sprintf("cannot retrieve boot revision for %s: ", kind) + if release.OnClassic { + return "", snap.Revision{}, fmt.Errorf(errorPrefix + "classic system") + } + + bootloader, err := partition.FindBootloader() + if err != nil { + return "", snap.Revision{}, fmt.Errorf(errorPrefix+"%s", err) + } + + m, err := bootloader.GetBootVars(bootVar, "snap_mode") + if err != nil { + return "", snap.Revision{}, fmt.Errorf(errorPrefix+"%s", err) + } + + if m["snap_mode"] != "" { + return "", snap.Revision{}, ErrBootNameAndRevisionAgain + } + + snapNameAndRevno := m[bootVar] + if snapNameAndRevno == "" { + return "", snap.Revision{}, fmt.Errorf(errorPrefix + "unset") + } + name, rev, err := nameAndRevnoFromSnap(snapNameAndRevno) + if err != nil { + return "", snap.Revision{}, fmt.Errorf(errorPrefix+"%s", err) + } + + return name, rev, nil +} diff -Nru snapd-2.22.6+16.10/overlord/snapstate/booted_test.go snapd-2.23.1+16.10/overlord/snapstate/booted_test.go --- snapd-2.22.6+16.10/overlord/snapstate/booted_test.go 2017-02-22 11:20:49.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/booted_test.go 2017-03-08 13:28:14.000000000 +0000 @@ -209,6 +209,15 @@ st.Lock() defer st.Unlock() + // have a kernel + snaptest.MockSnap(c, "name: canonical-pc-linux\ntype: os\nversion: 2", "", kernelSI2) + snapstate.Set(st, "canonical-pc-linux", &snapstate.SnapState{ + SnapType: "kernel", + Active: true, + Sequence: []*snap.SideInfo{kernelSI2}, + Current: snap.R(2), + }) + // put core into the state but add no files on disk // will break in the tasks snapstate.Set(st, "core", &snapstate.SnapState{ @@ -219,7 +228,7 @@ }) bs.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/core/1") - bs.bootloader.BootVars["snap_kernel"] = "core_1.snap" + bs.bootloader.BootVars["snap_core"] = "core_1.snap" err := snapstate.UpdateBootRevisions(st) c.Assert(err, IsNil) @@ -245,3 +254,24 @@ _, _, err := snapstate.NameAndRevnoFromSnap("invalid") c.Assert(err, ErrorMatches, `input "invalid" has invalid format \(not enough '_'\)`) } + +func (bs *bootedSuite) TestCurrentBootNameAndRevision(c *C) { + name, revision, err := snapstate.CurrentBootNameAndRevision(snap.TypeOS) + c.Check(err, IsNil) + c.Check(name, Equals, "core") + c.Check(revision, Equals, snap.R(2)) + + name, revision, err = snapstate.CurrentBootNameAndRevision(snap.TypeKernel) + c.Check(err, IsNil) + c.Check(name, Equals, "canonical-pc-linux") + c.Check(revision, Equals, snap.R(2)) + + bs.bootloader.BootVars["snap_mode"] = "trying" + _, _, err = snapstate.CurrentBootNameAndRevision(snap.TypeKernel) + c.Check(err, Equals, snapstate.ErrBootNameAndRevisionAgain) + + bs.bootloader.BootVars["snap_mode"] = "" + delete(bs.bootloader.BootVars, "snap_kernel") + _, _, err = snapstate.CurrentBootNameAndRevision(snap.TypeKernel) + c.Check(err, ErrorMatches, "cannot retrieve boot revision for kernel: unset") +} diff -Nru snapd-2.22.6+16.10/overlord/snapstate/check_snap.go snapd-2.23.1+16.10/overlord/snapstate/check_snap.go --- snapd-2.22.6+16.10/overlord/snapstate/check_snap.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/check_snap.go 2017-03-02 06:37:54.000000000 +0000 @@ -272,14 +272,9 @@ return nil } - if release.OnClassic { - // for the time being - return fmt.Errorf("cannot install a %s snap on classic", kind) - } - currentSnap, err := currentInfo(st) // in firstboot we have no gadget/kernel yet - that is ok - // devicestate considers that case + // first install rules are in devicestate! if err == state.ErrNoState { return nil } diff -Nru snapd-2.22.6+16.10/overlord/snapstate/check_snap_test.go snapd-2.23.1+16.10/overlord/snapstate/check_snap_test.go --- snapd-2.22.6+16.10/overlord/snapstate/check_snap_test.go 2017-02-22 21:55:42.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/check_snap_test.go 2017-03-02 06:37:54.000000000 +0000 @@ -460,34 +460,6 @@ c.Check(err, IsNil) } -func (s *checkSnapSuite) TestCheckSnapGadgetCannotBeInstalledOnClassic(c *C) { - reset := release.MockOnClassic(true) - defer reset() - - st := state.New(nil) - st.Lock() - defer st.Unlock() - - const yaml = `name: gadget -type: gadget -version: 1 -` - - info, err := snap.InfoFromSnapYaml([]byte(yaml)) - c.Assert(err, IsNil) - - var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) { - return info, nil, nil - } - restore := snapstate.MockOpenSnapFile(openSnapFile) - defer restore() - - st.Unlock() - err = snapstate.CheckSnap(st, "snap-path", nil, nil, snapstate.Flags{}) - st.Lock() - c.Check(err, ErrorMatches, "cannot install a gadget snap on classic") -} - func (s *checkSnapSuite) TestCheckSnapErrorOnDevModeDisallowed(c *C) { const yaml = `name: hello version: 1.10 diff -Nru snapd-2.22.6+16.10/overlord/snapstate/export_test.go snapd-2.23.1+16.10/overlord/snapstate/export_test.go --- snapd-2.22.6+16.10/overlord/snapstate/export_test.go 2017-02-22 21:55:49.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/export_test.go 2017-03-06 13:33:50.000000000 +0000 @@ -21,6 +21,7 @@ import ( "errors" + "time" "gopkg.in/tomb.v2" @@ -95,6 +96,17 @@ return func() { errtrackerReport = prev } } +func MockRefreshInterval(newMinRefreshInterval, newRefreshRandomness time.Duration) (restore func()) { + prevMinRefreshInterval := minRefreshInterval + prevDefaultRefreshRandomness := defaultRefreshRandomness + minRefreshInterval = newMinRefreshInterval + defaultRefreshRandomness = newRefreshRandomness + return func() { + minRefreshInterval = prevMinRefreshInterval + defaultRefreshRandomness = prevDefaultRefreshRandomness + } +} + var ( CheckSnap = checkSnap CanRemove = canRemove diff -Nru snapd-2.22.6+16.10/overlord/snapstate/snapmgr.go snapd-2.23.1+16.10/overlord/snapstate/snapmgr.go --- snapd-2.22.6+16.10/overlord/snapstate/snapmgr.go 2017-02-22 21:55:49.000000000 +0000 +++ snapd-2.23.1+16.10/overlord/snapstate/snapmgr.go 2017-03-08 13:28:17.000000000 +0000 @@ -21,8 +21,10 @@ package snapstate import ( + "encoding/json" "errors" "fmt" + "math/rand" "os" "strconv" "strings" @@ -36,11 +38,42 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/snapstate/backend" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/strutil" +) + +// FIXME: what we actually want is a schedule spec that is user configurable +// like: +// """ +// tue +// tue,thu +// tue-thu +// 9:00 +// 9:00,15:00 +// 9:00-15:00 +// tue,thu@9:00-15:00 +// tue@9:00;thu@15:00 +// mon,wed-fri@9:00-11:00,13:00-15:00 +// """ +// where 9:00 is implicitly taken as 9:00-10:00 +// and tue is implicitly taken as tue@ +// +// it is controlled via: +// $ snap refresh --schedule=