diff -Nru snapd-2.55.5+20.04/asserts/account.go snapd-2.57.5+20.04/asserts/account.go --- snapd-2.55.5+20.04/asserts/account.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/account.go 2022-10-17 16:25:18.000000000 +0000 @@ -58,9 +58,7 @@ } // Validation returns the level of confidence of the authority in the -// account's identity, expected to be "unproven" or "verified", and -// for forward compatibility any value != "unproven" can be considered -// at least "verified". +// account's identity, expected to be "unproven", "starred" or "verified". func (acc *Account) Validation() string { return acc.validation } diff -Nru snapd-2.55.5+20.04/asserts/asserts.go snapd-2.57.5+20.04/asserts/asserts.go --- snapd-2.55.5+20.04/asserts/asserts.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/asserts.go 2022-10-17 16:25:18.000000000 +0000 @@ -29,6 +29,9 @@ "strconv" "strings" "unicode/utf8" + + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap/naming" ) type typeFlags int @@ -57,11 +60,42 @@ // PrimaryKey holds the names of the headers that constitute the // unique primary key for this assertion type. PrimaryKey []string + // OptionalPrimaryKeyDefaults holds the default values for + // optional primary key headers. + // Optional primary key headers can be added to types defined + // in previous versions of snapd, as long as they are added at + // the end of the old primary key together with a default value set in + // this map. So they must form a contiguous suffix of PrimaryKey with + // each member having a default value set in this map. + // Optional primary key headers are not supported for sequence + // forming types. + OptionalPrimaryKeyDefaults map[string]string assembler func(assert assertionBase) (Assertion, error) flags typeFlags } +func (at *AssertionType) validate() { + if len(at.OptionalPrimaryKeyDefaults) != 0 && at.flags&sequenceForming != 0 { + panic(fmt.Sprintf("assertion type %q cannot be both sequence forming and have optional primary keys", at.Name)) + } + noptional := 0 + for _, k := range at.PrimaryKey { + defl := at.OptionalPrimaryKeyDefaults[k] + if noptional > 0 { + if defl == "" { + panic(fmt.Sprintf("assertion type %q primary key header %q has no default, optional primary keys must be a proper suffix of the primary key", at.Name, k)) + } + } + if defl != "" { + noptional++ + } + } + if len(at.OptionalPrimaryKeyDefaults) != noptional { + panic(fmt.Sprintf("assertion type %q has defaults values for unknown primary key headers", at.Name)) + } +} + // MaxSupportedFormat returns the maximum supported format iteration for the type. func (at *AssertionType) MaxSupportedFormat() int { return maxSupportedFormat[at.Name] @@ -77,33 +111,44 @@ return at.flags&sequenceForming != 0 } +// AcceptablePrimaryKey returns whether the given key could be an acceptable primary key for this type, allowing for the omission of optional primary key headers. +func (at *AssertionType) AcceptablePrimaryKey(key []string) bool { + n := len(at.PrimaryKey) + nopt := len(at.OptionalPrimaryKeyDefaults) + ninp := len(key) + if ninp > n || ninp < (n-nopt) { + return false + } + return true +} + // Understood assertion types. var ( - AccountType = &AssertionType{"account", []string{"account-id"}, assembleAccount, 0} - AccountKeyType = &AssertionType{"account-key", []string{"public-key-sha3-384"}, assembleAccountKey, 0} - RepairType = &AssertionType{"repair", []string{"brand-id", "repair-id"}, assembleRepair, sequenceForming} - ModelType = &AssertionType{"model", []string{"series", "brand-id", "model"}, assembleModel, 0} - SerialType = &AssertionType{"serial", []string{"brand-id", "model", "serial"}, assembleSerial, 0} - BaseDeclarationType = &AssertionType{"base-declaration", []string{"series"}, assembleBaseDeclaration, 0} - SnapDeclarationType = &AssertionType{"snap-declaration", []string{"series", "snap-id"}, assembleSnapDeclaration, 0} - SnapBuildType = &AssertionType{"snap-build", []string{"snap-sha3-384"}, assembleSnapBuild, 0} - SnapRevisionType = &AssertionType{"snap-revision", []string{"snap-sha3-384"}, assembleSnapRevision, 0} - SnapDeveloperType = &AssertionType{"snap-developer", []string{"snap-id", "publisher-id"}, assembleSnapDeveloper, 0} - SystemUserType = &AssertionType{"system-user", []string{"brand-id", "email"}, assembleSystemUser, 0} - ValidationType = &AssertionType{"validation", []string{"series", "snap-id", "approved-snap-id", "approved-snap-revision"}, assembleValidation, 0} - ValidationSetType = &AssertionType{"validation-set", []string{"series", "account-id", "name", "sequence"}, assembleValidationSet, sequenceForming} - StoreType = &AssertionType{"store", []string{"store"}, assembleStore, 0} - AuthorityDelegationType = &AssertionType{"authority-delegation", []string{"account-id", "delegate-id"}, assembleAuthorityDelegation, 0} - PreseedType = &AssertionType{"preseed", []string{"series", "brand-id", "model", "system-label"}, assemblePreseed, 0} + AccountType = &AssertionType{"account", []string{"account-id"}, nil, assembleAccount, 0} + AccountKeyType = &AssertionType{"account-key", []string{"public-key-sha3-384"}, nil, assembleAccountKey, 0} + RepairType = &AssertionType{"repair", []string{"brand-id", "repair-id"}, nil, assembleRepair, sequenceForming} + ModelType = &AssertionType{"model", []string{"series", "brand-id", "model"}, nil, assembleModel, 0} + SerialType = &AssertionType{"serial", []string{"brand-id", "model", "serial"}, nil, assembleSerial, 0} + BaseDeclarationType = &AssertionType{"base-declaration", []string{"series"}, nil, assembleBaseDeclaration, 0} + SnapDeclarationType = &AssertionType{"snap-declaration", []string{"series", "snap-id"}, nil, assembleSnapDeclaration, 0} + SnapBuildType = &AssertionType{"snap-build", []string{"snap-sha3-384"}, nil, assembleSnapBuild, 0} + SnapRevisionType = &AssertionType{"snap-revision", []string{"snap-sha3-384", "provenance"}, map[string]string{"provenance": naming.DefaultProvenance}, assembleSnapRevision, 0} + SnapDeveloperType = &AssertionType{"snap-developer", []string{"snap-id", "publisher-id"}, nil, assembleSnapDeveloper, 0} + SystemUserType = &AssertionType{"system-user", []string{"brand-id", "email"}, nil, assembleSystemUser, 0} + ValidationType = &AssertionType{"validation", []string{"series", "snap-id", "approved-snap-id", "approved-snap-revision"}, nil, assembleValidation, 0} + ValidationSetType = &AssertionType{"validation-set", []string{"series", "account-id", "name", "sequence"}, nil, assembleValidationSet, sequenceForming} + StoreType = &AssertionType{"store", []string{"store"}, nil, assembleStore, 0} + AuthorityDelegationType = &AssertionType{"authority-delegation", []string{"account-id", "delegate-id"}, nil, assembleAuthorityDelegation, 0} + PreseedType = &AssertionType{"preseed", []string{"series", "brand-id", "model", "system-label"}, nil, assemblePreseed, 0} // ... ) // Assertion types without a definite authority set (on the wire and/or self-signed). var ( - DeviceSessionRequestType = &AssertionType{"device-session-request", []string{"brand-id", "model", "serial"}, assembleDeviceSessionRequest, noAuthority} - SerialRequestType = &AssertionType{"serial-request", nil, assembleSerialRequest, noAuthority} - AccountKeyRequestType = &AssertionType{"account-key-request", []string{"public-key-sha3-384"}, assembleAccountKeyRequest, noAuthority} + DeviceSessionRequestType = &AssertionType{"device-session-request", []string{"brand-id", "model", "serial"}, nil, assembleDeviceSessionRequest, noAuthority} + SerialRequestType = &AssertionType{"serial-request", nil, nil, assembleSerialRequest, noAuthority} + AccountKeyRequestType = &AssertionType{"account-key-request", []string{"public-key-sha3-384"}, nil, assembleAccountKeyRequest, noAuthority} ) var typeRegistry = map[string]*AssertionType{ @@ -163,6 +208,10 @@ // done here to untangle initialization loop via Type() // XXX authority-delegation disabled // typeRegistry[AuthorityDelegationType.Name] = AuthorityDelegationType + + for _, at := range typeRegistry { + at.validate() + } } func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) { @@ -173,6 +222,23 @@ } } +func MockOptionalPrimaryKey(assertType *AssertionType, key, defaultValue string) (restore func()) { + osutil.MustBeTestBinary("mocking new assertion optional primary keys can be done only from tests") + oldPrimaryKey := assertType.PrimaryKey + oldOptionalPrimaryKeyDefaults := assertType.OptionalPrimaryKeyDefaults + newOptionalPrimaryKeyDefaults := make(map[string]string, len(oldOptionalPrimaryKeyDefaults)+1) + for k, defl := range oldOptionalPrimaryKeyDefaults { + newOptionalPrimaryKeyDefaults[k] = defl + } + assertType.PrimaryKey = append(assertType.PrimaryKey, key) + assertType.OptionalPrimaryKeyDefaults = newOptionalPrimaryKeyDefaults + newOptionalPrimaryKeyDefaults[key] = defaultValue + return func() { + assertType.PrimaryKey = oldPrimaryKey + assertType.OptionalPrimaryKeyDefaults = oldOptionalPrimaryKeyDefaults + } +} + var formatAnalyzer = map[*AssertionType]func(headers map[string]interface{}, body []byte) (formatnum int, err error){ SnapDeclarationType: snapDeclarationFormatAnalyze, SystemUserType: systemUserFormatAnalyze, @@ -212,16 +278,23 @@ // HeadersFromPrimaryKey constructs a headers mapping from the // primaryKey values and the assertion type, it errors if primaryKey -// has the wrong length. +// does not cover all the non-optional primary key headers or provides +// too many values. func HeadersFromPrimaryKey(assertType *AssertionType, primaryKey []string) (headers map[string]string, err error) { - if len(primaryKey) != len(assertType.PrimaryKey) { + if !assertType.AcceptablePrimaryKey(primaryKey) { return nil, fmt.Errorf("primary key has wrong length for %q assertion", assertType.Name) } + ninp := len(primaryKey) headers = make(map[string]string, len(assertType.PrimaryKey)) for i, name := range assertType.PrimaryKey { - keyVal := primaryKey[i] - if keyVal == "" { - return nil, fmt.Errorf("primary key %q header cannot be empty", name) + var keyVal string + if i < ninp { + keyVal = primaryKey[i] + if keyVal == "" { + return nil, fmt.Errorf("primary key %q header cannot be empty", name) + } + } else { + keyVal = assertType.OptionalPrimaryKeyDefaults[name] } headers[name] = keyVal } @@ -252,23 +325,52 @@ // PrimaryKeyFromHeaders extracts the tuple of values from headers // corresponding to a primary key under the assertion type, it errors -// if there are missing primary key headers. +// if there are missing primary key headers unless they are optional +// in which case it fills in their default values. func PrimaryKeyFromHeaders(assertType *AssertionType, headers map[string]string) (primaryKey []string, err error) { - return keysFromHeaders(assertType.PrimaryKey, headers) + return keysFromHeaders(assertType.PrimaryKey, headers, assertType.OptionalPrimaryKeyDefaults) } -func keysFromHeaders(keys []string, headers map[string]string) (keyValues []string, err error) { +func keysFromHeaders(keys []string, headers map[string]string, defaults map[string]string) (keyValues []string, err error) { keyValues = make([]string, len(keys)) for i, k := range keys { keyVal := headers[k] if keyVal == "" { - return nil, fmt.Errorf("must provide primary key: %v", k) + keyVal = defaults[k] + if keyVal == "" { + return nil, fmt.Errorf("must provide primary key: %v", k) + } } keyValues[i] = keyVal } return keyValues, nil } +// ReducePrimaryKey produces a primary key prefix by omitting any +// suffix of optional primary key headers default values. +// Too short or long primary keys are returned as is. +func ReducePrimaryKey(assertType *AssertionType, primaryKey []string) []string { + n := len(assertType.PrimaryKey) + nopt := len(assertType.OptionalPrimaryKeyDefaults) + ninp := len(primaryKey) + if ninp > n || ninp < (n-nopt) { + return primaryKey + } + reduced := make([]string, n-nopt, n) + copy(reduced, primaryKey[:n-nopt]) + rest := ninp - (n - nopt) + for i := ninp - 1; i >= n-nopt; i-- { + defl := assertType.OptionalPrimaryKeyDefaults[assertType.PrimaryKey[i]] + if primaryKey[i] != defl { + break + } + // it matches the default value, leave it out + rest-- + } + reduced = append(reduced, primaryKey[n-nopt:n-nopt+rest]...) + return reduced +} + // Ref expresses a reference to an assertion. type Ref struct { Type *AssertionType @@ -278,15 +380,26 @@ func (ref *Ref) String() string { pkStr := "-" n := len(ref.Type.PrimaryKey) - if n != len(ref.PrimaryKey) { + nopt := len(ref.Type.OptionalPrimaryKeyDefaults) + ninp := len(ref.PrimaryKey) + if ninp > n || ninp < (n-nopt) { pkStr = "???" } else if n > 0 { - pkStr = ref.PrimaryKey[n-1] + pkStr = ref.PrimaryKey[n-nopt-1] if n > 1 { sfx := []string{pkStr + ";"} - for i, k := range ref.Type.PrimaryKey[:n-1] { + for i, k := range ref.Type.PrimaryKey[:n-nopt-1] { sfx = append(sfx, fmt.Sprintf("%s:%s", k, ref.PrimaryKey[i])) } + // optional primary keys + for i := n - nopt; i < ninp; i++ { + v := ref.PrimaryKey[i] + k := ref.Type.PrimaryKey[i] + defl := ref.Type.OptionalPrimaryKeyDefaults[k] + if v != defl { + sfx = append(sfx, fmt.Sprintf("%s:%s", k, v)) + } + } pkStr = strings.Join(sfx, " ") } } @@ -295,7 +408,7 @@ // Unique returns a unique string representing the reference that can be used as a key in maps. func (ref *Ref) Unique() string { - return fmt.Sprintf("%s/%s", ref.Type.Name, strings.Join(ref.PrimaryKey, "/")) + return fmt.Sprintf("%s/%s", ref.Type.Name, strings.Join(ReducePrimaryKey(ref.Type, ref.PrimaryKey), "/")) } // Resolve resolves the reference using the given find function. @@ -943,6 +1056,11 @@ } for _, primKey := range assertType.PrimaryKey { + if _, ok := headers[primKey]; !ok { + if defl := assertType.OptionalPrimaryKeyDefaults[primKey]; defl != "" { + headers[primKey] = defl + } + } if _, err := checkPrimaryKey(headers, primKey); err != nil { return nil, fmt.Errorf("assertion %s: %v", assertType.Name, err) } @@ -1068,10 +1186,21 @@ "sign-key-sha3-384": true, } for _, primKey := range assertType.PrimaryKey { - if _, err := checkPrimaryKey(finalHeaders, primKey); err != nil { + defl := assertType.OptionalPrimaryKeyDefaults[primKey] + _, ok := finalHeaders[primKey] + if !ok && defl != "" { + // optional but expected to be set in headers + // in the result assertion + finalHeaders[primKey] = defl + continue + } + value, err := checkPrimaryKey(finalHeaders, primKey) + if err != nil { return nil, err } - writeHeader(buf, finalHeaders, primKey) + if value != defl { + writeHeader(buf, finalHeaders, primKey) + } written[primKey] = true } diff -Nru snapd-2.55.5+20.04/asserts/asserts_test.go snapd-2.57.5+20.04/asserts/asserts_test.go --- snapd-2.55.5+20.04/asserts/asserts_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/asserts_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -138,6 +138,40 @@ c.Check(err, ErrorMatches, `must provide primary key: pk2`) } +func (as *assertsSuite) TestPrimaryKeyHelpersOptionalPrimaryKeys(c *C) { + // optional primary key headers + r := asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt1", "o1-defl") + defer r() + + pk, err := asserts.PrimaryKeyFromHeaders(asserts.TestOnlyType, map[string]string{"primary-key": "k1"}) + c.Assert(err, IsNil) + c.Check(pk, DeepEquals, []string{"k1", "o1-defl"}) + + pk, err = asserts.PrimaryKeyFromHeaders(asserts.TestOnlyType, map[string]string{"primary-key": "k1", "opt1": "B"}) + c.Assert(err, IsNil) + c.Check(pk, DeepEquals, []string{"k1", "B"}) + + hdrs, err := asserts.HeadersFromPrimaryKey(asserts.TestOnlyType, []string{"k1", "B"}) + c.Assert(err, IsNil) + c.Check(hdrs, DeepEquals, map[string]string{ + "primary-key": "k1", + "opt1": "B", + }) + + hdrs, err = asserts.HeadersFromPrimaryKey(asserts.TestOnlyType, []string{"k1"}) + c.Assert(err, IsNil) + c.Check(hdrs, DeepEquals, map[string]string{ + "primary-key": "k1", + "opt1": "o1-defl", + }) + + _, err = asserts.HeadersFromPrimaryKey(asserts.TestOnlyType, nil) + c.Check(err, ErrorMatches, `primary key has wrong length for "test-only" assertion`) + + _, err = asserts.HeadersFromPrimaryKey(asserts.TestOnlyType, []string{"pk", "opt1", "what"}) + c.Check(err, ErrorMatches, `primary key has wrong length for "test-only" assertion`) +} + func (as *assertsSuite) TestRef(c *C) { ref := &asserts.Ref{ Type: asserts.TestOnly2Type, @@ -190,6 +224,110 @@ c.Check(err, ErrorMatches, `"test-only-2" assertion reference primary key has the wrong length \(expected \[pk1 pk2\]\): \[abc\]`) } +func (as *assertsSuite) TestReducePrimaryKey(c *C) { + // optional primary key headers + defer asserts.MockOptionalPrimaryKey(asserts.TestOnly2Type, "opt1", "o1-defl")() + defer asserts.MockOptionalPrimaryKey(asserts.TestOnly2Type, "opt2", "o2-defl")() + + tests := []struct { + pk []string + reduced []string + }{ + {nil, nil}, + {[]string{"k1"}, []string{"k1"}}, + {[]string{"k1", "k2"}, []string{"k1", "k2"}}, + {[]string{"k1", "k2", "A"}, []string{"k1", "k2", "A"}}, + {[]string{"k1", "k2", "o1-defl"}, []string{"k1", "k2"}}, + {[]string{"k1", "k2", "A", "o2-defl"}, []string{"k1", "k2", "A"}}, + {[]string{"k1", "k2", "A", "B"}, []string{"k1", "k2", "A", "B"}}, + {[]string{"k1", "k2", "o1-defl", "B"}, []string{"k1", "k2", "o1-defl", "B"}}, + {[]string{"k1", "k2", "o1-defl", "o2-defl"}, []string{"k1", "k2"}}, + {[]string{"k1", "k2", "o1-defl", "o2-defl", "what"}, []string{"k1", "k2", "o1-defl", "o2-defl", "what"}}, + } + + for _, t := range tests { + c.Check(asserts.ReducePrimaryKey(asserts.TestOnly2Type, t.pk), DeepEquals, t.reduced) + } +} + +func (as *assertsSuite) TestRefOptionalPrimaryKeys(c *C) { + // optional primary key headers + defer asserts.MockOptionalPrimaryKey(asserts.TestOnly2Type, "opt1", "o1-defl")() + defer asserts.MockOptionalPrimaryKey(asserts.TestOnly2Type, "opt2", "o2-defl")() + + ref := &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc", "xyz"}, + } + c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz") + c.Check(ref.String(), Equals, `test-only-2 (xyz; pk1:abc)`) + + ref = &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc", "xyz", "o1-defl"}, + } + c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz") + c.Check(ref.String(), Equals, `test-only-2 (xyz; pk1:abc)`) + + ref = &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc", "xyz", "o1-defl", "o2-defl"}, + } + c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz") + c.Check(ref.String(), Equals, `test-only-2 (xyz; pk1:abc)`) + + ref = &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc", "xyz", "A"}, + } + c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz/A") + c.Check(ref.String(), Equals, `test-only-2 (xyz; pk1:abc opt1:A)`) + + ref = &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc", "xyz", "A", "o2-defl"}, + } + c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz/A") + c.Check(ref.String(), Equals, `test-only-2 (xyz; pk1:abc opt1:A)`) + + ref = &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc", "xyz", "o1-defl", "B"}, + } + c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz/o1-defl/B") + c.Check(ref.String(), Equals, `test-only-2 (xyz; pk1:abc opt2:B)`) + + ref = &asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"abc", "xyz", "A", "B"}, + } + c.Check(ref.Unique(), Equals, "test-only-2/abc/xyz/A/B") + c.Check(ref.String(), Equals, `test-only-2 (xyz; pk1:abc opt1:A opt2:B)`) +} + +func (as *assertsSuite) TestAcceptablePrimaryKey(c *C) { + // optional primary key headers + defer asserts.MockOptionalPrimaryKey(asserts.TestOnly2Type, "opt1", "o1-defl")() + defer asserts.MockOptionalPrimaryKey(asserts.TestOnly2Type, "opt2", "o2-defl")() + + tests := []struct { + pk []string + ok bool + }{ + {nil, false}, + {[]string{"k1"}, false}, + {[]string{"k1", "k2"}, true}, + {[]string{"k1", "k2", "A"}, true}, + {[]string{"k1", "k2", "o1-defl"}, true}, + {[]string{"k1", "k2", "A", "B"}, true}, + {[]string{"k1", "k2", "o1-defl", "o2-defl", "what"}, false}, + } + + for _, t := range tests { + c.Check(asserts.TestOnly2Type.AcceptablePrimaryKey(t.pk), Equals, t.ok) + } +} + func (as *assertsSuite) TestAtRevisionString(c *C) { ref := asserts.Ref{ Type: asserts.AccountType, @@ -232,6 +370,47 @@ c.Check(a.SignKeyID(), Equals, exKeyID) } +const exampleEmptyBodyOptionalPrimaryKeySet = "type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: abc\n" + + "opt1: A\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + +func (as *assertsSuite) TestDecodeOptionalPrimaryKeys(c *C) { + r := asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt1", "o1-defl") + defer r() + + a, err := asserts.Decode([]byte(exampleEmptyBodyAllDefaults)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + _, ok := a.(*asserts.TestOnly) + c.Check(ok, Equals, true) + c.Check(a.Revision(), Equals, 0) + c.Check(a.Format(), Equals, 0) + c.Check(a.Body(), IsNil) + c.Check(a.HeaderString("opt1"), Equals, "o1-defl") + c.Check(a.Header("header1"), IsNil) + c.Check(a.HeaderString("header1"), Equals, "") + c.Check(a.AuthorityID(), Equals, "auth-id1") + c.Check(a.SignKeyID(), Equals, exKeyID) + + a, err = asserts.Decode([]byte(exampleEmptyBodyOptionalPrimaryKeySet)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.TestOnlyType) + _, ok = a.(*asserts.TestOnly) + c.Check(ok, Equals, true) + c.Check(a.Revision(), Equals, 0) + c.Check(a.Format(), Equals, 0) + c.Check(a.Body(), IsNil) + c.Check(a.HeaderString("opt1"), Equals, "A") + c.Check(a.Header("header1"), IsNil) + c.Check(a.HeaderString("header1"), Equals, "") + c.Check(a.AuthorityID(), Equals, "auth-id1") + c.Check(a.SignKeyID(), Equals, exKeyID) +} + const exampleEmptyBody2NlNl = "type: test-only\n" + "authority-id: auth-id1\n" + "primary-key: xyz\n" + @@ -688,7 +867,7 @@ c.Check(cont1, DeepEquals, cont0) } -func (as *assertsSuite) TestSignFormatSanityEmptyBody(c *C) { +func (as *assertsSuite) TestSignFormatValidityEmptyBody(c *C) { headers := map[string]interface{}{ "authority-id": "auth-id1", "primary-key": "0", @@ -700,7 +879,7 @@ c.Check(err, IsNil) } -func (as *assertsSuite) TestSignFormatSanitySignatoryId(c *C) { +func (as *assertsSuite) TestSignFormatValiditySignatoryId(c *C) { headers := map[string]interface{}{ "authority-id": "auth-id1", "primary-key": "0", @@ -719,7 +898,7 @@ c.Check(err, IsNil) } -func (as *assertsSuite) TestSignFormatSanitySignatoryIdCoalesce(c *C) { +func (as *assertsSuite) TestSignFormatValiditySignatoryIdCoalesce(c *C) { headers := map[string]interface{}{ "authority-id": "auth-id1", "primary-key": "0", @@ -735,7 +914,7 @@ c.Check(err, IsNil) } -func (as *assertsSuite) TestSignFormatSanityNonEmptyBody(c *C) { +func (as *assertsSuite) TestSignFormatValidityNonEmptyBody(c *C) { headers := map[string]interface{}{ "authority-id": "auth-id1", "primary-key": "0", @@ -750,7 +929,7 @@ c.Check(decoded.Body(), DeepEquals, body) } -func (as *assertsSuite) TestSignFormatSanitySupportMultilineHeaderValues(c *C) { +func (as *assertsSuite) TestSignFormatValiditySupportMultilineHeaderValues(c *C) { headers := map[string]interface{}{ "authority-id": "auth-id1", "primary-key": "0", @@ -804,6 +983,69 @@ c.Check(a1.SupportedFormat(), Equals, true) } +func (as *assertsSuite) TestSignFormatOptionalPrimaryKeys(c *C) { + r := asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt1", "o1-defl") + defer r() + + headers := map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "k1", + "header1": "a", + } + a, err := asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + b := asserts.Encode(a) + c.Check(bytes.HasPrefix(b, []byte(`type: test-only +authority-id: auth-id1 +primary-key: k1 +header1:`)), Equals, true) + c.Check(a.HeaderString("opt1"), Equals, "o1-defl") + + _, err = asserts.Decode(b) + c.Check(err, IsNil) + + // defaults are always normalized away + headers = map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "k1", + "opt1": "o1-defl", + "header1": "a", + } + a, err = asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + b = asserts.Encode(a) + c.Check(bytes.HasPrefix(b, []byte(`type: test-only +authority-id: auth-id1 +primary-key: k1 +header1:`)), Equals, true) + c.Check(a.HeaderString("opt1"), Equals, "o1-defl") + + _, err = asserts.Decode(b) + c.Check(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "auth-id1", + "primary-key": "k1", + "opt1": "A", + "header1": "a", + } + a, err = asserts.AssembleAndSignInTest(asserts.TestOnlyType, headers, nil, testPrivKey1) + c.Assert(err, IsNil) + + b = asserts.Encode(a) + c.Check(bytes.HasPrefix(b, []byte(`type: test-only +authority-id: auth-id1 +primary-key: k1 +opt1: A +header1:`)), Equals, true) + c.Check(a.HeaderString("opt1"), Equals, "A") + + _, err = asserts.Decode(b) + c.Check(err, IsNil) +} + func (as *assertsSuite) TestSignBodyIsUTF8Text(c *C) { headers := map[string]interface{}{ "authority-id": "auth-id1", diff -Nru snapd-2.55.5+20.04/asserts/database.go snapd-2.57.5+20.04/asserts/database.go --- snapd-2.55.5+20.04/asserts/database.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/database.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2021 Canonical Ltd + * Copyright (C) 2015-2022 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 @@ -60,7 +60,10 @@ // previously stored revision with the same primary key headers. Put(assertType *AssertionType, assert Assertion) error // Get returns the assertion with the given unique key for its - // primary key headers. If none is present it returns a + // primary key headers. + // A suffix of optional primary keys can be left out from key + // in which case their default values are implied. + // If the assertion is not present it returns a // NotFoundError, usually with omitted Headers. Get(assertType *AssertionType, key []string, maxFormat int) (Assertion, error) // Search returns assertions matching the given headers. @@ -196,18 +199,26 @@ IsTrustedAccount(accountID string) bool // Find an assertion based on arbitrary headers. // Provided headers must contain the primary key for the assertion type. + // Optional primary key headers can be omitted in which case + // their default values will be used. // It returns a NotFoundError if the assertion cannot be found. Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) // FindPredefined finds an assertion in the predefined sets // (trusted or not) based on arbitrary headers. Provided // headers must contain the primary key for the assertion - // type. It returns a NotFoundError if the assertion cannot + // type. + // Optional primary key headers can be omitted in which case + // their default values will be used. + // It returns a NotFoundError if the assertion cannot // be found. FindPredefined(assertionType *AssertionType, headers map[string]string) (Assertion, error) // FindTrusted finds an assertion in the trusted set based on // arbitrary headers. Provided headers must contain the - // primary key for the assertion type. It returns a - // NotFoundError if the assertion cannot be found. + // primary key for the assertion type. + // Optional primary key headers can be omitted in which case + // their default values will be used. + // It returns a NotFoundError if the assertion cannot be + // found. FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) // FindMany finds assertions based on arbitrary headers. // It returns a NotFoundError if no assertion can be found. @@ -577,6 +588,8 @@ // Find an assertion based on arbitrary headers. // Provided headers must contain the primary key for the assertion type. +// Optional primary key headers can be omitted in which case +// their default values will be used. // It returns a NotFoundError if the assertion cannot be found. func (db *Database) Find(assertionType *AssertionType, headers map[string]string) (Assertion, error) { return find(db.backstores, assertionType, headers, -1) @@ -591,14 +604,18 @@ // FindPredefined finds an assertion in the predefined sets (trusted // or not) based on arbitrary headers. Provided headers must contain -// the primary key for the assertion type. It returns a NotFoundError -// if the assertion cannot be found. +// the primary key for the assertion type. +// Optional primary key headers can be omitted in which case +// their default values will be used. +// It returns a NotFoundError if the assertion cannot be found. func (db *Database) FindPredefined(assertionType *AssertionType, headers map[string]string) (Assertion, error) { return find([]Backstore{db.trusted, db.predefined}, assertionType, headers, -1) } // FindTrusted finds an assertion in the trusted set based on arbitrary headers. // Provided headers must contain the primary key for the assertion type. +// Optional primary key headers can be omitted in which case +// their default values will be used. // It returns a NotFoundError if the assertion cannot be found. func (db *Database) FindTrusted(assertionType *AssertionType, headers map[string]string) (Assertion, error) { return find([]Backstore{db.trusted}, assertionType, headers, -1) @@ -674,7 +691,7 @@ // form the sequence key using all keys but the last one which // is the sequence number - seqKey, err := keysFromHeaders(assertType.PrimaryKey[:len(assertType.PrimaryKey)-1], sequenceHeaders) + seqKey, err := keysFromHeaders(assertType.PrimaryKey[:len(assertType.PrimaryKey)-1], sequenceHeaders, nil) if err != nil { return nil, err } @@ -729,11 +746,32 @@ return nil, nil } if !signingKey.isValidAssumingCurTimeWithin(checkTimeEarliest, checkTimeLatest) { - return nil, fmt.Errorf("assertion is signed with expired public key %q from %q", assert.SignKeyID(), assert.SignatoryID()) + mismatchReason := timeMismatchMsg(checkTimeEarliest, checkTimeLatest, signingKey.since, signingKey.until) + return nil, fmt.Errorf("assertion is signed with expired public key %q from %q: %s", assert.SignKeyID(), assert.SignatoryID(), mismatchReason) } return delegationConstraints, nil } +func timeMismatchMsg(earliest, latest, keySince, keyUntil time.Time) string { + var msg string + + validFrom := earliest.Format(time.RFC3339) + if !latest.IsZero() && !latest.Equal(earliest) { + validTo := latest.Format(time.RFC3339) + msg = fmt.Sprintf("current time range is [%s, %s]", validFrom, validTo) + } else { + msg = fmt.Sprintf("current time is %s", validFrom) + } + + keyFrom := keySince.Format(time.RFC3339) + if !keyUntil.IsZero() { + keyTo := keyUntil.Format(time.RFC3339) + return msg + fmt.Sprintf(" but key is valid during [%s, %s)", keyFrom, keyTo) + } + + return msg + fmt.Sprintf(" but key is valid from %s", keyFrom) +} + // CheckSignature checks that the signature is valid. func CheckSignature(assert Assertion, signingKey *AccountKey, _ []*AssertionConstraints, roDB RODatabase, checkTimeEarliest, checkTimeLatest time.Time) (delegationConstraints []*AssertionConstraints, err error) { var pubKey PublicKey diff -Nru snapd-2.55.5+20.04/asserts/database_test.go snapd-2.57.5+20.04/asserts/database_test.go --- snapd-2.55.5+20.04/asserts/database_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/database_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -24,9 +24,11 @@ "crypto" "encoding/base64" "errors" + "fmt" "io/ioutil" "os" "path/filepath" + "regexp" "sort" "testing" "time" @@ -235,17 +237,54 @@ } func (chks *checkSuite) TestCheckExpiredPubKey(c *C) { + fixedTimeStr := "0003-01-01T00:00:00Z" + fixedTime, err := time.Parse(time.RFC3339, fixedTimeStr) + c.Assert(err, IsNil) + + restore := asserts.MockTimeNow(fixedTime) + defer restore() + trustedKey := testPrivKey0 + expiredAccKey := asserts.ExpiredAccountKeyForTest("canonical", trustedKey.PublicKey()) cfg := &asserts.DatabaseConfig{ Backstore: chks.bs, - Trusted: []asserts.Assertion{asserts.ExpiredAccountKeyForTest("canonical", trustedKey.PublicKey())}, + Trusted: []asserts.Assertion{expiredAccKey}, } db, err := asserts.OpenDatabase(cfg) c.Assert(err, IsNil) + expSince := regexp.QuoteMeta(expiredAccKey.Since().Format(time.RFC3339)) + expUntil := regexp.QuoteMeta(expiredAccKey.Until().Format(time.RFC3339)) + curTime := regexp.QuoteMeta(fixedTimeStr) + err = db.Check(chks.a) + c.Assert(err, ErrorMatches, fmt.Sprintf(`assertion is signed with expired public key "[[:alnum:]_-]+" from "canonical": current time is %s but key is valid during \[%s, %s\)`, curTime, expSince, expUntil)) +} + +func (chks *checkSuite) TestCheckExpiredPubKeyNoUntil(c *C) { + curTimeStr := "0002-01-01T00:00:00Z" + curTime, err := time.Parse(time.RFC3339, curTimeStr) + c.Assert(err, IsNil) + + restore := asserts.MockTimeNow(curTime) + defer restore() + + trustedKey := testPrivKey0 + + keyTimeStr := "0003-01-01T00:00:00Z" + keyTime, err := time.Parse(time.RFC3339, keyTimeStr) + c.Assert(err, IsNil) + expiredAccKey := asserts.MakeAccountKeyForTestWithUntil("canonical", trustedKey.PublicKey(), keyTime, time.Time{}, 1) + cfg := &asserts.DatabaseConfig{ + Backstore: chks.bs, + Trusted: []asserts.Assertion{expiredAccKey}, + } + + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + err = db.Check(chks.a) - c.Assert(err, ErrorMatches, `assertion is signed with expired public key "[[:alnum:]_-]+" from "canonical"`) + c.Assert(err, ErrorMatches, fmt.Sprintf(`assertion is signed with expired public key "[[:alnum:]_-]+" from "canonical": current time is %s but key is valid from %s`, regexp.QuoteMeta(curTimeStr), regexp.QuoteMeta(keyTimeStr))) } func (chks *checkSuite) TestCheckForgery(c *C) { @@ -1510,6 +1549,99 @@ c.Check(err, ErrorMatches, `cannot find "test-only" assertions for format 3 higher than supported format 1`) } +func (safs *signAddFindSuite) TestFindOptionalPrimaryKeys(c *C) { + r := asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt1", "o1-defl") + defer r() + + headers := map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "k1", + } + a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a1) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "primary-key": "k2", + "opt1": "A", + } + a2, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + err = safs.db.Add(a2) + c.Assert(err, IsNil) + + a, err := safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "k1", + }) + c.Assert(err, IsNil) + c.Check(a.HeaderString("primary-key"), Equals, "k1") + c.Check(a.HeaderString("opt1"), Equals, "o1-defl") + + a, err = safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "k1", + "opt1": "o1-defl", + }) + c.Assert(err, IsNil) + c.Check(a.HeaderString("primary-key"), Equals, "k1") + c.Check(a.HeaderString("opt1"), Equals, "o1-defl") + + a, err = safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "k2", + "opt1": "A", + }) + c.Assert(err, IsNil) + c.Check(a.HeaderString("primary-key"), Equals, "k2") + c.Check(a.HeaderString("opt1"), Equals, "A") + + _, err = safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "k3", + }) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: map[string]string{ + "primary-key": "k3", + }, + }) + + _, err = safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "k2", + }) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: map[string]string{ + "primary-key": "k2", + }, + }) + + _, err = safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "k2", + "opt1": "B", + }) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: map[string]string{ + "primary-key": "k2", + "opt1": "B", + }, + }) + + _, err = safs.db.Find(asserts.TestOnlyType, map[string]string{ + "primary-key": "k1", + "opt1": "B", + }) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + Headers: map[string]string{ + "primary-key": "k1", + "opt1": "B", + }, + }) +} + func (safs *signAddFindSuite) TestWithStackedBackstore(c *C) { headers := map[string]interface{}{ "authority-id": "canonical", diff -Nru snapd-2.55.5+20.04/asserts/export_test.go snapd-2.57.5+20.04/asserts/export_test.go --- snapd-2.55.5+20.04/asserts/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -69,6 +69,10 @@ } func MakeAccountKeyForTest(authorityID string, openPGPPubKey PublicKey, since time.Time, validYears int) *AccountKey { + return MakeAccountKeyForTestWithUntil(authorityID, openPGPPubKey, since, since.AddDate(validYears, 0, 0), validYears) +} + +func MakeAccountKeyForTestWithUntil(authorityID string, openPGPPubKey PublicKey, since, until time.Time, validYears int) *AccountKey { return &AccountKey{ assertionBase: assertionBase{ headers: map[string]interface{}{ @@ -80,7 +84,7 @@ }, sinceUntil: sinceUntil{ since: since.UTC(), - until: since.UTC().AddDate(validYears, 0, 0), + until: until.UTC(), }, pubKey: openPGPPubKey, } @@ -104,7 +108,7 @@ } } -// define dummy assertion types to use in the tests +// define test assertion types to use in the tests type TestOnly struct { assertionBase @@ -118,7 +122,7 @@ return &TestOnly{assert}, nil } -var TestOnlyType = &AssertionType{"test-only", []string{"primary-key"}, assembleTestOnly, 0} +var TestOnlyType = &AssertionType{"test-only", []string{"primary-key"}, nil, assembleTestOnly, 0} type TestOnly2 struct { assertionBase @@ -128,7 +132,7 @@ return &TestOnly2{assert}, nil } -var TestOnly2Type = &AssertionType{"test-only-2", []string{"pk1", "pk2"}, assembleTestOnly2, 0} +var TestOnly2Type = &AssertionType{"test-only-2", []string{"pk1", "pk2"}, nil, assembleTestOnly2, 0} // TestOnlyDecl is a test-only assertion that mimics snap-declaration // relations with other assertions. @@ -154,7 +158,7 @@ return &TestOnlyDecl{assert}, nil } -var TestOnlyDeclType = &AssertionType{"test-only-decl", []string{"id"}, assembleTestOnlyDecl, 0} +var TestOnlyDeclType = &AssertionType{"test-only-decl", []string{"id"}, nil, assembleTestOnlyDecl, 0} // TestOnlyRev is a test-only assertion that mimics snap-revision // relations with other assertions. @@ -185,7 +189,7 @@ return &TestOnlyRev{assert}, nil } -var TestOnlyRevType = &AssertionType{"test-only-rev", []string{"h"}, assembleTestOnlyRev, 0} +var TestOnlyRevType = &AssertionType{"test-only-rev", []string{"h"}, nil, assembleTestOnlyRev, 0} // TestOnlySeq is a test-only assertion that is sequence-forming. type TestOnlySeq struct { @@ -212,7 +216,7 @@ }, nil } -var TestOnlySeqType = &AssertionType{"test-only-seq", []string{"n", "sequence"}, assembleTestOnlySeq, sequenceForming} +var TestOnlySeqType = &AssertionType{"test-only-seq", []string{"n", "sequence"}, nil, assembleTestOnlySeq, sequenceForming} type TestOnlyNoAuthority struct { assertionBase @@ -225,7 +229,7 @@ return &TestOnlyNoAuthority{assert}, nil } -var TestOnlyNoAuthorityType = &AssertionType{"test-only-no-authority", nil, assembleTestOnlyNoAuthority, noAuthority} +var TestOnlyNoAuthorityType = &AssertionType{"test-only-no-authority", nil, nil, assembleTestOnlyNoAuthority, noAuthority} type TestOnlyNoAuthorityPK struct { assertionBase @@ -235,7 +239,7 @@ return &TestOnlyNoAuthorityPK{assert}, nil } -var TestOnlyNoAuthorityPKType = &AssertionType{"test-only-no-authority-pk", []string{"pk"}, assembleTestOnlyNoAuthorityPK, noAuthority} +var TestOnlyNoAuthorityPKType = &AssertionType{"test-only-no-authority-pk", []string{"pk"}, nil, assembleTestOnlyNoAuthorityPK, noAuthority} func init() { typeRegistry[TestOnlyType.Name] = TestOnlyType diff -Nru snapd-2.55.5+20.04/asserts/fsbackstore.go snapd-2.57.5+20.04/asserts/fsbackstore.go --- snapd-2.55.5+20.04/asserts/fsbackstore.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/fsbackstore.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2020 Canonical Ltd + * Copyright (C) 2015-2022 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 @@ -100,14 +100,32 @@ return a, nil } -func diskPrimaryPathComps(primaryPath []string, active string) []string { +// diskPrimaryPathComps computes the components of the path for an assertion. +// The path will look like this: (all are query escaped) +// /...[/0:[/1:]...]/ +// The components #: for the optional primary path values +// appear only if their value is not the default. +// This makes it so that assertions with default values have the same +// paths as for snapd versions without those optional primary keys +// yet. +func diskPrimaryPathComps(assertType *AssertionType, primaryPath []string, active string) []string { n := len(primaryPath) - comps := make([]string, n+1) + comps := make([]string, 0, n+1) // safety against '/' etc + noptional := -1 for i, comp := range primaryPath { - comps[i] = url.QueryEscape(comp) + defl := assertType.OptionalPrimaryKeyDefaults[assertType.PrimaryKey[i]] + qvalue := url.QueryEscape(comp) + if defl != "" { + noptional++ + if comp == defl { + continue + } + qvalue = fmt.Sprintf("%d:%s", noptional, qvalue) + } + comps = append(comps, qvalue) } - comps[n] = active + comps = append(comps, active) return comps } @@ -122,7 +140,7 @@ return err } - comps := diskPrimaryPathComps(primaryPath, "active*") + comps := diskPrimaryPathComps(assertType, primaryPath, "active*") assertTypeTop := filepath.Join(fsbs.top, assertType.Name) err := findWildcard(assertTypeTop, comps, 0, namesCb) if err != nil { @@ -158,7 +176,7 @@ if formatnum > 0 { activeFn = fmt.Sprintf("active.%d", formatnum) } - diskPrimaryPath := filepath.Join(diskPrimaryPathComps(primaryPath, activeFn)...) + diskPrimaryPath := filepath.Join(diskPrimaryPathComps(assertType, primaryPath, activeFn)...) err = atomicWriteEntry(Encode(assert), false, fsbs.top, assertType.Name, diskPrimaryPath) if err != nil { return fmt.Errorf("broken assertion storage, cannot write assertion: %v", err) @@ -170,6 +188,10 @@ fsbs.mu.RLock() defer fsbs.mu.RUnlock() + if len(key) > len(assertType.PrimaryKey) { + return nil, fmt.Errorf("internal error: Backstore.Get given a key longer than expected for %q: %v", assertType.Name, key) + } + a, err := fsbs.currentAssertion(assertType, key, maxFormat) if err == errNotFound { return nil, &NotFoundError{Type: assertType} @@ -197,13 +219,42 @@ return nil } +func (fsbs *filesystemBackstore) searchOptional(assertType *AssertionType, kopt, pattPos, firstOpt int, diskPattern []string, headers map[string]string, foundCb func(Assertion), maxFormat int) error { + if kopt == len(assertType.PrimaryKey) { + candCb := func(a Assertion) { + if searchMatch(a, headers) { + foundCb(a) + } + } + + diskPattern[pattPos] = "active*" + return fsbs.search(assertType, diskPattern[:pattPos+1], candCb, maxFormat) + } + k := assertType.PrimaryKey[kopt] + keyVal := headers[k] + switch keyVal { + case "": + diskPattern[pattPos] = fmt.Sprintf("%d:*", kopt-firstOpt) + if err := fsbs.searchOptional(assertType, kopt+1, pattPos+1, firstOpt, diskPattern, headers, foundCb, maxFormat); err != nil { + return err + } + fallthrough + case assertType.OptionalPrimaryKeyDefaults[k]: + return fsbs.searchOptional(assertType, kopt+1, pattPos, firstOpt, diskPattern, headers, foundCb, maxFormat) + default: + diskPattern[pattPos] = fmt.Sprintf("%d:%s", kopt-firstOpt, url.QueryEscape(keyVal)) + return fsbs.searchOptional(assertType, kopt+1, pattPos+1, firstOpt, diskPattern, headers, foundCb, maxFormat) + } +} + func (fsbs *filesystemBackstore) Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error { fsbs.mu.RLock() defer fsbs.mu.RUnlock() n := len(assertType.PrimaryKey) + nopt := len(assertType.OptionalPrimaryKeyDefaults) diskPattern := make([]string, n+1) - for i, k := range assertType.PrimaryKey { + for i, k := range assertType.PrimaryKey[:n-nopt] { keyVal := headers[k] if keyVal == "" { diskPattern[i] = "*" @@ -211,14 +262,9 @@ diskPattern[i] = url.QueryEscape(keyVal) } } - diskPattern[n] = "active*" + pattPos := n - nopt - candCb := func(a Assertion) { - if searchMatch(a, headers) { - foundCb(a) - } - } - return fsbs.search(assertType, diskPattern, candCb, maxFormat) + return fsbs.searchOptional(assertType, pattPos, pattPos, pattPos, diskPattern, headers, foundCb, maxFormat) } // errFound marks the case an assertion was found diff -Nru snapd-2.55.5+20.04/asserts/fsbackstore_test.go snapd-2.57.5+20.04/asserts/fsbackstore_test.go --- snapd-2.55.5+20.04/asserts/fsbackstore_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/fsbackstore_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2020 Canonical Ltd + * Copyright (C) 2016-2022 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 @@ import ( "os" "path/filepath" + "strings" "syscall" . "gopkg.in/check.v1" @@ -370,3 +371,547 @@ Type: asserts.TestOnlySeqType, }) } + +func (fsbss *fsBackstoreSuite) TestOptionalPrimaryKeys(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + a1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k1\n" + + "marker: a1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + + a, err := bs.Get(asserts.TestOnlyType, []string{"k1"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k1"}) + + r := asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt1", "o1-defl") + defer r() + + a2, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k2\n" + + "marker: a2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a2) + c.Assert(err, IsNil) + a3, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k3\n" + + "opt1: o1-a3\n" + + "marker: a3\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a3) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"k1"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k1", "o1-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a1") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k1", "o1-defl"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k1", "o1-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a1") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k2"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k2", "o1-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a2") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k3", "o1-a3"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k3", "o1-a3"}) + c.Check(a.HeaderString("marker"), Equals, "a3") + + r2 := asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt2", "o2-defl") + defer r() + + a4, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k4\n" + + "marker: a4\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a4) + c.Assert(err, IsNil) + a5, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k3\n" + + "opt2: o2-a5\n" + + "marker: a5\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a5) + c.Assert(err, IsNil) + a6, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k5\n" + + "opt1: o1-a6\n" + + "opt2: o2-a6\n" + + "marker: a6\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a6) + c.Assert(err, IsNil) + + a, err = bs.Get(asserts.TestOnlyType, []string{"k1"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k1", "o1-defl", "o2-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a1") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k1", "o1-defl"}, 0) + c.Assert(err, IsNil) + c.Check(a.HeaderString("marker"), Equals, "a1") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k2", "o1-defl", "o2-defl"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k2", "o1-defl", "o2-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a2") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k3", "o1-a3"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k3", "o1-a3", "o2-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a3") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k4"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k4", "o1-defl", "o2-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a4") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k3", "o1-defl", "o2-a5"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k3", "o1-defl", "o2-a5"}) + c.Check(a.HeaderString("marker"), Equals, "a5") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k5", "o1-a6", "o2-a6"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k5", "o1-a6", "o2-a6"}) + c.Check(a.HeaderString("marker"), Equals, "a6") + + // revert the previous type definition + r2() + + a, err = bs.Get(asserts.TestOnlyType, []string{"k1"}, 0) + c.Assert(err, IsNil) + c.Check(a.HeaderString("marker"), Equals, "a1") + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k1", "o1-defl"}) + a, err = bs.Get(asserts.TestOnlyType, []string{"k1", "o1-defl"}, 0) + c.Assert(err, IsNil) + c.Check(a.HeaderString("marker"), Equals, "a1") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k2", "o1-defl"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k2", "o1-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a2") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k3", "o1-a3"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k3", "o1-a3"}) + c.Check(a.HeaderString("marker"), Equals, "a3") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k4"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k4", "o1-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a4") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k3", "o1-defl"}, 0) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + }) + a, err = bs.Get(asserts.TestOnlyType, []string{"k5", "o1-a6"}, 0) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + }) + + // revert to initial type definition + r() + a, err = bs.Get(asserts.TestOnlyType, []string{"k1"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k1"}) + a, err = bs.Get(asserts.TestOnlyType, []string{"k2"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k2"}) + a, err = bs.Get(asserts.TestOnlyType, []string{"k3"}, 0) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + }) + a, err = bs.Get(asserts.TestOnlyType, []string{"k4"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k4"}) + a, err = bs.Get(asserts.TestOnlyType, []string{"k5"}, 0) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + }) +} + +func (fsbss *fsBackstoreSuite) TestOptionalPrimaryKeysSearch(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + a1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k1\n" + + "v: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + + r := asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt1", "o1-defl") + defer r() + + a2, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k1\n" + + "opt1: A\n" + + "v: y\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a2) + c.Assert(err, IsNil) + + a3, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k2\n" + + "opt1: A\n" + + "v: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a3) + c.Assert(err, IsNil) + + a4, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k3\n" + + "opt1: B\n" + + "v: y\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a4) + c.Assert(err, IsNil) + + a5, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k4\n" + + "opt1: B\n" + + "v: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a5) + c.Assert(err, IsNil) + + a6, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k3\n" + + "v: y\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a6) + c.Assert(err, IsNil) + + var found map[string]string + foundCb := func(a asserts.Assertion) { + if found == nil { + found = make(map[string]string) + } + found[strings.Join(a.Ref().PrimaryKey, "/")] = a.HeaderString("v") + } + + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "k1", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/o1-defl": "x", + "k1/A": "y", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "k3", + "opt1": "o1-defl", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k3/o1-defl": "y", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "opt1": "o1-defl", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/o1-defl": "x", + "k3/o1-defl": "y", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "opt1": "A", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/A": "y", + "k2/A": "x", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "opt1": "B", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k3/B": "y", + "k4/B": "x", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "v": "x", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/o1-defl": "x", + "k2/A": "x", + "k4/B": "x", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "v": "y", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/A": "y", + "k3/B": "y", + "k3/o1-defl": "y", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, nil, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/o1-defl": "x", + "k1/A": "y", + "k2/A": "x", + "k3/o1-defl": "y", + "k3/B": "y", + "k4/B": "x", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "k4", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k4/B": "x", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "k3", + "opt1": "B", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k3/B": "y", + }) + + // revert to initial type definition + r() + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "k1", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1": "x", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "k3", + "opt1": "o1-defl", + }, foundCb, 0) + c.Assert(err, IsNil) + // found nothing + c.Check(found, IsNil) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "v": "x", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1": "x", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "v": "y", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k3": "y", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, nil, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1": "x", + "k3": "y", + }) +} + +func (fsbss *fsBackstoreSuite) TestOptionalPrimaryKeysSearchTwoOptional(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + a1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k1\n" + + "v: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + + a2, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k2\n" + + "v: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a2) + c.Assert(err, IsNil) + + r := asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt1", "o1-defl") + defer r() + asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt2", "o2-defl") + + a3, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k1\n" + + "opt1: A\n" + + "v: y\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a3) + c.Assert(err, IsNil) + + a4, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k2\n" + + "opt2: B\n" + + "v: y\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a4) + c.Assert(err, IsNil) + + a5, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k2\n" + + "opt1: A2\n" + + "opt2: B2\n" + + "v: x\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a5) + c.Assert(err, IsNil) + + var found map[string]string + foundCb := func(a asserts.Assertion) { + if found == nil { + found = make(map[string]string) + } + found[strings.Join(a.Ref().PrimaryKey, "/")] = a.HeaderString("v") + } + + err = bs.Search(asserts.TestOnlyType, nil, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/o1-defl/o2-defl": "x", + "k2/o1-defl/o2-defl": "x", + "k1/A/o2-defl": "y", + "k2/o1-defl/B": "y", + "k2/A2/B2": "x", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "opt2": "B", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k2/o1-defl/B": "y", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "opt1": "o1-defl", + "opt2": "B", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k2/o1-defl/B": "y", + }) + + found = nil + err = bs.Search(asserts.TestOnlyType, map[string]string{ + "opt1": "A2", + }, foundCb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k2/A2/B2": "x", + }) + +} diff -Nru snapd-2.55.5+20.04/asserts/info/main.go snapd-2.57.5+20.04/asserts/info/main.go --- snapd-2.55.5+20.04/asserts/info/main.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/info/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,37 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 . + * + */ + +// info produces information about assertions to include in /usr/lib/snapd/info. +package main + +import ( + "encoding/json" + "fmt" + + "github.com/snapcore/snapd/asserts" +) + +func main() { + maxFormats := asserts.MaxSupportedFormats(1) + b, err := json.Marshal(maxFormats) + if err != nil { + panic(fmt.Sprintf("cannot json marshal asserts info: %v", err)) + } + fmt.Printf("SNAPD_ASSERTS_FORMATS='%s'\n", b) +} diff -Nru snapd-2.55.5+20.04/asserts/membackstore.go snapd-2.57.5+20.04/asserts/membackstore.go --- snapd-2.55.5+20.04/asserts/membackstore.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/membackstore.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2020 Canonical Ltd + * Copyright (C) 2016-2022 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 @@ -235,9 +235,23 @@ mbs.mu.RLock() defer mbs.mu.RUnlock() + n := len(assertType.PrimaryKey) + if len(key) > n { + return nil, fmt.Errorf("internal error: Backstore.Get given a key longer than expected for %q: %v", assertType.Name, key) + } + internalKey := make([]string, 1+len(assertType.PrimaryKey)) internalKey[0] = assertType.Name copy(internalKey[1:], key) + if len(key) < n { + for kopt := len(key); kopt < n; kopt++ { + defl := assertType.OptionalPrimaryKeyDefaults[assertType.PrimaryKey[kopt]] + if defl == "" { + return nil, fmt.Errorf("internal error: Backstore.Get given a key missing mandatory elements for %q: %v", assertType.Name, key) + } + internalKey[kopt+1] = defl + } + } a, err := mbs.top.get(internalKey, maxFormat) if err == errNotFound { diff -Nru snapd-2.55.5+20.04/asserts/membackstore_test.go snapd-2.57.5+20.04/asserts/membackstore_test.go --- snapd-2.55.5+20.04/asserts/membackstore_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/membackstore_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2020 Canonical Ltd + * Copyright (C) 2016-2022 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,6 +20,8 @@ package asserts_test import ( + "strings" + . "gopkg.in/check.v1" "github.com/snapcore/snapd/asserts" @@ -537,3 +539,97 @@ Type: asserts.TestOnlySeqType, }) } + +func (mbss *memBackstoreSuite) TestOptionalPrimaryKeys(c *C) { + r := asserts.MockOptionalPrimaryKey(asserts.TestOnlyType, "opt1", "o1-defl") + defer r() + bs := mbss.bs + + a1, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k1\n" + + "marker: a1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a1) + c.Assert(err, IsNil) + + a2, err := asserts.Decode([]byte("type: test-only\n" + + "authority-id: auth-id1\n" + + "primary-key: k2\n" + + "opt1: A\n" + + "marker: a2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + err = bs.Put(asserts.TestOnlyType, a2) + c.Assert(err, IsNil) + + a, err := bs.Get(asserts.TestOnlyType, []string{"k1"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k1", "o1-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a1") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k1", "o1-defl"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k1", "o1-defl"}) + c.Check(a.HeaderString("marker"), Equals, "a1") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k2", "A"}, 0) + c.Assert(err, IsNil) + c.Check(a.Ref().PrimaryKey, DeepEquals, []string{"k2", "A"}) + c.Check(a.HeaderString("marker"), Equals, "a2") + + a, err = bs.Get(asserts.TestOnlyType, []string{"k2"}, 0) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlyType, + }) + + a, err = bs.Get(asserts.TestOnlyType, []string{}, 0) + c.Check(err, ErrorMatches, `internal error: Backstore.Get given a key missing mandatory elements for "test-only":.*`) + + var found map[string]string + cb := func(a asserts.Assertion) { + if found == nil { + found = make(map[string]string) + } + found[strings.Join(a.Ref().PrimaryKey, "/")] = a.HeaderString("marker") + + } + err = mbss.bs.Search(asserts.TestOnlyType, nil, cb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/o1-defl": "a1", + "k2/A": "a2", + }) + + found = nil + err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{ + "primary-key": "k1", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/o1-defl": "a1", + }) + + found = nil + err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{ + "opt1": "o1-defl", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k1/o1-defl": "a1", + }) + + found = nil + err = mbss.bs.Search(asserts.TestOnlyType, map[string]string{ + "opt1": "A", + }, cb, 0) + c.Assert(err, IsNil) + c.Check(found, DeepEquals, map[string]string{ + "k2/A": "a2", + }) +} diff -Nru snapd-2.55.5+20.04/asserts/model.go snapd-2.57.5+20.04/asserts/model.go --- snapd-2.55.5+20.04/asserts/model.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/model.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2020 Canonical Ltd + * Copyright (C) 2016-2022 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 @@ -369,6 +369,8 @@ ModelDangerous: 0x10000, ModelSigned: 0x80000, ModelSecured: 0x100000, + // reserved by secboot to measure classic models + // "ClassicModelGradeMask": 0x80000000 } // Code returns a bit representation of the grade, for example for @@ -436,6 +438,11 @@ return mod.classic } +// Distribution returns the linux distro specified in the model. +func (mod *Model) Distribution() string { + return mod.HeaderString("distribution") +} + // Architecture returns the architecture the model is based on. func (mod *Model) Architecture() string { return mod.HeaderString("architecture") @@ -633,9 +640,13 @@ var ( modelMandatory = []string{"architecture", "gadget", "kernel"} - extendedCoreMandatory = []string{"architecture", "base"} + extendedMandatory = []string{"architecture", "base"} extendedSnapsConflicting = []string{"gadget", "kernel", "required-snaps"} classicModelOptional = []string{"architecture", "gadget"} + + // The distribution header must be a valid ID according to + // https://www.freedesktop.org/software/systemd/man/os-release.html#ID= + validDistribution = regexp.MustCompile(`^[a-z0-9._-]*$`) ) func assembleModel(assert assertionBase) (Assertion, error) { @@ -657,10 +668,6 @@ // Core 20 extended snaps header extendedSnaps, extended := assert.headers["snaps"] if extended { - if classic { - return nil, fmt.Errorf("cannot use extended snaps header for a classic model (yet)") - } - for _, conflicting := range extendedSnapsConflicting { if _, ok := assert.headers[conflicting]; ok { return nil, fmt.Errorf("cannot specify separate %q header once using the extended snaps header", conflicting) @@ -675,19 +682,30 @@ } } - if classic { + if classic && !extended { if _, ok := assert.headers["kernel"]; ok { - return nil, fmt.Errorf("cannot specify a kernel with a classic model") + return nil, fmt.Errorf("cannot specify a kernel with a non-extended classic model") } if _, ok := assert.headers["base"]; ok { - return nil, fmt.Errorf("cannot specify a base with a classic model") + return nil, fmt.Errorf("cannot specify a base with a non-extended classic model") + } + } + + // distribution mandatory for classic with extended snaps, not + // allowed otherwise. + if classic && extended { + _, err := checkStringMatches(assert.headers, "distribution", validDistribution) + if err != nil { + return nil, fmt.Errorf("%v, see distribution ID in os-release spec", err) } + } else if _, ok := assert.headers["distribution"]; ok { + return nil, fmt.Errorf("cannot specify distribution for model unless it is classic and has an extended snaps header") } checker := checkNotEmptyString toCheck := modelMandatory if extended { - toCheck = extendedCoreMandatory + toCheck = extendedMandatory } else if classic { checker = checkOptionalString toCheck = classicModelOptional diff -Nru snapd-2.55.5+20.04/asserts/model_test.go snapd-2.57.5+20.04/asserts/model_test.go --- snapd-2.55.5+20.04/asserts/model_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/model_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -139,6 +139,60 @@ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + "AXNpZw==" + + classicModelWithSnapsExample = `type: model +authority-id: brand-id1 +series: 16 +brand-id: brand-id1 +model: baz-3000 +display-name: Baz 3000 +architecture: amd64 +system-user-authority: * +base: core20 +classic: true +distribution: ubuntu +store: brand-store +snaps: + - + name: baz-linux + id: bazlinuxidididididididididididid + type: kernel + default-channel: 20 + - + name: brand-gadget + id: brandgadgetdidididididididididid + type: gadget + - + name: other-base + id: otherbasedididididididididididid + type: base + modes: + - run + presence: required + - + name: nm + id: nmididididididididididididididid + modes: + - ephemeral + - run + default-channel: 1.0 + - + name: myapp + id: myappdididididididididididididid + type: app + default-channel: 2.0 + - + name: myappopt + id: myappoptidididididididididididid + type: app + presence: optional +OTHERgrade: secured +storage-safety: encrypted +` + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" ) func (mods *modelSuite) TestDecodeOK(c *C) { @@ -537,6 +591,7 @@ c.Check(model.Model(), Equals, "baz-3000") c.Check(model.DisplayName(), Equals, "Baz 3000") c.Check(model.Classic(), Equals, true) + c.Check(model.Distribution(), Equals, "") c.Check(model.Architecture(), Equals, "amd64") c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{ Name: "brand-gadget", @@ -595,8 +650,8 @@ {"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`}, - {"gadget: brand-gadget\n", "base: some-base\n", `cannot specify a base with a classic model`}, + {"gadget: brand-gadget\n", "kernel: brand-kernel\n", `cannot specify a kernel with a non-extended classic model`}, + {"gadget: brand-gadget\n", "base: some-base\n", `cannot specify a base with a non-extended classic model`}, {"gadget: brand-gadget\n", "gadget:\n - xyz\n", `"gadget" header must be a string`}, } @@ -616,14 +671,29 @@ c.Check(a.Type(), Equals, asserts.ModelType) model := a.(*asserts.Model) c.Check(model.Classic(), Equals, true) + c.Check(model.Distribution(), Equals, "") c.Check(model.Architecture(), Equals, "") c.Check(model.GadgetSnap(), IsNil) c.Check(model.Gadget(), Equals, "") c.Check(model.GadgetTrack(), Equals, "") } -func (mods *modelSuite) TestCore20DecodeOK(c *C) { - encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) +func (mods *modelSuite) TestWithSnapsDecodeOK(c *C) { + tt := []struct { + modelRaw string + isClassic bool + }{ + {modelRaw: core20ModelExample, isClassic: false}, + {modelRaw: classicModelWithSnapsExample, isClassic: true}, + } + + for _, t := range tt { + mods.testWithSnapsDecodeOK(c, t.modelRaw, t.isClassic) + } +} + +func (mods *modelSuite) testWithSnapsDecodeOK(c *C, modelRaw string, isClassic bool) { + encoded := strings.Replace(modelRaw, "TSLINE", mods.tsLine, 1) encoded = strings.Replace(encoded, "OTHER", "", 1) a, err := asserts.Decode([]byte(encoded)) c.Assert(err, IsNil) @@ -636,6 +706,12 @@ c.Check(model.Model(), Equals, "baz-3000") c.Check(model.DisplayName(), Equals, "Baz 3000") c.Check(model.Architecture(), Equals, "amd64") + c.Check(model.Classic(), Equals, isClassic) + if isClassic { + c.Check(model.Distribution(), Equals, "ubuntu") + } else { + c.Check(model.Distribution(), Equals, "") + } c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{ Name: "brand-gadget", SnapID: "brandgadgetdidididididididididid", @@ -892,15 +968,28 @@ } } -func (mods *modelSuite) TestCore20DecodeInvalid(c *C) { - encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) +func (mods *modelSuite) TestWithSnapsDecodeInvalid(c *C) { + tt := []struct { + modelRaw string + isClassic bool + }{ + {modelRaw: core20ModelExample, isClassic: false}, + {modelRaw: classicModelWithSnapsExample, isClassic: true}, + } + + for _, t := range tt { + mods.testWithSnapsDecodeInvalid(c, t.modelRaw, t.isClassic) + } +} + +func (mods *modelSuite) testWithSnapsDecodeInvalid(c *C, modelRaw string, isClassic bool) { + encoded := strings.Replace(modelRaw, "TSLINE", mods.tsLine, 1) snapsStanza := encoded[strings.Index(encoded, "snaps:"):strings.Index(encoded, "grade:")] invalidTests := []struct{ original, invalid, expectedErr string }{ {"base: core20\n", "", `"base" header is mandatory`}, {"base: core20\n", "base: alt-base\n", `cannot specify not well-known base "alt-base" without a corresponding "snaps" header entry`}, - {"OTHER", "classic: true\n", `cannot use extended snaps header for a classic model \(yet\)`}, {snapsStanza, "snaps: snap\n", `"snaps" header must be a list of maps`}, {snapsStanza, "snaps:\n - snap\n", `"snaps" header must be a list of maps`}, {"name: myapp\n", "other: 1\n", `"name" of snap is mandatory`}, @@ -933,6 +1022,19 @@ {"storage-safety: encrypted\n", "storage-safety: foo\n", `storage-safety for model must be encrypted\|prefer-encrypted\|prefer-unencrypted, not "foo"`}, {"storage-safety: encrypted\n", "storage-safety: prefer-unencrypted\n", `secured grade model must not have storage-safety overridden, only "encrypted" is valid`}, } + if isClassic { + classicInvalid := []struct{ original, invalid, expectedErr string }{ + {"distribution: ubuntu\n", "", `"distribution" header is mandatory, see distribution ID in os-release spec`}, + {"distribution: ubuntu\n", "distribution: Ubuntu\n", `"distribution" header contains invalid characters: "Ubuntu", see distribution ID in os-release spec`}, + {"distribution: ubuntu\n", "distribution: *buntu\n", `"distribution" header contains invalid characters: "\*buntu", see distribution ID in os-release spec`}, + } + invalidTests = append(invalidTests, classicInvalid...) + } else { + coreInvalid := []struct{ original, invalid, expectedErr string }{ + {"OTHER", "distribution: ubuntu\n", `cannot specify distribution for model unless it is classic and has an extended snaps header`}, + } + invalidTests = append(invalidTests, coreInvalid...) + } for _, test := range invalidTests { invalid := strings.Replace(encoded, test.original, test.invalid, 1) invalid = strings.Replace(invalid, "OTHER", "", 1) diff -Nru snapd-2.55.5+20.04/asserts/preseed.go snapd-2.57.5+20.04/asserts/preseed.go --- snapd-2.55.5+20.04/asserts/preseed.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/preseed.go 2022-10-17 16:25:18.000000000 +0000 @@ -173,10 +173,12 @@ } seen[preseedSnap.Name] = true snapID := preseedSnap.SnapID - if underName := seenIDs[snapID]; underName != "" { - return nil, fmt.Errorf("cannot specify the same snap id %q multiple times, specified for snaps %q and %q", snapID, underName, preseedSnap.Name) + if snapID != "" { + if underName := seenIDs[snapID]; underName != "" { + return nil, fmt.Errorf("cannot specify the same snap id %q multiple times, specified for snaps %q and %q", snapID, underName, preseedSnap.Name) + } + seenIDs[snapID] = preseedSnap.Name } - seenIDs[snapID] = preseedSnap.Name snaps = append(snaps, preseedSnap) } diff -Nru snapd-2.55.5+20.04/asserts/preseed_test.go snapd-2.57.5+20.04/asserts/preseed_test.go --- snapd-2.55.5+20.04/asserts/preseed_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/preseed_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -183,10 +183,14 @@ func (ps *preseedSuite) TestSnapIdOptional(c *C) { encoded := strings.Replace(preseedExample, "TSLINE", ps.tsLine, 1) - encoded = strings.Replace(encoded, "OTHER", "", 1) + encoded = strings.Replace(encoded, "OTHER", " -\n name: foo-linux\n", 1) encoded = strings.Replace(encoded, " revision: 99\n", "", 1) encoded = strings.Replace(encoded, " id: bazlinuxidididididididididididid\n", "", 1) - _, err := asserts.Decode([]byte(encoded)) + a, err := asserts.Decode([]byte(encoded)) c.Assert(err, IsNil) + snaps := a.(*asserts.Preseed).Snaps() + c.Assert(snaps, HasLen, 2) + c.Check(snaps[0].Name, Equals, "baz-linux") + c.Check(snaps[1].Name, Equals, "foo-linux") } diff -Nru snapd-2.55.5+20.04/asserts/signtool/keymgr.go snapd-2.57.5+20.04/asserts/signtool/keymgr.go --- snapd-2.55.5+20.04/asserts/signtool/keymgr.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/signtool/keymgr.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,101 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 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 signtool + +import ( + "errors" + "fmt" + "os" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/i18n" +) + +// KeypairManager is an interface for common methods of ExternalKeypairManager +// and GPGPKeypairManager. +type KeypairManager interface { + asserts.KeypairManager + + GetByName(keyNname string) (asserts.PrivateKey, error) + Export(keyName string) ([]byte, error) + List() ([]asserts.ExternalKeyInfo, error) + DeleteByName(keyName string) error +} + +// GetKeypairManager returns a KeypairManager - either the standrd gpg-based +// or external one if set via SNAPD_EXT_KEYMGR environment variable. +func GetKeypairManager() (KeypairManager, error) { + keymgrPath := os.Getenv("SNAPD_EXT_KEYMGR") + if keymgrPath != "" { + keypairMgr, err := asserts.NewExternalKeypairManager(keymgrPath) + if err != nil { + return nil, fmt.Errorf(i18n.G("cannot setup external keypair manager: %v"), err) + } + return keypairMgr, nil + } + keypairMgr := asserts.NewGPGKeypairManager() + return keypairMgr, nil +} + +type takingPassKeyGen interface { + Generate(passphrase string, keyName string) error +} + +type ownSecuringKeyGen interface { + Generate(keyName string) error +} + +// GenerateKey generates a private RSA key using the provided keypairMgr. +func GenerateKey(keypairMgr KeypairManager, keyName string) error { + switch keyGen := keypairMgr.(type) { + case takingPassKeyGen: + return takePassGenKey(keyGen, keyName) + case ownSecuringKeyGen: + err := keyGen.Generate(keyName) + if _, ok := err.(*asserts.ExternalUnsupportedOpError); ok { + return fmt.Errorf(i18n.G("cannot generate external keypair manager key via snap command, use the appropriate external procedure to create a 4096-bit RSA key under the name/label %q"), keyName) + } + return err + default: + return fmt.Errorf("internal error: unsupported keypair manager %T", keypairMgr) + } +} + +func takePassGenKey(keyGen takingPassKeyGen, keyName string) error { + fmt.Fprint(Stdout, i18n.G("Passphrase: ")) + passphrase, err := terminal.ReadPassword(0) + fmt.Fprint(Stdout, "\n") + if err != nil { + return err + } + fmt.Fprint(Stdout, i18n.G("Confirm passphrase: ")) + confirmPassphrase, err := terminal.ReadPassword(0) + fmt.Fprint(Stdout, "\n") + if err != nil { + return err + } + if string(passphrase) != string(confirmPassphrase) { + return errors.New(i18n.G("passphrases do not match")) + } + + return keyGen.Generate(string(passphrase), keyName) +} diff -Nru snapd-2.55.5+20.04/asserts/signtool/keymgr_test.go snapd-2.57.5+20.04/asserts/signtool/keymgr_test.go --- snapd-2.55.5+20.04/asserts/signtool/keymgr_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/signtool/keymgr_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 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 signtool_test + +import ( + "os" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/signtool" + "github.com/snapcore/snapd/testutil" +) + +type keymgrSuite struct{} + +var _ = check.Suite(&keymgrSuite{}) + +func (keymgrSuite) TestGPGKeypairManager(c *check.C) { + keypairMgr, err := signtool.GetKeypairManager() + c.Check(err, check.IsNil) + c.Check(keypairMgr, check.FitsTypeOf, &asserts.GPGKeypairManager{}) +} + +func mockNopExtKeyMgr(c *check.C) (pgm *testutil.MockCmd, restore func()) { + os.Setenv("SNAPD_EXT_KEYMGR", "keymgr") + pgm = testutil.MockCommand(c, "keymgr", ` +if [ "$1" == "features" ]; then + echo '{"signing":["RSA-PKCS"] , "public-keys":["DER"]}' + exit 0 +fi +exit 1 +`) + r := func() { + pgm.Restore() + os.Unsetenv("SNAPD_EXT_KEYMGR") + } + + return pgm, r +} + +func (keymgrSuite) TestExternalKeypairManager(c *check.C) { + pgm, restore := mockNopExtKeyMgr(c) + defer restore() + + keypairMgr, err := signtool.GetKeypairManager() + c.Check(err, check.IsNil) + c.Check(keypairMgr, check.FitsTypeOf, &asserts.ExternalKeypairManager{}) + c.Check(pgm.Calls(), check.HasLen, 1) +} + +func (keymgrSuite) TestExternalKeypairManagerError(c *check.C) { + os.Setenv("SNAPD_EXT_KEYMGR", "keymgr") + defer os.Unsetenv("SNAPD_EXT_KEYMGR") + + pgm := testutil.MockCommand(c, "keymgr", ` +exit 1 +`) + defer pgm.Restore() + + _, err := signtool.GetKeypairManager() + c.Check(err, check.ErrorMatches, `cannot setup external keypair manager: external keypair manager "keymgr" \[features\] failed: exit status 1.*`) +} + +func (keymgrSuite) TestExternalKeypairManagerGenerateKey(c *check.C) { + _, restore := mockNopExtKeyMgr(c) + defer restore() + + keypairMgr, err := signtool.GetKeypairManager() + c.Check(err, check.IsNil) + + err = signtool.GenerateKey(keypairMgr, "key") + c.Check(err, check.ErrorMatches, `cannot generate external keypair manager key via snap command, use the appropriate external procedure to create a 4096-bit RSA key under the name/label "key"`) +} diff -Nru snapd-2.55.5+20.04/asserts/signtool/sign.go snapd-2.57.5+20.04/asserts/signtool/sign.go --- snapd-2.55.5+20.04/asserts/signtool/sign.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/signtool/sign.go 2022-10-17 16:25:18.000000000 +0000 @@ -23,10 +23,15 @@ import ( "encoding/json" "fmt" + "os" "github.com/snapcore/snapd/asserts" ) +var ( + Stdout = os.Stdout +) + // Options specifies the complete input for signing an assertion. type Options struct { // KeyID specifies the key id of the key to use diff -Nru snapd-2.55.5+20.04/asserts/snapasserts/snapasserts.go snapd-2.57.5+20.04/asserts/snapasserts/snapasserts.go --- snapd-2.55.5+20.04/asserts/snapasserts/snapasserts.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/snapasserts/snapasserts.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2022 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,6 +26,8 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/snap/snapfile" ) type Finder interface { @@ -34,6 +36,9 @@ // type. It returns a asserts.NotFoundError if the assertion // cannot be found. Find(assertionType *asserts.AssertionType, headers map[string]string) (asserts.Assertion, error) + // FindMany finds assertions based on arbitrary headers. + // It returns a NotFoundError if no assertion can be found. + FindMany(assertionType *asserts.AssertionType, headers map[string]string) ([]asserts.Assertion, error) } func findSnapDeclaration(snapID, name string, db Finder) (*asserts.SnapDeclaration, error) { @@ -53,53 +58,161 @@ return snapDecl, nil } -// CrossCheck tries to cross check the instance name, hash digest and size of a snap plus its metadata in a SideInfo with the relevant snap assertions in a database that should have been populated with them. -func CrossCheck(instanceName, snapSHA3_384 string, snapSize uint64, si *snap.SideInfo, db Finder) error { +// CrossCheck tries to cross check the instance name, hash digest, provenance +// and size of a snap plus its metadata in a SideInfo with the relevant +// snap assertions in a database that should have been populated with +// them. +// The optional model assertion must be passed to have full cross +// checks in the case of delegated authority snap-revisions before +// installing a snap. +// It also returns the provenance if is is different from the default. +// Ultimately if not default the provenance must also be checked +// with the provenance in the snap metadata by the caller as well, +// if the provenance provided to the function was not read safely from +// there already. +func CrossCheck(instanceName, snapSHA3_384, provenance string, snapSize uint64, si *snap.SideInfo, model *asserts.Model, db Finder) (signedProvenance string, err error) { // get relevant assertions and do cross checks - a, err := db.Find(asserts.SnapRevisionType, map[string]string{ + headers := map[string]string{ "snap-sha3-384": snapSHA3_384, - }) + } + if provenance != "" { + headers["provenance"] = provenance + } + a, err := db.Find(asserts.SnapRevisionType, headers) if err != nil { - return fmt.Errorf("internal error: cannot find pre-populated snap-revision assertion for %q: %s", instanceName, snapSHA3_384) + provInf := "" + if provenance != "" { + provInf = fmt.Sprintf(" provenance: %s", provenance) + } + return "", fmt.Errorf("internal error: cannot find pre-populated snap-revision assertion for %q: %s%s", instanceName, snapSHA3_384, provInf) } snapRev := a.(*asserts.SnapRevision) if snapRev.SnapSize() != snapSize { - return fmt.Errorf("snap %q file does not have expected size according to signatures (download is broken or tampered): %d != %d", instanceName, snapSize, snapRev.SnapSize()) + return "", fmt.Errorf("snap %q file does not have expected size according to signatures (download is broken or tampered): %d != %d", instanceName, snapSize, snapRev.SnapSize()) } snapID := si.SnapID if snapRev.SnapID() != snapID || snapRev.SnapRevision() != si.Revision.N { - return fmt.Errorf("snap %q does not have expected ID or revision according to assertions (metadata is broken or tampered): %s / %s != %d / %s", instanceName, si.Revision, snapID, snapRev.SnapRevision(), snapRev.SnapID()) + return "", fmt.Errorf("snap %q does not have expected ID or revision according to assertions (metadata is broken or tampered): %s / %s != %d / %s", instanceName, si.Revision, snapID, snapRev.SnapRevision(), snapRev.SnapID()) } snapDecl, err := findSnapDeclaration(snapID, instanceName, db) if err != nil { - return err + return "", err } if snapDecl.SnapName() != snap.InstanceSnap(instanceName) { - return fmt.Errorf("cannot install %q, snap %q is undergoing a rename to %q", instanceName, snap.InstanceSnap(instanceName), snapDecl.SnapName()) + return "", fmt.Errorf("cannot install %q, snap %q is undergoing a rename to %q", instanceName, snap.InstanceSnap(instanceName), snapDecl.SnapName()) + } + + return CrossCheckProvenance(instanceName, snapRev, snapDecl, model, db) +} + +// CrossCheckProvenance tries to cross check the given snap-revision +// if it has a non default provenance with the revision-authority +// constraints of the given snap-declaration including any device +// scope constraints using model (and implied store). +// It also returns the provenance if it is different from the default. +// Ultimately if not default the provenance must also be checked +// with the provenance in the snap metadata by the caller. +func CrossCheckProvenance(instanceName string, snapRev *asserts.SnapRevision, snapDecl *asserts.SnapDeclaration, model *asserts.Model, db Finder) (signedProvenance string, err error) { + if snapRev.Provenance() == "global-upload" { + // nothing to check + return "", nil + } + var store *asserts.Store + if model != nil && model.Store() != "" { + a, err := db.Find(asserts.StoreType, map[string]string{ + "store": model.Store(), + }) + if err != nil && !asserts.IsNotFound(err) { + return "", err + } + if a != nil { + store = a.(*asserts.Store) + } + } + ras := snapDecl.RevisionAuthority(snapRev.Provenance()) + matchingRevAuthority := false + for _, ra := range ras { + if err := ra.Check(snapRev, model, store); err == nil { + matchingRevAuthority = true + break + } } + if !matchingRevAuthority { + return "", fmt.Errorf("snap %q revision assertion with provenance %q is not signed by an authority authorized on this device: %s", instanceName, snapRev.Provenance(), snapRev.AuthorityID()) + } + return snapRev.Provenance(), nil +} +// CheckProvenance checks that the given snap has the provided provenance. +// It is intended to be called safely on snaps for which a matching +// and authorized snap-revision has been already found which specify +// the given provenance. +// Its purpose is to check that a blob has not been re-signed under an +// inappropriate provenance. +func CheckProvenance(snapPath, provenance string) error { + snapf, err := snapfile.Open(snapPath) + if err != nil { + return err + } + info, err := snap.ReadInfoFromSnapFile(snapf, nil) + if err != nil { + return err + } + if provenance == "" { + provenance = naming.DefaultProvenance + } + if provenance != info.Provenance() { + return fmt.Errorf("snap %q has been signed under provenance %q different from the metadata one: %q", snapPath, provenance, info.Provenance()) + } return nil } -// DeriveSideInfo tries to construct a SideInfo for the given snap using its digest to find the relevant snap assertions with the information in the given database. It will fail with an asserts.NotFoundError if it cannot find them. -func DeriveSideInfo(snapPath string, db Finder) (*snap.SideInfo, error) { +// DeriveSideInfo tries to construct a SideInfo for the given snap +// using its digest to find the relevant snap assertions with the +// information in the given database. It will fail with an +// asserts.NotFoundError if it cannot find them. +// model is used to cross check that the found snap-revision is applicable +// on the device. +func DeriveSideInfo(snapPath string, model *asserts.Model, db Finder) (*snap.SideInfo, error) { snapSHA3_384, snapSize, err := asserts.SnapFileSHA3_384(snapPath) if err != nil { return nil, err } + return DeriveSideInfoFromDigestAndSize(snapPath, snapSHA3_384, snapSize, model, db) +} + +// DeriveSideInfoFromDigestAndSize tries to construct a SideInfo +// using digest and size as provided for the snap to find the relevant +// snap assertions with the information in the given database. It will +// fail with an asserts.NotFoundError if it cannot find them. +// model is used to cross check that the found snap-revision is applicable +// on the device. +func DeriveSideInfoFromDigestAndSize(snapPath string, snapSHA3_384 string, snapSize uint64, model *asserts.Model, db Finder) (*snap.SideInfo, error) { // get relevant assertions and reconstruct metadata - a, err := db.Find(asserts.SnapRevisionType, map[string]string{ + headers := map[string]string{ "snap-sha3-384": snapSHA3_384, - }) - if err != nil { + } + a, err := db.Find(asserts.SnapRevisionType, headers) + if err != nil && !asserts.IsNotFound(err) { return nil, err } + if a == nil { + // non-default provenance? + cands, err := db.FindMany(asserts.SnapRevisionType, headers) + if err != nil { + return nil, err + } + if len(cands) != 1 { + return nil, fmt.Errorf("safely handling snaps with different provenance but same hash not yet supported") + } + a = cands[0] + } snapRev := a.(*asserts.SnapRevision) @@ -114,6 +227,15 @@ return nil, err } + signedProv, err := CrossCheckProvenance(snapDecl.SnapName(), snapRev, snapDecl, model, db) + if err != nil { + return nil, err + } + + if err := CheckProvenance(snapPath, signedProv); err != nil { + return nil, err + } + return SideInfoFromSnapAssertions(snapDecl, snapRev), nil } @@ -126,13 +248,16 @@ } } -// FetchSnapAssertions fetches the assertions matching the snap file digest using the given fetcher. -func FetchSnapAssertions(f asserts.Fetcher, snapSHA3_384 string) error { +// FetchSnapAssertions fetches the assertions matching the snap file digest and optional provenance using the given fetcher. +func FetchSnapAssertions(f asserts.Fetcher, snapSHA3_384, provenance string) error { // for now starting from the snap-revision will get us all other relevant assertions ref := &asserts.Ref{ Type: asserts.SnapRevisionType, PrimaryKey: []string{snapSHA3_384}, } + if provenance != "" { + ref.PrimaryKey = append(ref.PrimaryKey, provenance) + } return f.Fetch(ref) } diff -Nru snapd-2.55.5+20.04/asserts/snapasserts/snapasserts_test.go snapd-2.57.5+20.04/asserts/snapasserts/snapasserts_test.go --- snapd-2.55.5+20.04/asserts/snapasserts/snapasserts_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/snapasserts/snapasserts_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2022 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,13 +34,18 @@ "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" ) func TestSnapasserts(t *testing.T) { TestingT(t) } type snapassertsSuite struct { + testutil.BaseTest + storeSigning *assertstest.StoreStack dev1Acct *asserts.Account + dev1Signing *assertstest.SigningDB localDB *asserts.Database } @@ -66,6 +71,13 @@ err = s.localDB.Add(s.dev1Acct) c.Assert(err, IsNil) + privKey, _ := assertstest.GenerateKey(752) + accKey := assertstest.NewAccountKey(s.storeSigning, s.dev1Acct, nil, privKey.PublicKey(), "") + err = s.localDB.Add(accKey) + c.Assert(err, IsNil) + + s.dev1Signing = assertstest.NewSigningDB(s.dev1Acct.AccountID(), privKey) + headers := map[string]interface{}{ "series": "16", "snap-id": "snap-id-1", @@ -77,6 +89,8 @@ c.Assert(err, IsNil) err = s.localDB.Add(snapDecl) c.Assert(err, IsNil) + + s.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) } func fakeSnap(rev int) []byte { @@ -119,10 +133,12 @@ } // everything cross checks, with the regular snap name - err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB) + prov, err := snapasserts.CrossCheck("foo", digest, "", size, si, nil, s.localDB) c.Check(err, IsNil) + c.Check(prov, Equals, "") + // and a snap instance name - err = snapasserts.CrossCheck("foo_instance", digest, size, si, s.localDB) + _, err = snapasserts.CrossCheck("foo_instance", digest, "", size, si, nil, s.localDB) c.Check(err, IsNil) } @@ -148,39 +164,39 @@ } // different size - err = snapasserts.CrossCheck("foo", digest, size+1, si, s.localDB) + _, err = snapasserts.CrossCheck("foo", digest, "", size+1, si, nil, s.localDB) c.Check(err, ErrorMatches, fmt.Sprintf(`snap "foo" file does not have expected size according to signatures \(download is broken or tampered\): %d != %d`, size+1, size)) - err = snapasserts.CrossCheck("foo_instance", digest, size+1, si, s.localDB) + _, err = snapasserts.CrossCheck("foo_instance", digest, "", size+1, si, nil, s.localDB) c.Check(err, ErrorMatches, fmt.Sprintf(`snap "foo_instance" file does not have expected size according to signatures \(download is broken or tampered\): %d != %d`, size+1, size)) // mismatched revision vs what we got from store original info - err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{ + _, err = snapasserts.CrossCheck("foo", digest, "", size, &snap.SideInfo{ SnapID: "snap-id-1", Revision: snap.R(21), - }, s.localDB) + }, nil, s.localDB) c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 21 / snap-id-1 != 12 / snap-id-1`) - err = snapasserts.CrossCheck("foo_instance", digest, size, &snap.SideInfo{ + _, err = snapasserts.CrossCheck("foo_instance", digest, "", size, &snap.SideInfo{ SnapID: "snap-id-1", Revision: snap.R(21), - }, s.localDB) + }, nil, s.localDB) c.Check(err, ErrorMatches, `snap "foo_instance" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 21 / snap-id-1 != 12 / snap-id-1`) // mismatched snap id vs what we got from store original info - err = snapasserts.CrossCheck("foo", digest, size, &snap.SideInfo{ + _, err = snapasserts.CrossCheck("foo", digest, "", size, &snap.SideInfo{ SnapID: "snap-id-other", Revision: snap.R(12), - }, s.localDB) + }, nil, s.localDB) c.Check(err, ErrorMatches, `snap "foo" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 12 / snap-id-other != 12 / snap-id-1`) - err = snapasserts.CrossCheck("foo_instance", digest, size, &snap.SideInfo{ + _, err = snapasserts.CrossCheck("foo_instance", digest, "", size, &snap.SideInfo{ SnapID: "snap-id-other", Revision: snap.R(12), - }, s.localDB) + }, nil, s.localDB) c.Check(err, ErrorMatches, `snap "foo_instance" does not have expected ID or revision according to assertions \(metadata is broken or tampered\): 12 / snap-id-other != 12 / snap-id-1`) // changed name - err = snapasserts.CrossCheck("baz", digest, size, si, s.localDB) + _, err = snapasserts.CrossCheck("baz", digest, "", size, si, nil, s.localDB) c.Check(err, ErrorMatches, `cannot install "baz", snap "baz" is undergoing a rename to "foo"`) - err = snapasserts.CrossCheck("baz_instance", digest, size, si, s.localDB) + _, err = snapasserts.CrossCheck("baz_instance", digest, "", size, si, nil, s.localDB) c.Check(err, ErrorMatches, `cannot install "baz_instance", snap "baz" is undergoing a rename to "foo"`) } @@ -220,15 +236,18 @@ Revision: snap.R(12), } - err = snapasserts.CrossCheck("foo", digest, size, si, s.localDB) + _, err = snapasserts.CrossCheck("foo", digest, "", size, si, nil, s.localDB) c.Check(err, ErrorMatches, `cannot install snap "foo" with a revoked snap declaration`) - err = snapasserts.CrossCheck("foo_instance", digest, size, si, s.localDB) + _, err = snapasserts.CrossCheck("foo_instance", digest, "", size, si, nil, s.localDB) c.Check(err, ErrorMatches, `cannot install snap "foo_instance" with a revoked snap declaration`) } func (s *snapassertsSuite) TestDeriveSideInfoHappy(c *C) { - digest := makeDigest(42) - size := uint64(len(fakeSnap(42))) + fooSnap := snaptest.MakeTestSnapWithFiles(c, `name: foo +version: 1`, nil) + digest, size, err := asserts.SnapFileSHA3_384(fooSnap) + c.Assert(err, IsNil) + headers := map[string]interface{}{ "snap-id": "snap-id-1", "snap-sha3-384": digest, @@ -242,12 +261,7 @@ err = s.localDB.Add(snapRev) c.Assert(err, IsNil) - tempdir := c.MkDir() - snapPath := filepath.Join(tempdir, "anon.snap") - err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644) - c.Assert(err, IsNil) - - si, err := snapasserts.DeriveSideInfo(snapPath, s.localDB) + si, err := snapasserts.DeriveSideInfo(fooSnap, nil, s.localDB) c.Assert(err, IsNil) c.Check(si, DeepEquals, &snap.SideInfo{ RealName: "foo", @@ -263,7 +277,7 @@ err := ioutil.WriteFile(snapPath, fakeSnap(42), 0644) c.Assert(err, IsNil) - _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB) + _, err = snapasserts.DeriveSideInfo(snapPath, nil, s.localDB) // cannot find signatures with metadata for snap c.Assert(asserts.IsNotFound(err), Equals, true) } @@ -289,7 +303,7 @@ err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644) c.Assert(err, IsNil) - _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB) + _, err = snapasserts.DeriveSideInfo(snapPath, nil, s.localDB) c.Check(err, ErrorMatches, fmt.Sprintf(`snap %q does not have expected size according to signatures \(broken or tampered\): %d != %d`, snapPath, size, size+5)) } @@ -328,6 +342,413 @@ err = ioutil.WriteFile(snapPath, fakeSnap(42), 0644) c.Assert(err, IsNil) - _, err = snapasserts.DeriveSideInfo(snapPath, s.localDB) + _, err = snapasserts.DeriveSideInfo(snapPath, nil, s.localDB) c.Check(err, ErrorMatches, fmt.Sprintf(`cannot install snap %q with a revoked snap declaration`, snapPath)) } + +func (s *snapassertsSuite) TestCrossCheckDelegatedSnapHappy(c *C) { + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "revision-authority": []interface{}{ + map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{ + "prov1", + }, + }, + }, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + digest := makeDigest(42) + size := uint64(len(fakeSnap(42))) + headers := map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "provenance": "prov1", + "snap-revision": "42", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.dev1Signing.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = s.localDB.Add(snapRev) + c.Check(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(42), + } + + // everything cross checks, with the regular snap name + prov, err := snapasserts.CrossCheck("foo", digest, "prov1", size, si, nil, s.localDB) + c.Check(err, IsNil) + c.Check(prov, Equals, "prov1") + // and a snap instance name + _, err = snapasserts.CrossCheck("foo_instance", digest, "prov1", size, si, nil, s.localDB) + c.Check(err, IsNil) +} + +func (s *snapassertsSuite) TestCrossCheckWithDeviceDelegatedSnapHappy(c *C) { + a, err := s.dev1Signing.Sign(asserts.ModelType, map[string]interface{}{ + "brand-id": s.dev1Acct.AccountID(), + "series": "16", + "model": "dev-model", + "store": "substore", + "architecture": "amd64", + "base": "core18", + "kernel": "krnl", + "gadget": "gadget", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + model := a.(*asserts.Model) + + substore, err := s.storeSigning.Sign(asserts.StoreType, map[string]interface{}{ + "store": "substore", + "operator-id": "can0nical", + "friendly-stores": []interface{}{"store1"}, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(substore) + c.Assert(err, IsNil) + + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "revision-authority": []interface{}{ + map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{ + "prov1", + }, + "on-store": []interface{}{ + "store1", + }, + }, + }, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + digest := makeDigest(42) + size := uint64(len(fakeSnap(42))) + headers := map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "provenance": "prov1", + "snap-revision": "42", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.dev1Signing.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = s.localDB.Add(snapRev) + c.Check(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(42), + } + + // everything cross checks, with the regular snap name + prov, err := snapasserts.CrossCheck("foo", digest, "prov1", size, si, model, s.localDB) + c.Check(err, IsNil) + c.Check(prov, Equals, "prov1") + // and a snap instance name + _, err = snapasserts.CrossCheck("foo_instance", digest, "prov1", size, si, model, s.localDB) + c.Check(err, IsNil) +} + +func (s *snapassertsSuite) TestCrossCheckWithDeviceDelegatedSnapUnhappy(c *C) { + a, err := s.dev1Signing.Sign(asserts.ModelType, map[string]interface{}{ + "brand-id": s.dev1Acct.AccountID(), + "series": "16", + "model": "dev-model", + "store": "substore", + "architecture": "amd64", + "base": "core18", + "kernel": "krnl", + "gadget": "gadget", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + model := a.(*asserts.Model) + + substore, err := s.storeSigning.Sign(asserts.StoreType, map[string]interface{}{ + "store": "substore", + "operator-id": "can0nical", + "friendly-stores": []interface{}{"store1"}, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(substore) + c.Assert(err, IsNil) + + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "revision-authority": []interface{}{ + map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{ + "prov1", + }, + "on-store": []interface{}{ + "store2", + }, + }, + }, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + digest := makeDigest(42) + size := uint64(len(fakeSnap(42))) + headers := map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "provenance": "prov1", + "snap-revision": "42", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.dev1Signing.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = s.localDB.Add(snapRev) + c.Check(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(42), + } + + _, err = snapasserts.CrossCheck("foo", digest, "prov1", size, si, model, s.localDB) + c.Check(err, ErrorMatches, `snap "foo" revision assertion with provenance "prov1" is not signed by an authority authorized on this device: .*`) +} + +func (s *snapassertsSuite) TestCrossCheckSpuriousProvenanceUnhappy(c *C) { + digest := makeDigest(12) + size := uint64(len(fakeSnap(12))) + headers := map[string]interface{}{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-revision": "12", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapRev) + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + SnapID: "snap-id-1", + Revision: snap.R(12), + } + + _, err = snapasserts.CrossCheck("foo", digest, "prov", size, si, nil, s.localDB) + c.Check(err, ErrorMatches, `.*cannot find pre-populated snap-revision assertion for "foo": .*provenance: prov`) +} + +func (s *snapassertsSuite) TestCheckProvenance(c *C) { + withProv := snaptest.MakeTestSnapWithFiles(c, `name: with-prov +version: 1 +provenance: prov`, nil) + defaultProv := snaptest.MakeTestSnapWithFiles(c, `name: defl +version: 1 +`, nil) + + // matching + c.Check(snapasserts.CheckProvenance(withProv, "prov"), IsNil) + c.Check(snapasserts.CheckProvenance(defaultProv, ""), IsNil) + c.Check(snapasserts.CheckProvenance(defaultProv, "global-upload"), IsNil) + // mismatches + mismatches := []struct { + path, prov, metadataProv string + }{ + {withProv, "prov2", "prov"}, + {withProv, "global-upload", "prov"}, + {defaultProv, "prov", "global-upload"}, + } + for _, mism := range mismatches { + c.Check(snapasserts.CheckProvenance(mism.path, mism.prov), ErrorMatches, fmt.Sprintf("snap %q has been signed under provenance %q different from the metadata one: %q", mism.path, mism.prov, mism.metadataProv)) + } + +} + +func (s *snapassertsSuite) TestDeriveSideInfoFromDigestAndSizeDelegatedSnap(c *C) { + withProv := snaptest.MakeTestSnapWithFiles(c, `name: with-prov +version: 1 +provenance: prov`, nil) + digest, size, err := asserts.SnapFileSHA3_384(withProv) + c.Assert(err, IsNil) + + a, err := s.dev1Signing.Sign(asserts.ModelType, map[string]interface{}{ + "brand-id": s.dev1Acct.AccountID(), + "series": "16", + "model": "dev-model", + "store": "substore", + "architecture": "amd64", + "base": "core18", + "kernel": "krnl", + "gadget": "gadget", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + model := a.(*asserts.Model) + + substore, err := s.storeSigning.Sign(asserts.StoreType, map[string]interface{}{ + "store": "substore", + "operator-id": "can0nical", + "friendly-stores": []interface{}{"store1"}, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(substore) + c.Assert(err, IsNil) + + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "revision-authority": []interface{}{ + map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{ + "prov", + }, + "on-store": []interface{}{ + "store1", + }, + }, + }, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "provenance": "prov", + "snap-revision": "41", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.dev1Signing.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = s.localDB.Add(snapRev) + c.Check(err, IsNil) + + si, err := snapasserts.DeriveSideInfoFromDigestAndSize(withProv, digest, size, model, s.localDB) + c.Check(err, IsNil) + c.Check(si, DeepEquals, &snap.SideInfo{ + RealName: "foo", + SnapID: "snap-id-1", + Revision: snap.R(41), + Channel: "", + }) +} + +func (s *snapassertsSuite) TestDeriveSideInfoFromDigestAndSizeDelegatedSnapAmbiguous(c *C) { + // this is not a fully realistic test as this unlikely + // scenario would happen possibly across different delegated + // accounts, the goal is simply to trigger the error + // even if not in a realistic way + withProv := snaptest.MakeTestSnapWithFiles(c, `name: with-prov +version: 1 +provenance: prov`, nil) + digest, size, err := asserts.SnapFileSHA3_384(withProv) + c.Assert(err, IsNil) + + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "revision": "1", + "revision-authority": []interface{}{ + map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{ + "prov", + "prov2", + }, + }, + }, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = s.localDB.Add(snapDecl) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "provenance": "prov", + "snap-revision": "41", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.dev1Signing.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = s.localDB.Add(snapRev) + c.Check(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "provenance": "prov2", + "snap-revision": "82", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev2, err := s.dev1Signing.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = s.localDB.Add(snapRev2) + c.Check(err, IsNil) + + _, err = snapasserts.DeriveSideInfoFromDigestAndSize(withProv, digest, size, nil, s.localDB) + c.Check(err, ErrorMatches, `safely handling snaps with different provenance but same hash not yet supported`) +} diff -Nru snapd-2.55.5+20.04/asserts/snapasserts/validation_sets.go snapd-2.57.5+20.04/asserts/snapasserts/validation_sets.go --- snapd-2.55.5+20.04/asserts/snapasserts/validation_sets.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/snapasserts/validation_sets.go 2022-10-17 16:25:18.000000000 +0000 @@ -63,8 +63,9 @@ // ValidationSetsValidationError describes an error arising // from validation of snaps against ValidationSets. type ValidationSetsValidationError struct { - // MissingSnaps maps missing snap names to the validation sets requiring them. - MissingSnaps map[string][]string + // MissingSnaps maps missing snap names to the expected revisions and respective validation sets requiring them. + // Revisions may be unset if no specific revision is required + MissingSnaps map[string]map[snap.Revision][]string // InvalidSnaps maps snap names to the validation sets declaring them invalid. InvalidSnaps map[string][]string // WronRevisionSnaps maps snap names to the expected revisions and respective @@ -94,9 +95,27 @@ } } - printDetails("missing required snaps", e.MissingSnaps, func(snapName string, validationSetKeys []string) string { - return fmt.Sprintf("%s (required by sets %s)", snapName, strings.Join(validationSetKeys, ",")) - }) + if len(e.MissingSnaps) > 0 { + fmt.Fprintf(buf, "\n- missing required snaps:") + for snapName, revisions := range e.MissingSnaps { + revisionsSorted := make([]snap.Revision, 0, len(revisions)) + for rev := range revisions { + revisionsSorted = append(revisionsSorted, rev) + } + sort.Sort(byRevision(revisionsSorted)) + t := make([]string, 0, len(revisionsSorted)) + for _, rev := range revisionsSorted { + keys := revisions[rev] + if rev.Unset() { + t = append(t, fmt.Sprintf("at any revision by sets %s", strings.Join(keys, ","))) + } else { + t = append(t, fmt.Sprintf("at revision %s by sets %s", rev, strings.Join(keys, ","))) + } + } + fmt.Fprintf(buf, "\n - %s (required %s)", snapName, strings.Join(t, ", ")) + } + } + printDetails("invalid snaps", e.InvalidSnaps, func(snapName string, validationSetKeys []string) string { return fmt.Sprintf("%s (invalid for sets %s)", snapName, strings.Join(validationSetKeys, ",")) }) @@ -367,7 +386,7 @@ // snapName -> validationSet key -> validation set invalid := make(map[string]map[string]bool) - missing := make(map[string]map[string]bool) + missing := make(map[string]map[snap.Revision]map[string]bool) wrongrev := make(map[string]map[snap.Revision]map[string]bool) sets := make(map[string]*asserts.ValidationSet) @@ -411,9 +430,12 @@ // is only possible to have it with a wrong revision, or installed while invalid, in both // cases through --ignore-validation flag). if missing[rc.Name] == nil { - missing[rc.Name] = make(map[string]bool) + missing[rc.Name] = make(map[snap.Revision]map[string]bool) } - missing[rc.Name][rc.validationSetKey] = true + if missing[rc.Name][rev] == nil { + missing[rc.Name][rev] = make(map[string]bool) + } + missing[rc.Name][rev][rc.validationSetKey] = true sets[rc.validationSetKey] = v.sets[rc.validationSetKey] } } @@ -438,9 +460,20 @@ if len(invalid) > 0 || len(missing) > 0 || len(wrongrev) > 0 { verr := &ValidationSetsValidationError{ InvalidSnaps: setsToLists(invalid), - MissingSnaps: setsToLists(missing), Sets: sets, } + if len(missing) > 0 { + verr.MissingSnaps = make(map[string]map[snap.Revision][]string) + for snapName, revs := range missing { + verr.MissingSnaps[snapName] = make(map[snap.Revision][]string) + for rev, keys := range revs { + for key := range keys { + verr.MissingSnaps[snapName][rev] = append(verr.MissingSnaps[snapName][rev], key) + } + sort.Strings(verr.MissingSnaps[snapName][rev]) + } + } + } if len(wrongrev) > 0 { verr.WrongRevisionSnaps = make(map[string]map[snap.Revision][]string) for snapName, revs := range wrongrev { diff -Nru snapd-2.55.5+20.04/asserts/snapasserts/validation_sets_test.go snapd-2.57.5+20.04/asserts/snapasserts/validation_sets_test.go --- snapd-2.55.5+20.04/asserts/snapasserts/validation_sets_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/snapasserts/validation_sets_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -366,11 +366,64 @@ }, }).(*asserts.ValidationSet) + vs5 := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "acme", + "series": "16", + "account-id": "acme", + "name": "huhname", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "snap-f", + "id": "mysnapffffffffffffffffffffffffff", + "revision": "4", + "presence": "required", + }, + }, + }).(*asserts.ValidationSet) + + vs6 := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "acme", + "series": "16", + "account-id": "acme", + "name": "duhname", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "snap-f", + "id": "mysnapffffffffffffffffffffffffff", + "revision": "4", + "presence": "required", + }, + }, + }).(*asserts.ValidationSet) + + vs7 := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "acme", + "series": "16", + "account-id": "acme", + "name": "bahname", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "snap-f", + "id": "mysnapffffffffffffffffffffffffff", + "presence": "required", + }, + }, + }).(*asserts.ValidationSet) + valsets := snapasserts.NewValidationSets() c.Assert(valsets.Add(vs1), IsNil) c.Assert(valsets.Add(vs2), IsNil) c.Assert(valsets.Add(vs3), IsNil) c.Assert(valsets.Add(vs4), IsNil) + c.Assert(valsets.Add(vs5), IsNil) + c.Assert(valsets.Add(vs6), IsNil) + c.Assert(valsets.Add(vs7), IsNil) snapA := snapasserts.NewInstalledSnap("snap-a", "mysnapaaaaaaaaaaaaaaaaaaaaaaaaaa", snap.R(1)) snapAlocal := snapasserts.NewInstalledSnap("snap-a", "", snap.R("x2")) @@ -383,21 +436,30 @@ snapDrev99 := snapasserts.NewInstalledSnap("snap-d", "mysnapdddddddddddddddddddddddddd", snap.R(99)) snapDlocal := snapasserts.NewInstalledSnap("snap-d", "", snap.R("x3")) snapE := snapasserts.NewInstalledSnap("snap-e", "mysnapeeeeeeeeeeeeeeeeeeeeeeeeee", snap.R(2)) + snapF := snapasserts.NewInstalledSnap("snap-f", "mysnapffffffffffffffffffffffffff", snap.R(4)) // extra snap, not referenced by any validation set snapZ := snapasserts.NewInstalledSnap("snap-z", "mysnapzzzzzzzzzzzzzzzzzzzzzzzzzz", snap.R(1)) tests := []struct { snaps []*snapasserts.InstalledSnap expectedInvalid map[string][]string - expectedMissing map[string][]string + expectedMissing map[string]map[snap.Revision][]string expectedWrongRev map[string]map[snap.Revision][]string }{ { // required snaps not installed snaps: nil, - expectedMissing: map[string][]string{ - "snap-b": {"acme/fooname"}, - "snap-d": {"acme/barname"}, + expectedMissing: map[string]map[snap.Revision][]string{ + "snap-b": { + snap.R(3): {"acme/fooname"}, + }, + "snap-d": { + snap.R(0): {"acme/barname"}, + }, + "snap-f": { + snap.R(0): {"acme/bahname"}, + snap.R(4): {"acme/duhname", "acme/huhname"}, + }, }, }, { @@ -405,9 +467,17 @@ snaps: []*snapasserts.InstalledSnap{ snapZ, }, - expectedMissing: map[string][]string{ - "snap-b": {"acme/fooname"}, - "snap-d": {"acme/barname"}, + expectedMissing: map[string]map[snap.Revision][]string{ + "snap-b": { + snap.R(3): {"acme/fooname"}, + }, + "snap-d": { + snap.R(0): {"acme/barname"}, + }, + "snap-f": { + snap.R(0): {"acme/bahname"}, + snap.R(4): {"acme/duhname", "acme/huhname"}, + }, }, }, { @@ -415,7 +485,10 @@ // covered by acme/fooname validation-set snapB, // covered by acme/barname validation-set. snap-e not installed but optional - snapDrev99}, + snapDrev99, + // covered by acme/duhname and acme/huhname + snapF, + }, // ale fine }, { @@ -424,7 +497,10 @@ snapA, snapB, // covered by acme/barname validation-set. snap-e not installed but optional - snapDrev99}, + snapDrev99, + // covered by acme/duhname and acme/huhname + snapF, + }, expectedInvalid: map[string][]string{ "snap-a": {"acme/booname", "acme/fooname"}, }, @@ -434,12 +510,16 @@ // covered by acme/fooname and acme/booname validation-sets, snapB missing, snap-a presence is invalid snapA, // covered by acme/barname validation-set. snap-e not installed but optional - snapDrev99}, + snapDrev99, + snapF, + }, expectedInvalid: map[string][]string{ "snap-a": {"acme/booname", "acme/fooname"}, }, - expectedMissing: map[string][]string{ - "snap-b": {"acme/fooname"}, + expectedMissing: map[string]map[snap.Revision][]string{ + "snap-b": { + snap.R(3): {"acme/fooname"}, + }, }, }, { @@ -448,7 +528,10 @@ snapB, snapC, // covered by acme/barname validation-set. snap-e not installed but optional - snapD}, + snapD, + // covered by acme/duhname and acme/huhname + snapF, + }, // ale fine }, { @@ -457,7 +540,10 @@ snapB, snapCinvRev, // covered by acme/barname validation-set. snap-e not installed but optional - snapD}, + snapD, + // covered by acme/duhname and acme/huhname + snapF, + }, expectedWrongRev: map[string]map[snap.Revision][]string{ "snap-c": { snap.R(2): {"acme/fooname"}, @@ -469,7 +555,10 @@ // covered by acme/fooname validation-set but wrong revision snapBinvRev, // covered by acme/barname validation-set. - snapD}, + snapD, + // covered by acme/duhname and acme/huhname + snapF, + }, expectedWrongRev: map[string]map[snap.Revision][]string{ "snap-b": { snap.R(3): {"acme/fooname"}, @@ -481,9 +570,14 @@ // covered by acme/fooname validation-set snapB, // covered by acme/barname validation-set. snap-d not installed. - snapE}, - expectedMissing: map[string][]string{ - "snap-d": {"acme/barname"}, + snapE, + // covered by acme/duhname and acme/huhname + snapF, + }, + expectedMissing: map[string]map[snap.Revision][]string{ + "snap-d": { + snap.R(0): {"acme/barname"}, + }, }, }, { @@ -491,9 +585,14 @@ // required snaps from acme/fooname are not installed. // covered by acme/barname validation-set snapDrev99, - snapE}, - expectedMissing: map[string][]string{ - "snap-b": {"acme/fooname"}, + snapE, + // covered by acme/duhname and acme/huhname + snapF, + }, + expectedMissing: map[string]map[snap.Revision][]string{ + "snap-b": { + snap.R(3): {"acme/fooname"}, + }, }, }, { @@ -501,10 +600,17 @@ // covered by acme/fooname validation-set, required missing. snapC, // covered by acme/barname validation-set, required missing. - snapE}, - expectedMissing: map[string][]string{ - "snap-b": {"acme/fooname"}, - "snap-d": {"acme/barname"}, + snapE, + // covered by acme/duhname and acme/huhname + snapF, + }, + expectedMissing: map[string]map[snap.Revision][]string{ + "snap-b": { + snap.R(3): {"acme/fooname"}, + }, + "snap-d": { + snap.R(0): {"acme/barname"}, + }, }, }, // local snaps @@ -513,7 +619,10 @@ // covered by acme/fooname validation-set. snapB, // covered by acme/barname validation-set, local snap-d. - snapDlocal}, + snapDlocal, + // covered by acme/duhname and acme/huhname + snapF, + }, // all fine }, { @@ -522,7 +631,9 @@ snapAlocal, snapB, // covered by acme/barname validation-set. - snapD}, + snapD, + snapF, + }, expectedInvalid: map[string][]string{ "snap-a": {"acme/booname", "acme/fooname"}, }, @@ -532,7 +643,10 @@ // covered by acme/fooname validation-set, snap-b is wrong rev (local). snapBlocal, // covered by acme/barname validation-set. - snapD}, + snapD, + // covered by acme/duhname and acme/huhname + snapF, + }, expectedWrongRev: map[string]map[snap.Revision][]string{ "snap-b": { snap.R(3): {"acme/fooname"}, @@ -561,9 +675,9 @@ } verr, ok := err.(*snapasserts.ValidationSetsValidationError) c.Assert(ok, Equals, true, Commentf("#%d", i)) - c.Assert(tc.expectedInvalid, DeepEquals, verr.InvalidSnaps, Commentf("#%d", i)) - c.Assert(tc.expectedMissing, DeepEquals, verr.MissingSnaps, Commentf("#%d", i)) - c.Assert(tc.expectedWrongRev, DeepEquals, verr.WrongRevisionSnaps, Commentf("#%d", i)) + c.Assert(verr.InvalidSnaps, DeepEquals, tc.expectedInvalid, Commentf("#%d", i)) + c.Assert(verr.MissingSnaps, DeepEquals, tc.expectedMissing, Commentf("#%d", i)) + c.Assert(verr.WrongRevisionSnaps, DeepEquals, tc.expectedWrongRev, Commentf("#%d", i)) checkSets(verr.InvalidSnaps, verr.Sets) } } @@ -654,7 +768,6 @@ map[string]interface{}{ "name": "snap-b", "id": "mysnapbbbbbbbbbbbbbbbbbbbbbbbbbb", - "revision": "5", "presence": "required", }, }, @@ -664,6 +777,9 @@ c.Assert(valsets.Add(vs1), IsNil) c.Assert(valsets.Add(vs2), IsNil) + // not strictly important, but ensures test data makes sense and avoids confusing results + c.Assert(valsets.Conflict(), IsNil) + snapA := snapasserts.NewInstalledSnap("snap-a", "mysnapaaaaaaaaaaaaaaaaaaaaaaaaaa", snap.R(1)) snapBlocal := snapasserts.NewInstalledSnap("snap-b", "", snap.R("x3")) @@ -675,13 +791,13 @@ nil, "validation sets assertions are not met:\n" + "- missing required snaps:\n" + - " - snap-b \\(required by sets acme/barname,acme/fooname\\)", + " - snap-b \\(required at any revision by sets acme/barname, at revision 3 by sets acme/fooname\\)", }, { []*snapasserts.InstalledSnap{snapA}, "validation sets assertions are not met:\n" + "- missing required snaps:\n" + - " - snap-b \\(required by sets acme/barname,acme/fooname\\)\n" + + " - snap-b \\(required at any revision by sets acme/barname, at revision 3 by sets acme/fooname\\)\n" + "- invalid snaps:\n" + " - snap-a \\(invalid for sets acme/fooname\\)", }, @@ -689,7 +805,7 @@ []*snapasserts.InstalledSnap{snapBlocal}, "validation sets assertions are not met:\n" + "- snaps at wrong revisions:\n" + - " - snap-b \\(required at revision 3 by sets acme/fooname, at revision 5 by sets acme/barname\\)", + " - snap-b \\(required at revision 3 by sets acme/fooname\\)", }, } diff -Nru snapd-2.55.5+20.04/asserts/snap_asserts.go snapd-2.57.5+20.04/asserts/snap_asserts.go --- snapd-2.55.5+20.04/asserts/snap_asserts.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/snap_asserts.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2020 Canonical Ltd + * Copyright (C) 2015-2022 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,6 +31,7 @@ "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/strutil" ) // SnapDeclaration holds a snap-declaration assertion, declaring a @@ -38,12 +39,13 @@ // publisher and its other properties. type SnapDeclaration struct { assertionBase - refreshControl []string - plugRules map[string]*PlugRule - slotRules map[string]*SlotRule - autoAliases []string - aliases map[string]string - timestamp time.Time + refreshControl []string + plugRules map[string]*PlugRule + slotRules map[string]*SlotRule + autoAliases []string + aliases map[string]string + revisionAuthorities []*RevisionAuthority + timestamp time.Time } // Series returns the series for which the snap is being declared. @@ -97,6 +99,21 @@ return snapdcl.aliases } +// RevisionAuthority return any revision authority entries matching the given +// provenance. +func (snapdcl *SnapDeclaration) RevisionAuthority(provenance string) []*RevisionAuthority { + res := make([]*RevisionAuthority, 0, 1) + for _, ra := range snapdcl.revisionAuthorities { + if strutil.ListContains(ra.Provenance, provenance) { + res = append(res, ra) + } + } + if len(res) == 0 { + return nil + } + return res +} + // Implement further consistency checks. func (snapdcl *SnapDeclaration) checkConsistency(db RODatabase, acck *AccountKey) error { if !db.IsTrustedAccount(snapdcl.AuthorityID()) { @@ -314,17 +331,119 @@ return nil, err } + var ras []*RevisionAuthority + + ra, ok := assert.headers["revision-authority"] + if ok { + ramaps, ok := ra.([]interface{}) + if !ok { + return nil, fmt.Errorf("revision-authority stanza must be a list of maps") + } + if len(ramaps) == 0 { + // there is no syntax producing this scenario but be robust + return nil, fmt.Errorf("revision-authority stanza cannot be empty") + } + ras = make([]*RevisionAuthority, 0, len(ramaps)) + for _, ramap := range ramaps { + m, ok := ramap.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("revision-authority stanza must be a list of maps") + } + accountID, err := checkStringMatchesWhat(m, "account-id", "in revision authority", validAccountID) + if err != nil { + return nil, err + } + prov, err := checkStringListInMap(m, "provenance", "provenance in revision authority", naming.ValidProvenance) + if err != nil { + return nil, err + } + if len(prov) == 0 { + return nil, fmt.Errorf("provenance in revision authority cannot be empty") + } + minRevision := 1 + maxRevision := 0 + if _, ok := m["min-revision"]; ok { + var err error + minRevision, err = checkSnapRevisionWhat(m, "min-revision", "in revision authority") + if err != nil { + return nil, err + } + } + if _, ok := m["max-revision"]; ok { + var err error + maxRevision, err = checkSnapRevisionWhat(m, "max-revision", "in revision authority") + if err != nil { + return nil, err + } + } + if maxRevision != 0 && maxRevision < minRevision { + return nil, fmt.Errorf("optional max-revision cannot be less than min-revision in revision-authority") + } + devscope, err := compileDeviceScopeConstraint(m, "revision-authority") + if err != nil { + return nil, err + } + ras = append(ras, &RevisionAuthority{ + AccountID: accountID, + Provenance: prov, + MinRevision: minRevision, + MaxRevision: maxRevision, + DeviceScope: devscope, + }) + } + + } + return &SnapDeclaration{ - assertionBase: assert, - refreshControl: refControl, - plugRules: plugRules, - slotRules: slotRules, - autoAliases: autoAliases, - aliases: aliases, - timestamp: timestamp, + assertionBase: assert, + refreshControl: refControl, + plugRules: plugRules, + slotRules: slotRules, + autoAliases: autoAliases, + aliases: aliases, + revisionAuthorities: ras, + timestamp: timestamp, }, nil } +// RevisionAuthority holds information about an account that can sign revisions +// for a given snap. +type RevisionAuthority struct { + AccountID string + Provenance []string + + MinRevision int + MaxRevision int + + DeviceScope *DeviceScopeConstraint +} + +// Check tests whether rev matches the revision authority constraints. +// Optional model and store must be provided to cross-check device-specific +// constraints. +func (ra *RevisionAuthority) Check(rev *SnapRevision, model *Model, store *Store) error { + if !strutil.ListContains(ra.Provenance, rev.Provenance()) { + return fmt.Errorf("provenance mismatch") + } + if rev.AuthorityID() != ra.AccountID { + return fmt.Errorf("authority-id mismatch") + } + revno := rev.SnapRevision() + if revno < ra.MinRevision { + return fmt.Errorf("snap revision %d is less than min-revision %d", revno, ra.MinRevision) + } + if ra.MaxRevision != 0 && revno > ra.MaxRevision { + return fmt.Errorf("snap revision %d is greater than max-revision %d", revno, ra.MaxRevision) + } + if ra.DeviceScope != nil && model != nil { + opts := DeviceScopeConstraintCheckOptions{UseFriendlyStores: true} + if err := ra.DeviceScope.Check(model, store, &opts); err != nil { + return err + } + } + return nil +} + // SnapFileSHA3_384 computes the SHA3-384 digest of the given snap file. // It also returns its size. func SnapFileSHA3_384(snapPath string) (digest string, size uint64, err error) { @@ -421,6 +540,12 @@ return snaprev.HeaderString("snap-sha3-384") } +// Provenance returns the optional provenance of the snap (defaults to +// global-upload (naming.DefaultProvenance)). +func (snaprev *SnapRevision) Provenance() string { + return snaprev.HeaderString("provenance") +} + // SnapID returns the snap id of the snap. func (snaprev *SnapRevision) SnapID() string { return snaprev.HeaderString("snap-id") @@ -449,8 +574,9 @@ // Implement further consistency checks. func (snaprev *SnapRevision) checkConsistency(db RODatabase, acck *AccountKey) error { - // TODO: expand this to consider other stores signing on their own - if !db.IsTrustedAccount(snaprev.AuthorityID()) { + otherProvenance := snaprev.Provenance() != naming.DefaultProvenance + if !otherProvenance && !db.IsTrustedAccount(snaprev.AuthorityID()) { + // delegating global-upload revisions is not allowed return fmt.Errorf("snap-revision assertion for snap id %q is not signed by a store: %s", snaprev.SnapID(), snaprev.AuthorityID()) } _, err := db.Find(AccountType, map[string]string{ @@ -462,7 +588,7 @@ if err != nil { return err } - _, err = db.Find(SnapDeclarationType, map[string]string{ + a, err := db.Find(SnapDeclarationType, map[string]string{ // XXX: mediate getting current series through some context object? this gets the job done for now "series": release.Series, "snap-id": snaprev.SnapID(), @@ -473,6 +599,23 @@ if err != nil { return err } + if otherProvenance { + decl := a.(*SnapDeclaration) + ras := decl.RevisionAuthority(snaprev.Provenance()) + matchingRevAuthority := false + for _, ra := range ras { + // model==store==nil, we do not perform device-specific + // checks at this level, those are performed at + // higher-level guarding installing actual snaps + if err := ra.Check(snaprev, nil, nil); err == nil { + matchingRevAuthority = true + break + } + } + if !matchingRevAuthority { + return fmt.Errorf("snap-revision assertion with provenance %q for snap id %q is not signed by an authorized authority: %s", snaprev.Provenance(), snaprev.SnapID(), snaprev.AuthorityID()) + } + } return nil } @@ -504,6 +647,11 @@ if err != nil { return nil, err } + + _, err = checkStringMatches(assert.headers, "provenance", naming.ValidProvenance) + if err != nil { + return nil, err + } _, err = checkNotEmptyString(assert.headers, "snap-id") if err != nil { diff -Nru snapd-2.55.5+20.04/asserts/snap_asserts_test.go snapd-2.57.5+20.04/asserts/snap_asserts_test.go --- snapd-2.55.5+20.04/asserts/snap_asserts_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/asserts/snap_asserts_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -106,6 +106,110 @@ "Cmd-3": "cmd-3", "CMD.4": "cmd-4", }) + c.Check(snapDecl.RevisionAuthority(""), IsNil) +} + +func (sds *snapDeclSuite) TestDecodeOKWithRevisionAuthority(c *C) { + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: first\n" + + "publisher-id: dev-id1\n" + + "refresh-control:\n - foo\n - bar\n" + + sds.tsLine + + `revision-authority: + - + account-id: delegated-acc-id + provenance: + - prov1 + - prov2 + min-revision: 100 + max-revision: 1000000 + on-store: + - store1 +` + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapDeclarationType) + snapDecl := a.(*asserts.SnapDeclaration) + c.Check(snapDecl.AuthorityID(), Equals, "canonical") + c.Check(snapDecl.Timestamp(), Equals, sds.ts) + c.Check(snapDecl.Series(), Equals, "16") + c.Check(snapDecl.SnapID(), Equals, "snap-id-1") + c.Check(snapDecl.SnapName(), Equals, "first") + c.Check(snapDecl.PublisherID(), Equals, "dev-id1") + c.Check(snapDecl.RefreshControl(), DeepEquals, []string{"foo", "bar"}) + ras := snapDecl.RevisionAuthority("prov1") + c.Check(ras, DeepEquals, []*asserts.RevisionAuthority{ + { + AccountID: "delegated-acc-id", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 100, + MaxRevision: 1000000, + DeviceScope: &asserts.DeviceScopeConstraint{ + Store: []string{"store1"}, + }, + }, + }) +} + +func (sds *snapDeclSuite) TestDecodeOKWithRevisionAuthorityDefaults(c *C) { + initial := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: first\n" + + "publisher-id: dev-id1\n" + + "refresh-control:\n - foo\n - bar\n" + + sds.tsLine + + `revision-authority: + - + account-id: delegated-acc-id + provenance: + - prov1 + - prov2 + min-revision: 100 +` + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + tests := []struct { + original, replaced string + revAuth asserts.RevisionAuthority + }{ + {"min", "min", asserts.RevisionAuthority{ + AccountID: "delegated-acc-id", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 100, + }}, + {"min", "max", asserts.RevisionAuthority{ + AccountID: "delegated-acc-id", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + MaxRevision: 100, + }}, + {" min-revision: 100\n", "", asserts.RevisionAuthority{ + AccountID: "delegated-acc-id", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + }}, + } + + for _, t := range tests { + encoded := strings.Replace(initial, t.original, t.replaced, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + snapDecl := a.(*asserts.SnapDeclaration) + ras := snapDecl.RevisionAuthority("prov2") + c.Check(ras, HasLen, 1) + c.Check(*ras[0], DeepEquals, t.revAuth) + } } func (sds *snapDeclSuite) TestEmptySnapName(c *C) { @@ -207,6 +311,52 @@ } +func (sds *snapDeclSuite) TestDecodeInvalidWithRevisionAuthority(c *C) { + const revAuth = `revision-authority: + - + account-id: delegated-acc-id + provenance: + - prov1 + - prov2 + min-revision: 100 + max-revision: 1000000 + on-store: + - store1 +` + encoded := "type: snap-declaration\n" + + "authority-id: canonical\n" + + "series: 16\n" + + "snap-id: snap-id-1\n" + + "snap-name: first\n" + + "publisher-id: dev-id1\n" + + "refresh-control:\n - foo\n - bar\n" + + sds.tsLine + + revAuth + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {revAuth, "revision-authority: x\n", `revision-authority stanza must be a list of maps`}, + {revAuth, "revision-authority:\n - x\n", `revision-authority stanza must be a list of maps`}, + {" account-id: delegated-acc-id\n", "", `"account-id" in revision authority is mandatory`}, + {"account-id: delegated-acc-id\n", "account-id: *\n", `"account-id" in revision authority contains invalid characters: "\*"`}, + {" provenance:\n - prov1\n - prov2\n", " provenance: \n", `provenance in revision authority must be a list of strings`}, + {"prov2\n", "*\n", `provenance in revision authority contains an invalid element: "\*"`}, + {" min-revision: 100\n", " min-revision: 0\n", `"min-revision" in revision authority must be >=1: 0`}, + {" max-revision: 1000000\n", " max-revision: 0\n", `"max-revision" in revision authority must be >=1: 0`}, + {" max-revision: 1000000\n", " max-revision: 10\n", `optional max-revision cannot be less than min-revision in revision-authority`}, + {" on-store:\n - store1\n", " on-store: foo", `on-store in revision-authority must be a list of strings`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, snapDeclErrPrefix+test.expectedErr) + } +} + func (sds *snapDeclSuite) TestDecodePlugsAndSlots(c *C) { encoded := `type: snap-declaration format: 1 @@ -887,6 +1037,25 @@ c.Check(snapRev.SnapRevision(), Equals, 1) c.Check(snapRev.DeveloperID(), Equals, "dev-id1") c.Check(snapRev.Revision(), Equals, 1) + c.Check(snapRev.Provenance(), Equals, "global-upload") +} + +func (srs *snapRevSuite) TestDecodeOKWithProvenance(c *C) { + encoded := srs.makeValidEncoded() + encoded = strings.Replace(encoded, "snap-id: snap-id-1", "provenance: foo\nsnap-id: snap-id-1", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SnapRevisionType) + snapRev := a.(*asserts.SnapRevision) + c.Check(snapRev.AuthorityID(), Equals, "store-id1") + c.Check(snapRev.Timestamp(), Equals, srs.ts) + c.Check(snapRev.SnapID(), Equals, "snap-id-1") + c.Check(snapRev.SnapSHA3_384(), Equals, blobSHA3_384) + c.Check(snapRev.SnapSize(), Equals, uint64(123)) + c.Check(snapRev.SnapRevision(), Equals, 1) + c.Check(snapRev.DeveloperID(), Equals, "dev-id1") + c.Check(snapRev.Revision(), Equals, 1) + c.Check(snapRev.Provenance(), Equals, "foo") } const ( @@ -904,6 +1073,8 @@ {digestHdr, "snap-sha3-384: \n", `"snap-sha3-384" header should not be empty`}, {digestHdr, "snap-sha3-384: #\n", `"snap-sha3-384" header cannot be decoded:.*`}, {digestHdr, "snap-sha3-384: eHl6\n", `"snap-sha3-384" header does not have the expected bit length: 24`}, + {"snap-id: snap-id-1\n", "provenance: \nsnap-id: snap-id-1\n", `"provenance" header should not be empty`}, + {"snap-id: snap-id-1\n", "provenance: *\nsnap-id: snap-id-1\n", `"provenance" header contains invalid characters: "\*"`}, {"snap-size: 123\n", "", `"snap-size" header is mandatory`}, {"snap-size: 123\n", "snap-size: \n", `"snap-size" header should not be empty`}, {"snap-size: 123\n", "snap-size: -1\n", `"snap-size" header is not an unsigned integer: -1`}, @@ -1006,48 +1177,285 @@ c.Assert(err, ErrorMatches, `snap-revision assertion for snap id "snap-id-1" does not have a matching snap-declaration assertion`) } -func (srs *snapRevSuite) TestSnapRevisionDelegation(c *C) { - c.Skip("authority-delegation disabled") +func (srs *snapRevSuite) TestRevisionAuthorityCheck(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + delegatedDB := setup3rdPartySigning(c, "delegated-id", storeDB, db) + headers := srs.makeHeaders(map[string]interface{}{ + "authority-id": "delegated-id", + "developer-id": "delegated-id", + "snap-revision": "200", + "provenance": "prov1", + }) + a, err := delegatedDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + snapRev := a.(*asserts.SnapRevision) + + tests := []struct { + revAuth asserts.RevisionAuthority + err string + }{ + {asserts.RevisionAuthority{ + AccountID: "delegated-id", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + }, ""}, + {asserts.RevisionAuthority{ + AccountID: "delegated-id", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + MaxRevision: 1000, + }, ""}, + {asserts.RevisionAuthority{ + AccountID: "delegated-id", + Provenance: []string{"prov2"}, + MinRevision: 1, + MaxRevision: 1000, + }, "provenance mismatch"}, + {asserts.RevisionAuthority{ + AccountID: "delegated-id-2", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + MaxRevision: 1000, + }, "authority-id mismatch"}, + {asserts.RevisionAuthority{ + AccountID: "delegated-id", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1000, + }, "snap revision 200 is less than min-revision 1000"}, + {asserts.RevisionAuthority{ + AccountID: "delegated-id", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 10, + MaxRevision: 110, + }, "snap revision 200 is greater than max-revision 110"}, + } + + for _, t := range tests { + err := t.revAuth.Check(snapRev, nil, nil) + if t.err == "" { + c.Check(err, IsNil) + } else { + c.Check(err, ErrorMatches, t.err) + } + } +} + +func (srs *snapRevSuite) TestRevisionAuthorityCheckDeviceScope(c *C) { + a, err := asserts.Decode([]byte(`type: model +authority-id: my-brand +series: 16 +brand-id: my-brand +model: my-model +store: substore +architecture: armhf +kernel: krnl +gadget: gadget +timestamp: 2018-09-12T12:00:00Z +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AXNpZw==`)) + c.Assert(err, IsNil) + myModel := a.(*asserts.Model) + + a, err = asserts.Decode([]byte(`type: store +store: substore +authority-id: canonical +operator-id: canonical +friendly-stores: + - a-store + - store1 + - store2 +timestamp: 2018-09-12T12:00:00Z +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AXNpZw==`)) + c.Assert(err, IsNil) + substore := a.(*asserts.Store) storeDB, db := makeStoreAndCheckDB(c) - prereqDevAccount(c, storeDB, db) - prereqSnapDecl(c, storeDB, db) + delegatedDB := setup3rdPartySigning(c, "my-brand", storeDB, db) + headers := srs.makeHeaders(map[string]interface{}{ + "authority-id": "my-brand", + "developer-id": "my-brand", + "snap-revision": "200", + "provenance": "prov1", + }) + a, err = delegatedDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + snapRev := a.(*asserts.SnapRevision) - otherDB := setup3rdPartySigning(c, "other", storeDB, db) + tests := []struct { + revAuth asserts.RevisionAuthority + substore *asserts.Store + err string + }{ + {asserts.RevisionAuthority{ + AccountID: "my-brand", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + }, nil, ""}, + {asserts.RevisionAuthority{ + AccountID: "my-brand", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + DeviceScope: &asserts.DeviceScopeConstraint{ + Store: []string{"other-store"}, + }, + }, nil, "on-store mismatch"}, + {asserts.RevisionAuthority{ + AccountID: "my-brand", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + DeviceScope: &asserts.DeviceScopeConstraint{ + Store: []string{"substore"}, + }, + }, nil, ""}, + {asserts.RevisionAuthority{ + AccountID: "my-brand", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + DeviceScope: &asserts.DeviceScopeConstraint{ + Store: []string{"substore"}, + }, + }, substore, ""}, + {asserts.RevisionAuthority{ + AccountID: "my-brand", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + DeviceScope: &asserts.DeviceScopeConstraint{ + Store: []string{"a-store"}, + }, + }, substore, ""}, + {asserts.RevisionAuthority{ + AccountID: "my-brand", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + DeviceScope: &asserts.DeviceScopeConstraint{ + Store: []string{"store1"}, + }, + }, nil, "on-store mismatch"}, + {asserts.RevisionAuthority{ + AccountID: "my-brand", + Provenance: []string{"prov1", "prov2"}, + MinRevision: 1, + DeviceScope: &asserts.DeviceScopeConstraint{ + Store: []string{"store1", "other-store"}, + }, + }, substore, ""}, + } + + for _, t := range tests { + err := t.revAuth.Check(snapRev, myModel, t.substore) + if t.err == "" { + c.Check(err, IsNil) + } else { + c.Check(err, ErrorMatches, t.err) + } + } +} + +func (srs *snapRevSuite) TestSnapRevisionDelegation(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + delegatedDB := setup3rdPartySigning(c, "delegated-id", storeDB, db) + + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "delegated-id", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) - since := time.Now() headers := srs.makeHeaders(map[string]interface{}{ - "authority-id": "canonical", - "signatory-id": "other", - "timestamp": since.Format(time.RFC3339), + "authority-id": "delegated-id", + "developer-id": "delegated-id", + "provenance": "prov1", }) - snapRev, err := otherDB.Sign(asserts.SnapRevisionType, headers, nil, "") + snapRev, err := delegatedDB.Sign(asserts.SnapRevisionType, headers, nil, "") c.Assert(err, IsNil) - // now add authority-delegation - headers = map[string]interface{}{ - "authority-id": "canonical", - "account-id": "canonical", - "delegate-id": "other", - "assertions": []interface{}{ + err = db.Check(snapRev) + c.Check(err, ErrorMatches, `snap-revision assertion with provenance "prov1" for snap id "snap-id-1" is not signed by an authorized authority: delegated-id`) + + // establish delegation + snapDecl, err = storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "delegated-id", + "revision": "1", + "revision-authority": []interface{}{ map[string]interface{}{ - "type": "snap-revision", - "headers": map[string]interface{}{ - "snap-id": "snap-id-1", + "account-id": "delegated-id", + "provenance": []interface{}{ + "prov1", + }, + // present but not checked at this level + "on-store": []interface{}{ + "store1", }, - "since": since.AddDate(0, -1, 0).Format(time.RFC3339), }, }, - } - ad, err := storeDB.Sign(asserts.AuthorityDelegationType, headers, nil, "") + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) c.Assert(err, IsNil) - c.Check(db.Add(ad), IsNil) + // now revision should be accepted err = db.Check(snapRev) c.Check(err, IsNil) } +func (srs *snapRevSuite) TestSnapRevisionDelegationRevisionOutOfRange(c *C) { + storeDB, db := makeStoreAndCheckDB(c) + + delegatedDB := setup3rdPartySigning(c, "delegated-id", storeDB, db) + + // establish delegation + snapDecl, err := storeDB.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": "delegated-id", + "revision-authority": []interface{}{ + map[string]interface{}{ + "account-id": "delegated-id", + "provenance": []interface{}{ + "prov1", + }, + // present but not checked at this level + "on-store": []interface{}{ + "store1", + }, + "max-revision": "200", + }, + }, + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + err = db.Add(snapDecl) + c.Assert(err, IsNil) + + headers := srs.makeHeaders(map[string]interface{}{ + "authority-id": "delegated-id", + "developer-id": "delegated-id", + "provenance": "prov1", + "snap-revision": "1000", + }) + snapRev, err := delegatedDB.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + + err = db.Check(snapRev) + c.Check(err, ErrorMatches, `snap-revision assertion with provenance "prov1" for snap id "snap-id-1" is not signed by an authorized authority: delegated-id`) +} + func (srs *snapRevSuite) TestSnapRevisionDelegationInconsistentTimestamp(c *C) { c.Skip("authority-delegation disabled") diff -Nru snapd-2.55.5+20.04/boot/assets.go snapd-2.57.5+20.04/boot/assets.go --- snapd-2.55.5+20.04/boot/assets.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/assets.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,9 +34,10 @@ "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/strutil" ) @@ -263,8 +264,8 @@ trustedRecoveryAssets []string trackedRecoveryAssets bootAssetsMap - dataEncryptionKey secboot.EncryptionKey - saveEncryptionKey secboot.EncryptionKey + dataEncryptionKey keys.EncryptionKey + saveEncryptionKey keys.EncryptionKey } // Observe observes the operation related to the content of a given gadget @@ -339,7 +340,7 @@ return o.trackedRecoveryAssets } -func (o *TrustedAssetsInstallObserver) ChosenEncryptionKeys(key, saveKey secboot.EncryptionKey) { +func (o *TrustedAssetsInstallObserver) ChosenEncryptionKeys(key, saveKey keys.EncryptionKey) { o.dataEncryptionKey = key o.saveEncryptionKey = saveKey } @@ -356,11 +357,11 @@ // trusted assets need tracking only when the system is using encryption // for its data partitions trackTrustedAssets := false - _, err := sealedKeysMethod(dirs.GlobalRootDir) + _, err := device.SealedKeysMethod(dirs.GlobalRootDir) switch { case err == nil: trackTrustedAssets = true - case err == errNoSealedKeys: + case err == device.ErrNoSealedKeys: // nothing to do case err != nil: // all other errors diff -Nru snapd-2.55.5+20.04/boot/assets_test.go snapd-2.57.5+20.04/boot/assets_test.go --- snapd-2.55.5+20.04/boot/assets_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/assets_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -38,6 +38,7 @@ "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" @@ -479,9 +480,9 @@ obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) c.Assert(err, IsNil) c.Assert(obs, NotNil) - obs.ChosenEncryptionKeys(secboot.EncryptionKey{1, 2, 3, 4}, secboot.EncryptionKey{5, 6, 7, 8}) - c.Check(obs.CurrentDataEncryptionKey(), DeepEquals, secboot.EncryptionKey{1, 2, 3, 4}) - c.Check(obs.CurrentSaveEncryptionKey(), DeepEquals, secboot.EncryptionKey{5, 6, 7, 8}) + obs.ChosenEncryptionKeys(keys.EncryptionKey{1, 2, 3, 4}, keys.EncryptionKey{5, 6, 7, 8}) + c.Check(obs.CurrentDataEncryptionKey(), DeepEquals, keys.EncryptionKey{1, 2, 3, 4}) + c.Check(obs.CurrentSaveEncryptionKey(), DeepEquals, keys.EncryptionKey{5, 6, 7, 8}) } func (s *assetsSuite) TestInstallObserverTrustedButNoAssets(c *C) { @@ -500,9 +501,9 @@ obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) c.Assert(err, IsNil) c.Assert(obs, NotNil) - obs.ChosenEncryptionKeys(secboot.EncryptionKey{1, 2, 3, 4}, secboot.EncryptionKey{5, 6, 7, 8}) - c.Check(obs.CurrentDataEncryptionKey(), DeepEquals, secboot.EncryptionKey{1, 2, 3, 4}) - c.Check(obs.CurrentSaveEncryptionKey(), DeepEquals, secboot.EncryptionKey{5, 6, 7, 8}) + obs.ChosenEncryptionKeys(keys.EncryptionKey{1, 2, 3, 4}, keys.EncryptionKey{5, 6, 7, 8}) + c.Check(obs.CurrentDataEncryptionKey(), DeepEquals, keys.EncryptionKey{1, 2, 3, 4}) + c.Check(obs.CurrentSaveEncryptionKey(), DeepEquals, keys.EncryptionKey{5, 6, 7, 8}) } func (s *assetsSuite) TestInstallObserverTrustedReuseNameErr(c *C) { diff -Nru snapd-2.55.5+20.04/boot/bootchain.go snapd-2.57.5+20.04/boot/bootchain.go --- snapd-2.55.5+20.04/boot/bootchain.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/bootchain.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2020 Canonical Ltd + * Copyright (C) 2020-2022 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 @@ -38,6 +38,7 @@ type bootChain struct { BrandID string `json:"brand-id"` Model string `json:"model"` + Classic bool `json:"classic,omitempty"` Grade asserts.ModelGrade `json:"grade"` ModelSignKeyID string `json:"model-sign-key-id"` AssetChain []bootAsset `json:"asset-chain"` @@ -54,6 +55,7 @@ return &modelForSealing{ brandID: b.BrandID, model: b.Model, + classic: b.Classic, grade: b.Grade, modelSignKeyID: b.ModelSignKeyID, } diff -Nru snapd-2.55.5+20.04/boot/bootchain_test.go snapd-2.57.5+20.04/boot/bootchain_test.go --- snapd-2.55.5+20.04/boot/bootchain_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/bootchain_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2020 Canonical Ltd + * Copyright (C) 2020-2022 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 @@ -1241,9 +1241,26 @@ modelForSealing := bc.SecbootModelForSealing() c.Check(modelForSealing.Model(), Equals, "my-model") c.Check(modelForSealing.BrandID(), Equals, "my-brand") + c.Check(modelForSealing.Classic(), Equals, false) c.Check(modelForSealing.Grade(), Equals, asserts.ModelGrade("signed")) c.Check(modelForSealing.SignKeyID(), Equals, "my-key-id") c.Check(modelForSealing.Series(), Equals, "16") c.Check(boot.ModelUniqueID(modelForSealing), Equals, "my-brand/my-model,signed,my-key-id") } + +func (s *bootchainSuite) TestClassicModelForSealing(c *C) { + bc := boot.BootChain{ + BrandID: "my-brand", + Model: "my-model", + Classic: true, + Grade: "signed", + ModelSignKeyID: "my-key-id", + } + + modelForSealing := bc.SecbootModelForSealing() + c.Check(modelForSealing.Model(), Equals, "my-model") + c.Check(modelForSealing.BrandID(), Equals, "my-brand") + c.Check(modelForSealing.Classic(), Equals, true) + c.Check(boot.ModelUniqueID(modelForSealing), Equals, "my-brand/my-model,signed,my-key-id") +} diff -Nru snapd-2.55.5+20.04/boot/boot.go snapd-2.57.5+20.04/boot/boot.go --- snapd-2.55.5+20.04/boot/boot.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/boot.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019-2020 Canonical Ltd + * Copyright (C) 2019-2022 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,13 +50,23 @@ RebootBootloader bootloader.RebootBootloader } +// NextBootContext carries additional significative information used when +// setting the next boot. +type NextBootContext struct { + // BootWithoutTry is sets if we don't want to use the "try" logic. This + // is useful if the next boot is part of an installation undo. + BootWithoutTry bool +} + // A BootParticipant handles the boot process details for a snap involved in it. type BootParticipant interface { - // SetNextBoot will schedule the snap to be used in the next boot. For - // base snaps it is up to the caller to select the right bootable base - // (from the model assertion). It is a noop for not relevant snaps. - // Otherwise it returns whether a reboot is required. - SetNextBoot() (rebootInfo RebootInfo, err error) + // SetNextBoot will schedule the snap to be used in the next + // boot. bootCtx contains context information that influences how the + // next boot is performed. For base snaps it is up to the caller to + // select the right bootable base (from the model assertion). It is a + // noop for not relevant snaps. Otherwise it returns whether a reboot + // is required. + SetNextBoot(bootCtx NextBootContext) (rebootInfo RebootInfo, err error) // Is this a trivial implementation of the interface? IsTrivial() bool @@ -77,7 +87,9 @@ type trivial struct{} -func (trivial) SetNextBoot() (RebootInfo, error) { return RebootInfo{RebootRequired: false}, nil } +func (trivial) SetNextBoot(bootCtx NextBootContext) (RebootInfo, error) { + return RebootInfo{RebootRequired: false}, nil +} func (trivial) IsTrivial() bool { return true } func (trivial) RemoveKernelAssets() error { return nil } func (trivial) ExtractKernelAssets(snap.Container) error { return nil } @@ -139,15 +151,12 @@ return false } - if t != snap.TypeOS && t != snap.TypeKernel && t != snap.TypeBase { - // note we don't currently have anything useful to do with gadgets - return false - } - switch t { case snap.TypeKernel: if s.InstanceName() != dev.Kernel() { - // a remodel might leave you in this state + // a remodel might leave behind installed a kernel that + // is not the device kernel anymore, ignore such a + // kernel by checking the name return false } case snap.TypeBase, snap.TypeOS: @@ -158,6 +167,16 @@ if s.InstanceName() != base { return false } + case snap.TypeGadget: + // First condition: gadget is not a boot participant for UC16/18 + // Second condition: a remodel might leave behind installed a + // gadget that is not the device gadget anymore, ignore such a + // gadget by checking the name + if !dev.HasModeenv() || s.InstanceName() != dev.Gadget() { + return false + } + default: + return false } return true @@ -175,11 +194,12 @@ // curSnap instead if the error is only for the trySnap or tryingStatus. revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) - // setNext lazily implements setting the next boot target for - // the type's boot snap. actually committing the update - // is done via the returned bootStateUpdate's commit method. - // It will return information for rebooting if necessary. - setNext(s snap.PlaceInfo) (rbi RebootInfo, u bootStateUpdate, err error) + // setNext lazily implements setting the next boot target for the type's + // boot snap. bootCtx specifies additional information bits we might + // need. Actually committing the update is done via the returned + // bootStateUpdate's commit method. It will return information for + // rebooting if necessary. + setNext(s snap.PlaceInfo, bootCtx NextBootContext) (rbi RebootInfo, u bootStateUpdate, err error) // markSuccessful lazily implements marking the boot // successful for the type's boot snap. The actual committing @@ -202,18 +222,21 @@ if !dev.RunMode() { return nil, fmt.Errorf("internal error: no boot state handling for ephemeral modes") } + if typ == snap.TypeOS { + typ = snap.TypeBase + } newBootState := newBootState16 + participantTypes := []snap.Type{snap.TypeBase, snap.TypeKernel} if dev.HasModeenv() { newBootState = newBootState20 + participantTypes = append(participantTypes, snap.TypeGadget) } - switch typ { - case snap.TypeOS, snap.TypeBase: - return newBootState(snap.TypeBase, dev), nil - case snap.TypeKernel: - return newBootState(snap.TypeKernel, dev), nil - default: - return nil, fmt.Errorf("internal error: no boot state handling for snap type %q", typ) + for _, partTyp := range participantTypes { + if typ == partTyp { + return newBootState(typ, dev), nil + } } + return nil, fmt.Errorf("internal error: no boot state handling for snap type %q", typ) } // InUseFunc is a function to check if the snap is in use or not. @@ -439,7 +462,7 @@ func UpdateCommandLineForGadgetComponent(dev snap.Device, gadgetSnapOrDir string) (needsReboot bool, err error) { if !dev.HasModeenv() { // only UC20 devices are supported - return false, fmt.Errorf("internal error: command line component cannot be updated on non UC20 devices") + return false, fmt.Errorf("internal error: command line component cannot be updated on pre-UC20 devices") } opts := &bootloader.Options{ Role: bootloader.RoleRunMode, @@ -473,3 +496,16 @@ } return cmdlineChange, nil } + +// MarkFactoryResetComplete runs a series of steps in a run system that complete a +// factory reset process. +func MarkFactoryResetComplete(encrypted bool) error { + if !encrypted { + // there is nothing to do on an unencrypted system + return nil + } + if err := postFactoryResetCleanup(); err != nil { + return fmt.Errorf("cannot perform post factory reset boot cleanup: %v", err) + } + return nil +} diff -Nru snapd-2.55.5+20.04/boot/boot_robustness_test.go snapd-2.57.5+20.04/boot/boot_robustness_test.go --- snapd-2.55.5+20.04/boot/boot_robustness_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/boot_robustness_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -304,7 +304,7 @@ setNextFunc := func(snap.Device) error { // we don't care about the reboot required logic here - _, err := bootKern.SetNextBoot() + _, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) return err } diff -Nru snapd-2.55.5+20.04/boot/bootstate16.go snapd-2.57.5+20.04/boot/bootstate16.go --- snapd-2.55.5+20.04/boot/bootstate16.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/bootstate16.go 2022-10-17 16:25:18.000000000 +0000 @@ -159,9 +159,7 @@ return u16, nil } -func (s16 *bootState16) setNext(s snap.PlaceInfo) (rbi RebootInfo, u bootStateUpdate, err error) { - nextBoot := s.Filename() - +func (s16 *bootState16) setNext(s snap.PlaceInfo, bootCtx NextBootContext) (rbi RebootInfo, u bootStateUpdate, err error) { nextBootVar := fmt.Sprintf("snap_try_%s", s16.varSuffix) goodBootVar := fmt.Sprintf("snap_%s", s16.varSuffix) @@ -173,8 +171,9 @@ env := u16.env toCommit := u16.toCommit - snapMode := TryStatus rbi.RebootRequired = true + snapMode := TryStatus + nextBoot := s.Filename() if env[goodBootVar] == nextBoot { // If we were in anything but default ("") mode before // and switched to the good core/kernel again, make @@ -188,6 +187,10 @@ snapMode = DefaultStatus nextBoot = "" rbi.RebootRequired = false + } else if bootCtx.BootWithoutTry { + toCommit[goodBootVar] = nextBoot + snapMode = DefaultStatus + nextBoot = "" } toCommit["snap_mode"] = snapMode diff -Nru snapd-2.55.5+20.04/boot/bootstate20_bloader_kernel_state.go snapd-2.57.5+20.04/boot/bootstate20_bloader_kernel_state.go --- snapd-2.55.5+20.04/boot/bootstate20_bloader_kernel_state.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/bootstate20_bloader_kernel_state.go 2022-10-17 16:25:18.000000000 +0000 @@ -177,6 +177,26 @@ return nil } +func (bks *extractedRunKernelImageBootloaderKernelState) setNextKernelNoTry(sn snap.PlaceInfo) error { + if sn.Filename() != bks.currentKernel.Filename() { + err := bks.ebl.EnableKernel(sn) + if err != nil { + return err + } + } + + if bks.currentKernelStatus != DefaultStatus { + m := map[string]string{ + "kernel_status": DefaultStatus, + } + + // set the boot variables + return bks.ebl.SetBootVars(m) + } + + return nil +} + // envRefExtractedKernelBootloaderKernelState implements bootloaderKernelState20 for // bootloaders that only support using bootloader env and i.e. don't support // ExtractedRunKernelImageBootloader @@ -289,6 +309,17 @@ if bootenvChanged { return envbks.bl.SetBootVars(envbks.toCommit) + } + + return nil +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) setNextKernelNoTry(sn snap.PlaceInfo) error { + envbks.toCommit["kernel_status"] = "" + bootenvChanged := envbks.commonStateCommitUpdate(sn, "snap_kernel") + + if bootenvChanged { + return envbks.bl.SetBootVars(envbks.toCommit) } return nil diff -Nru snapd-2.55.5+20.04/boot/bootstate20.go snapd-2.57.5+20.04/boot/bootstate20.go --- snapd-2.55.5+20.04/boot/bootstate20.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/bootstate20.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019-2020 Canonical Ltd + * Copyright (C) 2019-2022 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 @@ -39,6 +39,8 @@ return &bootState20Kernel{ dev: dev, } + case snap.TypeGadget: + return &bootState20Gadget{} default: panic(fmt.Sprintf("cannot make a bootState20 for snap type %q", typ)) } @@ -52,6 +54,26 @@ return modeenv, nil } +// selectGadgetSnap finds the currently active gadget snap +func selectGadgetSnap(modeenv *Modeenv, rootfsDir string) (snap.PlaceInfo, error) { + gadgetInfo, err := snap.ParsePlaceInfoFromSnapFileName(modeenv.Gadget) + if err != nil { + return nil, fmt.Errorf("cannot get snap revision: modeenv gadget boot variable is invalid: %v", err) + } + + // check that the current snap actually exists + file := modeenv.Gadget + snapPath := filepath.Join(dirs.SnapBlobDirUnder(rootfsDir), file) + if !osutil.FileExists(snapPath) { + // somehow the gadget snap doesn't exist in ubuntu-data + // this could happen if the modeenv is manipulated + // out-of-band from snapd + return nil, fmt.Errorf("gadget snap %q does not exist on ubuntu-data", file) + } + + return gadgetInfo, nil +} + // // bootloaderKernelState20 methods // @@ -73,6 +95,12 @@ // markSuccessfulKernel marks the specified kernel as having booted // successfully, whether that kernel is the current kernel or the try-kernel markSuccessfulKernel(sn snap.PlaceInfo) error + // setNextKernelNoTry changes boot configuration so the specified kernel will + // be the one used in next boot, without the "try" logic. This shall be + // used only when we have already booted to a new kernel but for some + // reason we need to revert to the previous kernel (for instance, in a + // transactional update when the failing snap is not the kernel). + setNextKernelNoTry(sn snap.PlaceInfo) error } // @@ -145,13 +173,13 @@ } } - modeenvRewritten := false + expectReseal := false // next write the modeenv if it changed if !u20.writeModeenv.deepEqual(u20.modeenv) { if err := u20.writeModeenv.Write(); err != nil { return err } - modeenvRewritten = true + expectReseal = resealExpectedByModeenvChange(u20.writeModeenv, u20.modeenv) } // next reseal using the modeenv values, we do this before any @@ -163,7 +191,6 @@ // changed because of unasserted kernels, then pass a // flag as hint whether to reseal based on whether we // wrote the modeenv - expectReseal := modeenvRewritten if err := resealKeyToModeenv(dirs.GlobalRootDir, u20.writeModeenv, expectReseal); err != nil { return err } @@ -299,16 +326,19 @@ return u20, nil } -func (ks20 *bootState20Kernel) setNext(next snap.PlaceInfo) (rbi RebootInfo, u bootStateUpdate, err error) { - u20, nextStatus, err := genericSetNext(ks20, next) +func (ks20 *bootState20Kernel) setNext(next snap.PlaceInfo, bootCtx NextBootContext) (rbi RebootInfo, u bootStateUpdate, err error) { + u20, rebootRequired, err := genericSetNext(ks20, next) if err != nil { return RebootInfo{RebootRequired: false}, nil, err } - // if we are setting a snap as a try snap, then we need to reboot - rbi.RebootRequired = false - if nextStatus == TryStatus { - rbi.RebootRequired = true + nextStatus := DefaultStatus + rbi.RebootRequired = rebootRequired + if rbi.RebootRequired { + // if we need to reboot and we are not undoing, we set the try status + if !bootCtx.BootWithoutTry { + nextStatus = TryStatus + } // kernels are usually loaded directly by the bootloader, for // which we may need to pass additional data to make 'try' // operation more robust - that might be provided by the @@ -321,10 +351,22 @@ currentKernel := ks20.bks.kernel() if next.Filename() != currentKernel.Filename() { // on commit, add this kernel to the modeenv - u20.writeModeenv.CurrentKernels = append( - u20.writeModeenv.CurrentKernels, - next.Filename(), - ) + if bootCtx.BootWithoutTry { + // when undoing, the current kernel is being removed + u20.writeModeenv.CurrentKernels = []string{next.Filename()} + } else { + u20.writeModeenv.CurrentKernels = append( + u20.writeModeenv.CurrentKernels, + next.Filename(), + ) + } + } + + bootTask := func() error { return ks20.bks.setNextKernel(next, nextStatus) } + if bootCtx.BootWithoutTry { + // force revert to "next" kernel (actually it is the old one) + // and ignore the try status, that will be empty in this case. + bootTask = func() error { return ks20.bks.setNextKernelNoTry(next) } } // On commit, if we are about to try an update, and need to set the next @@ -333,7 +375,7 @@ // kernel and updating the modeenv, the initramfs would fail the boot // because the modeenv doesn't "trust" or expect the new kernel that booted. // As such, set the next kernel as a post modeenv task. - u20.postModeenv(func() error { return ks20.bks.setNextKernel(next, nextStatus) }) + u20.postModeenv(bootTask) return rbi, u20, nil } @@ -343,9 +385,9 @@ // Choosing to boot/mount the base snap needs to be committed to the // modeenv, but no state needs to be committed when choosing to mount a // kernel snap. -func (ks20 *bootState20Kernel) selectAndCommitSnapInitramfsMount(modeenv *Modeenv) (sn snap.PlaceInfo, err error) { +func (ks20 *bootState20Kernel) selectAndCommitSnapInitramfsMount(modeenv *Modeenv, rootfsDir string) (sn snap.PlaceInfo, err error) { // first do the generic choice of which snap to use - first, second, err := genericInitramfsSelectSnap(ks20, modeenv, TryingStatus, "kernel") + first, second, err := genericInitramfsSelectSnap(ks20, modeenv, rootfsDir, TryingStatus, "kernel") if err != nil && err != errTrySnapFallback { return nil, err } @@ -381,6 +423,35 @@ } // +// gadget snap methods +// + +// bootState20Gadget implements the bootState interface for gadget +// snaps on UC20+. It is used for both setNext() and markSuccessful(), +// with both of those methods returning bootStateUpdate20 to be used +// with bootStateUpdate. +type bootState20Gadget struct{} + +func (bs20 *bootState20Gadget) revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) { + return nil, nil, "", fmt.Errorf("internal error, revisions not implemented for gadget") +} + +func (bs20 *bootState20Gadget) setNext(next snap.PlaceInfo, bootCtx NextBootContext) (rbi RebootInfo, u bootStateUpdate, err error) { + u20, err := newBootStateUpdate20(nil) + if err != nil { + return RebootInfo{RebootRequired: false}, nil, err + } + + u20.writeModeenv.Gadget = next.Filename() + + return RebootInfo{RebootRequired: false}, u20, err +} + +func (bs20 *bootState20Gadget) markSuccessful(bootStateUpdate) (bootStateUpdate, error) { + return nil, fmt.Errorf("internal error, markSuccessful not implemented for gadget") +} + +// // base snap methods // @@ -441,21 +512,28 @@ return u20, nil } -func (bs20 *bootState20Base) setNext(next snap.PlaceInfo) (rbi RebootInfo, u bootStateUpdate, err error) { - u20, nextStatus, err := genericSetNext(bs20, next) +func (bs20 *bootState20Base) setNext(next snap.PlaceInfo, bootCtx NextBootContext) (rbi RebootInfo, u bootStateUpdate, err error) { + // bases are handled by snap-bootstrap, hence we are not interested in + // the bootloader's opinion (no need for rbi.RebootBootloader, so it is + // not filled anywhere in this method). + u20, rebootRequired, err := genericSetNext(bs20, next) if err != nil { return RebootInfo{RebootRequired: false}, nil, err } - // if we are setting a snap as a try snap, then we need to reboot - rbi.RebootRequired = false - if nextStatus == TryStatus { - // only update the try base if we are actually in try status - u20.writeModeenv.TryBase = next.Filename() - // a 'try' base is handled by snap-bootstrap, hence we are not - // interested in the bootloader's opinion (no need for - // rbi.RebootBootloader, so it is not filled). - rbi.RebootRequired = true + nextStatus := DefaultStatus + rbi.RebootRequired = rebootRequired + if rbi.RebootRequired { + if bootCtx.BootWithoutTry { + // we must make sure we boot with the base we revert to + u20.writeModeenv.Base = next.Filename() + u20.writeModeenv.TryBase = "" + } else { + // if we need to reboot and we are not undoing, we set the try status + // and set appropriately the base we want to try + nextStatus = TryStatus + u20.writeModeenv.TryBase = next.Filename() + } } // always update the base status @@ -470,13 +548,14 @@ // Choosing to boot/mount the base snap needs to be committed to the // modeenv, but no state needs to be committed when choosing to mount a // kernel snap. -func (bs20 *bootState20Base) selectAndCommitSnapInitramfsMount(modeenv *Modeenv) (sn snap.PlaceInfo, err error) { +func (bs20 *bootState20Base) selectAndCommitSnapInitramfsMount(modeenv *Modeenv, rootfsDir string) (sn snap.PlaceInfo, err error) { // first do the generic choice of which snap to use // the logic in that function is sufficient to pick the base snap entirely, // so we don't ever need to look at the fallback snap, we just need to know // whether the chosen snap is a try snap or not, if it is then we process // the modeenv in the "try" -> "trying" case - first, second, err := genericInitramfsSelectSnap(bs20, modeenv, TryStatus, "base") + first, second, err := + genericInitramfsSelectSnap(bs20, modeenv, rootfsDir, TryStatus, "base") // errTrySnapFallback is handled manually by inspecting second below if err != nil && err != errTrySnapFallback { return nil, err @@ -530,16 +609,16 @@ // genericSetNext implements the generic logic for setting up a snap to be tried // for boot and works for both kernel and base snaps (though not // simultaneously). -func genericSetNext(b bootState20, next snap.PlaceInfo) (u20 *bootStateUpdate20, setStatus string, err error) { +func genericSetNext(b bootState20, next snap.PlaceInfo) (u20 *bootStateUpdate20, rebootRequired bool, err error) { u20, err = newBootStateUpdate20(nil) if err != nil { - return nil, "", err + return nil, false, err } // get the current snap current, _, _, err := b.revisionsFromModeenv(u20.modeenv) if err != nil { - return nil, "", err + return nil, false, err } // check if the next snap is really the same as the current snap, in which @@ -547,12 +626,11 @@ if current.SnapName() == next.SnapName() && next.SnapRevision() == current.SnapRevision() { // if we are setting the next snap as the current snap, don't need to // change any snaps, just reset the status to default - return u20, DefaultStatus, nil + return u20, false, nil } - // by default we will set the status as "try" to prepare for an update, - // which also by default will require a reboot - return u20, TryStatus, nil + // next != current so we need to reboot + return u20, true, nil } func toBootStateUpdate20(update bootStateUpdate) (u20 *bootStateUpdate20, err error) { @@ -560,7 +638,7 @@ if update != nil { var ok bool if u20, ok = update.(*bootStateUpdate20); !ok { - return nil, fmt.Errorf("internal error, cannot thread %T with update for UC20", update) + return nil, fmt.Errorf("internal error, cannot thread %T with update for UC20+", update) } } if u20 == nil { @@ -612,7 +690,7 @@ // the first and second choice for what snaps to mount. If there is a second // snap, then that snap is the fallback or non-trying snap and the first snap is // the try snap. -func genericInitramfsSelectSnap(bs bootState20, modeenv *Modeenv, expectedTryStatus, typeString string) ( +func genericInitramfsSelectSnap(bs bootState20, modeenv *Modeenv, rootfsDir string, expectedTryStatus, typeString string) ( firstChoice, secondChoice snap.PlaceInfo, err error, ) { @@ -625,7 +703,7 @@ // check that the current snap actually exists file := curSnap.Filename() - snapPath := filepath.Join(dirs.SnapBlobDirUnder(InitramfsWritableDir), file) + snapPath := filepath.Join(dirs.SnapBlobDirUnder(rootfsDir), file) if !osutil.FileExists(snapPath) { // somehow the boot snap doesn't exist in ubuntu-data // for a kernel, this could happen if we have some bug where ubuntu-boot @@ -662,7 +740,7 @@ logger.Noticef("try-%[1]s snap is empty, but \"%[1]s_status\" is \"trying\"", typeString) return curSnap, nil, errTrySnapFallback } - trySnapPath := filepath.Join(dirs.SnapBlobDirUnder(InitramfsWritableDir), trySnap.Filename()) + trySnapPath := filepath.Join(dirs.SnapBlobDirUnder(rootfsDir), trySnap.Filename()) if !osutil.FileExists(trySnapPath) { // or when the snap file does not exist logger.Noticef("try-%s snap %q does not exist", typeString, trySnap.Filename()) diff -Nru snapd-2.55.5+20.04/boot/boottest/device.go snapd-2.57.5+20.04/boot/boottest/device.go --- snapd-2.55.5+20.04/boot/boottest/device.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/boottest/device.go 2022-10-17 16:25:18.000000000 +0000 @@ -38,7 +38,7 @@ // [@], no means classic, empty // defaults to "run" for UC16/18. If mode is set HasModeenv // returns true for UC20 and an empty boot snap name panics. -// It returns for both Base and Kernel, for more +// It returns for Base, Kernel and gadget, for more // control mock a DeviceContext. func MockDevice(s string) snap.Device { bootsnap, mode, uc20 := snapAndMode(s) @@ -88,6 +88,12 @@ } return d.bootSnap } +func (d *mockDevice) Gadget() string { + if d.model != nil { + return d.model.Gadget() + } + return d.bootSnap +} func (d *mockDevice) Model() *asserts.Model { if d.model == nil { panic("Device.Model called but MockUC20Device not used") diff -Nru snapd-2.55.5+20.04/boot/boottest/device_test.go snapd-2.57.5+20.04/boot/boottest/device_test.go --- snapd-2.55.5+20.04/boot/boottest/device_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/boottest/device_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -38,6 +38,7 @@ c.Check(dev.Classic(), Equals, true) c.Check(dev.Kernel(), Equals, "") c.Check(dev.Base(), Equals, "") + c.Check(dev.Gadget(), Equals, "") c.Check(dev.RunMode(), Equals, true) c.Check(dev.HasModeenv(), Equals, false) @@ -51,6 +52,7 @@ c.Check(dev.Classic(), Equals, false) c.Check(dev.Kernel(), Equals, "boot-snap") c.Check(dev.Base(), Equals, "boot-snap") + c.Check(dev.Gadget(), Equals, "boot-snap") c.Check(dev.RunMode(), Equals, true) c.Check(dev.HasModeenv(), Equals, false) c.Check(func() { dev.Model() }, Panics, "Device.Model called but MockUC20Device not used") @@ -59,6 +61,7 @@ c.Check(dev.Classic(), Equals, false) c.Check(dev.Kernel(), Equals, "boot-snap") c.Check(dev.Base(), Equals, "boot-snap") + c.Check(dev.Gadget(), Equals, "boot-snap") c.Check(dev.RunMode(), Equals, true) c.Check(dev.HasModeenv(), Equals, true) c.Check(func() { dev.Model() }, PanicMatches, "Device.Model called.*") @@ -67,6 +70,7 @@ c.Check(dev.Classic(), Equals, false) c.Check(dev.Kernel(), Equals, "boot-snap") c.Check(dev.Base(), Equals, "boot-snap") + c.Check(dev.Gadget(), Equals, "boot-snap") c.Check(dev.RunMode(), Equals, false) c.Check(dev.HasModeenv(), Equals, true) c.Check(func() { dev.Model() }, PanicMatches, "Device.Model called.*") @@ -79,6 +83,7 @@ c.Check(dev.RunMode(), Equals, true) c.Check(dev.Kernel(), Equals, "pc-kernel") c.Check(dev.Base(), Equals, "core20") + c.Check(dev.Gadget(), Equals, "pc") c.Check(dev.Model().Model(), Equals, "my-model-uc20") diff -Nru snapd-2.55.5+20.04/boot/boot_test.go snapd-2.57.5+20.04/boot/boot_test.go --- snapd-2.55.5+20.04/boot/boot_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/boot_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2021 Canonical Ltd + * Copyright (C) 2014-2022 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 @@ -116,12 +116,14 @@ type baseBootenv20Suite struct { baseBootenvSuite - kern1 snap.PlaceInfo - kern2 snap.PlaceInfo - ukern1 snap.PlaceInfo - ukern2 snap.PlaceInfo - base1 snap.PlaceInfo - base2 snap.PlaceInfo + kern1 snap.PlaceInfo + kern2 snap.PlaceInfo + ukern1 snap.PlaceInfo + ukern2 snap.PlaceInfo + base1 snap.PlaceInfo + base2 snap.PlaceInfo + gadget1 snap.PlaceInfo + gadget2 snap.PlaceInfo normalDefaultState *bootenv20Setup normalTryingKernelState *bootenv20Setup @@ -146,6 +148,11 @@ s.base2, err = snap.ParsePlaceInfoFromSnapFileName("core20_2.snap") c.Assert(err, IsNil) + s.gadget1, err = snap.ParsePlaceInfoFromSnapFileName("pc_1.snap") + c.Assert(err, IsNil) + s.gadget2, err = snap.ParsePlaceInfoFromSnapFileName("pc_2.snap") + c.Assert(err, IsNil) + // default boot state for robustness tests, etc. s.normalDefaultState = &bootenv20Setup{ modeenv: &boot.Modeenv{ @@ -155,6 +162,8 @@ TryBase: "", // base status is default BaseStatus: boot.DefaultStatus, + // gadget is gadget1 + Gadget: s.gadget1.Filename(), // current kernels is just kern1 CurrentKernels: []string{s.kern1.Filename()}, // operating mode is run @@ -181,6 +190,8 @@ TryBase: "", // base status is default BaseStatus: boot.DefaultStatus, + // gadget is gadget2 + Gadget: s.gadget2.Filename(), // current kernels is kern1 + kern2 CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, }, @@ -548,6 +559,7 @@ func (s *bootenvSuite) TestParticipantBaseWithModel(c *C) { core := &snap.Info{SideInfo: snap.SideInfo{RealName: "core"}, SnapType: snap.TypeOS} core18 := &snap.Info{SideInfo: snap.SideInfo{RealName: "core18"}, SnapType: snap.TypeBase} + core20 := &snap.Info{SideInfo: snap.SideInfo{RealName: "core20"}, SnapType: snap.TypeBase} type tableT struct { with *snap.Info @@ -594,6 +606,55 @@ model: "core@install", nop: true, }, + { + with: core20, + model: "core@run", + nop: true, + }, + } + + for i, t := range table { + dev := boottest.MockDevice(t.model) + bp := boot.Participant(t.with, t.with.Type(), dev) + c.Check(bp.IsTrivial(), Equals, t.nop, Commentf("%d", i)) + if !t.nop { + c.Check(bp, DeepEquals, boot.NewCoreBootParticipant(t.with, t.with.Type(), dev)) + } + } +} + +func (s *bootenvSuite) TestParticipantGadgetWithModel(c *C) { + gadget := &snap.Info{SideInfo: snap.SideInfo{RealName: "pc"}, SnapType: snap.TypeGadget} + + type tableT struct { + with *snap.Info + model string + nop bool + } + + table := []tableT{ + { + with: gadget, + model: "", + nop: true, + }, { + with: gadget, + model: "pc", + nop: true, + }, { + with: gadget, + model: "pc@run", + nop: false, + }, { + with: gadget, + model: "other-gadget", + nop: true, + }, + { + with: gadget, + model: "pc@install", + nop: true, + }, } for i, t := range table { @@ -764,7 +825,7 @@ c.Assert(bootKern.IsTrivial(), Equals, false) // make the kernel used on next boot - rebootRequired, err := bootKern.SetNextBoot() + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) @@ -807,7 +868,7 @@ c.Assert(bootKern.IsTrivial(), Equals, false) // make the kernel used on next boot - rebootRequired, err := bootKern.SetNextBoot() + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) @@ -847,7 +908,7 @@ c.Assert(bootKern.IsTrivial(), Equals, false) // make the kernel used on next boot - rebootRequired, err := bootKern.SetNextBoot() + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) @@ -960,7 +1021,7 @@ c.Assert(bootKern.IsTrivial(), Equals, false) // make the kernel used on next boot - rebootRequired, err := bootKern.SetNextBoot() + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) @@ -1073,7 +1134,7 @@ c.Assert(bootKern.IsTrivial(), Equals, false) // make the kernel used on next boot - rebootRequired, err := bootKern.SetNextBoot() + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) @@ -1185,7 +1246,7 @@ c.Assert(err, IsNil) // make the kernel used on next boot - rebootRequired, err := bootKern.SetNextBoot() + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) @@ -1303,7 +1364,7 @@ c.Assert(err, IsNil) // make the kernel used on next boot - rebootRequired, err := bootKern.SetNextBoot() + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) @@ -1346,7 +1407,7 @@ c.Assert(bootKern.IsTrivial(), Equals, false) // make the kernel used on next boot - rebootRequired, err := bootKern.SetNextBoot() + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) @@ -1534,7 +1595,7 @@ c.Assert(bootBase.IsTrivial(), Equals, false) // make the base used on next boot - rebootRequired, err := bootBase.SetNextBoot() + rebootRequired, err := bootBase.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) // we don't need to reboot because it's the same base snap @@ -1573,7 +1634,7 @@ c.Assert(bootBase.IsTrivial(), Equals, false) // make the base used on next boot - rebootRequired, err := bootBase.SetNextBoot() + rebootRequired, err := bootBase.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) @@ -1667,7 +1728,7 @@ c.Assert(bootBase.IsTrivial(), Equals, false) // make the base used on next boot - rebootRequired, err := bootBase.SetNextBoot() + rebootRequired, err := bootBase.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) @@ -3603,7 +3664,7 @@ }) reboot, err := boot.UpdateCommandLineForGadgetComponent(nonUC20dev, sf) - c.Assert(err, ErrorMatches, "internal error: command line component cannot be updated on non UC20 devices") + c.Assert(err, ErrorMatches, `internal error: command line component cannot be updated on pre-UC20 devices`) c.Assert(reboot, Equals, false) } @@ -4207,7 +4268,7 @@ c.Assert(bootKern.IsTrivial(), Equals, false) // make the kernel used on next boot - rebootRequired, err := bootKern.SetNextBoot() + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Assert(rebootRequired.RebootRequired, Equals, true) // Test that we retrieve a RebootBootloader interface @@ -4226,3 +4287,855 @@ c.Assert(err, IsNil) c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename(), s.kern2.Filename()}) } + +func (s *bootenv20Suite) TestCoreParticipant20SetNextSameGadgetSnap(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + r = boot.MockResealKeyToModeenv(func(_ string, _ *boot.Modeenv, expectReseal bool) error { + c.Assert(expectReseal, Equals, false) + return nil + }) + defer r() + + // get the gadget participant + bootGadget := boot.Participant(s.gadget1, snap.TypeGadget, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootGadget.IsTrivial(), Equals, false) + + // make the gadget used on next boot + rebootRequired, err := bootGadget.SetNextBoot(boot.NextBootContext{}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) + + // the modeenv is still the same + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Gadget, Equals, s.gadget1.Filename()) + + // we didn't call SetBootVars on the bootloader (unneeded for gadget) + c.Assert(s.bootloader.SetBootVarsCalls, Equals, 0) +} + +func (s *bootenv20Suite) TestCoreParticipant20SetNextNewGadgetSnap(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + r = boot.MockResealKeyToModeenv(func(_ string, _ *boot.Modeenv, expectReseal bool) error { + c.Assert(expectReseal, Equals, false) + return nil + }) + defer r() + + // get the gadget participant + bootGadget := boot.Participant(s.gadget2, snap.TypeGadget, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootGadget.IsTrivial(), Equals, false) + + // make the gadget used on next boot + rebootRequired, err := bootGadget.SetNextBoot(boot.NextBootContext{}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) + + // and that the modeenv now contains gadget2 + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Gadget, Equals, s.gadget2.Filename()) + + // we didn't call SetBootVars on the bootloader (unneeded for gadget) + c.Assert(s.bootloader.SetBootVarsCalls, Equals, 0) +} + +func (s *bootenv20Suite) TestCoreParticipant20UndoKernelSnapInstallSame(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + // get the boot kernel participant from our kernel snap + bootKern := boot.Participant(s.kern1, snap.TypeKernel, coreDev) + + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) + + // make sure that the bootloader was asked for the current kernel + _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") + c.Assert(nKernelCalls, Equals, 1) + + // ensure that kernel_status is still empty + c.Assert(s.bootloader.BootVars["kernel_status"], Equals, boot.DefaultStatus) + + // there was no attempt to try a kernel + _, enableKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") + c.Assert(enableKernelCalls, Equals, 0) + + // the modeenv is still the same as well + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) + + // finally we didn't call SetBootVars on the bootloader because nothing + // changed + c.Assert(s.bootloader.SetBootVarsCalls, Equals, 0) +} + +func (s *bootenv20EnvRefKernelSuite) TestCoreParticipant20UndoKernelSnapInstallSame(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + // get the boot kernel participant from our kernel snap + bootKern := boot.Participant(s.kern1, snap.TypeKernel, coreDev) + + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) + + // ensure that bootenv is unchanged + m, err := s.bootloader.GetBootVars("kernel_status", "snap_kernel", "snap_try_kernel") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern1.Filename(), + "snap_try_kernel": "", + }) + + // the modeenv is still the same as well + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) + + // finally we didn't call SetBootVars on the bootloader because nothing + // changed + c.Assert(s.bootloader.SetBootVarsCalls, Equals, 0) +} + +func (s *bootenv20Suite) TestCoreParticipant20UndoKernelSnapInstallNew(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + // get the boot kernel participant from our new kernel snap + bootKern := boot.Participant(s.kern2, snap.TypeKernel, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot, reverting the installation + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) + + // make sure that the bootloader was asked for the current kernel + _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") + c.Assert(nKernelCalls, Equals, 1) + + // ensure that kernel_status is the default + c.Assert(s.bootloader.BootVars["kernel_status"], Equals, boot.DefaultStatus) + + // and we were asked to enable kernel2 as kernel, not as try kernel + _, numTry := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") + c.Assert(numTry, Equals, 0) + actual, _ := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(actual, DeepEquals, []snap.PlaceInfo{s.kern2}) + + // and that the modeenv now has only this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern2.Filename()}) +} + +func (s *bootenv20EnvRefKernelSuite) TestCoreParticipant20UndoKernelSnapInstallNew(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + // get the boot kernel participant from our new kernel snap + bootKern := boot.Participant(s.kern2, snap.TypeKernel, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) + + // make sure the env was updated + m := s.bootloader.BootVars + c.Assert(m, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern2.Filename(), + "snap_try_kernel": "", + }) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern2.Filename()}) +} + +func (s *bootenv20Suite) TestCoreParticipant20UndoKernelSnapInstallNewWithReseal(c *C) { + // checked by resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + + tab := s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0644), IsNil) + + // mock the files in cache + mockAssetsCache(c, dirs.GlobalRootDir, "trusted", []string{ + "asset-" + dataHash, + }) + + assetBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, + "trusted", fmt.Sprintf("asset-%s", dataHash)), bootloader.RoleRunMode) + runKernelBf := bootloader.NewBootFile(filepath.Join(s.kern2.Filename()), + "kernel.efi", bootloader.RoleRunMode) + + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "asset", bootloader.RoleRunMode), + // TODO:UC20: fix mocked trusted assets bootloader to actually + // geenerate kernel boot files + runKernelBf, + } + + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + model := coreDev.Model() + + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + Model: model.Model(), + BrandID: model.BrandID(), + Grade: string(model.Grade()), + ModelSignKeyID: model.SignKeyID(), + } + + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + kernStatus: boot.DefaultStatus, + }, + ) + defer r() + + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + + c.Assert(params.ModelParams, HasLen, 1) + mp := params.ModelParams[0] + c.Check(mp.Model.Model(), Equals, model.Model()) + for _, ch := range mp.EFILoadChains { + printChain(c, ch, "-") + } + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(runKernelBf)), + }) + // actual paths are seen only here + c.Check(tab.BootChainKernelPath, DeepEquals, []string{ + s.kern2.MountFile(), + }) + return nil + }) + defer restore() + + // get the boot kernel participant from our new kernel snap + bootKern := boot.Participant(s.kern2, snap.TypeKernel, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) + + // make sure the env was updated + bvars, err := tab.GetBootVars("kernel_status", "snap_kernel", "snap_try_kernel") + c.Assert(err, IsNil) + c.Assert(bvars, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern2.Filename(), + "snap_try_kernel": "", + }) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern2.Filename()}) + + c.Check(resealCalls, Equals, 1) +} + +func (s *bootenv20Suite) TestCoreParticipant20UndoUnassertedKernelSnapInstallNewWithReseal(c *C) { + // checked by resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + + tab := s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0644), IsNil) + + // mock the files in cache + mockAssetsCache(c, dirs.GlobalRootDir, "trusted", []string{ + "asset-" + dataHash, + }) + + assetBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), bootloader.RoleRunMode) + runKernelBf := bootloader.NewBootFile(filepath.Join(s.ukern2.Filename()), "kernel.efi", bootloader.RoleRunMode) + + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "asset", bootloader.RoleRunMode), + // TODO:UC20: fix mocked trusted assets bootloader to actually + // geenerate kernel boot files + runKernelBf, + } + + uc20Model := boottest.MakeMockUC20Model() + coreDev := boottest.MockUC20Device("", uc20Model) + c.Assert(coreDev.HasModeenv(), Equals, true) + + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.ukern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + Model: uc20Model.Model(), + BrandID: uc20Model.BrandID(), + Grade: string(uc20Model.Grade()), + ModelSignKeyID: uc20Model.SignKeyID(), + } + + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.ukern1, + kernStatus: boot.DefaultStatus, + }, + ) + defer r() + + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + + c.Assert(params.ModelParams, HasLen, 1) + mp := params.ModelParams[0] + c.Check(mp.Model.Model(), Equals, uc20Model.Model()) + for _, ch := range mp.EFILoadChains { + printChain(c, ch, "-") + } + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(runKernelBf)), + }) + // actual paths are seen only here + c.Check(tab.BootChainKernelPath, DeepEquals, []string{ + s.ukern2.MountFile(), + }) + return nil + }) + defer restore() + + // get the boot kernel participant from our new kernel snap + bootKern := boot.Participant(s.ukern2, snap.TypeKernel, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) + + bvars, err := tab.GetBootVars("kernel_status", "snap_kernel", "snap_try_kernel") + c.Assert(err, IsNil) + c.Assert(bvars, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.ukern2.Filename(), + "snap_try_kernel": "", + }) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.ukern2.Filename()}) + + c.Check(resealCalls, Equals, 1) +} + +func (s *bootenv20Suite) TestCoreParticipant20UndoKernelSnapInstallSameNoReseal(c *C) { + // checked by resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + + tab := s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0644), IsNil) + + // mock the files in cache + mockAssetsCache(c, dirs.GlobalRootDir, "trusted", []string{ + "asset-" + dataHash, + }) + + runKernelBf := bootloader.NewBootFile(filepath.Join(s.kern1.Filename()), "kernel.efi", bootloader.RoleRunMode) + + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "asset", bootloader.RoleRunMode), + runKernelBf, + } + + uc20Model := boottest.MakeMockUC20Model() + coreDev := boottest.MockUC20Device("", uc20Model) + c.Assert(coreDev.HasModeenv(), Equals, true) + + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + CurrentKernelCommandLines: boot.BootCommandLines{"snapd_recovery_mode=run"}, + + Model: uc20Model.Model(), + BrandID: uc20Model.BrandID(), + Grade: string(uc20Model.Grade()), + ModelSignKeyID: uc20Model.SignKeyID(), + } + + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + kernStatus: boot.DefaultStatus, + }, + ) + defer r() + + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return fmt.Errorf("unexpected call to mocked secbootResealKeys") + }) + defer restore() + + // get the boot kernel participant from our kernel snap + bootKern := boot.Participant(s.kern1, snap.TypeKernel, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // write boot-chains for current state that will stay unchanged + bootChains := []boot.BootChain{{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: bootloader.RoleRunMode, + Name: "asset", + Hashes: []string{ + "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8", + }, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "1", + KernelCmdlines: []string{"snapd_recovery_mode=run"}, + }} + err := boot.WriteBootChains(bootChains, filepath.Join(dirs.SnapFDEDir, "boot-chains"), 0) + c.Assert(err, IsNil) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) + + // make sure the env is as expected + bvars, err := tab.GetBootVars("kernel_status", "snap_kernel", "snap_try_kernel") + c.Assert(err, IsNil) + c.Assert(bvars, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern1.Filename(), + "snap_try_kernel": "", + }) + + // and that the modeenv now has the one kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) + + // boot chains were built + c.Check(tab.BootChainKernelPath, DeepEquals, []string{ + s.kern1.MountFile(), + }) + // no actual reseal + c.Check(resealCalls, Equals, 0) +} + +func (s *bootenv20Suite) TestCoreParticipant20UndoUnassertedKernelSnapInstallSameNoReseal(c *C) { + // checked by resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + + tab := s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0644), IsNil) + + // mock the files in cache + mockAssetsCache(c, dirs.GlobalRootDir, "trusted", []string{ + "asset-" + dataHash, + }) + + runKernelBf := bootloader.NewBootFile(filepath.Join(s.ukern1.Filename()), "kernel.efi", bootloader.RoleRunMode) + + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "asset", bootloader.RoleRunMode), + runKernelBf, + } + + uc20Model := boottest.MakeMockUC20Model() + coreDev := boottest.MockUC20Device("", uc20Model) + c.Assert(coreDev.HasModeenv(), Equals, true) + + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.ukern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + CurrentKernelCommandLines: boot.BootCommandLines{"snapd_recovery_mode=run"}, + + Model: uc20Model.Model(), + BrandID: uc20Model.BrandID(), + Grade: string(uc20Model.Grade()), + ModelSignKeyID: uc20Model.SignKeyID(), + } + + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.ukern1, + kernStatus: boot.DefaultStatus, + }, + ) + defer r() + + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return fmt.Errorf("unexpected call") + }) + defer restore() + + // get the boot kernel participant from our kernel snap + bootKern := boot.Participant(s.ukern1, snap.TypeKernel, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // write boot-chains for current state that will stay unchanged + bootChains := []boot.BootChain{{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: bootloader.RoleRunMode, + Name: "asset", + Hashes: []string{ + "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8", + }, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "", + KernelCmdlines: []string{"snapd_recovery_mode=run"}, + }} + err := boot.WriteBootChains(bootChains, filepath.Join(dirs.SnapFDEDir, "boot-chains"), 0) + c.Assert(err, IsNil) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) + + // make sure the env is as expected + bvars, err := tab.GetBootVars("kernel_status", "snap_kernel", "snap_try_kernel") + c.Assert(err, IsNil) + c.Assert(bvars, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.ukern1.Filename(), + "snap_try_kernel": "", + }) + + // and that the modeenv now has the one kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.ukern1.Filename()}) + + // boot chains were built + c.Check(tab.BootChainKernelPath, DeepEquals, []string{ + s.ukern1.MountFile(), + }) + // no actual reseal + c.Check(resealCalls, Equals, 0) +} + +func (s *bootenv20Suite) TestCoreParticipant20UndoBaseSnapInstallSame(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + // no kernel setup necessary + }, + ) + defer r() + + // get the boot base participant from our base snap + bootBase := boot.Participant(s.base1, snap.TypeBase, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootBase.IsTrivial(), Equals, false) + + // make the base used on next boot + rebootRequired, err := bootBase.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + + // we don't need to reboot because it's the same base snap + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: false}) + + // make sure the modeenv wasn't changed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Base, Equals, m.Base) + c.Assert(m2.BaseStatus, Equals, m.BaseStatus) + c.Assert(m2.TryBase, Equals, m.TryBase) +} + +func (s *bootenv20Suite) TestCoreParticipant20UndoBaseSnapInstallNew(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + // default state + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + // no kernel setup necessary + }, + ) + defer r() + + // get the boot base participant from our new base snap + bootBase := boot.Participant(s.base2, snap.TypeBase, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootBase.IsTrivial(), Equals, false) + + // make the base used on next boot, reverting the current one installed + rebootRequired, err := bootBase.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) + + // make sure the modeenv was updated + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Base, Equals, s.base2.Filename()) + c.Assert(m2.BaseStatus, Equals, "") + c.Assert(m2.TryBase, Equals, "") +} + +func (s *bootenv20Suite) TestCoreParticipant20UndoBaseSnapInstallNewNoReseal(c *C) { + // checked by resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + + // set up all the bits required for an encrypted system + tab := s.bootloaderWithTrustedAssets(c, []string{"asset"}) + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + // mock the files in cache + mockAssetsCache(c, dirs.GlobalRootDir, "trusted", []string{ + "asset-" + dataHash, + }) + runKernelBf := bootloader.NewBootFile(filepath.Join(s.kern1.Filename()), "kernel.efi", bootloader.RoleRunMode) + // write boot-chains for current state that will stay unchanged even + // though base is changed + bootChains := []boot.BootChain{{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{{ + Role: bootloader.RoleRunMode, Name: "asset", Hashes: []string{ + "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8", + }, + }}, + Kernel: "pc-kernel", + KernelRevision: "1", + KernelCmdlines: []string{"snapd_recovery_mode=run"}, + }} + + err := boot.WriteBootChains(boot.ToPredictableBootChains(bootChains), filepath.Join(dirs.SnapFDEDir, "boot-chains"), 0) + c.Assert(err, IsNil) + + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + model := coreDev.Model() + + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return nil + }) + defer restore() + + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "asset", bootloader.RoleRunMode), + runKernelBf, + } + + // default state + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + + Model: model.Model(), + BrandID: model.BrandID(), + Grade: string(model.Grade()), + ModelSignKeyID: model.SignKeyID(), + } + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + // no kernel setup necessary + }, + ) + defer r() + + // get the boot base participant from our new base snap + bootBase := boot.Participant(s.base2, snap.TypeBase, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootBase.IsTrivial(), Equals, false) + + // make the base used on next boot + rebootRequired, err := bootBase.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, boot.RebootInfo{RebootRequired: true}) + + // make sure the modeenv was updated + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Base, Equals, s.base2.Filename()) + c.Assert(m2.BaseStatus, Equals, boot.DefaultStatus) + c.Assert(m2.TryBase, Equals, "") + + // no reseal + c.Check(resealCalls, Equals, 0) +} diff -Nru snapd-2.55.5+20.04/boot/cmdline.go snapd-2.57.5+20.04/boot/cmdline.go --- snapd-2.55.5+20.04/boot/cmdline.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/cmdline.go 2022-10-17 16:25:18.000000000 +0000 @@ -44,10 +44,12 @@ // ModeFactoryReset is a mode in which the device performs a factory // reset. ModeFactoryReset = "factory-reset" + // ModeRunCVM is Azure CVM specific run mode fde + classic debs + ModeRunCVM = "cloudimg-rootfs" ) var ( - validModes = []string{ModeInstall, ModeRecover, ModeFactoryReset, ModeRun} + validModes = []string{ModeInstall, ModeRecover, ModeFactoryReset, ModeRun, ModeRunCVM} ) // ModeAndRecoverySystemFromKernelCommandLine returns the current system mode diff -Nru snapd-2.55.5+20.04/boot/cmdline_test.go snapd-2.57.5+20.04/boot/cmdline_test.go --- snapd-2.55.5+20.04/boot/cmdline_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/cmdline_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -108,6 +108,9 @@ cmd: "snapd_recovery_system=not-this-one snapd_recovery_mode=install snapd_recovery_system=1234", mode: "install", label: "1234", + }, { + cmd: "snapd_recovery_mode=cloudimg-rootfs", + mode: boot.ModeRunCVM, }} { c.Logf("tc: %q", tc) s.mockProcCmdlineContent(c, tc.cmd) diff -Nru snapd-2.55.5+20.04/boot/export_test.go snapd-2.57.5+20.04/boot/export_test.go --- snapd-2.55.5+20.04/boot/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -26,8 +26,10 @@ "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/kernel/fde" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/timings" ) @@ -75,6 +77,7 @@ type BootAssetsMap = bootAssetsMap type BootCommandLines = bootCommandLines type TrackedAsset = trackedAsset +type SealKeyToModeenvFlags = sealKeyToModeenvFlags func (t *TrackedAsset) Equals(blName, name, hash string) error { equal := t.hash == hash && @@ -94,14 +97,20 @@ return o.currentTrustedRecoveryBootAssetsMap() } -func (o *TrustedAssetsInstallObserver) CurrentDataEncryptionKey() secboot.EncryptionKey { +func (o *TrustedAssetsInstallObserver) CurrentDataEncryptionKey() keys.EncryptionKey { return o.dataEncryptionKey } -func (o *TrustedAssetsInstallObserver) CurrentSaveEncryptionKey() secboot.EncryptionKey { +func (o *TrustedAssetsInstallObserver) CurrentSaveEncryptionKey() keys.EncryptionKey { return o.saveEncryptionKey } +func MockSecbootProvisionTPM(f func(mode secboot.TPMProvisionMode, lockoutAuthFile string) error) (restore func()) { + restore = testutil.Backup(&secbootProvisionTPM) + secbootProvisionTPM = f + return restore +} + func MockSecbootSealKeys(f func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error) (restore func()) { old := secbootSealKeys secbootSealKeys = f @@ -126,6 +135,18 @@ } } +func MockSecbootPCRHandleOfSealedKey(f func(p string) (uint32, error)) (restore func()) { + restore = testutil.Backup(&secbootPCRHandleOfSealedKey) + secbootPCRHandleOfSealedKey = f + return restore +} + +func MockSecbootReleasePCRResourceHandles(f func(handles ...uint32) error) (restore func()) { + restore = testutil.Backup(&secbootReleasePCRResourceHandles) + secbootReleasePCRResourceHandles = f + return restore +} + func (o *TrustedAssetsUpdateObserver) InjectChangedAsset(blName, assetName, hash string, recovery bool) { ta := &trackedAsset{ blName: blName, diff -Nru snapd-2.55.5+20.04/boot/flags.go snapd-2.57.5+20.04/boot/flags.go --- snapd-2.55.5+20.04/boot/flags.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/flags.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2021 Canonical Ltd + * Copyright (C) 2022 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 @@ -35,7 +35,7 @@ ) var ( - errNotUC20 = fmt.Errorf("cannot get boot flags on non-UC20 device") + errNotUC20 = fmt.Errorf("cannot get boot flags on pre-UC20 device") understoodBootFlags = []string{ // the factory boot flag is set to indicate that this is a @@ -137,15 +137,19 @@ // used by a userspace that is newer than the initramfs, but empty flags will be // dropped automatically. // Only to be used on UC20+ systems with recovery systems. -func InitramfsActiveBootFlags(mode string) ([]string, error) { +func InitramfsActiveBootFlags(mode string, rootfsDir string) ([]string, error) { switch mode { case ModeRecover: // no boot flags are consumed / used on recover mode, so return nothing return nil, nil + case ModeRunCVM: + // no boot flags are consumed / used on CVM mode, so return nothing + return nil, nil + case ModeRun: // boot flags come from the modeenv - modeenv, err := ReadModeenv(InitramfsWritableDir) + modeenv, err := ReadModeenv(rootfsDir) if err != nil { return nil, err } @@ -154,30 +158,37 @@ // to reduce the number of times we read the modeenv ? return modeenv.BootFlags, nil + case ModeFactoryReset: + // Reuse the code from ModeInstall as we have a lot of + // identical conditions. + fallthrough case ModeInstall: // boot flags always come from the bootenv of the recovery bootloader // in install mode - - opts := &bootloader.Options{ - Role: bootloader.RoleRecovery, - } - bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts) - if err != nil { - return nil, err - } - - m, err := bl.GetBootVars("snapd_boot_flags") - if err != nil { - return nil, err - } - - return splitBootFlagString(m["snapd_boot_flags"]), nil + return readBootFlagsFromRecoveryBootloader() default: return nil, fmt.Errorf("internal error: unsupported mode %q", mode) } } +func readBootFlagsFromRecoveryBootloader() ([]string, error) { + opts := &bootloader.Options{ + Role: bootloader.RoleRecovery, + } + bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts) + if err != nil { + return nil, err + } + + m, err := bl.GetBootVars("snapd_boot_flags") + if err != nil { + return nil, err + } + + return splitBootFlagString(m["snapd_boot_flags"]), nil +} + // InitramfsExposeBootFlagsForSystem sets the boot flags for the current boot in // the /run file that will be consulted in userspace by BootFlags() below. It is // meant to be used only from the initramfs. @@ -277,6 +288,7 @@ // mode root filesystem is mounted for the given mode. // For run mode, it's "/run/mnt/data" and "/". // For install mode it's "/run/mnt/ubuntu-data". +// For factory-reset mode it's "/run/mnt/ubuntu-data" // For recover mode it's either "/host/ubuntu-data" or nil if that is not // mounted. Note that, for recover mode, this function only returns a non-empty // return value if the partition is mounted and trusted, there are certain @@ -330,6 +342,16 @@ if exists, _, _ := osutil.DirExists(installModeLocation); exists { runDataRootfsMountLocations = []string{installModeLocation} } + + case ModeFactoryReset: + // In factory reset, our conditions are a lot similar to install mode, + // as we recreate the ubuntu-data partition. Make similar assumptions + // and checks like ModeInstall. Take into account ubuntu-data might not + // be mounted when this check is called. + factoryResetModeLocation := filepath.Dir(InstallHostWritableDir) + if exists, _, _ := osutil.DirExists(factoryResetModeLocation); exists { + runDataRootfsMountLocations = []string{factoryResetModeLocation} + } default: return nil, ErrUnsupportedSystemMode } diff -Nru snapd-2.55.5+20.04/boot/flags_test.go snapd-2.57.5+20.04/boot/flags_test.go --- snapd-2.55.5+20.04/boot/flags_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/flags_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2021 Canonical Ltd + * Copyright (C) 2022 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 @@ -49,13 +49,13 @@ defer bootloader.ForceError(nil) _, err := boot.NextBootFlags(classicDev) - c.Assert(err, ErrorMatches, "cannot get boot flags on non-UC20 device") + c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) err = boot.SetNextBootFlags(classicDev, "", []string{"foo"}) - c.Assert(err, ErrorMatches, "cannot get boot flags on non-UC20 device") + c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) _, err = boot.BootFlags(classicDev) - c.Assert(err, ErrorMatches, "cannot get boot flags on non-UC20 device") + c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) } func (s *bootFlagsSuite) TestBootFlagsFamilyUC16(c *C) { @@ -66,13 +66,13 @@ defer bootloader.ForceError(nil) _, err := boot.NextBootFlags(coreDev) - c.Assert(err, ErrorMatches, "cannot get boot flags on non-UC20 device") + c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) err = boot.SetNextBootFlags(coreDev, "", []string{"foo"}) - c.Assert(err, ErrorMatches, "cannot get boot flags on non-UC20 device") + c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) _, err = boot.BootFlags(coreDev) - c.Assert(err, ErrorMatches, "cannot get boot flags on non-UC20 device") + c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) } func setupRealGrub(c *C, rootDir, baseDir string, opts *bootloader.Options) bootloader.Bootloader { @@ -107,7 +107,7 @@ setupRealGrub(c, blDir, "EFI/ubuntu", &bootloader.Options{Role: bootloader.RoleRecovery}) - flags, err := boot.InitramfsActiveBootFlags(boot.ModeInstall) + flags, err := boot.InitramfsActiveBootFlags(boot.ModeInstall, boot.InitramfsWritableDir) c.Assert(err, IsNil) c.Assert(flags, HasLen, 0) @@ -117,7 +117,35 @@ err = boot.SetBootFlagsInBootloader([]string{"factory"}, blDir) c.Assert(err, IsNil) - flags, err = boot.InitramfsActiveBootFlags(boot.ModeInstall) + flags, err = boot.InitramfsActiveBootFlags(boot.ModeInstall, boot.InitramfsWritableDir) + c.Assert(err, IsNil) + c.Assert(flags, DeepEquals, []string{"factory"}) +} + +func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20FactoryResetModeHappy(c *C) { + // FactoryReset and Install run identical code, as their condition match pretty closely + // so this unit test is to reconfirm that we expect same behavior as we see in the unit + // test for install mode. + dir := c.MkDir() + + dirs.SetRootDir(dir) + defer func() { dirs.SetRootDir("") }() + + blDir := boot.InitramfsUbuntuSeedDir + + setupRealGrub(c, blDir, "EFI/ubuntu", &bootloader.Options{Role: bootloader.RoleRecovery}) + + flags, err := boot.InitramfsActiveBootFlags(boot.ModeFactoryReset, boot.InitramfsWritableDir) + c.Assert(err, IsNil) + c.Assert(flags, HasLen, 0) + + // if we set some flags via ubuntu-image customizations then we get them + // back + + err = boot.SetBootFlagsInBootloader([]string{"factory"}, blDir) + c.Assert(err, IsNil) + + flags, err = boot.InitramfsActiveBootFlags(boot.ModeFactoryReset, boot.InitramfsWritableDir) c.Assert(err, IsNil) c.Assert(flags, DeepEquals, []string{"factory"}) } @@ -163,7 +191,7 @@ err = m.WriteTo(boot.InitramfsWritableDir) c.Assert(err, IsNil) - flags, err := boot.InitramfsActiveBootFlags(boot.ModeRecover) + flags, err := boot.InitramfsActiveBootFlags(boot.ModeRecover, boot.InitramfsWritableDir) c.Assert(err, IsNil) c.Assert(flags, HasLen, 0) @@ -175,12 +203,12 @@ c.Assert(err, IsNil) // still no flags since we are in recovery mode - flags, err = boot.InitramfsActiveBootFlags(boot.ModeRecover) + flags, err = boot.InitramfsActiveBootFlags(boot.ModeRecover, boot.InitramfsWritableDir) c.Assert(err, IsNil) c.Assert(flags, HasLen, 0) } -func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20RRunModeHappy(c *C) { +func (s *bootFlagsSuite) testInitramfsActiveBootFlagsUC20RRunModeHappy(c *C, flagsDir string) { dir := c.MkDir() dirs.SetRootDir(dir) @@ -192,26 +220,31 @@ BootFlags: []string{}, } - err := os.MkdirAll(boot.InitramfsWritableDir, 0755) + err := os.MkdirAll(flagsDir, 0755) c.Assert(err, IsNil) - err = m.WriteTo(boot.InitramfsWritableDir) + err = m.WriteTo(flagsDir) c.Assert(err, IsNil) - flags, err := boot.InitramfsActiveBootFlags(boot.ModeRun) + flags, err := boot.InitramfsActiveBootFlags(boot.ModeRun, flagsDir) c.Assert(err, IsNil) c.Assert(flags, HasLen, 0) m.BootFlags = []string{"factory", "other-flag"} - err = m.WriteTo(boot.InitramfsWritableDir) + err = m.WriteTo(flagsDir) c.Assert(err, IsNil) // now some flags after we set them in the modeenv - flags, err = boot.InitramfsActiveBootFlags(boot.ModeRun) + flags, err = boot.InitramfsActiveBootFlags(boot.ModeRun, flagsDir) c.Assert(err, IsNil) c.Assert(flags, DeepEquals, []string{"factory", "other-flag"}) } +func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20RRunModeHappy(c *C) { + s.testInitramfsActiveBootFlagsUC20RRunModeHappy(c, boot.InitramfsWritableDir) + s.testInitramfsActiveBootFlagsUC20RRunModeHappy(c, c.MkDir()) +} + func (s *bootFlagsSuite) TestInitramfsSetBootFlags(c *C) { tt := []struct { flags []string @@ -351,12 +384,22 @@ comment: "install mode before partition creation", }, { + mode: boot.ModeFactoryReset, + comment: "factory-reset mode before partition is recreated", + }, + { mode: boot.ModeInstall, expDirs: []string{"/run/mnt/ubuntu-data"}, createExpDirs: true, comment: "install mode after partition creation", }, { + mode: boot.ModeFactoryReset, + expDirs: []string{"/run/mnt/ubuntu-data"}, + createExpDirs: true, + comment: "factory-reset mode after partition creation", + }, + { mode: boot.ModeRecover, degradedJSON: ` { diff -Nru snapd-2.55.5+20.04/boot/initramfs.go snapd-2.57.5+20.04/boot/initramfs.go --- snapd-2.55.5+20.04/boot/initramfs.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/initramfs.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,17 +34,24 @@ func InitramfsRunModeSelectSnapsToMount( typs []snap.Type, modeenv *Modeenv, + rootfsDir string, ) (map[snap.Type]snap.PlaceInfo, error) { var sn snap.PlaceInfo var err error m := make(map[snap.Type]snap.PlaceInfo) for _, typ := range typs { // TODO: consider passing a bootStateUpdate20 instead? - var selectSnapFn func(*Modeenv) (snap.PlaceInfo, error) + var selectSnapFn func(*Modeenv, string) (snap.PlaceInfo, error) switch typ { case snap.TypeBase: bs := &bootState20Base{} selectSnapFn = bs.selectAndCommitSnapInitramfsMount + case snap.TypeGadget: + // Do not mount if modeenv does not have gadget entry + if modeenv.Gadget == "" { + continue + } + selectSnapFn = selectGadgetSnap case snap.TypeKernel: blOpts := &bootloader.Options{ Role: bootloader.RoleRunMode, @@ -57,7 +64,7 @@ } selectSnapFn = bs.selectAndCommitSnapInitramfsMount } - sn, err = selectSnapFn(modeenv) + sn, err = selectSnapFn(modeenv, rootfsDir) if err != nil { return nil, err } diff -Nru snapd-2.55.5+20.04/boot/initramfs_test.go snapd-2.57.5+20.04/boot/initramfs_test.go --- snapd-2.55.5+20.04/boot/initramfs_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/initramfs_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -96,9 +96,9 @@ }) } -func makeSnapFilesOnInitramfsUbuntuData(c *C, comment CommentInterface, snaps ...snap.PlaceInfo) (restore func()) { +func makeSnapFilesOnInitramfsUbuntuData(c *C, rootfsDir string, comment CommentInterface, snaps ...snap.PlaceInfo) (restore func()) { // also make sure the snaps also exist on ubuntu-data - snapDir := dirs.SnapBlobDirUnder(boot.InitramfsWritableDir) + snapDir := dirs.SnapBlobDirUnder(rootfsDir) err := os.MkdirAll(snapDir, 0755) c.Assert(err, IsNil, comment) paths := make([]string, 0, len(snaps)) @@ -130,8 +130,12 @@ base2, err := snap.ParsePlaceInfoFromSnapFileName("core20_2.snap") c.Assert(err, IsNil) + gadget, err := snap.ParsePlaceInfoFromSnapFileName("pc_1.snap") + c.Assert(err, IsNil) + baseT := snap.TypeBase kernelT := snap.TypeKernel + gadgetT := snap.TypeGadget tt := []struct { m *boot.Modeenv @@ -143,8 +147,9 @@ snapsToMake []snap.PlaceInfo expected map[snap.Type]snap.PlaceInfo errPattern string - comment string expRebootPanic string + rootfsDir string + comment string }{ // // default paths @@ -156,8 +161,27 @@ typs: []snap.Type{baseT}, snapsToMake: []snap.PlaceInfo{base1}, expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + rootfsDir: boot.InitramfsWritableDir, comment: "default base path", }, + // gadget base path + { + m: &boot.Modeenv{Mode: "run", Gadget: gadget.Filename()}, + typs: []snap.Type{gadgetT}, + snapsToMake: []snap.PlaceInfo{gadget}, + expected: map[snap.Type]snap.PlaceInfo{gadgetT: gadget}, + rootfsDir: boot.InitramfsWritableDir, + comment: "default gadget path", + }, + // gadget base path, but not in modeenv, so it is not selected + { + m: &boot.Modeenv{Mode: "run"}, + typs: []snap.Type{gadgetT}, + snapsToMake: []snap.PlaceInfo{gadget}, + expected: map[snap.Type]snap.PlaceInfo{}, + rootfsDir: boot.InitramfsWritableDir, + comment: "default gadget path", + }, // default kernel path { m: &boot.Modeenv{Mode: "run", CurrentKernels: []string{kernel1.Filename()}}, @@ -165,8 +189,28 @@ typs: []snap.Type{kernelT}, snapsToMake: []snap.PlaceInfo{kernel1}, expected: map[snap.Type]snap.PlaceInfo{kernelT: kernel1}, + rootfsDir: boot.InitramfsWritableDir, comment: "default kernel path", }, + // gadget base path for classic with modes + { + m: &boot.Modeenv{Mode: "run", Gadget: gadget.Filename()}, + typs: []snap.Type{gadgetT}, + snapsToMake: []snap.PlaceInfo{gadget}, + expected: map[snap.Type]snap.PlaceInfo{gadgetT: gadget}, + rootfsDir: boot.InitramfsDataDir, + comment: "default gadget path for classic with modes", + }, + // default kernel path for classic with modes + { + m: &boot.Modeenv{Mode: "run", CurrentKernels: []string{kernel1.Filename()}}, + kernel: kernel1, + typs: []snap.Type{kernelT}, + snapsToMake: []snap.PlaceInfo{kernel1}, + expected: map[snap.Type]snap.PlaceInfo{kernelT: kernel1}, + rootfsDir: boot.InitramfsDataDir, + comment: "default kernel path for classic with modes", + }, // // happy kernel upgrade paths @@ -181,6 +225,7 @@ blvars: map[string]string{"kernel_status": boot.TryingStatus}, snapsToMake: []snap.PlaceInfo{kernel1, kernel2}, expected: map[snap.Type]snap.PlaceInfo{kernelT: kernel2}, + rootfsDir: boot.InitramfsWritableDir, comment: "successful kernel upgrade path", }, // extraneous kernel extracted/set, but kernel_status is default, @@ -198,6 +243,7 @@ blvars: map[string]string{"kernel_status": boot.DefaultStatus}, snapsToMake: []snap.PlaceInfo{kernel1, kernel2}, expected: map[snap.Type]snap.PlaceInfo{kernelT: kernel1}, + rootfsDir: boot.InitramfsWritableDir, comment: "fallback kernel upgrade path, due to kernel_status empty (default)", }, @@ -214,6 +260,7 @@ blvars: map[string]string{"kernel_status": boot.TryingStatus}, snapsToMake: []snap.PlaceInfo{kernel1, kernel2}, expRebootPanic: "reboot due to modeenv untrusted try kernel", + rootfsDir: boot.InitramfsWritableDir, comment: "fallback kernel upgrade path, due to modeenv untrusted try kernel", }, // kernel upgrade path, but reboots to fallback due to try kernel file not existing @@ -225,6 +272,7 @@ blvars: map[string]string{"kernel_status": boot.TryingStatus}, snapsToMake: []snap.PlaceInfo{kernel1}, expRebootPanic: "reboot due to try kernel file not existing", + rootfsDir: boot.InitramfsWritableDir, comment: "fallback kernel upgrade path, due to try kernel file not existing", }, // kernel upgrade path, but reboots to fallback due to invalid kernel_status @@ -236,6 +284,7 @@ blvars: map[string]string{"kernel_status": boot.TryStatus}, snapsToMake: []snap.PlaceInfo{kernel1, kernel2}, expRebootPanic: "reboot due to kernel_status wrong", + rootfsDir: boot.InitramfsWritableDir, comment: "fallback kernel upgrade path, due to kernel_status wrong", }, @@ -250,6 +299,7 @@ typs: []snap.Type{kernelT}, snapsToMake: []snap.PlaceInfo{kernel1}, errPattern: fmt.Sprintf("fallback kernel snap %q is not trusted in the modeenv", kernel1.Filename()), + rootfsDir: boot.InitramfsWritableDir, comment: "fallback kernel not trusted in modeenv", }, // fallback kernel file doesn't exist @@ -258,6 +308,7 @@ kernel: kernel1, typs: []snap.Type{kernelT}, errPattern: fmt.Sprintf("kernel snap %q does not exist on ubuntu-data", kernel1.Filename()), + rootfsDir: boot.InitramfsWritableDir, comment: "fallback kernel file doesn't exist", }, @@ -282,6 +333,7 @@ typs: []snap.Type{baseT}, snapsToMake: []snap.PlaceInfo{base1, base2}, expected: map[snap.Type]snap.PlaceInfo{baseT: base2}, + rootfsDir: boot.InitramfsWritableDir, comment: "successful base upgrade path", }, // base upgrade path, but uses fallback due to try base file not existing @@ -301,6 +353,7 @@ typs: []snap.Type{baseT}, snapsToMake: []snap.PlaceInfo{base1}, expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + rootfsDir: boot.InitramfsWritableDir, comment: "fallback base upgrade path, due to missing try base file", }, // base upgrade path, but uses fallback due to base_status trying @@ -320,6 +373,7 @@ typs: []snap.Type{baseT}, snapsToMake: []snap.PlaceInfo{base1, base2}, expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + rootfsDir: boot.InitramfsWritableDir, comment: "fallback base upgrade path, due to base_status trying", }, // base upgrade path, but uses fallback due to base_status default @@ -339,6 +393,7 @@ typs: []snap.Type{baseT}, snapsToMake: []snap.PlaceInfo{base1, base2}, expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + rootfsDir: boot.InitramfsWritableDir, comment: "fallback base upgrade path, due to missing base_status", }, @@ -352,6 +407,7 @@ typs: []snap.Type{baseT}, snapsToMake: []snap.PlaceInfo{base1}, errPattern: "fallback base snap unusable: cannot get snap revision: modeenv base boot variable is empty", + rootfsDir: boot.InitramfsWritableDir, comment: "base snap unset in modeenv", }, // base snap file doesn't exist @@ -359,6 +415,7 @@ m: &boot.Modeenv{Mode: "run", Base: base1.Filename()}, typs: []snap.Type{baseT}, errPattern: fmt.Sprintf("base snap %q does not exist on ubuntu-data", base1.Filename()), + rootfsDir: boot.InitramfsWritableDir, comment: "base snap unset in modeenv", }, // unhappy, but silent path with fallback, due to invalid try base snap name @@ -372,6 +429,7 @@ typs: []snap.Type{baseT}, snapsToMake: []snap.PlaceInfo{base1}, expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + rootfsDir: boot.InitramfsWritableDir, comment: "corrupted base snap name", }, @@ -398,7 +456,8 @@ baseT: base1, kernelT: kernel1, }, - comment: "default combined kernel + base", + rootfsDir: boot.InitramfsWritableDir, + comment: "default combined kernel + base", }, // combined, upgrade only the kernel { @@ -421,7 +480,8 @@ baseT: base1, kernelT: kernel2, }, - comment: "combined kernel + base, successful kernel upgrade", + rootfsDir: boot.InitramfsWritableDir, + comment: "combined kernel + base, successful kernel upgrade", }, // combined, upgrade only the base { @@ -446,7 +506,8 @@ baseT: base2, kernelT: kernel1, }, - comment: "combined kernel + base, successful base upgrade", + rootfsDir: boot.InitramfsWritableDir, + comment: "combined kernel + base, successful base upgrade", }, // bonus points: combined upgrade kernel and base { @@ -473,7 +534,8 @@ baseT: base2, kernelT: kernel2, }, - comment: "combined kernel + base, successful base + kernel upgrade", + rootfsDir: boot.InitramfsWritableDir, + comment: "combined kernel + base, successful base + kernel upgrade", }, // combined, fallback upgrade on kernel { @@ -496,7 +558,8 @@ baseT: base1, kernelT: kernel1, }, - comment: "combined kernel + base, fallback kernel upgrade, due to missing boot var", + rootfsDir: boot.InitramfsWritableDir, + comment: "combined kernel + base, fallback kernel upgrade, due to missing boot var", }, // combined, fallback upgrade on base { @@ -521,7 +584,8 @@ baseT: base1, kernelT: kernel1, }, - comment: "combined kernel + base, fallback base upgrade, due to base_status trying", + rootfsDir: boot.InitramfsWritableDir, + comment: "combined kernel + base, fallback base upgrade, due to base_status trying", }, } @@ -583,27 +647,27 @@ } if len(t.snapsToMake) != 0 { - r := makeSnapFilesOnInitramfsUbuntuData(c, comment, t.snapsToMake...) + r := makeSnapFilesOnInitramfsUbuntuData(c, t.rootfsDir, comment, t.snapsToMake...) cleanups = append(cleanups, r) } // write the modeenv to somewhere so we can read it and pass that to // InitramfsRunModeChooseSnapsToMount - err := t.m.WriteTo(boot.InitramfsWritableDir) + err := t.m.WriteTo(t.rootfsDir) // remove it because we are writing many modeenvs in this single test cleanups = append(cleanups, func() { - c.Assert(os.Remove(dirs.SnapModeenvFileUnder(boot.InitramfsWritableDir)), IsNil, Commentf(t.comment)) + c.Assert(os.Remove(dirs.SnapModeenvFileUnder(t.rootfsDir)), IsNil, Commentf(t.comment)) }) c.Assert(err, IsNil, comment) - m, err := boot.ReadModeenv(boot.InitramfsWritableDir) + m, err := boot.ReadModeenv(t.rootfsDir) c.Assert(err, IsNil, comment) if t.expRebootPanic != "" { - f := func() { boot.InitramfsRunModeSelectSnapsToMount(t.typs, m) } + f := func() { boot.InitramfsRunModeSelectSnapsToMount(t.typs, m, t.rootfsDir) } c.Assert(f, PanicMatches, t.expRebootPanic, comment) } else { - mountSnaps, err := boot.InitramfsRunModeSelectSnapsToMount(t.typs, m) + mountSnaps, err := boot.InitramfsRunModeSelectSnapsToMount(t.typs, m, t.rootfsDir) if t.errPattern != "" { c.Assert(err, ErrorMatches, t.errPattern, comment) } else { @@ -614,7 +678,7 @@ // check that the modeenv changed as expected if t.expectedM != nil { - newM, err := boot.ReadModeenv(boot.InitramfsWritableDir) + newM, err := boot.ReadModeenv(t.rootfsDir) c.Assert(err, IsNil, comment) c.Assert(newM.Base, Equals, t.expectedM.Base, comment) c.Assert(newM.BaseStatus, Equals, t.expectedM.BaseStatus, comment) diff -Nru snapd-2.55.5+20.04/boot/kernel_os.go snapd-2.57.5+20.04/boot/kernel_os.go --- snapd-2.55.5+20.04/boot/kernel_os.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/kernel_os.go 2022-10-17 16:25:18.000000000 +0000 @@ -36,10 +36,10 @@ func (*coreBootParticipant) IsTrivial() bool { return false } -func (bp *coreBootParticipant) SetNextBoot() (rebootInfo RebootInfo, err error) { +func (bp *coreBootParticipant) SetNextBoot(bootCtx NextBootContext) (rebootInfo RebootInfo, err error) { const errPrefix = "cannot set next boot: %s" - rebootInfo, u, err := bp.bs.setNext(bp.s) + rebootInfo, u, err := bp.bs.setNext(bp.s, bootCtx) if err != nil { return RebootInfo{RebootRequired: false}, fmt.Errorf(errPrefix, err) } diff -Nru snapd-2.55.5+20.04/boot/kernel_os_test.go snapd-2.57.5+20.04/boot/kernel_os_test.go --- snapd-2.55.5+20.04/boot/kernel_os_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/kernel_os_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -61,11 +61,11 @@ coreDev := boottest.MockDevice("some-snap") s.bootloader.GetErr = errors.New("zap") - _, err := boot.NewCoreBootParticipant(&snap.Info{}, snap.TypeKernel, coreDev).SetNextBoot() + _, err := boot.NewCoreBootParticipant(&snap.Info{}, snap.TypeKernel, coreDev).SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Check(err, ErrorMatches, `cannot set next boot: zap`) bootloader.ForceError(errors.New("brkn")) - _, err = boot.NewCoreBootParticipant(&snap.Info{}, snap.TypeKernel, coreDev).SetNextBoot() + _, err = boot.NewCoreBootParticipant(&snap.Info{}, snap.TypeKernel, coreDev).SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Check(err, ErrorMatches, `cannot set next boot: brkn`) } @@ -78,7 +78,7 @@ info.Revision = snap.R(100) bs := boot.NewCoreBootParticipant(info, info.Type(), coreDev) - rebootInfo, err := bs.SetNextBoot() + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) v, err := s.bootloader.GetBootVars("snap_try_core", "snap_mode") @@ -100,7 +100,7 @@ info.Revision = snap.R(1818) bs := boot.NewCoreBootParticipant(info, info.Type(), coreDev) - rebootInfo, err := bs.SetNextBoot() + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) v, err := s.bootloader.GetBootVars("snap_try_core", "snap_mode") @@ -122,7 +122,7 @@ info.Revision = snap.R(42) bp := boot.NewCoreBootParticipant(info, snap.TypeKernel, coreDev) - rebootInfo, err := bp.SetNextBoot() + rebootInfo, err := bp.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) v, err := s.bootloader.GetBootVars("snap_try_kernel", "snap_mode") @@ -142,7 +142,7 @@ bootVars = map[string]string{"snap_kernel": "krnl_42.snap"} s.bootloader.SetBootVars(bootVars) - rebootInfo, err = bp.SetNextBoot() + rebootInfo, err = bp.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) c.Check(rebootInfo, Equals, boot.RebootInfo{RebootRequired: false}) } @@ -160,7 +160,7 @@ bs := boot.NewCoreBootParticipant(s.kern2, snap.TypeKernel, coreDev) c.Assert(bs.IsTrivial(), Equals, false) - rebootInfo, err := bs.SetNextBoot() + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) // check that kernel_status is now try @@ -203,7 +203,7 @@ bs := boot.NewCoreBootParticipant(s.kern2, snap.TypeKernel, coreDev) c.Assert(bs.IsTrivial(), Equals, false) - rebootInfo, err := bs.SetNextBoot() + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) m := s.bootloader.BootVars @@ -232,7 +232,7 @@ bootVars := map[string]string{"snap_kernel": "krnl_40.snap"} s.bootloader.SetBootVars(bootVars) - rebootInfo, err := boot.NewCoreBootParticipant(info, snap.TypeKernel, coreDev).SetNextBoot() + rebootInfo, err := boot.NewCoreBootParticipant(info, snap.TypeKernel, coreDev).SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) v, err := s.bootloader.GetBootVars("snap_kernel") @@ -257,7 +257,7 @@ bs := boot.NewCoreBootParticipant(s.kern1, snap.TypeKernel, coreDev) c.Assert(bs.IsTrivial(), Equals, false) - rebootInfo, err := bs.SetNextBoot() + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) // check that kernel_status is cleared @@ -300,7 +300,7 @@ bs := boot.NewCoreBootParticipant(s.kern1, snap.TypeKernel, coreDev) c.Assert(bs.IsTrivial(), Equals, false) - rebootInfo, err := bs.SetNextBoot() + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) // check that kernel_status is cleared @@ -333,7 +333,7 @@ "snap_mode": boot.TryStatus} s.bootloader.SetBootVars(bootVars) - rebootInfo, err := boot.NewCoreBootParticipant(info, snap.TypeKernel, coreDev).SetNextBoot() + rebootInfo, err := boot.NewCoreBootParticipant(info, snap.TypeKernel, coreDev).SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) v, err := s.bootloader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_mode") @@ -371,7 +371,7 @@ bs := boot.NewCoreBootParticipant(s.kern1, snap.TypeKernel, coreDev) c.Assert(bs.IsTrivial(), Equals, false) - rebootInfo, err := bs.SetNextBoot() + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) // check that kernel_status is cleared @@ -425,7 +425,7 @@ bs := boot.NewCoreBootParticipant(s.kern1, snap.TypeKernel, coreDev) c.Assert(bs.IsTrivial(), Equals, false) - rebootInfo, err := bs.SetNextBoot() + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) // check that kernel_status is cleared @@ -705,7 +705,7 @@ bs := boot.NewCoreBootParticipant(s.kern2, snap.TypeKernel, coreDev) c.Assert(bs.IsTrivial(), Equals, false) - rebootInfo, err := bs.SetNextBoot() + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: false}) c.Assert(err, IsNil) m := s.bootloader.BootVars @@ -724,3 +724,72 @@ c.Assert(err, IsNil) c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename(), s.kern2.Filename()}) } + +func (s *bootenvSuite) TestSetNextBootForCoreUndo(c *C) { + coreDev := boottest.MockDevice("core") + + info := &snap.Info{} + info.SnapType = snap.TypeOS + info.RealName = "core" + info.Revision = snap.R(100) + + bs := boot.NewCoreBootParticipant(info, info.Type(), coreDev) + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + + v, err := s.bootloader.GetBootVars("snap_core", "snap_try_core", "snap_mode") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ + "snap_core": "core_100.snap", + "snap_try_core": "", + "snap_mode": boot.DefaultStatus, + }) + + c.Check(rebootInfo, Equals, boot.RebootInfo{RebootRequired: true}) +} + +func (s *bootenvSuite) TestSetNextBootWithBaseForCoreUndo(c *C) { + coreDev := boottest.MockDevice("core18") + + info := &snap.Info{} + info.SnapType = snap.TypeBase + info.RealName = "core18" + info.Revision = snap.R(1818) + + bs := boot.NewCoreBootParticipant(info, info.Type(), coreDev) + rebootInfo, err := bs.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + + v, err := s.bootloader.GetBootVars("snap_core", "snap_try_core", "snap_mode") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ + "snap_core": "core18_1818.snap", + "snap_try_core": "", + "snap_mode": boot.DefaultStatus, + }) + + c.Check(rebootInfo, Equals, boot.RebootInfo{RebootRequired: true}) +} + +func (s *bootenvSuite) TestSetNextBootForKernelUndo(c *C) { + coreDev := boottest.MockDevice("krnl") + + info := &snap.Info{} + info.SnapType = snap.TypeKernel + info.RealName = "krnl" + info.Revision = snap.R(42) + + bp := boot.NewCoreBootParticipant(info, snap.TypeKernel, coreDev) + rebootInfo, err := bp.SetNextBoot(boot.NextBootContext{BootWithoutTry: true}) + c.Assert(err, IsNil) + + v, err := s.bootloader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_mode") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ + "snap_kernel": "krnl_42.snap", + "snap_try_kernel": "", + "snap_mode": boot.DefaultStatus, + }) + + c.Check(rebootInfo, Equals, boot.RebootInfo{RebootRequired: true}) +} diff -Nru snapd-2.55.5+20.04/boot/makebootable.go snapd-2.57.5+20.04/boot/makebootable.go --- snapd-2.55.5+20.04/boot/makebootable.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/makebootable.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2019 Canonical Ltd + * Copyright (C) 2014-2022 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 @@ -38,6 +38,8 @@ BasePath string Kernel *snap.Info KernelPath string + Gadget *snap.Info + GadgetPath string RecoverySystemLabel string // RecoverySystemDir is a path to a directory with recovery system @@ -290,30 +292,25 @@ return nil } -// MakeRunnableSystem is like MakeBootableImage in that it sets up a system to -// be able to boot, but is unique in that it is intended to be called from UC20 -// install mode and makes the run system bootable (hence it is called -// "runnable"). -// Note that this function does not update the recovery bootloader env to -// actually transition to run mode here, that is left to the caller via -// something like boot.EnsureNextBootToRunMode(). This is to enable separately -// setting up a run system and actually transitioning to it, with hooks, etc. -// running in between. -func MakeRunnableSystem(model *asserts.Model, bootWith *BootableSet, sealer *TrustedAssetsInstallObserver) error { +type makeRunnableOptions struct { + AfterDataReset bool +} + +func makeRunnableSystem(model *asserts.Model, bootWith *BootableSet, sealer *TrustedAssetsInstallObserver, makeOpts makeRunnableOptions) error { if model.Grade() == asserts.ModelGradeUnset { - return fmt.Errorf("internal error: cannot make non-uc20 system runnable") + return fmt.Errorf("internal error: cannot make pre-UC20 system runnable") } // TODO:UC20: // - figure out what to do for uboot gadgets, currently we require them to // install the boot.sel onto ubuntu-boot directly, but the file should be // managed by snapd instead - // copy kernel/base into the ubuntu-data partition + // copy kernel/base/gadget into the ubuntu-data partition snapBlobDir := dirs.SnapBlobDirUnder(InstallHostWritableDir) if err := os.MkdirAll(snapBlobDir, 0755); err != nil { return err } - for _, fn := range []string{bootWith.BasePath, bootWith.KernelPath} { + for _, fn := range []string{bootWith.BasePath, bootWith.KernelPath, bootWith.GadgetPath} { dst := filepath.Join(snapBlobDir, filepath.Base(fn)) // if the source filename is a symlink, don't copy the symlink, copy the // target file instead of copying the symlink, as the initramfs won't @@ -359,9 +356,12 @@ CurrentKernelCommandLines: nil, // keep this comment to make gofmt 1.9 happy Base: filepath.Base(bootWith.BasePath), + Gadget: filepath.Base(bootWith.GadgetPath), CurrentKernels: []string{bootWith.Kernel.Filename()}, BrandID: model.BrandID(), Model: model.Model(), + // TODO: test this + Classic: model.Classic(), Grade: string(model.Grade()), ModelSignKeyID: model.SignKeyID(), } @@ -457,8 +457,11 @@ } if sealer != nil { + flags := sealKeyToModeenvFlags{ + FactoryReset: makeOpts.AfterDataReset, + } // seal the encryption key to the parameters specified in modeenv - if err := sealKeyToModeenv(sealer.dataEncryptionKey, sealer.saveEncryptionKey, model, modeenv); err != nil { + if err := sealKeyToModeenv(sealer.dataEncryptionKey, sealer.saveEncryptionKey, model, modeenv, flags); err != nil { return err } } @@ -470,3 +473,25 @@ } return nil } + +// MakeRunnableSystem is like MakeBootableImage in that it sets up a system to +// be able to boot, but is unique in that it is intended to be called from UC20 +// install mode and makes the run system bootable (hence it is called +// "runnable"). +// Note that this function does not update the recovery bootloader env to +// actually transition to run mode here, that is left to the caller via +// something like boot.EnsureNextBootToRunMode(). This is to enable separately +// setting up a run system and actually transitioning to it, with hooks, etc. +// running in between. +func MakeRunnableSystem(model *asserts.Model, bootWith *BootableSet, sealer *TrustedAssetsInstallObserver) error { + return makeRunnableSystem(model, bootWith, sealer, makeRunnableOptions{}) +} + +// MakeRunnableSystemAfterDataReset sets up the system to be able to boot, but it is +// intended to be called from UC20 factory reset mode right before switching +// back to the new run system. +func MakeRunnableSystemAfterDataReset(model *asserts.Model, bootWith *BootableSet, sealer *TrustedAssetsInstallObserver) error { + return makeRunnableSystem(model, bootWith, sealer, makeRunnableOptions{ + AfterDataReset: true, + }) +} diff -Nru snapd-2.55.5+20.04/boot/makebootable_test.go snapd-2.57.5+20.04/boot/makebootable_test.go --- snapd-2.55.5+20.04/boot/makebootable_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/makebootable_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2021 Canonical Ltd + * Copyright (C) 2014-2022 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 @@ -40,6 +40,7 @@ "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapfile" @@ -453,10 +454,10 @@ model := boottest.MakeMockModel() err := boot.MakeRunnableSystem(model, nil, nil) - c.Assert(err, ErrorMatches, "internal error: cannot make non-uc20 system runnable") + c.Assert(err, ErrorMatches, `internal error: cannot make pre-UC20 system runnable`) } -func (s *makeBootable20Suite) TestMakeSystemRunnable20(c *C) { +func (s *makeBootable20Suite) testMakeSystemRunnable20(c *C, factoryReset bool) { bootloader.Force(nil) model := boottest.MakeMockUC20Model() @@ -520,6 +521,13 @@ baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) err = os.Symlink(baseFn, baseInSeed) c.Assert(err, IsNil) + gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc +type: gadget +version: 5.0 +`, snap.R(4)) + gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename()) + err = os.Symlink(gadgetFn, gadgetInSeed) + c.Assert(err, IsNil) kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel type: kernel version: 5.0 @@ -536,6 +544,8 @@ RecoverySystemDir: "20191216", BasePath: baseInSeed, Base: baseInfo, + Gadget: gadgetInfo, + GadgetPath: gadgetInSeed, KernelPath: kernelInSeed, Kernel: kernelInfo, Recovery: false, @@ -563,8 +573,8 @@ c.Assert(err, IsNil) // set encryption key - myKey := secboot.EncryptionKey{} - myKey2 := secboot.EncryptionKey{} + myKey := keys.EncryptionKey{} + myKey2 := keys.EncryptionKey{} for i := range myKey { myKey[i] = byte(i) myKey2[i] = byte(128 + i) @@ -579,18 +589,81 @@ }) defer restore() + provisionCalls := 0 + restore = boot.MockSecbootProvisionTPM(func(mode secboot.TPMProvisionMode, lockoutAuthFile string) error { + provisionCalls++ + c.Check(lockoutAuthFile, Equals, filepath.Join(boot.InstallHostFDESaveDir, "tpm-lockout-auth")) + if factoryReset { + c.Check(mode, Equals, secboot.TPMPartialReprovision) + } else { + c.Check(mode, Equals, secboot.TPMProvisionFull) + } + return nil + }) + defer restore() + + pcrHandleOfKeyCalls := 0 + restore = boot.MockSecbootPCRHandleOfSealedKey(func(p string) (uint32, error) { + pcrHandleOfKeyCalls++ + c.Check(provisionCalls, Equals, 0) + if !factoryReset { + c.Errorf("unexpected call in non-factory-reset scenario") + return 0, fmt.Errorf("unexpected call") + } + c.Check(p, Equals, + filepath.Join(s.rootdir, "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + // trigger use of alt handles as current key is using the main handle + return secboot.FallbackObjectPCRPolicyCounterHandle, nil + }) + defer restore() + + releasePCRHandleCalls := 0 + restore = boot.MockSecbootReleasePCRResourceHandles(func(handles ...uint32) error { + c.Check(factoryReset, Equals, true) + releasePCRHandleCalls++ + c.Check(handles, DeepEquals, []uint32{ + secboot.AltRunObjectPCRPolicyCounterHandle, + secboot.AltFallbackObjectPCRPolicyCounterHandle, + }) + return nil + }) + defer restore() + // set mock key sealing sealKeysCalls := 0 restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error { + c.Assert(provisionCalls, Equals, 1, Commentf("TPM must have been provisioned before")) sealKeysCalls++ switch sealKeysCalls { case 1: c.Check(keys, HasLen, 1) c.Check(keys[0].Key, DeepEquals, myKey) + c.Check(keys[0].KeyFile, Equals, + filepath.Join(s.rootdir, "/run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + if factoryReset { + c.Check(params.PCRPolicyCounterHandle, Equals, secboot.AltRunObjectPCRPolicyCounterHandle) + } else { + c.Check(params.PCRPolicyCounterHandle, Equals, secboot.RunObjectPCRPolicyCounterHandle) + } case 2: c.Check(keys, HasLen, 2) c.Check(keys[0].Key, DeepEquals, myKey) c.Check(keys[1].Key, DeepEquals, myKey2) + c.Check(keys[0].KeyFile, Equals, + filepath.Join(s.rootdir, + "/run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + if factoryReset { + c.Check(params.PCRPolicyCounterHandle, Equals, secboot.AltFallbackObjectPCRPolicyCounterHandle) + c.Check(keys[1].KeyFile, Equals, + filepath.Join(s.rootdir, + "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key.factory-reset")) + + } else { + c.Check(params.PCRPolicyCounterHandle, Equals, secboot.FallbackObjectPCRPolicyCounterHandle) + c.Check(keys[1].KeyFile, Equals, + filepath.Join(s.rootdir, + "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + } default: c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls) } @@ -637,7 +710,11 @@ }) defer restore() - err = boot.MakeRunnableSystem(model, bootWith, obs) + if !factoryReset { + err = boot.MakeRunnableSystem(model, bootWith, obs) + } else { + err = boot.MakeRunnableSystemAfterDataReset(model, bootWith, obs) + } c.Assert(err, IsNil) // also do the logical thing and make the next boot go to run mode @@ -647,10 +724,12 @@ // ensure grub.cfg in boot was installed from internal assets c.Check(mockBootGrubCfg, testutil.FileEquals, string(grubCfgAsset)) - // ensure base/kernel got copied to /var/lib/snapd/snaps + // ensure base/gadget/kernel got copied to /var/lib/snapd/snaps core20Snap := filepath.Join(dirs.SnapBlobDirUnder(boot.InstallHostWritableDir), "core20_3.snap") + gadgetSnap := filepath.Join(dirs.SnapBlobDirUnder(boot.InstallHostWritableDir), "pc_4.snap") pcKernelSnap := filepath.Join(dirs.SnapBlobDirUnder(boot.InstallHostWritableDir), "pc-kernel_5.snap") c.Check(core20Snap, testutil.FilePresent) + c.Check(gadgetSnap, testutil.FilePresent) c.Check(pcKernelSnap, testutil.FilePresent) c.Check(osutil.IsSymlink(core20Snap), Equals, false) c.Check(osutil.IsSymlink(pcKernelSnap), Equals, false) @@ -685,6 +764,7 @@ current_recovery_systems=20191216 good_recovery_systems=20191216 base=core20_3.snap +gadget=pc_4.snap current_kernels=pc-kernel_5.snap model=my-brand/my-model-uc20 grade=dangerous @@ -720,8 +800,18 @@ c.Check(copiedRecoveryGrubBin, testutil.FileEquals, "recovery grub content") c.Check(copiedRecoveryShimBin, testutil.FileEquals, "recovery shim content") + // make sure TPM was provisioned + c.Check(provisionCalls, Equals, 1) // make sure SealKey was called for the run object and the fallback object c.Check(sealKeysCalls, Equals, 2) + // PCR handle checks + if factoryReset { + c.Check(pcrHandleOfKeyCalls, Equals, 1) + c.Check(releasePCRHandleCalls, Equals, 1) + } else { + c.Check(pcrHandleOfKeyCalls, Equals, 0) + c.Check(releasePCRHandleCalls, Equals, 0) + } // make sure the marker file for sealed key was created c.Check(filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "sealed-keys"), testutil.FilePresent) @@ -730,6 +820,16 @@ c.Check(filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "boot-chains"), testutil.FilePresent) } +func (s *makeBootable20Suite) TestMakeSystemRunnable20Install(c *C) { + const factoryReset = false + s.testMakeSystemRunnable20(c, factoryReset) +} + +func (s *makeBootable20Suite) TestMakeSystemRunnable20FactoryReset(c *C) { + const factoryReset = true + s.testMakeSystemRunnable20(c, factoryReset) +} + func (s *makeBootable20Suite) TestMakeRunnableSystem20ModeInstallBootConfigErr(c *C) { bootloader.Force(nil) @@ -774,6 +874,13 @@ kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) err = os.Symlink(kernelFn, kernelInSeed) c.Assert(err, IsNil) + gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc +type: gadget +version: 5.0 +`, snap.R(4)) + gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename()) + err = os.Symlink(gadgetFn, gadgetInSeed) + c.Assert(err, IsNil) bootWith := &boot.BootableSet{ RecoverySystemDir: "20191216", @@ -781,6 +888,8 @@ Base: baseInfo, KernelPath: kernelInSeed, Kernel: kernelInfo, + Gadget: gadgetInfo, + GadgetPath: gadgetInSeed, Recovery: false, UnpackedGadgetDir: unpackedGadgetDir, } @@ -874,6 +983,13 @@ kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) err = os.Symlink(kernelFn, kernelInSeed) c.Assert(err, IsNil) + gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc +type: gadget +version: 5.0 +`, snap.R(4)) + gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename()) + err = os.Symlink(gadgetFn, gadgetInSeed) + c.Assert(err, IsNil) bootWith := &boot.BootableSet{ RecoverySystemDir: "20191216", @@ -881,6 +997,8 @@ Base: baseInfo, KernelPath: kernelInSeed, Kernel: kernelInfo, + Gadget: gadgetInfo, + GadgetPath: gadgetInSeed, Recovery: false, UnpackedGadgetDir: unpackedGadgetDir, } @@ -906,8 +1024,8 @@ c.Assert(err, IsNil) // set encryption key - myKey := secboot.EncryptionKey{} - myKey2 := secboot.EncryptionKey{} + myKey := keys.EncryptionKey{} + myKey2 := keys.EncryptionKey{} for i := range myKey { myKey[i] = byte(i) myKey2[i] = byte(128 + i) @@ -922,6 +1040,14 @@ }) defer restore() + provisionCalls := 0 + restore = boot.MockSecbootProvisionTPM(func(mode secboot.TPMProvisionMode, lockoutAuthFile string) error { + provisionCalls++ + c.Check(lockoutAuthFile, Equals, filepath.Join(boot.InstallHostFDESaveDir, "tpm-lockout-auth")) + c.Check(mode, Equals, secboot.TPMProvisionFull) + return nil + }) + defer restore() // set mock key sealing sealKeysCalls := 0 restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error { @@ -968,6 +1094,8 @@ err = boot.MakeRunnableSystem(model, bootWith, obs) c.Assert(err, ErrorMatches, "cannot seal the encryption keys: seal error") + // the TPM was provisioned + c.Check(provisionCalls, Equals, 1) } func (s *makeBootable20Suite) testMakeSystemRunnable20WithCustomKernelArgs(c *C, whichFile, content, errMsg string, cmdlines map[string]string) { @@ -1039,6 +1167,13 @@ baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) err = os.Symlink(baseFn, baseInSeed) c.Assert(err, IsNil) + gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc +type: gadget +version: 5.0 +`, snap.R(4)) + gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename()) + err = os.Symlink(gadgetFn, gadgetInSeed) + c.Assert(err, IsNil) kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel type: kernel version: 5.0 @@ -1055,6 +1190,8 @@ RecoverySystemDir: "20191216", BasePath: baseInSeed, Base: baseInfo, + Gadget: gadgetInfo, + GadgetPath: gadgetInSeed, KernelPath: kernelInSeed, Kernel: kernelInfo, Recovery: false, @@ -1089,6 +1226,14 @@ }) defer restore() + provisionCalls := 0 + restore = boot.MockSecbootProvisionTPM(func(mode secboot.TPMProvisionMode, lockoutAuthFile string) error { + provisionCalls++ + c.Check(lockoutAuthFile, Equals, filepath.Join(boot.InstallHostFDESaveDir, "tpm-lockout-auth")) + c.Check(mode, Equals, secboot.TPMProvisionFull) + return nil + }) + defer restore() // set mock key sealing sealKeysCalls := 0 restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error { @@ -1158,6 +1303,7 @@ current_recovery_systems=20191216 good_recovery_systems=20191216 base=core20_3.snap +gadget=pc_4.snap current_kernels=pc-kernel_5.snap model=my-brand/my-model-uc20 grade=dangerous @@ -1166,6 +1312,8 @@ current_trusted_recovery_boot_assets={"bootx64.efi":["39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37"],"grubx64.efi":["aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5"]} current_kernel_command_lines=["%v"] `, cmdlines["run"])) + // make sure the TPM was provisioned + c.Check(provisionCalls, Equals, 1) // make sure SealKey was called for the run object and the fallback object c.Check(sealKeysCalls, Equals, 2) @@ -1271,6 +1419,13 @@ kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) err = os.Symlink(kernelFn, kernelInSeed) c.Assert(err, IsNil) + gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc +type: gadget +version: 5.0 +`, snap.R(4)) + gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename()) + err = os.Symlink(gadgetFn, gadgetInSeed) + c.Assert(err, IsNil) bootWith := &boot.BootableSet{ RecoverySystemDir: "20191216", @@ -1278,6 +1433,8 @@ Base: baseInfo, KernelPath: kernelInSeed, Kernel: kernelInfo, + Gadget: gadgetInfo, + GadgetPath: gadgetInSeed, Recovery: false, UnpackedGadgetDir: unpackedGadgetDir, } @@ -1344,7 +1501,7 @@ // TODO:UC20: enable this use case err = boot.MakeBootableImage(model, s.rootdir, bootWith, nil) - c.Assert(err, ErrorMatches, "non-empty uboot.env not supported on UC20 yet") + c.Assert(err, ErrorMatches, `non-empty uboot.env not supported on UC20\+ yet`) } func (s *makeBootable20UbootSuite) TestUbootMakeBootableImage20BootScr(c *C) { @@ -1451,6 +1608,13 @@ baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) err = os.Rename(baseFn, baseInSeed) c.Assert(err, IsNil) + gadgetFn, gadgetInfo := makeSnap(c, "pc", `name: pc +type: gadget +version: 5.0 +`, snap.R(4)) + gadgetInSeed := filepath.Join(seedSnapsDirs, gadgetInfo.Filename()) + err = os.Symlink(gadgetFn, gadgetInSeed) + c.Assert(err, IsNil) kernelSnapFiles := [][]string{ {"kernel.img", "I'm a kernel"}, {"initrd.img", "...and I'm an initrd"}, @@ -1469,6 +1633,8 @@ RecoverySystemDir: "20191216", BasePath: baseInSeed, Base: baseInfo, + Gadget: gadgetInfo, + GadgetPath: gadgetInSeed, KernelPath: kernelInSeed, Kernel: kernelInfo, Recovery: false, @@ -1514,6 +1680,7 @@ current_recovery_systems=20191216 good_recovery_systems=20191216 base=core20_3.snap +gadget=pc_4.snap current_kernels=arm-kernel_5.snap model=my-brand/my-model-uc20 grade=dangerous diff -Nru snapd-2.55.5+20.04/boot/modeenv.go snapd-2.57.5+20.04/boot/modeenv.go --- snapd-2.55.5+20.04/boot/modeenv.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/modeenv.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019-2020 Canonical Ltd + * Copyright (C) 2019-2022 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,6 +28,7 @@ "path/filepath" "reflect" "sort" + "strconv" "strings" "github.com/mvo5/goconfigparser" @@ -62,11 +63,14 @@ Base string `key:"base"` TryBase string `key:"try_base"` BaseStatus string `key:"base_status"` - CurrentKernels []string `key:"current_kernels"` + // Gadget is the currently active gadget snap + Gadget string `key:"gadget"` + CurrentKernels []string `key:"current_kernels"` // Model, BrandID, Grade, SignKeyID describe the properties of current // device model. Model string `key:"model"` BrandID string `key:"model,secondary"` + Classic bool `key:"classic"` Grade string `key:"grade"` ModelSignKeyID string `key:"model_sign_key_id"` // TryModel, TryBrandID, TryGrade, TrySignKeyID describe the properties @@ -153,7 +157,7 @@ } // ReadModeenv attempts to read the modeenv file at -// /var/iib/snapd/modeenv. +// /var/lib/snapd/modeenv. func ReadModeenv(rootdir string) (*Modeenv, error) { if snapdenv.Preseeding() { return nil, fmt.Errorf("internal error: modeenv cannot be read during preseeding") @@ -183,6 +187,7 @@ } unmarshalModeenvValueFromCfg(cfg, "base", &m.Base) unmarshalModeenvValueFromCfg(cfg, "base_status", &m.BaseStatus) + unmarshalModeenvValueFromCfg(cfg, "gadget", &m.Gadget) unmarshalModeenvValueFromCfg(cfg, "try_base", &m.TryBase) // current_kernels is a comma-delimited list in a string @@ -191,6 +196,7 @@ unmarshalModeenvValueFromCfg(cfg, "model", &bm) m.BrandID = bm.brandID m.Model = bm.model + unmarshalModeenvValueFromCfg(cfg, "classic", &m.Classic) // expect the caller to validate the grade unmarshalModeenvValueFromCfg(cfg, "grade", &m.Grade) unmarshalModeenvValueFromCfg(cfg, "model_sign_key_id", &m.ModelSignKeyID) @@ -292,6 +298,7 @@ marshalModeenvEntryTo(buf, "base", m.Base) marshalModeenvEntryTo(buf, "try_base", m.TryBase) marshalModeenvEntryTo(buf, "base_status", m.BaseStatus) + marshalModeenvEntryTo(buf, "gadget", m.Gadget) marshalModeenvEntryTo(buf, "current_kernels", strings.Join(m.CurrentKernels, ",")) if m.Model != "" || m.Grade != "" { if m.Model == "" { @@ -302,6 +309,9 @@ } marshalModeenvEntryTo(buf, "model", &modeenvModel{brandID: m.BrandID, model: m.Model}) } + if m.Classic { + marshalModeenvEntryTo(buf, "classic", true) + } // TODO: complain when grade or key are unset marshalModeenvEntryTo(buf, "grade", m.Grade) marshalModeenvEntryTo(buf, "model_sign_key_id", m.ModelSignKeyID) @@ -342,16 +352,18 @@ type modelForSealing struct { brandID string model string + classic bool grade asserts.ModelGrade modelSignKeyID string } -// dummy to verify interface match +// verify interface match var _ secboot.ModelForSealing = (*modelForSealing)(nil) func (m *modelForSealing) BrandID() string { return m.brandID } func (m *modelForSealing) SignKeyID() string { return m.modelSignKeyID } func (m *modelForSealing) Model() string { return m.model } +func (m *modelForSealing) Classic() bool { return m.classic } func (m *modelForSealing) Grade() asserts.ModelGrade { return m.grade } func (m *modelForSealing) Series() string { return release.Series } @@ -368,6 +380,7 @@ return &modelForSealing{ brandID: m.BrandID, model: m.Model, + classic: m.Classic, grade: asserts.ModelGrade(m.Grade), modelSignKeyID: m.ModelSignKeyID, } @@ -380,6 +393,7 @@ return &modelForSealing{ brandID: m.TryBrandID, model: m.TryModel, + classic: m.Classic, grade: asserts.ModelGrade(m.TryGrade), modelSignKeyID: m.TryModelSignKeyID, } @@ -429,6 +443,8 @@ return nil } asString = asModeenvStringList(v) + case bool: + asString = strconv.FormatBool(v) default: if vm, ok := what.(modeenvValueMarshaller); ok { marshalled, err := vm.MarshalModeenvValue() @@ -455,7 +471,7 @@ } // unmarshalModeenvValueFromCfg unmarshals the value of the entry with -// th given key to dest. If there's no such entry dest might be left +// the given key to dest. If there's no such entry dest might be left // empty. func unmarshalModeenvValueFromCfg(cfg *goconfigparser.ConfigParser, key string, dest interface{}) error { if dest == nil { @@ -468,6 +484,16 @@ *v = kv case *[]string: *v = splitModeenvStringList(kv) + case *bool: + if kv == "" { + *v = false + return nil + } + var err error + *v, err = strconv.ParseBool(kv) + if err != nil { + return fmt.Errorf("cannot parse modeenv value %q to bool: %v", kv, err) + } default: if vm, ok := v.(modeenvValueUnmarshaller); ok { if err := vm.UnmarshalModeenvValue(kv); err != nil { diff -Nru snapd-2.55.5+20.04/boot/modeenv_test.go snapd-2.57.5+20.04/boot/modeenv_test.go --- snapd-2.55.5+20.04/boot/modeenv_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/modeenv_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019-2020 Canonical Ltd + * Copyright (C) 2019-2022 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 @@ -63,10 +63,12 @@ "boot_flags": true, // keep this comment to make old go fmt happy "base": true, + "gadget": true, "try_base": true, "base_status": true, "current_kernels": true, "model": true, + "classic": true, "grade": true, "model_sign_key_id": true, "try_model": true, @@ -92,7 +94,7 @@ c.Assert(err, IsNil) } -func (s *modeenvSuite) TestWasReadSanity(c *C) { +func (s *modeenvSuite) TestWasReadValidity(c *C) { modeenv := &boot.Modeenv{} c.Check(modeenv.WasRead(), Equals, false) } @@ -113,12 +115,14 @@ c.Check(modeenv.Mode, Equals, "run") c.Check(modeenv.RecoverySystem, Equals, "") c.Check(modeenv.Base, Equals, "") + c.Check(modeenv.Gadget, Equals, "") } func (s *modeenvSuite) TestDeepEqualDiskVsMemoryInvariant(c *C) { s.makeMockModeenvFile(c, `mode=recovery recovery_system=20191126 base=core20_123.snap +gadget=pc_1.snap try_base=core20_124.snap base_status=try `) @@ -129,6 +133,7 @@ Mode: "recovery", RecoverySystem: "20191126", Base: "core20_123.snap", + Gadget: "pc_1.snap", TryBase: "core20_124.snap", BaseStatus: "try", } @@ -808,6 +813,7 @@ c.Assert(err, IsNil) c.Check(modeenv.Model, Equals, "ubuntu-core-20-amd64") c.Check(modeenv.BrandID, Equals, "canonical") + c.Check(modeenv.Classic, Equals, false) c.Check(modeenv.Grade, Equals, "dangerous") c.Check(modeenv.ModelSignKeyID, Equals, "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn") // candidate model @@ -840,6 +846,55 @@ `) } +func (s *modeenvSuite) TestModeenvWithClassicModelGradeSignKeyID(c *C) { + s.makeMockModeenvFile(c, `mode=run +model=canonical/ubuntu-classic-20-amd64 +grade=dangerous +classic=true +model_sign_key_id=9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn +try_model=developer1/testkeys-snapd-secured-classic-20-amd64 +try_grade=secured +try_model_sign_key_id=EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu +`) + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Model, Equals, "ubuntu-classic-20-amd64") + c.Check(modeenv.BrandID, Equals, "canonical") + c.Check(modeenv.Classic, Equals, true) + c.Check(modeenv.Grade, Equals, "dangerous") + c.Check(modeenv.ModelSignKeyID, Equals, "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn") + // candidate model + c.Check(modeenv.TryModel, Equals, "testkeys-snapd-secured-classic-20-amd64") + c.Check(modeenv.TryBrandID, Equals, "developer1") + c.Check(modeenv.TryGrade, Equals, "secured") + c.Check(modeenv.TryModelSignKeyID, Equals, "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu") + + // change some model data now + modeenv.Model = "testkeys-snapd-signed-classic-20-amd64" + modeenv.BrandID = "developer1" + modeenv.Grade = "signed" + modeenv.ModelSignKeyID = "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu" + + modeenv.TryModel = "bar" + modeenv.TryBrandID = "foo" + modeenv.TryGrade = "dangerous" + modeenv.TryModelSignKeyID = "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn" + + // and write it + c.Assert(modeenv.Write(), IsNil) + + c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run +model=developer1/testkeys-snapd-signed-classic-20-amd64 +classic=true +grade=signed +model_sign_key_id=EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu +try_model=foo/bar +try_grade=dangerous +try_model_sign_key_id=9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn +`) +} + func (s *modeenvSuite) TestModelForSealing(c *C) { s.makeMockModeenvFile(c, `mode=run model=canonical/ubuntu-core-20-amd64 @@ -856,6 +911,42 @@ modelForSealing := modeenv.ModelForSealing() c.Check(modelForSealing.Model(), Equals, "ubuntu-core-20-amd64") c.Check(modelForSealing.BrandID(), Equals, "canonical") + c.Check(modelForSealing.Classic(), Equals, false) + c.Check(modelForSealing.Grade(), Equals, asserts.ModelGrade("dangerous")) + c.Check(modelForSealing.SignKeyID(), Equals, "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn") + c.Check(modelForSealing.Series(), Equals, "16") + c.Check(boot.ModelUniqueID(modelForSealing), Equals, + "canonical/ubuntu-core-20-amd64,dangerous,9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn") + + tryModelForSealing := modeenv.TryModelForSealing() + c.Check(tryModelForSealing.Model(), Equals, "testkeys-snapd-secured-core-20-amd64") + c.Check(tryModelForSealing.BrandID(), Equals, "developer1") + c.Check(tryModelForSealing.Classic(), Equals, false) + c.Check(tryModelForSealing.Grade(), Equals, asserts.ModelGrade("secured")) + c.Check(tryModelForSealing.SignKeyID(), Equals, "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu") + c.Check(tryModelForSealing.Series(), Equals, "16") + c.Check(boot.ModelUniqueID(tryModelForSealing), Equals, + "developer1/testkeys-snapd-secured-core-20-amd64,secured,EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu") +} + +func (s *modeenvSuite) TestClassicModelForSealing(c *C) { + s.makeMockModeenvFile(c, `mode=run +model=canonical/ubuntu-core-20-amd64 +classic=true +grade=dangerous +model_sign_key_id=9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn +try_model=developer1/testkeys-snapd-secured-core-20-amd64 +try_grade=secured +try_model_sign_key_id=EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu +`) + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + + modelForSealing := modeenv.ModelForSealing() + c.Check(modelForSealing.Model(), Equals, "ubuntu-core-20-amd64") + c.Check(modelForSealing.BrandID(), Equals, "canonical") + c.Check(modelForSealing.Classic(), Equals, true) c.Check(modelForSealing.Grade(), Equals, asserts.ModelGrade("dangerous")) c.Check(modelForSealing.SignKeyID(), Equals, "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn") c.Check(modelForSealing.Series(), Equals, "16") @@ -865,6 +956,7 @@ tryModelForSealing := modeenv.TryModelForSealing() c.Check(tryModelForSealing.Model(), Equals, "testkeys-snapd-secured-core-20-amd64") c.Check(tryModelForSealing.BrandID(), Equals, "developer1") + c.Check(tryModelForSealing.Classic(), Equals, true) c.Check(tryModelForSealing.Grade(), Equals, asserts.ModelGrade("secured")) c.Check(tryModelForSealing.SignKeyID(), Equals, "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu") c.Check(tryModelForSealing.Series(), Equals, "16") diff -Nru snapd-2.55.5+20.04/boot/seal.go snapd-2.57.5+20.04/boot/seal.go --- snapd-2.55.5+20.04/boot/seal.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/seal.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2020 Canonical Ltd + * Copyright (C) 2020-2022 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,19 +24,19 @@ "crypto/elliptic" "crypto/rand" "encoding/json" - "errors" "fmt" - "io/ioutil" "os" "path/filepath" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/kernel/fde" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/strutil" @@ -44,9 +44,12 @@ ) var ( - secbootSealKeys = secboot.SealKeys - secbootSealKeysWithFDESetupHook = secboot.SealKeysWithFDESetupHook - secbootResealKeys = secboot.ResealKeys + secbootProvisionTPM = secboot.ProvisionTPM + secbootSealKeys = secboot.SealKeys + secbootSealKeysWithFDESetupHook = secboot.SealKeysWithFDESetupHook + secbootResealKeys = secboot.ResealKeys + secbootPCRHandleOfSealedKey = secboot.PCRHandleOfSealedKey + secbootReleasePCRResourceHandles = secboot.ReleasePCRResourceHandles seedReadSystemEssential = seed.ReadSystemEssential ) @@ -63,14 +66,6 @@ } ) -type sealingMethod string - -const ( - sealingMethodLegacyTPM = sealingMethod("") - sealingMethodTPM = sealingMethod("tpm") - sealingMethodFDESetupHook = sealingMethod("fde-setup-hook") -) - // MockSecbootResealKeys is only useful in testing. Note that this is a very low // level call and may need significant environment setup. func MockSecbootResealKeys(f func(params *secboot.ResealKeysParams) error) (restore func()) { @@ -100,10 +95,16 @@ return filepath.Join(dirs.SnapFDEDirUnder(rootdir), "recovery-boot-chains") } +type sealKeyToModeenvFlags struct { + // FactoryReset indicates that the sealing is happening during factory + // reset. + FactoryReset bool +} + // sealKeyToModeenv seals the supplied keys to the parameters specified // in modeenv. // It assumes to be invoked in install mode. -func sealKeyToModeenv(key, saveKey secboot.EncryptionKey, model *asserts.Model, modeenv *Modeenv) error { +func sealKeyToModeenv(key, saveKey keys.EncryptionKey, model *asserts.Model, modeenv *Modeenv, flags sealKeyToModeenvFlags) error { // make sure relevant locations exist for _, p := range []string{ InitramfsSeedEncryptionKeyDir, @@ -122,45 +123,53 @@ return fmt.Errorf("cannot check for fde-setup hook %v", err) } if hasHook { - return sealKeyToModeenvUsingFDESetupHook(key, saveKey, modeenv) + return sealKeyToModeenvUsingFDESetupHook(key, saveKey, modeenv, flags) } - return sealKeyToModeenvUsingSecboot(key, saveKey, modeenv) + return sealKeyToModeenvUsingSecboot(key, saveKey, modeenv, flags) } -func runKeySealRequests(key secboot.EncryptionKey) []secboot.SealKeyRequest { +func runKeySealRequests(key keys.EncryptionKey) []secboot.SealKeyRequest { return []secboot.SealKeyRequest{ { Key: key, KeyName: "ubuntu-data", - KeyFile: filepath.Join(InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"), + KeyFile: device.DataSealedKeyUnder(InitramfsBootEncryptionKeyDir), }, } } -func fallbackKeySealRequests(key, saveKey secboot.EncryptionKey) []secboot.SealKeyRequest { +func fallbackKeySealRequests(key, saveKey keys.EncryptionKey, factoryReset bool) []secboot.SealKeyRequest { + saveFallbackKey := device.FallbackSaveSealedKeyUnder(InitramfsSeedEncryptionKeyDir) + + if factoryReset { + // factory reset uses alternative sealed key location, such that + // until we boot into the run mode, both sealed keys are present + // on disk + saveFallbackKey = device.FactoryResetFallbackSaveSealedKeyUnder(InitramfsSeedEncryptionKeyDir) + } return []secboot.SealKeyRequest{ { Key: key, KeyName: "ubuntu-data", - KeyFile: filepath.Join(InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), + KeyFile: device.FallbackDataSealedKeyUnder(InitramfsSeedEncryptionKeyDir), }, { Key: saveKey, KeyName: "ubuntu-save", - KeyFile: filepath.Join(InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + KeyFile: saveFallbackKey, }, } } -func sealKeyToModeenvUsingFDESetupHook(key, saveKey secboot.EncryptionKey, modeenv *Modeenv) error { +func sealKeyToModeenvUsingFDESetupHook(key, saveKey keys.EncryptionKey, modeenv *Modeenv, flags sealKeyToModeenvFlags) error { // XXX: Move the auxKey creation to a more generic place, see // PR#10123 for a possible way of doing this. However given // that the equivalent key for the TPM case is also created in // sealKeyToModeenvUsingTPM more symetric to create the auxKey // here and when we also move TPM to use the auxKey to move // the creation of it. - auxKey, err := secboot.NewAuxKey() + auxKey, err := keys.NewAuxKey() if err != nil { return fmt.Errorf("cannot create aux key: %v", err) } @@ -169,19 +178,20 @@ AuxKey: auxKey, AuxKeyFile: filepath.Join(InstallHostFDESaveDir, "aux-key"), } - skrs := append(runKeySealRequests(key), fallbackKeySealRequests(key, saveKey)...) + factoryReset := flags.FactoryReset + skrs := append(runKeySealRequests(key), fallbackKeySealRequests(key, saveKey, factoryReset)...) if err := secbootSealKeysWithFDESetupHook(RunFDESetupHook, skrs, ¶ms); err != nil { return err } - if err := stampSealedKeys(InstallHostWritableDir, "fde-setup-hook"); err != nil { + if err := device.StampSealedKeys(InstallHostWritableDir, "fde-setup-hook"); err != nil { return err } return nil } -func sealKeyToModeenvUsingSecboot(key, saveKey secboot.EncryptionKey, modeenv *Modeenv) error { +func sealKeyToModeenvUsingSecboot(key, saveKey keys.EncryptionKey, modeenv *Modeenv, flags sealKeyToModeenvFlags) error { // build the recovery mode boot chain rbl, err := bootloader.Find(InitramfsUbuntuSeedDir, &bootloader.Options{ Role: bootloader.RoleRecovery, @@ -239,15 +249,62 @@ return fmt.Errorf("cannot generate key for signing dynamic authorization policies: %v", err) } - if err := sealRunObjectKeys(key, pbc, authKey, roleToBlName); err != nil { + runObjectKeyPCRHandle := uint32(secboot.RunObjectPCRPolicyCounterHandle) + fallbackObjectKeyPCRHandle := uint32(secboot.FallbackObjectPCRPolicyCounterHandle) + if flags.FactoryReset { + // during factory reset we may need to rotate the PCR handles, + // seal the new keys using a new set of handles such that the + // old sealed ubuntu-save key is still usable, for this we + // switch between two sets of handles in a round robin fashion, + // first looking at the PCR handle used by the current fallback + // key and then using the other set when sealing the new keys; + // the currently used handles will be released during the first + // boot of a new run system + usesAlt, err := usesAltPCRHandles() + if err != nil { + return err + } + if !usesAlt { + logger.Noticef("using alternative PCR handles") + runObjectKeyPCRHandle = secboot.AltRunObjectPCRPolicyCounterHandle + fallbackObjectKeyPCRHandle = secboot.AltFallbackObjectPCRPolicyCounterHandle + } + } + + // we are preparing a new system, hence the TPM needs to be provisioned + lockoutAuthFile := device.TpmLockoutAuthUnder(InstallHostFDESaveDir) + tpmProvisionMode := secboot.TPMProvisionFull + if flags.FactoryReset { + tpmProvisionMode = secboot.TPMPartialReprovision + } + if err := secbootProvisionTPM(tpmProvisionMode, lockoutAuthFile); err != nil { + return err + } + + if flags.FactoryReset { + // it is possible that we are sealing the keys again, after a + // previously running factory reset was interrupted by a reboot, + // in which case the PCR handles of the new sealed keys might + // have already been used + if err := secbootReleasePCRResourceHandles(runObjectKeyPCRHandle, fallbackObjectKeyPCRHandle); err != nil { + return err + } + } + + // TODO: refactor sealing functions to take a struct instead of so many + // parameters + err = sealRunObjectKeys(key, pbc, authKey, roleToBlName, runObjectKeyPCRHandle) + if err != nil { return err } - if err := sealFallbackObjectKeys(key, saveKey, rpbc, authKey, roleToBlName); err != nil { + err = sealFallbackObjectKeys(key, saveKey, rpbc, authKey, roleToBlName, flags.FactoryReset, + fallbackObjectKeyPCRHandle) + if err != nil { return err } - if err := stampSealedKeys(InstallHostWritableDir, sealingMethodTPM); err != nil { + if err := device.StampSealedKeys(InstallHostWritableDir, device.SealingMethodTPM); err != nil { return err } @@ -264,7 +321,18 @@ return nil } -func sealRunObjectKeys(key secboot.EncryptionKey, pbc predictableBootChains, authKey *ecdsa.PrivateKey, roleToBlName map[bootloader.Role]string) error { +func usesAltPCRHandles() (bool, error) { + saveFallbackKey := device.FallbackSaveSealedKeyUnder(InitramfsSeedEncryptionKeyDir) + // inspect the PCR handle of the ubuntu-save fallback key + handle, err := secbootPCRHandleOfSealedKey(saveFallbackKey) + if err != nil { + return false, err + } + logger.Noticef("fallback sealed key %v PCR handle: %#x", saveFallbackKey, handle) + return handle == secboot.AltFallbackObjectPCRPolicyCounterHandle, nil +} + +func sealRunObjectKeys(key keys.EncryptionKey, pbc predictableBootChains, authKey *ecdsa.PrivateKey, roleToBlName map[bootloader.Role]string, pcrHandle uint32) error { modelParams, err := sealKeyModelParams(pbc, roleToBlName) if err != nil { return fmt.Errorf("cannot prepare for key sealing: %v", err) @@ -274,10 +342,10 @@ ModelParams: modelParams, TPMPolicyAuthKey: authKey, TPMPolicyAuthKeyFile: filepath.Join(InstallHostFDESaveDir, "tpm-policy-auth-key"), - TPMLockoutAuthFile: filepath.Join(InstallHostFDESaveDir, "tpm-lockout-auth"), - TPMProvision: true, - PCRPolicyCounterHandle: secboot.RunObjectPCRPolicyCounterHandle, + PCRPolicyCounterHandle: pcrHandle, } + + logger.Debugf("sealing run key with PCR handle: %#x", sealKeyParams.PCRPolicyCounterHandle) // The run object contains only the ubuntu-data key; the ubuntu-save key // is then stored inside the encrypted data partition, so that the normal run // path only unseals one object because unsealing is expensive. @@ -290,7 +358,7 @@ return nil } -func sealFallbackObjectKeys(key, saveKey secboot.EncryptionKey, pbc predictableBootChains, authKey *ecdsa.PrivateKey, roleToBlName map[bootloader.Role]string) error { +func sealFallbackObjectKeys(key, saveKey keys.EncryptionKey, pbc predictableBootChains, authKey *ecdsa.PrivateKey, roleToBlName map[bootloader.Role]string, factoryReset bool, pcrHandle uint32) error { // also seal the keys to the recovery bootchains as a fallback modelParams, err := sealKeyModelParams(pbc, roleToBlName) if err != nil { @@ -299,44 +367,20 @@ sealKeyParams := &secboot.SealKeysParams{ ModelParams: modelParams, TPMPolicyAuthKey: authKey, - PCRPolicyCounterHandle: secboot.FallbackObjectPCRPolicyCounterHandle, + PCRPolicyCounterHandle: pcrHandle, } + logger.Debugf("sealing fallback key with PCR handle: %#x", sealKeyParams.PCRPolicyCounterHandle) // The fallback object contains the ubuntu-data and ubuntu-save keys. The // key files are stored on ubuntu-seed, separate from ubuntu-data so they // can be used if ubuntu-data and ubuntu-boot are corrupted or unavailable. - if err := secbootSealKeys(fallbackKeySealRequests(key, saveKey), sealKeyParams); err != nil { - return fmt.Errorf("cannot seal the fallback encryption keys: %v", err) - } - return nil -} - -func stampSealedKeys(rootdir string, content sealingMethod) error { - stamp := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "sealed-keys") - if err := os.MkdirAll(filepath.Dir(stamp), 0755); err != nil { - return fmt.Errorf("cannot create device fde state directory: %v", err) + if err := secbootSealKeys(fallbackKeySealRequests(key, saveKey, factoryReset), sealKeyParams); err != nil { + return fmt.Errorf("cannot seal the fallback encryption keys: %v", err) } - if err := osutil.AtomicWriteFile(stamp, []byte(content), 0644, 0); err != nil { - return fmt.Errorf("cannot create fde sealed keys stamp file: %v", err) - } return nil } -var errNoSealedKeys = errors.New("no sealed keys") - -// sealedKeysMethod return whether any keys were sealed at all -func sealedKeysMethod(rootdir string) (sm sealingMethod, err error) { - // TODO:UC20: consider more than the marker for cases where we reseal - // outside of run mode - stamp := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "sealed-keys") - content, err := ioutil.ReadFile(stamp) - if os.IsNotExist(err) { - return sm, errNoSealedKeys - } - return sealingMethod(content), err -} - var resealKeyToModeenv = resealKeyToModeenvImpl // resealKeyToModeenv reseals the existing encryption key to the @@ -347,8 +391,8 @@ // transient/in-memory information with the risk that successive // reseals during in-progress operations produce diverging outcomes. func resealKeyToModeenvImpl(rootdir string, modeenv *Modeenv, expectReseal bool) error { - method, err := sealedKeysMethod(rootdir) - if err == errNoSealedKeys { + method, err := device.SealedKeysMethod(rootdir) + if err == device.ErrNoSealedKeys { // nothing to do return nil } @@ -356,9 +400,9 @@ return err } switch method { - case sealingMethodFDESetupHook: + case device.SealingMethodFDESetupHook: return resealKeyToModeenvUsingFDESetupHook(rootdir, modeenv, expectReseal) - case sealingMethodTPM, sealingMethodLegacyTPM: + case device.SealingMethodTPM, device.SealingMethodLegacyTPM: return resealKeyToModeenvSecboot(rootdir, modeenv, expectReseal) default: return fmt.Errorf("unknown key sealing method: %q", method) @@ -518,9 +562,7 @@ } // list all the key files to reseal - keyFiles := []string{ - filepath.Join(InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"), - } + keyFiles := []string{device.DataSealedKeyUnder(InitramfsBootEncryptionKeyDir)} resealKeyParams := &secboot.ResealKeysParams{ ModelParams: modelParams, @@ -543,8 +585,8 @@ // list all the key files to reseal keyFiles := []string{ - filepath.Join(InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), - filepath.Join(InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + device.FallbackDataSealedKeyUnder(InitramfsSeedEncryptionKeyDir), + device.FallbackSaveSealedKeyUnder(InitramfsSeedEncryptionKeyDir), } resealKeyParams := &secboot.ResealKeysParams{ @@ -648,8 +690,10 @@ } chains = append(chains, bootChain{ - BrandID: model.BrandID(), - Model: model.Model(), + BrandID: model.BrandID(), + Model: model.Model(), + // TODO: test this + Classic: model.Classic(), Grade: model.Grade(), ModelSignKeyID: model.SignKeyID(), AssetChain: assetChain, @@ -703,8 +747,10 @@ kernelRev = info.SnapRevision().String() } chains = append(chains, bootChain{ - BrandID: model.BrandID(), - Model: model.Model(), + BrandID: model.BrandID(), + Model: model.Model(), + // TODO: test this + Classic: model.Classic(), Grade: model.Grade(), ModelSignKeyID: model.SignKeyID(), AssetChain: assetChain, @@ -815,3 +861,61 @@ } return true, c + 1, nil } + +func postFactoryResetCleanupSecboot() error { + // we are inspecting a key which was generated during factory reset, in + // the simplest case the sealed key generated previously used the main + // handles, while the current key uses alt handles, hence we need to + // release the main handles corresponding to the old key + handles := []uint32{secboot.RunObjectPCRPolicyCounterHandle, secboot.FallbackObjectPCRPolicyCounterHandle} + usesAlt, err := usesAltPCRHandles() + if err != nil { + return fmt.Errorf("cannot inspect fallback key: %v", err) + } + if !usesAlt { + // current fallback key using the main handles, which is + // possible of there were subsequent factory reset steps, + // release the alt handles associated with the old key + handles = []uint32{secboot.AltRunObjectPCRPolicyCounterHandle, secboot.AltFallbackObjectPCRPolicyCounterHandle} + } + return secbootReleasePCRResourceHandles(handles...) +} + +func postFactoryResetCleanup() error { + hasHook, err := HasFDESetupHook() + if err != nil { + return fmt.Errorf("cannot check for fde-setup hook %v", err) + } + + saveFallbackKeyFactory := device.FactoryResetFallbackSaveSealedKeyUnder(InitramfsSeedEncryptionKeyDir) + saveFallbackKey := device.FallbackSaveSealedKeyUnder(InitramfsSeedEncryptionKeyDir) + if err := os.Rename(saveFallbackKeyFactory, saveFallbackKey); err != nil { + // it is possible that the key file was already renamed if we + // came back here after an unexpected reboot + if !os.IsNotExist(err) { + return fmt.Errorf("cannot rotate fallback key: %v", err) + } + } + + if hasHook { + // TODO: do we need to invoke FDE hook? + return nil + } + + if err := postFactoryResetCleanupSecboot(); err != nil { + return fmt.Errorf("cannot cleanup secboot state: %v", err) + } + + return nil +} + +// resealExpectedByModeenvChange returns true if resealing is expected +// due to modeenv changes, false otherwise. Reseal might not be needed +// if the only change in modeenv is the gadget (if the boot assets +// change that is detected in resealKeyToModeenv() and reseal will +// happen anyway) +func resealExpectedByModeenvChange(m1, m2 *Modeenv) bool { + auxModeenv := *m2 + auxModeenv.Gadget = m1.Gadget + return !auxModeenv.deepEqual(m1) +} diff -Nru snapd-2.55.5+20.04/boot/seal_test.go snapd-2.57.5+20.04/boot/seal_test.go --- snapd-2.55.5+20.04/boot/seal_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/seal_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -40,6 +40,7 @@ "github.com/snapcore/snapd/kernel/fde" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" @@ -99,13 +100,41 @@ } func (s *sealSuite) TestSealKeyToModeenv(c *C) { - for _, tc := range []struct { - sealErr error - err string + for idx, tc := range []struct { + sealErr error + provisionErr error + factoryReset bool + pcrHandleOfKey uint32 + pcrHandleOfKeyErr error + expErr string + expProvisionCalls int + expSealCalls int + expReleasePCRHandleCalls int + expPCRHandleOfKeyCalls int }{ - {sealErr: nil, err: ""}, - {sealErr: errors.New("seal error"), err: "cannot seal the encryption keys: seal error"}, + { + sealErr: nil, expErr: "", + expProvisionCalls: 1, expSealCalls: 2, + }, { + sealErr: nil, factoryReset: true, pcrHandleOfKey: secboot.FallbackObjectPCRPolicyCounterHandle, + expProvisionCalls: 1, expSealCalls: 2, expPCRHandleOfKeyCalls: 1, expReleasePCRHandleCalls: 1, + }, { + sealErr: nil, factoryReset: true, pcrHandleOfKey: secboot.AltFallbackObjectPCRPolicyCounterHandle, + expProvisionCalls: 1, expSealCalls: 2, expPCRHandleOfKeyCalls: 1, expReleasePCRHandleCalls: 1, + }, { + sealErr: nil, factoryReset: true, pcrHandleOfKeyErr: errors.New("PCR handle error"), + expErr: "PCR handle error", + expPCRHandleOfKeyCalls: 1, + }, { + sealErr: errors.New("seal error"), expErr: "cannot seal the encryption keys: seal error", + expProvisionCalls: 1, expSealCalls: 1, + }, { + provisionErr: errors.New("provision error"), sealErr: errors.New("unexpected call"), + expErr: "provision error", + expProvisionCalls: 1, + }, } { + c.Logf("tc %v", idx) rootdir := c.MkDir() dirs.SetRootDir(rootdir) defer dirs.SetRootDir("") @@ -148,8 +177,8 @@ }) // set encryption key - myKey := secboot.EncryptionKey{} - myKey2 := secboot.EncryptionKey{} + myKey := keys.EncryptionKey{} + myKey2 := keys.EncryptionKey{} for i := range myKey { myKey[i] = byte(i) myKey2[i] = byte(128 + i) @@ -163,26 +192,80 @@ }) defer restore() + provisionCalls := 0 + restore = boot.MockSecbootProvisionTPM(func(mode secboot.TPMProvisionMode, lockoutAuthFile string) error { + provisionCalls++ + c.Check(lockoutAuthFile, Equals, filepath.Join(boot.InstallHostFDESaveDir, "tpm-lockout-auth")) + if tc.factoryReset { + c.Check(mode, Equals, secboot.TPMPartialReprovision) + } else { + c.Check(mode, Equals, secboot.TPMProvisionFull) + } + return tc.provisionErr + }) + defer restore() + + pcrHandleOfKeyCalls := 0 + restore = boot.MockSecbootPCRHandleOfSealedKey(func(p string) (uint32, error) { + pcrHandleOfKeyCalls++ + c.Check(provisionCalls, Equals, 0) + c.Check(p, Equals, filepath.Join(rootdir, "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + return tc.pcrHandleOfKey, tc.pcrHandleOfKeyErr + }) + defer restore() + + releasePCRHandleCalls := 0 + restore = boot.MockSecbootReleasePCRResourceHandles(func(handles ...uint32) error { + c.Check(tc.factoryReset, Equals, true) + releasePCRHandleCalls++ + if tc.pcrHandleOfKey == secboot.FallbackObjectPCRPolicyCounterHandle { + c.Check(handles, DeepEquals, []uint32{ + secboot.AltRunObjectPCRPolicyCounterHandle, + secboot.AltFallbackObjectPCRPolicyCounterHandle, + }) + } else { + c.Check(handles, DeepEquals, []uint32{ + secboot.RunObjectPCRPolicyCounterHandle, + secboot.FallbackObjectPCRPolicyCounterHandle, + }) + } + return nil + }) + defer restore() + // set mock key sealing sealKeysCalls := 0 restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error { + c.Assert(provisionCalls, Equals, 1, Commentf("TPM must have been provisioned before")) sealKeysCalls++ switch sealKeysCalls { case 1: // the run object seals only the ubuntu-data key c.Check(params.TPMPolicyAuthKeyFile, Equals, filepath.Join(boot.InstallHostFDESaveDir, "tpm-policy-auth-key")) - c.Check(params.TPMLockoutAuthFile, Equals, filepath.Join(boot.InstallHostFDESaveDir, "tpm-lockout-auth")) dataKeyFile := filepath.Join(rootdir, "/run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key") c.Check(keys, DeepEquals, []secboot.SealKeyRequest{{Key: myKey, KeyName: "ubuntu-data", KeyFile: dataKeyFile}}) + if tc.pcrHandleOfKey == secboot.FallbackObjectPCRPolicyCounterHandle { + c.Check(params.PCRPolicyCounterHandle, Equals, secboot.AltRunObjectPCRPolicyCounterHandle) + } else { + c.Check(params.PCRPolicyCounterHandle, Equals, secboot.RunObjectPCRPolicyCounterHandle) + } case 2: // the fallback object seals the ubuntu-data and the ubuntu-save keys c.Check(params.TPMPolicyAuthKeyFile, Equals, "") - c.Check(params.TPMLockoutAuthFile, Equals, "") dataKeyFile := filepath.Join(rootdir, "/run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key") saveKeyFile := filepath.Join(rootdir, "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key") + if tc.factoryReset { + // during factory reset we use a different key location + saveKeyFile = filepath.Join(rootdir, "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key.factory-reset") + } c.Check(keys, DeepEquals, []secboot.SealKeyRequest{{Key: myKey, KeyName: "ubuntu-data", KeyFile: dataKeyFile}, {Key: myKey2, KeyName: "ubuntu-save", KeyFile: saveKeyFile}}) + if tc.pcrHandleOfKey == secboot.FallbackObjectPCRPolicyCounterHandle { + c.Check(params.PCRPolicyCounterHandle, Equals, secboot.AltFallbackObjectPCRPolicyCounterHandle) + } else { + c.Check(params.PCRPolicyCounterHandle, Equals, secboot.FallbackObjectPCRPolicyCounterHandle) + } default: c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls) } @@ -233,16 +316,17 @@ }) defer restore() - err = boot.SealKeyToModeenv(myKey, myKey2, model, modeenv) - if tc.sealErr != nil { - c.Assert(sealKeysCalls, Equals, 1) - } else { - c.Assert(sealKeysCalls, Equals, 2) - } - if tc.err == "" { + err = boot.SealKeyToModeenv(myKey, myKey2, model, modeenv, boot.SealKeyToModeenvFlags{ + FactoryReset: tc.factoryReset, + }) + c.Check(pcrHandleOfKeyCalls, Equals, tc.expPCRHandleOfKeyCalls) + c.Check(provisionCalls, Equals, tc.expProvisionCalls) + c.Check(sealKeysCalls, Equals, tc.expSealCalls) + c.Check(releasePCRHandleCalls, Equals, tc.expReleasePCRHandleCalls) + if tc.expErr == "" { c.Assert(err, IsNil) } else { - c.Assert(err, ErrorMatches, tc.err) + c.Assert(err, ErrorMatches, tc.expErr) continue } @@ -1584,10 +1668,10 @@ Grade: string(model.Grade()), ModelSignKeyID: model.SignKeyID(), } - key := secboot.EncryptionKey{1, 2, 3, 4} - saveKey := secboot.EncryptionKey{5, 6, 7, 8} + key := keys.EncryptionKey{1, 2, 3, 4} + saveKey := keys.EncryptionKey{5, 6, 7, 8} - err := boot.SealKeyToModeenv(key, saveKey, model, modeenv) + err := boot.SealKeyToModeenv(key, saveKey, model, modeenv, boot.SealKeyToModeenvFlags{}) c.Assert(err, IsNil) // check that runFDESetupHook was called the expected way c.Check(runFDESetupHookReqs, DeepEquals, []*fde.SetupRequest{ @@ -1628,11 +1712,11 @@ modeenv := &boot.Modeenv{ RecoverySystem: "20200825", } - key := secboot.EncryptionKey{1, 2, 3, 4} - saveKey := secboot.EncryptionKey{5, 6, 7, 8} + key := keys.EncryptionKey{1, 2, 3, 4} + saveKey := keys.EncryptionKey{5, 6, 7, 8} model := boottest.MakeMockUC20Model() - err := boot.SealKeyToModeenv(key, saveKey, model, modeenv) + err := boot.SealKeyToModeenv(key, saveKey, model, modeenv, boot.SealKeyToModeenvFlags{}) c.Assert(err, ErrorMatches, "hook failed") marker := filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "sealed-keys") c.Check(marker, testutil.FileAbsent) @@ -2020,3 +2104,116 @@ }, }) } + +func (s *sealSuite) TestMarkFactoryResetComplete(c *C) { + + for i, tc := range []struct { + encrypted bool + factoryKeyAlreadyMigrated bool + pcrHandleOfKey uint32 + pcrHandleOfKeyErr error + pcrHandleOfKeyCalls int + releasePCRHandlesErr error + releasePCRHandleCalls int + hasFDEHook bool + err string + }{ + { + // unencrypted is a nop + encrypted: false, + }, { + // the old fallback key uses the main handle + encrypted: true, pcrHandleOfKey: secboot.FallbackObjectPCRPolicyCounterHandle, + factoryKeyAlreadyMigrated: true, pcrHandleOfKeyCalls: 1, releasePCRHandleCalls: 1, + }, { + // the old fallback key uses the alt handle + encrypted: true, pcrHandleOfKey: secboot.AltFallbackObjectPCRPolicyCounterHandle, + factoryKeyAlreadyMigrated: true, pcrHandleOfKeyCalls: 1, releasePCRHandleCalls: 1, + }, { + // unexpected reboot, the key file was already moved + encrypted: true, pcrHandleOfKey: secboot.AltFallbackObjectPCRPolicyCounterHandle, + pcrHandleOfKeyCalls: 1, releasePCRHandleCalls: 1, + }, { + // do nothing if we have the FDE hook + encrypted: true, pcrHandleOfKeyErr: errors.New("unexpected call"), + hasFDEHook: true, + }, + // error cases + { + encrypted: true, pcrHandleOfKey: secboot.FallbackObjectPCRPolicyCounterHandle, + factoryKeyAlreadyMigrated: true, + pcrHandleOfKeyCalls: 1, + pcrHandleOfKeyErr: errors.New("handle error"), + err: "cannot perform post factory reset boot cleanup: cannot cleanup secboot state: cannot inspect fallback key: handle error", + }, { + encrypted: true, pcrHandleOfKey: secboot.FallbackObjectPCRPolicyCounterHandle, + factoryKeyAlreadyMigrated: true, + pcrHandleOfKeyCalls: 1, releasePCRHandleCalls: 1, + releasePCRHandlesErr: errors.New("release error"), + err: "cannot perform post factory reset boot cleanup: cannot cleanup secboot state: release error", + }, + } { + c.Logf("tc %v", i) + + saveSealedKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key") + saveSealedKeyByFactoryReset := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset") + + if tc.encrypted { + c.Assert(os.MkdirAll(boot.InitramfsSeedEncryptionKeyDir, 0755), IsNil) + if tc.factoryKeyAlreadyMigrated { + c.Assert(ioutil.WriteFile(saveSealedKey, []byte{'o', 'l', 'd'}, 0644), IsNil) + c.Assert(ioutil.WriteFile(saveSealedKeyByFactoryReset, []byte{'n', 'e', 'w'}, 0644), IsNil) + } else { + c.Assert(ioutil.WriteFile(saveSealedKey, []byte{'n', 'e', 'w'}, 0644), IsNil) + } + } + + restore := boot.MockHasFDESetupHook(func() (bool, error) { + return tc.hasFDEHook, nil + }) + defer restore() + + pcrHandleOfKeyCalls := 0 + restore = boot.MockSecbootPCRHandleOfSealedKey(func(p string) (uint32, error) { + pcrHandleOfKeyCalls++ + // XXX we're inspecting the current key after it got rotated + c.Check(p, Equals, filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + return tc.pcrHandleOfKey, tc.pcrHandleOfKeyErr + }) + defer restore() + + releasePCRHandleCalls := 0 + restore = boot.MockSecbootReleasePCRResourceHandles(func(handles ...uint32) error { + releasePCRHandleCalls++ + if tc.pcrHandleOfKey == secboot.FallbackObjectPCRPolicyCounterHandle { + c.Check(handles, DeepEquals, []uint32{ + secboot.AltRunObjectPCRPolicyCounterHandle, + secboot.AltFallbackObjectPCRPolicyCounterHandle, + }) + } else { + c.Check(handles, DeepEquals, []uint32{ + secboot.RunObjectPCRPolicyCounterHandle, + secboot.FallbackObjectPCRPolicyCounterHandle, + }) + } + return tc.releasePCRHandlesErr + }) + defer restore() + + err := boot.MarkFactoryResetComplete(tc.encrypted) + if tc.err != "" { + c.Assert(err, ErrorMatches, tc.err) + } else { + c.Assert(err, IsNil) + } + c.Check(pcrHandleOfKeyCalls, Equals, tc.pcrHandleOfKeyCalls) + c.Check(releasePCRHandleCalls, Equals, tc.releasePCRHandleCalls) + if tc.encrypted { + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + testutil.FileEquals, []byte{'n', 'e', 'w'}) + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"), + testutil.FileAbsent) + } + } + +} diff -Nru snapd-2.55.5+20.04/boot/systems.go snapd-2.57.5+20.04/boot/systems.go --- snapd-2.55.5+20.04/boot/systems.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/boot/systems.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2021 Canonical Ltd + * Copyright (C) 2022 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 @@ -44,7 +44,7 @@ // variables state is inconsistent. func ClearTryRecoverySystem(dev snap.Device, systemLabel string) error { if !dev.HasModeenv() { - return fmt.Errorf("internal error: recovery systems can only be used on UC20") + return fmt.Errorf("internal error: recovery systems can only be used on UC20+") } m, err := loadModeenv() @@ -105,7 +105,7 @@ // caller should request switching to the given recovery system. func SetTryRecoverySystem(dev snap.Device, systemLabel string) (err error) { if !dev.HasModeenv() { - return fmt.Errorf("internal error: recovery systems can only be used on UC20") + return fmt.Errorf("internal error: recovery systems can only be used on UC20+") } m, err := loadModeenv() @@ -380,7 +380,7 @@ // attempt to restore the previous state is made func PromoteTriedRecoverySystem(dev snap.Device, systemLabel string, triedSystems []string) (err error) { if !dev.HasModeenv() { - return fmt.Errorf("internal error: recovery systems can only be used on UC20") + return fmt.Errorf("internal error: recovery systems can only be used on UC20+") } if !strutil.ListContains(triedSystems, systemLabel) { @@ -422,7 +422,7 @@ // this call *DOES NOT* clear the boot environment variables. func DropRecoverySystem(dev snap.Device, systemLabel string) error { if !dev.HasModeenv() { - return fmt.Errorf("internal error: recovery systems can only be used on UC20") + return fmt.Errorf("internal error: recovery systems can only be used on UC20+") } m, err := loadModeenv() diff -Nru snapd-2.55.5+20.04/bootloader/assets/data/grub.cfg snapd-2.57.5+20.04/bootloader/assets/data/grub.cfg --- snapd-2.55.5+20.04/bootloader/assets/data/grub.cfg 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/assets/data/grub.cfg 2022-10-17 16:25:18.000000000 +0000 @@ -1,4 +1,4 @@ -# Snapd-Boot-Config-Edition: 1 +# Snapd-Boot-Config-Edition: 3 set default=0 set timeout=3 @@ -10,7 +10,7 @@ set snapd_static_cmdline_args='panic=-1' if [ "$grub_cpu" = "x86_64" ]; then - set snapd_static_cmdline_args='console=ttyS0 console=tty1 panic=-1' + set snapd_static_cmdline_args='console=ttyS0,115200n8 console=tty1 panic=-1' fi set cmdline_args="$snapd_static_cmdline_args $snapd_extra_cmdline_args" if [ -n "$snapd_full_cmdline_args" ]; then @@ -23,6 +23,8 @@ # a new kernel got installed set kernel_status="trying" save_env kernel_status + # run fallback (menu entry #1) if we cannot start the kernel + set fallback=1 # use try-kernel.efi set kernel=try-kernel.efi @@ -39,14 +41,17 @@ save_env kernel_status fi -if [ -e $prefix/$kernel ]; then menuentry "Run Ubuntu Core 20" { # use $prefix because the symlink manipulation at runtime for kernel snap # upgrades, etc. should only need the /boot/grub/ directory, not the # /EFI/ubuntu/ directory chainloader $prefix/$kernel snapd_recovery_mode=run $cmdline_args } -else - # nothing to boot :-/ - echo "missing kernel at $prefix/$kernel!" -fi +menuentry "Fallback on failed update" { + # kernel_status has already been set to "trying", rebooting now + # will fail the pending kernel update. Note that we cannot simply + # chainload the fallback kernel as TPM measurements need to be + # cleaned-up to be able to unseal the key. + echo "Cannot start new kernel - booting previous one" + reboot +} diff -Nru snapd-2.55.5+20.04/bootloader/assets/data/grub-recovery.cfg snapd-2.57.5+20.04/bootloader/assets/data/grub-recovery.cfg --- snapd-2.55.5+20.04/bootloader/assets/data/grub-recovery.cfg 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/assets/data/grub-recovery.cfg 2022-10-17 16:25:18.000000000 +0000 @@ -72,6 +72,10 @@ loopback loop $2 chainloader (loop)/kernel.efi snapd_recovery_mode=$3 snapd_recovery_system=$4 $cmdline_args } + menuentry "Factory reset using $label" --hotkey=i --id=factory-reset-$label $snapd_recovery_kernel factory-reset $label { + loopback loop $2 + chainloader (loop)/kernel.efi snapd_recovery_mode=$3 snapd_recovery_system=$4 $cmdline_args + } done menuentry 'UEFI Firmware Settings' --hotkey=f 'uefi-firmware' { diff -Nru snapd-2.55.5+20.04/bootloader/assets/grub_cfg_asset.go snapd-2.57.5+20.04/bootloader/assets/grub_cfg_asset.go --- snapd-2.55.5+20.04/bootloader/assets/grub_cfg_asset.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/assets/grub_cfg_asset.go 2022-10-17 16:25:18.000000000 +0000 @@ -24,7 +24,7 @@ func init() { registerInternal("grub.cfg", []byte{ 0x23, 0x20, 0x53, 0x6e, 0x61, 0x70, 0x64, 0x2d, 0x42, 0x6f, 0x6f, 0x74, 0x2d, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x2d, 0x45, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x31, 0x0a, 0x0a, + 0x66, 0x69, 0x67, 0x2d, 0x45, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x33, 0x0a, 0x0a, 0x73, 0x65, 0x74, 0x20, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x3d, 0x30, 0x0a, 0x73, 0x65, 0x74, 0x20, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x3d, 0x33, 0x0a, 0x73, 0x65, 0x74, 0x20, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x74, 0x79, 0x6c, 0x65, 0x3d, 0x68, 0x69, @@ -48,81 +48,103 @@ 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x5f, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x3d, 0x27, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x3d, 0x74, 0x74, - 0x79, 0x53, 0x30, 0x20, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x3d, 0x74, 0x74, 0x79, 0x31, - 0x20, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x3d, 0x2d, 0x31, 0x27, 0x0a, 0x66, 0x69, 0x0a, 0x73, 0x65, + 0x79, 0x53, 0x30, 0x2c, 0x31, 0x31, 0x35, 0x32, 0x30, 0x30, 0x6e, 0x38, 0x20, 0x63, 0x6f, 0x6e, + 0x73, 0x6f, 0x6c, 0x65, 0x3d, 0x74, 0x74, 0x79, 0x31, 0x20, 0x70, 0x61, 0x6e, 0x69, 0x63, 0x3d, + 0x2d, 0x31, 0x27, 0x0a, 0x66, 0x69, 0x0a, 0x73, 0x65, 0x74, 0x20, 0x63, 0x6d, 0x64, 0x6c, 0x69, + 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x3d, 0x22, 0x24, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x5f, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, + 0x72, 0x67, 0x73, 0x20, 0x24, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x65, 0x78, 0x74, 0x72, 0x61, + 0x5f, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x22, 0x0a, 0x69, + 0x66, 0x20, 0x5b, 0x20, 0x2d, 0x6e, 0x20, 0x22, 0x24, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x66, + 0x75, 0x6c, 0x6c, 0x5f, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, + 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x3d, 0x22, - 0x24, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x5f, 0x63, 0x6d, - 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x20, 0x24, 0x73, 0x6e, 0x61, 0x70, - 0x64, 0x5f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, - 0x61, 0x72, 0x67, 0x73, 0x22, 0x0a, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x2d, 0x6e, 0x20, 0x22, 0x24, - 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x63, 0x6d, 0x64, 0x6c, 0x69, - 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, - 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, - 0x5f, 0x61, 0x72, 0x67, 0x73, 0x3d, 0x22, 0x24, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x66, 0x75, - 0x6c, 0x6c, 0x5f, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x22, - 0x0a, 0x66, 0x69, 0x0a, 0x0a, 0x73, 0x65, 0x74, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x3d, - 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x2e, 0x65, 0x66, 0x69, 0x0a, 0x0a, 0x69, 0x66, 0x20, 0x5b, - 0x20, 0x22, 0x24, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x22, 0x20, 0x3d, 0x20, 0x22, 0x74, 0x72, 0x79, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, - 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x61, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, - 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x67, 0x6f, 0x74, 0x20, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, - 0x65, 0x64, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, - 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3d, 0x22, 0x74, 0x72, 0x79, 0x69, 0x6e, 0x67, - 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x61, 0x76, 0x65, 0x5f, 0x65, 0x6e, 0x76, 0x20, 0x6b, - 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x0a, 0x0a, 0x20, 0x20, - 0x20, 0x20, 0x23, 0x20, 0x75, 0x73, 0x65, 0x20, 0x74, 0x72, 0x79, 0x2d, 0x6b, 0x65, 0x72, 0x6e, - 0x65, 0x6c, 0x2e, 0x65, 0x66, 0x69, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6b, - 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x3d, 0x74, 0x72, 0x79, 0x2d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, - 0x2e, 0x65, 0x66, 0x69, 0x0a, 0x65, 0x6c, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x22, 0x24, 0x6b, 0x65, - 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x20, 0x3d, 0x20, 0x22, - 0x74, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, - 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x6e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x63, 0x6c, - 0x65, 0x61, 0x72, 0x65, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x22, 0x74, 0x72, 0x79, 0x69, 0x6e, - 0x67, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x22, 0x20, 0x73, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, - 0x6f, 0x6f, 0x74, 0x20, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, - 0x20, 0x77, 0x65, 0x20, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6d, 0x6f, - 0x64, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x20, 0x6e, 0x6f, 0x72, 0x6d, - 0x61, 0x6c, 0x6c, 0x79, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6b, 0x65, 0x72, - 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3d, 0x22, 0x22, 0x0a, 0x20, 0x20, - 0x20, 0x20, 0x73, 0x61, 0x76, 0x65, 0x5f, 0x65, 0x6e, 0x76, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, - 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x0a, 0x65, 0x6c, 0x69, 0x66, 0x20, 0x5b, 0x20, - 0x2d, 0x6e, 0x20, 0x22, 0x24, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x20, - 0x23, 0x20, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x20, 0x69, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, - 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x2c, 0x20, 0x72, 0x65, 0x73, 0x65, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x6d, - 0x70, 0x74, 0x79, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x63, 0x68, 0x6f, 0x20, 0x22, 0x69, 0x6e, - 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x21, 0x21, 0x21, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x63, 0x68, 0x6f, - 0x20, 0x22, 0x72, 0x65, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x74, 0x6f, 0x20, 0x65, - 0x6d, 0x70, 0x74, 0x79, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6b, 0x65, - 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3d, 0x22, 0x22, 0x0a, 0x20, - 0x20, 0x20, 0x20, 0x73, 0x61, 0x76, 0x65, 0x5f, 0x65, 0x6e, 0x76, 0x20, 0x6b, 0x65, 0x72, 0x6e, - 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x0a, 0x66, 0x69, 0x0a, 0x0a, 0x69, 0x66, - 0x20, 0x5b, 0x20, 0x2d, 0x65, 0x20, 0x24, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x2f, 0x24, 0x6b, - 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x6d, 0x65, - 0x6e, 0x75, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x22, 0x52, 0x75, 0x6e, 0x20, 0x55, 0x62, 0x75, - 0x6e, 0x74, 0x75, 0x20, 0x43, 0x6f, 0x72, 0x65, 0x20, 0x32, 0x30, 0x22, 0x20, 0x7b, 0x0a, 0x20, - 0x20, 0x20, 0x20, 0x23, 0x20, 0x75, 0x73, 0x65, 0x20, 0x24, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x20, 0x62, 0x65, 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x79, 0x6d, - 0x6c, 0x69, 0x6e, 0x6b, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x70, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x20, 0x61, 0x74, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x20, 0x66, 0x6f, 0x72, - 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x0a, 0x20, 0x20, 0x20, - 0x20, 0x23, 0x20, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x73, 0x2c, 0x20, 0x65, 0x74, 0x63, - 0x2e, 0x20, 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x6e, 0x65, - 0x65, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x67, 0x72, 0x75, - 0x62, 0x2f, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x2c, 0x20, 0x6e, 0x6f, - 0x74, 0x20, 0x74, 0x68, 0x65, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x2f, 0x45, 0x46, 0x49, - 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x2f, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x79, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x61, 0x64, - 0x65, 0x72, 0x20, 0x24, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x2f, 0x24, 0x6b, 0x65, 0x72, 0x6e, - 0x65, 0x6c, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, - 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x72, 0x75, 0x6e, 0x20, 0x24, 0x63, 0x6d, 0x64, 0x6c, - 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x0a, 0x7d, 0x0a, 0x65, 0x6c, 0x73, 0x65, 0x0a, - 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x6e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x74, 0x6f, - 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x20, 0x3a, 0x2d, 0x2f, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x63, - 0x68, 0x6f, 0x20, 0x22, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x20, 0x6b, 0x65, 0x72, 0x6e, - 0x65, 0x6c, 0x20, 0x61, 0x74, 0x20, 0x24, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x2f, 0x24, 0x6b, - 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x21, 0x22, 0x0a, 0x66, 0x69, 0x0a, + 0x24, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x63, 0x6d, 0x64, 0x6c, + 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x22, 0x0a, 0x66, 0x69, 0x0a, 0x0a, 0x73, 0x65, + 0x74, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x3d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x2e, + 0x65, 0x66, 0x69, 0x0a, 0x0a, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x22, 0x24, 0x6b, 0x65, 0x72, 0x6e, + 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x20, 0x3d, 0x20, 0x22, 0x74, 0x72, + 0x79, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, + 0x20, 0x61, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x67, 0x6f, + 0x74, 0x20, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x73, 0x65, 0x74, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x3d, 0x22, 0x74, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, + 0x61, 0x76, 0x65, 0x5f, 0x65, 0x6e, 0x76, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x72, 0x75, 0x6e, 0x20, + 0x66, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x20, 0x28, 0x6d, 0x65, 0x6e, 0x75, 0x20, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x20, 0x23, 0x31, 0x29, 0x20, 0x69, 0x66, 0x20, 0x77, 0x65, 0x20, 0x63, + 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x66, + 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x3d, 0x31, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, + 0x20, 0x75, 0x73, 0x65, 0x20, 0x74, 0x72, 0x79, 0x2d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x2e, + 0x65, 0x66, 0x69, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6b, 0x65, 0x72, 0x6e, + 0x65, 0x6c, 0x3d, 0x74, 0x72, 0x79, 0x2d, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x2e, 0x65, 0x66, + 0x69, 0x0a, 0x65, 0x6c, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x22, 0x24, 0x6b, 0x65, 0x72, 0x6e, 0x65, + 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x20, 0x3d, 0x20, 0x22, 0x74, 0x72, 0x79, + 0x69, 0x6e, 0x67, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x23, 0x20, 0x6e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x20, 0x63, 0x6c, 0x65, 0x61, 0x72, + 0x65, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x22, 0x74, 0x72, 0x79, 0x69, 0x6e, 0x67, 0x20, 0x73, + 0x6e, 0x61, 0x70, 0x22, 0x20, 0x73, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x6f, 0x6f, 0x74, + 0x20, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x77, 0x65, + 0x20, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x20, + 0x61, 0x6e, 0x64, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x20, 0x6e, 0x6f, 0x72, 0x6d, 0x61, 0x6c, 0x6c, + 0x79, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, + 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3d, 0x22, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, + 0x61, 0x76, 0x65, 0x5f, 0x65, 0x6e, 0x76, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x0a, 0x65, 0x6c, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x2d, 0x6e, 0x20, + 0x22, 0x24, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x20, 0x69, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x6b, 0x65, 0x72, + 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x2c, 0x20, 0x72, 0x65, 0x73, 0x65, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x63, 0x68, 0x6f, 0x20, 0x22, 0x69, 0x6e, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x21, 0x21, 0x21, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x65, 0x63, 0x68, 0x6f, 0x20, 0x22, 0x72, + 0x65, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x74, 0x6f, 0x20, 0x65, 0x6d, 0x70, 0x74, + 0x79, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, + 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3d, 0x22, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x73, 0x61, 0x76, 0x65, 0x5f, 0x65, 0x6e, 0x76, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x0a, 0x66, 0x69, 0x0a, 0x0a, 0x6d, 0x65, 0x6e, 0x75, 0x65, + 0x6e, 0x74, 0x72, 0x79, 0x20, 0x22, 0x52, 0x75, 0x6e, 0x20, 0x55, 0x62, 0x75, 0x6e, 0x74, 0x75, + 0x20, 0x43, 0x6f, 0x72, 0x65, 0x20, 0x32, 0x30, 0x22, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x23, 0x20, 0x75, 0x73, 0x65, 0x20, 0x24, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x20, 0x62, 0x65, + 0x63, 0x61, 0x75, 0x73, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x79, 0x6d, 0x6c, 0x69, 0x6e, + 0x6b, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x70, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, + 0x74, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x20, 0x6b, 0x65, + 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, + 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x73, 0x2c, 0x20, 0x65, 0x74, 0x63, 0x2e, 0x20, 0x73, + 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x6e, 0x65, 0x65, 0x64, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x67, 0x72, 0x75, 0x62, 0x2f, 0x20, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x2c, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x74, + 0x68, 0x65, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x2f, 0x45, 0x46, 0x49, 0x2f, 0x75, 0x62, + 0x75, 0x6e, 0x74, 0x75, 0x2f, 0x20, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x72, 0x20, + 0x24, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x2f, 0x24, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, + 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x3d, 0x72, 0x75, 0x6e, 0x20, 0x24, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, + 0x5f, 0x61, 0x72, 0x67, 0x73, 0x0a, 0x7d, 0x0a, 0x6d, 0x65, 0x6e, 0x75, 0x65, 0x6e, 0x74, 0x72, + 0x79, 0x20, 0x22, 0x46, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x20, 0x6f, 0x6e, 0x20, 0x66, + 0x61, 0x69, 0x6c, 0x65, 0x64, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x20, 0x7b, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x20, 0x68, 0x61, 0x73, 0x20, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x20, + 0x62, 0x65, 0x65, 0x6e, 0x20, 0x73, 0x65, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x22, 0x74, 0x72, 0x79, + 0x69, 0x6e, 0x67, 0x22, 0x2c, 0x20, 0x72, 0x65, 0x62, 0x6f, 0x6f, 0x74, 0x69, 0x6e, 0x67, 0x20, + 0x6e, 0x6f, 0x77, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x66, + 0x61, 0x69, 0x6c, 0x20, 0x74, 0x68, 0x65, 0x20, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x20, + 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x20, 0x4e, + 0x6f, 0x74, 0x65, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x77, 0x65, 0x20, 0x63, 0x61, 0x6e, 0x6e, + 0x6f, 0x74, 0x20, 0x73, 0x69, 0x6d, 0x70, 0x6c, 0x79, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, + 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x66, 0x61, + 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x61, 0x73, + 0x20, 0x54, 0x50, 0x4d, 0x20, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x20, 0x6e, 0x65, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x23, 0x20, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x2d, 0x75, 0x70, 0x20, 0x74, 0x6f, + 0x20, 0x62, 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x75, 0x6e, 0x73, 0x65, + 0x61, 0x6c, 0x20, 0x74, 0x68, 0x65, 0x20, 0x6b, 0x65, 0x79, 0x2e, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x65, 0x63, 0x68, 0x6f, 0x20, 0x22, 0x43, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x20, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x20, 0x6e, 0x65, 0x77, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x2d, 0x20, + 0x62, 0x6f, 0x6f, 0x74, 0x69, 0x6e, 0x67, 0x20, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, + 0x20, 0x6f, 0x6e, 0x65, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x62, 0x6f, 0x6f, 0x74, + 0x0a, 0x7d, 0x0a, }) } diff -Nru snapd-2.55.5+20.04/bootloader/assets/grub.go snapd-2.57.5+20.04/bootloader/assets/grub.go --- snapd-2.55.5+20.04/bootloader/assets/grub.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/assets/grub.go 2022-10-17 16:25:18.000000000 +0000 @@ -24,9 +24,17 @@ ) var cmdlineForArch = map[string][]ForEditions{ - "amd64": {{FirstEdition: 1, Snippet: []byte("console=ttyS0 console=tty1 panic=-1")}}, - "arm64": {{FirstEdition: 1, Snippet: []byte("panic=-1")}}, - "i386": {{FirstEdition: 1, Snippet: []byte("console=ttyS0 console=tty1 panic=-1")}}, + "amd64": { + {FirstEdition: 1, Snippet: []byte("console=ttyS0 console=tty1 panic=-1")}, + {FirstEdition: 3, Snippet: []byte("console=ttyS0,115200n8 console=tty1 panic=-1")}, + }, + "arm64": { + {FirstEdition: 1, Snippet: []byte("panic=-1")}, + }, + "i386": { + {FirstEdition: 1, Snippet: []byte("console=ttyS0 console=tty1 panic=-1")}, + {FirstEdition: 3, Snippet: []byte("console=ttyS0,115200n8 console=tty1 panic=-1")}, + }, } func registerGrubSnippets() { diff -Nru snapd-2.55.5+20.04/bootloader/assets/grub_recovery_cfg_asset.go snapd-2.57.5+20.04/bootloader/assets/grub_recovery_cfg_asset.go --- snapd-2.55.5+20.04/bootloader/assets/grub_recovery_cfg_asset.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/assets/grub_recovery_cfg_asset.go 2022-10-17 16:25:18.000000000 +0000 @@ -181,11 +181,27 @@ 0x72, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x24, 0x33, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x3d, 0x24, 0x34, 0x20, 0x24, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, - 0x73, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x64, 0x6f, 0x6e, 0x65, 0x0a, 0x0a, 0x6d, 0x65, - 0x6e, 0x75, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x27, 0x55, 0x45, 0x46, 0x49, 0x20, 0x46, 0x69, - 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x20, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x27, - 0x20, 0x2d, 0x2d, 0x68, 0x6f, 0x74, 0x6b, 0x65, 0x79, 0x3d, 0x66, 0x20, 0x27, 0x75, 0x65, 0x66, - 0x69, 0x2d, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x27, 0x20, 0x7b, 0x0a, 0x20, 0x20, - 0x20, 0x20, 0x66, 0x77, 0x73, 0x65, 0x74, 0x75, 0x70, 0x0a, 0x7d, 0x0a, + 0x73, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x65, 0x6e, 0x75, + 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x22, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x20, 0x72, + 0x65, 0x73, 0x65, 0x74, 0x20, 0x75, 0x73, 0x69, 0x6e, 0x67, 0x20, 0x24, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x22, 0x20, 0x2d, 0x2d, 0x68, 0x6f, 0x74, 0x6b, 0x65, 0x79, 0x3d, 0x69, 0x20, 0x2d, 0x2d, + 0x69, 0x64, 0x3d, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x2d, 0x72, 0x65, 0x73, 0x65, 0x74, + 0x2d, 0x24, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x20, 0x24, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, + 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x20, 0x66, + 0x61, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x2d, 0x72, 0x65, 0x73, 0x65, 0x74, 0x20, 0x24, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6c, 0x6f, + 0x6f, 0x70, 0x62, 0x61, 0x63, 0x6b, 0x20, 0x6c, 0x6f, 0x6f, 0x70, 0x20, 0x24, 0x32, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x61, 0x64, + 0x65, 0x72, 0x20, 0x28, 0x6c, 0x6f, 0x6f, 0x70, 0x29, 0x2f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, + 0x2e, 0x65, 0x66, 0x69, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, + 0x65, 0x72, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x24, 0x33, 0x20, 0x73, 0x6e, 0x61, 0x70, + 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, + 0x6d, 0x3d, 0x24, 0x34, 0x20, 0x24, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, + 0x67, 0x73, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x64, 0x6f, 0x6e, 0x65, 0x0a, 0x0a, 0x6d, + 0x65, 0x6e, 0x75, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x27, 0x55, 0x45, 0x46, 0x49, 0x20, 0x46, + 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x20, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x27, 0x20, 0x2d, 0x2d, 0x68, 0x6f, 0x74, 0x6b, 0x65, 0x79, 0x3d, 0x66, 0x20, 0x27, 0x75, 0x65, + 0x66, 0x69, 0x2d, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x27, 0x20, 0x7b, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x66, 0x77, 0x73, 0x65, 0x74, 0x75, 0x70, 0x0a, 0x7d, 0x0a, }) } diff -Nru snapd-2.55.5+20.04/bootloader/assets/grub_test.go snapd-2.57.5+20.04/bootloader/assets/grub_test.go --- snapd-2.55.5+20.04/bootloader/assets/grub_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/assets/grub_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -49,12 +49,13 @@ s.AddCleanup(archtest.MockArchitecture("amd64")) snippets := []assets.ForEditions{ {FirstEdition: 1, Snippet: []byte("console=ttyS0 console=tty1 panic=-1")}, + {FirstEdition: 3, Snippet: []byte("console=ttyS0,115200n8 console=tty1 panic=-1")}, } s.AddCleanup(assets.MockSnippetsForEdition("grub.cfg:static-cmdline", snippets)) s.AddCleanup(assets.MockSnippetsForEdition("grub-recovery.cfg:static-cmdline", snippets)) } -func (s *grubAssetsTestSuite) testGrubConfigContains(c *C, name string, keys ...string) { +func (s *grubAssetsTestSuite) testGrubConfigContains(c *C, name string, edition int, keys ...string) { a := assets.Internal(name) c.Assert(a, NotNil) as := string(a) @@ -63,18 +64,19 @@ } idx := bytes.IndexRune(a, '\n') c.Assert(idx, Not(Equals), -1) - c.Assert(string(a[:idx]), Equals, "# Snapd-Boot-Config-Edition: 1") + prefix := fmt.Sprintf("# Snapd-Boot-Config-Edition: %d", edition) + c.Assert(string(a[:idx]), Equals, prefix) } func (s *grubAssetsTestSuite) TestGrubConf(c *C) { - s.testGrubConfigContains(c, "grub.cfg", + s.testGrubConfigContains(c, "grub.cfg", 3, "snapd_recovery_mode", - "set snapd_static_cmdline_args='console=ttyS0 console=tty1 panic=-1'", + "set snapd_static_cmdline_args='console=ttyS0,115200n8 console=tty1 panic=-1'", ) } func (s *grubAssetsTestSuite) TestGrubRecoveryConf(c *C) { - s.testGrubConfigContains(c, "grub-recovery.cfg", + s.testGrubConfigContains(c, "grub-recovery.cfg", 1, "snapd_recovery_mode", "snapd_recovery_system", "set snapd_static_cmdline_args='console=ttyS0 console=tty1 panic=-1'", @@ -126,8 +128,8 @@ pattern string }{ { - asset: "grub.cfg", snippet: "grub.cfg:static-cmdline", edition: 1, - content: []byte("console=ttyS0 console=tty1 panic=-1"), + asset: "grub.cfg", snippet: "grub.cfg:static-cmdline", edition: 3, + content: []byte("console=ttyS0,115200n8 console=tty1 panic=-1"), pattern: "set snapd_static_cmdline_args='%s'\n", }, { @@ -143,7 +145,7 @@ // get a matching snippet snip := assets.SnippetForEdition(tc.snippet, tc.edition) c.Assert(snip, NotNil) - c.Assert(snip, DeepEquals, tc.content) + c.Assert(snip, DeepEquals, tc.content, Commentf("%s: '%s' != '%s'", tc.asset, snip, tc.content)) c.Assert(string(grubCfg), testutil.Contains, fmt.Sprintf(tc.pattern, string(snip))) } } diff -Nru snapd-2.55.5+20.04/bootloader/asset_test.go snapd-2.57.5+20.04/bootloader/asset_test.go --- snapd-2.55.5+20.04/bootloader/asset_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/asset_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -74,6 +74,14 @@ c.Assert(grubConfig, NotNil) e, err := bootloader.EditionFromConfigAsset(bytes.NewReader(grubConfig)) c.Assert(err, IsNil) + c.Assert(e, Equals, uint(3)) +} + +func (s *configAssetTestSuite) TestRealRecoveryConfig(c *C) { + grubRecoveryConfig := assets.Internal("grub-recovery.cfg") + c.Assert(grubRecoveryConfig, NotNil) + e, err := bootloader.EditionFromConfigAsset(bytes.NewReader(grubRecoveryConfig)) + c.Assert(err, IsNil) c.Assert(e, Equals, uint(1)) } diff -Nru snapd-2.55.5+20.04/bootloader/lkenv/lkenv.go snapd-2.57.5+20.04/bootloader/lkenv/lkenv.go --- snapd-2.55.5+20.04/bootloader/lkenv/lkenv.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/lkenv/lkenv.go 2022-10-17 16:25:18.000000000 +0000 @@ -553,7 +553,7 @@ // dropBootPartValue will remove the specified bootPartValue from the boot image // matrix - it _only_ deletes the value, not the boot image partition label -// itself, , as the boot image partition labels are static for the lifetime of a +// itself, as the boot image partition labels are static for the lifetime of a // device and should never be changed (as those values correspond to physical // names of the formatted partitions and we don't yet support repartitioning of // any kind). diff -Nru snapd-2.55.5+20.04/bootloader/lk.go snapd-2.57.5+20.04/bootloader/lk.go --- snapd-2.55.5+20.04/bootloader/lk.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/lk.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019 Canonical Ltd + * Copyright (C) 2022 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 @@ -106,7 +106,7 @@ return filepath.Join(l.rootdir, "/dev/disk/by-partlabel/") case RoleRecovery, RoleRunMode: // TODO: maybe panic'ing here is a bit harsh... - panic("internal error: shouldn't be using lk.dir() for uc20 runtime modes!") + panic("internal error: shouldn't be using lk.dir() for UC20+ runtime modes!") default: panic("unexpected bootloader role for lk dir") } diff -Nru snapd-2.55.5+20.04/bootloader/piboot.go snapd-2.57.5+20.04/bootloader/piboot.go --- snapd-2.55.5+20.04/bootloader/piboot.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/piboot.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,7 +34,7 @@ "github.com/snapcore/snapd/snap" ) -// sanity - piboot implements the required interfaces +// ensure piboot implements the required interfaces var ( _ Bootloader = (*piboot)(nil) _ ExtractedRecoveryKernelImageBootloader = (*piboot)(nil) diff -Nru snapd-2.55.5+20.04/bootloader/uboot.go snapd-2.57.5+20.04/bootloader/uboot.go --- snapd-2.55.5+20.04/bootloader/uboot.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/bootloader/uboot.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2015 Canonical Ltd + * Copyright (C) 2014-2022 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 @@ -131,7 +131,7 @@ if blOpts != nil && blOpts.Role == RoleRecovery { // not supported yet, this is traditional uboot.env from gadget // TODO:UC20: support this use-case - return fmt.Errorf("non-empty uboot.env not supported on UC20 yet") + return fmt.Errorf("non-empty uboot.env not supported on UC20+ yet") } systemFile := u.envFile() diff -Nru snapd-2.55.5+20.04/build-aux/snap/snapcraft.yaml snapd-2.57.5+20.04/build-aux/snap/snapcraft.yaml --- snapd-2.55.5+20.04/build-aux/snap/snapcraft.yaml 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/build-aux/snap/snapcraft.yaml 2022-10-17 16:25:18.000000000 +0000 @@ -12,6 +12,9 @@ # build-base is needed here for snapcraft to build this snap as with "modern" # snapcraft build-base: core +package-repositories: + - type: apt + ppa: snappy-dev/image grade: stable license: GPL-3.0 @@ -77,6 +80,8 @@ # bother compressing it too much) dpkg-buildpackage -b -Zgzip -zfast dpkg-deb -x $(pwd)/../snapd_*.deb $SNAPCRAFT_PART_INSTALL + # not included in the deb as it's only used with UC20 preseeding. + cp -a data/preseed.json $SNAPCRAFT_PART_INSTALL/usr/lib/snapd/ # xdelta is used to enable delta downloads (even if the host does not have it) xdelta3: diff -Nru snapd-2.55.5+20.04/client/client.go snapd-2.57.5+20.04/client/client.go --- snapd-2.55.5+20.04/client/client.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/client/client.go 2022-10-17 16:25:18.000000000 +0000 @@ -762,10 +762,25 @@ type SystemRecoveryKeysResponse struct { RecoveryKey string `json:"recovery-key"` - ReinstallKey string `json:"reinstall-key"` + ReinstallKey string `json:"reinstall-key,omitempty"` } func (client *Client) SystemRecoveryKeys(result interface{}) error { _, err := client.doSync("GET", "/v2/system-recovery-keys", nil, nil, nil, &result) return err } + +func (c *Client) MigrateSnapHome(snaps []string) (changeID string, err error) { + body, err := json.Marshal(struct { + Action string `json:"action"` + Snaps []string `json:"snaps"` + }{ + Action: "migrate-home", + Snaps: snaps, + }) + if err != nil { + return "", err + } + + return c.doAsync("POST", "/v2/debug", nil, nil, bytes.NewReader(body)) +} diff -Nru snapd-2.55.5+20.04/client/client_test.go snapd-2.57.5+20.04/client/client_test.go --- snapd-2.55.5+20.04/client/client_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/client/client_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -629,6 +629,23 @@ c.Check(cs.reqs[0].URL.Query(), DeepEquals, url.Values{"aspect": []string{"do-something"}, "foo": []string{"bar"}}) } +func (cs *clientSuite) TestDebugMigrateHome(c *C) { + cs.status = 202 + cs.rsp = `{"type": "async", "status-code": 202, "change": "123"}` + + snaps := []string{"foo", "bar"} + changeID, err := cs.cli.MigrateSnapHome(snaps) + c.Check(err, IsNil) + c.Check(changeID, Equals, "123") + + c.Check(cs.reqs, HasLen, 1) + c.Check(cs.reqs[0].Method, Equals, "POST") + c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug") + data, err := ioutil.ReadAll(cs.reqs[0].Body) + c.Assert(err, IsNil) + c.Check(string(data), Equals, `{"action":"migrate-home","snaps":["foo","bar"]}`) +} + type integrationSuite struct{} var _ = Suite(&integrationSuite{}) diff -Nru snapd-2.55.5+20.04/client/clientutil/modelinfo.go snapd-2.57.5+20.04/client/clientutil/modelinfo.go --- snapd-2.55.5+20.04/client/clientutil/modelinfo.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/client/clientutil/modelinfo.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,448 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 clientutil + +import ( + "encoding/json" + "fmt" + "strings" + "text/tabwriter" + "time" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/timeutil" +) + +var ( + // this list is a "nice" "human" "readable" "ordering" of headers to print. + // it also contains both serial and model assertion headers, but we + // follow the same code path for both assertion types and some of the + // headers are shared between the two, so it still works out correctly + niceOrdering = [...]string{ + "architecture", + "base", + "classic", + "display-name", + "gadget", + "kernel", + "revision", + "store", + "system-user-authority", + "timestamp", + "required-snaps", // for uc16 and uc18 models + "snaps", // for uc20 models + "device-key-sha3-384", + "device-key", + } +) + +// ModelAssertJSON is used to represent a model assertion as-is in JSON. +type ModelAssertJSON struct { + Headers map[string]interface{} `json:"headers,omitempty"` + Body string `json:"body,omitempty"` +} + +// ModelFormatter is a helper interface to format special model elements +// like the publisher, which needs additional formatting. The formatting +// varies based on where this code needs to be used, which is why this +// interface is defined. +type ModelFormatter interface { + // LongPublisher returns the publisher as a nicely formatted string. + LongPublisher(storeAccountID string) string + // GetEscapedDash returns either a double dash which is YAML safe, or the + // special unicode dash character. + GetEscapedDash() string +} + +type PrintModelAssertionOptions struct { + // TermWidth is the width of the terminal for the output. This is used to format + // the device keys in a more readable way. + TermWidth int + // AbsTime determines how the timestamps are formatted, if set the timestamp + // will be formatted as RFC3339, otherwise as a human readable time. + AbsTime bool + // Verbose prints additional information about the provided assertion, + // which includes most of the assertion headers. This is implicitly always + // true when printing in JSON. + Verbose bool + // Assertion controls whether the provided assertion will be serialized + // without any prior processing, which means if set, it will serialize + // the entire assertion as-is. + Assertion bool +} + +func fmtTime(t time.Time, abs bool) string { + if abs { + return t.Format(time.RFC3339) + } + return timeutil.Human(t) +} + +func formatInvalidTypeErr(headers ...string) error { + return fmt.Errorf("invalid type for %q header", strings.Join(headers, "/")) +} + +func printVerboseSnapsList(w *tabwriter.Writer, snaps []interface{}) error { + printModes := func(snapName string, members map[string]interface{}) error { + modes, ok := members["modes"] + if !ok { + return nil + } + + modesSlice, ok := modes.([]interface{}) + if !ok { + return formatInvalidTypeErr("snaps", snapName, "modes") + } + + if len(modesSlice) == 0 { + return nil + } + + modeStrSlice := make([]string, 0, len(modesSlice)) + for _, mode := range modesSlice { + modeStr, ok := mode.(string) + if !ok { + return formatInvalidTypeErr("snaps", snapName, "modes") + } + modeStrSlice = append(modeStrSlice, modeStr) + } + modesSliceYamlStr := "[" + strings.Join(modeStrSlice, ", ") + "]" + fmt.Fprintf(w, " modes:\t%s\n", modesSliceYamlStr) + return nil + } + + for _, sn := range snaps { + snMap, ok := sn.(map[string]interface{}) + if !ok { + return formatInvalidTypeErr("snaps") + } + + // Print all the desired keys in the map in a stable, visually + // appealing ordering + // first do snap name, which will always be present since we + // parsed a valid assertion + name := snMap["name"].(string) + fmt.Fprintf(w, " - name:\t%s\n", name) + + // the rest of these may be absent, but they are all still + // simple strings + for _, snKey := range []string{"id", "type", "default-channel", "presence"} { + snValue, ok := snMap[snKey] + if !ok { + continue + } + snStrValue, ok := snValue.(string) + if !ok { + return formatInvalidTypeErr("snaps", snKey) + } + if snStrValue != "" { + fmt.Fprintf(w, " %s:\t%s\n", snKey, snStrValue) + } + } + + // finally handle "modes" which is a list + if err := printModes(name, snMap); err != nil { + return err + } + } + return nil +} + +func printVerboseModelAssertionHeaders(w *tabwriter.Writer, assertion asserts.Assertion, opts PrintModelAssertionOptions) error { + allHeadersMap := assertion.Headers() + for _, headerName := range niceOrdering { + headerValue, ok := allHeadersMap[headerName] + // make sure the header is in the map + if !ok { + continue + } + + // switch on which header it is to handle some special cases + switch headerName { + // list of scalars + case "required-snaps", "system-user-authority": + headerIfaceList, ok := headerValue.([]interface{}) + if !ok { + // system-user-authority can also appear as string + headerString, ok := headerValue.(string) + if ok { + fmt.Fprintf(w, "%s:\t%s\n", headerName, headerString) + continue + } + return formatInvalidTypeErr(headerName) + } + if len(headerIfaceList) == 0 { + continue + } + + fmt.Fprintf(w, "%s:\t\n", headerName) + for _, elem := range headerIfaceList { + headerStringElem, ok := elem.(string) + if !ok { + return formatInvalidTypeErr(headerName) + } + // note we don't wrap these, since for now this is + // specifically just required-snaps and so all of these + // will be snap names which are required to be short + fmt.Fprintf(w, " - %s\n", headerStringElem) + } + + // timestamp needs to be formatted in an identical manner to how fmtTime works + // from timeMixin package in cmd/snap + case "timestamp": + timestamp, ok := headerValue.(string) + if !ok { + return formatInvalidTypeErr(headerName) + } + + // parse the time string as RFC3339, which is what the format is + // always in for assertions + t, err := time.Parse(time.RFC3339, timestamp) + if err != nil { + return err + } + fmt.Fprintf(w, "timestamp:\t%s\n", fmtTime(t, opts.AbsTime)) + + // long string key we don't want to rewrap but can safely handle + // on "reasonable" width terminals + case "device-key-sha3-384": + // also flush the writer before continuing so the previous keys + // don't try to align with this key + w.Flush() + headerString, ok := headerValue.(string) + if !ok { + return formatInvalidTypeErr(headerName) + } + + switch { + case opts.TermWidth > 86: + fmt.Fprintf(w, "device-key-sha3-384: %s\n", headerString) + case opts.TermWidth > 66: + fmt.Fprintln(w, "device-key-sha3-384: |") + strutil.WordWrapPadded(w, []rune(headerString), " ", opts.TermWidth) + } + case "snaps": + // also flush the writer before continuing so the previous keys + // don't try to align with this key + w.Flush() + snapsHeader, ok := headerValue.([]interface{}) + if !ok { + return formatInvalidTypeErr(headerName) + } + if len(snapsHeader) == 0 { + // unexpected why this is an empty list, but just ignore for + // now + continue + } + + fmt.Fprintf(w, "snaps:\n") + if err := printVerboseSnapsList(w, snapsHeader); err != nil { + return err + } + + // long base64 key we can rewrap safely + case "device-key": + headerString, ok := headerValue.(string) + if !ok { + return formatInvalidTypeErr(headerName) + } + // the string value here has newlines inserted as part of the + // raw assertion, but base64 doesn't care about whitespace, so + // it's safe to replace the newlines + headerString = strings.ReplaceAll(headerString, "\n", "") + fmt.Fprintln(w, "device-key: |") + strutil.WordWrapPadded(w, []rune(headerString), " ", opts.TermWidth) + + // The rest of the values should be single strings + default: + headerString, ok := headerValue.(string) + if !ok { + return formatInvalidTypeErr(headerName) + } + fmt.Fprintf(w, "%s:\t%s\n", headerName, headerString) + } + } + return w.Flush() +} + +// PrintModelAssertion will format the provided serial or model assertion based on the parameters given in +// YAML format, or serialize it raw if Assertion is set. The output will be written to the provided io.Writer. +func PrintModelAssertion(w *tabwriter.Writer, modelAssertion asserts.Model, serialAssertion *asserts.Serial, modelFormatter ModelFormatter, opts PrintModelAssertionOptions) error { + // if assertion was requested we want it raw + if opts.Assertion { + _, err := w.Write(asserts.Encode(&modelAssertion)) + return err + } + + // the rest of this function is the main flow for outputting either the + // model or serial assertion in normal or verbose mode + + // for the `snap model` case with no options, we don't want colons, we want + // to be like `snap version` + separator := ":" + if !opts.Verbose { + separator = "" + } + + // ordering of the primary keys for model: brand, model, serial + brandIDHeader := modelAssertion.HeaderString("brand-id") + modelHeader := modelAssertion.HeaderString("model") + + // for the serial header, if there's no serial yet, it's not an error for + // model (and we already handled the serial error above) but need to add a + // parenthetical about the device not being registered yet + var serial string + if serialAssertion == nil { + if opts.Verbose { + // verbose and serial are yamlish, so we need to escape the dash + serial = modelFormatter.GetEscapedDash() + } else { + serial = "-" + } + serial += " (device not registered yet)" + } else { + serial = serialAssertion.HeaderString("serial") + } + + // handle brand/brand-id and model/model + display-name differently on just + // `snap model` w/o opts + if opts.Verbose { + fmt.Fprintf(w, "brand-id:\t%s\n", brandIDHeader) + fmt.Fprintf(w, "model:\t%s\n", modelHeader) + } else { + publisher := modelFormatter.LongPublisher(brandIDHeader) + + // use the longPublisher helper to format the brand store account + // like we do in `snap info` + fmt.Fprintf(w, "brand%s\t%s\n", separator, publisher) + + // for model, if there's a display-name, we show that first with the + // real model in parenthesis + if displayName := modelAssertion.HeaderString("display-name"); displayName != "" { + modelHeader = fmt.Sprintf("%s (%s)", displayName, modelHeader) + } + fmt.Fprintf(w, "model%s\t%s\n", separator, modelHeader) + } + + grade := modelAssertion.HeaderString("grade") + if grade != "" { + fmt.Fprintf(w, "grade%s\t%s\n", separator, grade) + } + + storageSafety := modelAssertion.HeaderString("storage-safety") + if storageSafety != "" { + fmt.Fprintf(w, "storage-safety%s\t%s\n", separator, storageSafety) + } + + fmt.Fprintf(w, "serial%s\t%s\n", separator, serial) + + if opts.Verbose { + if err := printVerboseModelAssertionHeaders(w, &modelAssertion, opts); err != nil { + return err + } + } + return w.Flush() +} + +// PrintModelAssertionYAML will format the provided serial or model assertion based on the parameters given in +// YAML format. The output will be written to the provided io.Writer. +func PrintSerialAssertionYAML(w *tabwriter.Writer, serialAssertion asserts.Serial, modelFormatter ModelFormatter, opts PrintModelAssertionOptions) error { + // if assertion was requested we want it raw + if opts.Assertion { + _, err := w.Write(asserts.Encode(&serialAssertion)) + return err + } + + // the rest of this function is the main flow for outputting either the + // serial assertion in normal or verbose mode + + // ordering of primary keys for serial is brand-id, model, serial + brandIDHeader := serialAssertion.HeaderString("brand-id") + modelHeader := serialAssertion.HeaderString("model") + serial := serialAssertion.HeaderString("serial") + + fmt.Fprintf(w, "brand-id:\t%s\n", brandIDHeader) + fmt.Fprintf(w, "model:\t%s\n", modelHeader) + fmt.Fprintf(w, "serial:\t%s\n", serial) + + if opts.Verbose { + if err := printVerboseModelAssertionHeaders(w, &serialAssertion, opts); err != nil { + return err + } + } + return w.Flush() +} + +// PrintModelAssertionJSON will format the provided serial or model assertion based on the parameters given in +// JSON format. The output will be written to the provided io.Writer. +func PrintModelAssertionJSON(w *tabwriter.Writer, modelAssertion asserts.Model, serialAssertion *asserts.Serial, opts PrintModelAssertionOptions) error { + serializeJSON := func(v interface{}) error { + marshalled, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + + _, err = w.Write(marshalled) + if err != nil { + return err + } + return w.Flush() + } + + if opts.Assertion { + modelJSON := ModelAssertJSON{} + modelJSON.Headers = modelAssertion.Headers() + modelJSON.Body = string(modelAssertion.Body()) + return serializeJSON(modelJSON) + } + + modelData := make(map[string]interface{}) + modelData["brand-id"] = modelAssertion.HeaderString("brand-id") + modelData["model"] = modelAssertion.HeaderString("model") + + grade := modelAssertion.HeaderString("grade") + if grade != "" { + modelData["grade"] = grade + } + + storageSafety := modelAssertion.HeaderString("storage-safety") + if storageSafety != "" { + modelData["storage-safety"] = storageSafety + } + + if serialAssertion != nil { + modelData["serial"] = serialAssertion.HeaderString("serial") + } else { + modelData["serial"] = nil + } + allHeadersMap := modelAssertion.Headers() + + // always print extra information for JSON + for _, headerName := range niceOrdering { + headerValue, ok := allHeadersMap[headerName] + if !ok { + continue + } + modelData[headerName] = headerValue + } + + return serializeJSON(modelData) +} diff -Nru snapd-2.55.5+20.04/client/clientutil/modelinfo_test.go snapd-2.57.5+20.04/client/clientutil/modelinfo_test.go --- snapd-2.55.5+20.04/client/clientutil/modelinfo_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/client/clientutil/modelinfo_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,502 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 clientutil_test + +import ( + "bytes" + "fmt" + "strings" + "text/tabwriter" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/client/clientutil" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/timeutil" +) + +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" + + "base: core18\n" + + "kernel: baz-linux\n" + + "store: brand-store\n" + + "serial-authority:\n - generic\n" + + "system-user-authority: *\n" + + "required-snaps:\n - foo\n - bar\n" + + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + core20ModelExample = `type: model +authority-id: brand-id1 +series: 16 +brand-id: brand-id1 +model: baz-3000 +display-name: Baz 3000 +architecture: amd64 +system-user-authority: + - partner + - brand-id1 +base: core20 +store: brand-store +grade: dangerous +storage-safety: prefer-unencrypted +snaps: + - + name: baz-linux + id: bazlinuxidididididididididididid + type: kernel + default-channel: 20 + - + name: brand-gadget + id: brandgadgetdidididididididididid + type: gadget + - + name: other-base + id: otherbasedididididididididididid + type: base + modes: + - run + presence: required + - + name: nm + id: nmididididididididididididididid + modes: + - ephemeral + - run + default-channel: 1.0 + - + name: myapp + id: myappdididididididididididididid + type: app + default-channel: 2.0 + - + name: myappopt + id: myappoptidididididididididididid + type: app + presence: optional + grade: secured + storage-safety: encrypted +` + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" + + serialExample = "type: serial\n" + + "authority-id: brand-id1\n" + + "brand-id: brand-id1\n" + + "model: baz-3000\n" + + "serial: 2700\n" + + "device-key:\n DEVICEKEY\n" + + "device-key-sha3-384: KEYID\n" + + "TSLINE" + + "body-length: 2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" + + "HW" + + "\n\n" + + "AXNpZw==" +) + +type testFormatter struct { +} + +func (tf testFormatter) GetEscapedDash() string { + return "--" +} + +func (tf testFormatter) LongPublisher(storeAccountID string) string { + return storeAccountID +} + +type modelInfoSuite struct { + testutil.BaseTest + ts time.Time + tsLine string + deviceKey asserts.PrivateKey + encodedDevKey string + formatter testFormatter +} + +var testPrivKey2, _ = assertstest.GenerateKey(752) + +var _ = Suite(&modelInfoSuite{}) + +func (s *modelInfoSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.ts = time.Now().Truncate(time.Second).UTC() + s.tsLine = "timestamp: " + s.ts.Format(time.RFC3339) + "\n" + + s.deviceKey = testPrivKey2 + encodedPubKey, err := asserts.EncodePublicKey(s.deviceKey.PublicKey()) + c.Assert(err, IsNil) + s.encodedDevKey = string(encodedPubKey) +} + +func (s *modelInfoSuite) getModel(c *C, modelText string) asserts.Model { + encoded := strings.Replace(modelText, "TSLINE", s.tsLine, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + return *model +} + +func (s *modelInfoSuite) getDeviceKey(padding string, termWidth int) string { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + key := strings.Join(strings.Split(s.encodedDevKey, "\n"), "") + strutil.WordWrapPadded(w, []rune(key), padding, termWidth) + return buffer.String() +} + +func (s *modelInfoSuite) getSerial(c *C, serialText string) asserts.Serial { + encoded := strings.Replace(serialText, "TSLINE", s.tsLine, 1) + encoded = strings.Replace(encoded, "DEVICEKEY", strings.Replace(s.encodedDevKey, "\n", "\n ", -1), 1) + encoded = strings.Replace(encoded, "KEYID", s.deviceKey.PublicKey().ID(), 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SerialType) + serial := a.(*asserts.Serial) + return *serial +} + +func (s *modelInfoSuite) TestPrintModelYAML(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + model := s.getModel(c, modelExample) + + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: false, + AbsTime: false, + Assertion: false, + } + err := clientutil.PrintModelAssertion(w, model, nil, s.formatter, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, `brand brand-id1 +model Baz 3000 (baz-3000) +serial - (device not registered yet) +`) +} + +func (s *modelInfoSuite) TestPrintModelWithSerialYAML(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + model := s.getModel(c, modelExample) + serial := s.getSerial(c, serialExample) + + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: false, + AbsTime: false, + Assertion: false, + } + err := clientutil.PrintModelAssertion(w, model, &serial, s.formatter, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, `brand brand-id1 +model Baz 3000 (baz-3000) +serial 2700 +`) +} + +func (s *modelInfoSuite) TestPrintModelYAMLVerbose(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + model := s.getModel(c, core20ModelExample) + + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: true, + AbsTime: false, + Assertion: false, + } + err := clientutil.PrintModelAssertion(w, model, nil, s.formatter, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, fmt.Sprintf(`brand-id: brand-id1 +model: baz-3000 +grade: dangerous +storage-safety: prefer-unencrypted +serial: -- (device not registered yet) +architecture: amd64 +base: core20 +display-name: Baz 3000 +store: brand-store +system-user-authority: + - partner + - brand-id1 +timestamp: %s +snaps: + - name: baz-linux + id: bazlinuxidididididididididididid + type: kernel + default-channel: 20 + - name: brand-gadget + id: brandgadgetdidididididididididid + type: gadget + - name: other-base + id: otherbasedididididididididididid + type: base + presence: required + modes: [run] + - name: nm + id: nmididididididididididididididid + default-channel: 1.0 + modes: [ephemeral, run] + - name: myapp + id: myappdididididididididididididid + type: app + default-channel: 2.0 + - name: myappopt + id: myappoptidididididididididididid + type: app + presence: optional +`, timeutil.Human(s.ts))) +} + +func (s *modelInfoSuite) TestPrintModelAssertion(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + model := s.getModel(c, modelExample) + + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: false, + AbsTime: false, + Assertion: true, + } + err := clientutil.PrintModelAssertion(w, model, nil, s.formatter, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, fmt.Sprintf(`type: model +authority-id: brand-id1 +series: 16 +brand-id: brand-id1 +model: baz-3000 +display-name: Baz 3000 +architecture: amd64 +gadget: brand-gadget +base: core18 +kernel: baz-linux +store: brand-store +serial-authority: + - generic +system-user-authority: * +required-snaps: + - foo + - bar +timestamp: %s +body-length: 0 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +`, s.ts.Format(time.RFC3339))) +} + +func (s *modelInfoSuite) TestPrintSerialYAML(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + serial := s.getSerial(c, serialExample) + + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: false, + AbsTime: false, + Assertion: false, + } + err := clientutil.PrintSerialAssertionYAML(w, serial, s.formatter, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, `brand-id: brand-id1 +model: baz-3000 +serial: 2700 +`) +} + +func (s *modelInfoSuite) TestPrintSerialAssertion(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + serial := s.getSerial(c, serialExample) + + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: false, + AbsTime: false, + Assertion: true, + } + err := clientutil.PrintSerialAssertionYAML(w, serial, s.formatter, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, fmt.Sprintf(`type: serial +authority-id: brand-id1 +brand-id: brand-id1 +model: baz-3000 +serial: 2700 +device-key: +%sdevice-key-sha3-384: %s +timestamp: %s +body-length: 2 +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +HW + +`, s.getDeviceKey(" ", options.TermWidth), s.deviceKey.PublicKey().ID(), s.ts.Format(time.RFC3339))) +} + +func (s *modelInfoSuite) TestPrintSerialVerboseYAML(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + serial := s.getSerial(c, serialExample) + + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: true, + AbsTime: false, + Assertion: false, + } + err := clientutil.PrintSerialAssertionYAML(w, serial, s.formatter, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, fmt.Sprintf(`brand-id: brand-id1 +model: baz-3000 +serial: 2700 +timestamp: %s +device-key-sha3-384: | + %s +device-key: | +%s`, timeutil.Human(s.ts), s.deviceKey.PublicKey().ID(), s.getDeviceKey(" ", options.TermWidth))) +} + +func (s *modelInfoSuite) TestPrintModelJSON(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + model := s.getModel(c, modelExample) + + // For JSON verbose is always implictly true + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: true, + AbsTime: false, + Assertion: false, + } + err := clientutil.PrintModelAssertionJSON(w, model, nil, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, fmt.Sprintf(`{ + "architecture": "amd64", + "base": "core18", + "brand-id": "brand-id1", + "display-name": "Baz 3000", + "gadget": "brand-gadget", + "kernel": "baz-linux", + "model": "baz-3000", + "required-snaps": [ + "foo", + "bar" + ], + "serial": null, + "store": "brand-store", + "system-user-authority": "*", + "timestamp": "%s" +}`, s.ts.Format(time.RFC3339))) +} + +func (s *modelInfoSuite) TestPrintModelWithSerialJSON(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + model := s.getModel(c, modelExample) + serial := s.getSerial(c, serialExample) + + // For JSON verbose is always implictly true + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: true, + AbsTime: false, + Assertion: false, + } + err := clientutil.PrintModelAssertionJSON(w, model, &serial, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, fmt.Sprintf(`{ + "architecture": "amd64", + "base": "core18", + "brand-id": "brand-id1", + "display-name": "Baz 3000", + "gadget": "brand-gadget", + "kernel": "baz-linux", + "model": "baz-3000", + "required-snaps": [ + "foo", + "bar" + ], + "serial": "2700", + "store": "brand-store", + "system-user-authority": "*", + "timestamp": "%s" +}`, s.ts.Format(time.RFC3339))) +} + +func (s *modelInfoSuite) TestPrintModelJSONAssertion(c *C) { + var buffer bytes.Buffer + w := tabwriter.NewWriter(&buffer, 0, 5, 4, ' ', 0) + model := s.getModel(c, modelExample) + + // For JSON verbose is always implictly true + options := clientutil.PrintModelAssertionOptions{ + TermWidth: 80, + Verbose: true, + AbsTime: false, + Assertion: true, + } + err := clientutil.PrintModelAssertionJSON(w, model, nil, options) + c.Assert(err, IsNil) + c.Check(buffer.String(), Equals, fmt.Sprintf(`{ + "headers": { + "architecture": "amd64", + "authority-id": "brand-id1", + "base": "core18", + "body-length": "0", + "brand-id": "brand-id1", + "display-name": "Baz 3000", + "gadget": "brand-gadget", + "kernel": "baz-linux", + "model": "baz-3000", + "required-snaps": [ + "foo", + "bar" + ], + "serial-authority": [ + "generic" + ], + "series": "16", + "sign-key-sha3-384": "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + "store": "brand-store", + "system-user-authority": "*", + "timestamp": "%s", + "type": "model" + } +}`, s.ts.Format(time.RFC3339))) +} diff -Nru snapd-2.55.5+20.04/client/quota.go snapd-2.57.5+20.04/client/quota.go --- snapd-2.55.5+20.04/client/quota.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/client/quota.go 2022-10-17 16:25:18.000000000 +0000 @@ -23,6 +23,7 @@ "bytes" "encoding/json" "fmt" + "time" "github.com/snapcore/snapd/gadget/quantity" ) @@ -53,11 +54,22 @@ CPUs []int `json:"cpus,omitempty"` } +type QuotaJournalRate struct { + RateCount int `json:"rate-count"` + RatePeriod time.Duration `json:"rate-period"` +} + +type QuotaJournalValues struct { + Size quantity.Size `json:"size,omitempty"` + *QuotaJournalRate +} + type QuotaValues struct { - Memory quantity.Size `json:"memory,omitempty"` - CPU *QuotaCPUValues `json:"cpu,omitempty"` - CPUSet *QuotaCPUSetValues `json:"cpu-set,omitempty"` - Threads int `json:"threads,omitempty"` + Memory quantity.Size `json:"memory,omitempty"` + CPU *QuotaCPUValues `json:"cpu,omitempty"` + CPUSet *QuotaCPUSetValues `json:"cpu-set,omitempty"` + Threads int `json:"threads,omitempty"` + Journal *QuotaJournalValues `json:"journal,omitempty"` } // EnsureQuota creates a quota group or updates an existing group. diff -Nru snapd-2.55.5+20.04/client/quota_test.go snapd-2.57.5+20.04/client/quota_test.go --- snapd-2.55.5+20.04/client/quota_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/client/quota_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -23,6 +23,7 @@ "bytes" "encoding/json" "io/ioutil" + "time" "gopkg.in/check.v1" @@ -54,6 +55,13 @@ CPUs: []int{0}, }, Threads: 32, + Journal: &client.QuotaJournalValues{ + Size: quantity.SizeMiB, + QuotaJournalRate: &client.QuotaJournalRate{ + RateCount: 150, + RatePeriod: time.Minute, + }, + }, } chgID, err := cs.cli.EnsureQuota("foo", "bar", []string{"snap-a", "snap-b"}, quotaValues) @@ -81,6 +89,11 @@ "cpus": []interface{}{json.Number("0")}, }, "threads": json.Number("32"), + "journal": map[string]interface{}{ + "size": json.Number("1048576"), + "rate-count": json.Number("150"), + "rate-period": json.Number("60000000000"), + }, }, }) } diff -Nru snapd-2.55.5+20.04/client/snapshot_test.go snapd-2.57.5+20.04/client/snapshot_test.go --- snapd-2.55.5+20.04/client/snapshot_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/client/snapshot_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -151,8 +151,8 @@ } table := []tableT{ - {"dummy-export", client.SnapshotExportMediaType, 200}, - {"dummy-export", "application/x-tar", 400}, + {"test-export", client.SnapshotExportMediaType, 200}, + {"test-export", "application/x-tar", 400}, {"", "", 400}, } diff -Nru snapd-2.55.5+20.04/client/validate.go snapd-2.57.5+20.04/client/validate.go --- snapd-2.55.5+20.04/client/validate.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/client/validate.go 2022-10-17 16:25:18.000000000 +0000 @@ -77,10 +77,12 @@ return nil } -// ApplyValidationSet applies the given validation set identified by account and name. -func (client *Client) ApplyValidationSet(accountID, name string, opts *ValidateApplyOptions) error { +// ApplyValidationSet applies the given validation set identified by account and name and returns +// the new validation set tracking info. For monitoring mode the returned res may indicate invalid +// state. +func (client *Client) ApplyValidationSet(accountID, name string, opts *ValidateApplyOptions) (res *ValidationSetResult, err error) { if accountID == "" || name == "" { - return xerrors.Errorf("cannot apply validation set without account ID and name") + return nil, xerrors.Errorf("cannot apply validation set without account ID and name") } data := &postValidationSetData{ @@ -91,14 +93,15 @@ var body bytes.Buffer if err := json.NewEncoder(&body).Encode(data); err != nil { - return err + return nil, err } path := fmt.Sprintf("/v2/validation-sets/%s/%s", accountID, name) - if _, err := client.doSync("POST", path, nil, nil, &body, nil); err != nil { + + if _, err := client.doSync("POST", path, nil, nil, &body, &res); err != nil { fmt := "cannot apply validation set: %w" - return xerrors.Errorf(fmt, err) + return nil, xerrors.Errorf(fmt, err) } - return nil + return res, nil } // ListValidationsSets queries all validation sets. diff -Nru snapd-2.55.5+20.04/client/validate_test.go snapd-2.57.5+20.04/client/validate_test.go --- snapd-2.55.5+20.04/client/validate_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/client/validate_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -78,13 +78,22 @@ }) } -func (cs *clientSuite) TestApplyValidationSet(c *check.C) { +func (cs *clientSuite) TestApplyValidationSetMonitor(c *check.C) { cs.rsp = `{ "type": "sync", - "status-code": 200 + "status-code": 200, + "result": {"account-id": "foo", "name": "bar", "mode": "monitor", "sequence": 3, "valid": true} }` opts := &client.ValidateApplyOptions{Mode: "monitor", Sequence: 3} - c.Assert(cs.cli.ApplyValidationSet("foo", "bar", opts), check.IsNil) + vs, err := cs.cli.ApplyValidationSet("foo", "bar", opts) + c.Assert(err, check.IsNil) + c.Check(vs, check.DeepEquals, &client.ValidationSetResult{ + AccountID: "foo", + Name: "bar", + Mode: "monitor", + Sequence: 3, + Valid: true, + }) c.Check(cs.req.Method, check.Equals, "POST") c.Check(cs.req.URL.Path, check.Equals, "/v2/validation-sets/foo/bar") body, err := ioutil.ReadAll(cs.req.Body) @@ -99,11 +108,41 @@ }) } +func (cs *clientSuite) TestApplyValidationSetEnforce(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {"account-id": "foo", "name": "bar", "mode": "enforce", "sequence": 3, "valid": true} + }` + opts := &client.ValidateApplyOptions{Mode: "enforce", Sequence: 3} + vs, err := cs.cli.ApplyValidationSet("foo", "bar", opts) + c.Assert(err, check.IsNil) + c.Check(vs, check.DeepEquals, &client.ValidationSetResult{ + AccountID: "foo", + Name: "bar", + Mode: "enforce", + Sequence: 3, + Valid: true, + }) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/validation-sets/foo/bar") + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + var req map[string]interface{} + err = json.Unmarshal(body, &req) + c.Assert(err, check.IsNil) + c.Assert(req, check.DeepEquals, map[string]interface{}{ + "action": "apply", + "mode": "enforce", + "sequence": float64(3), + }) +} + func (cs *clientSuite) TestApplyValidationSetError(c *check.C) { cs.status = 500 cs.rsp = errorResponseJSON opts := &client.ValidateApplyOptions{Mode: "monitor"} - err := cs.cli.ApplyValidationSet("foo", "bar", opts) + _, err := cs.cli.ApplyValidationSet("foo", "bar", opts) c.Assert(err, check.ErrorMatches, "cannot apply validation set: failed") c.Check(cs.req.Method, check.Equals, "POST") c.Check(cs.req.URL.Path, check.Equals, "/v2/validation-sets/foo/bar") @@ -111,9 +150,9 @@ func (cs *clientSuite) TestApplyValidationSetInvalidArgs(c *check.C) { opts := &client.ValidateApplyOptions{} - err := cs.cli.ApplyValidationSet("", "bar", opts) + _, err := cs.cli.ApplyValidationSet("", "bar", opts) c.Assert(err, check.ErrorMatches, `cannot apply validation set without account ID and name`) - err = cs.cli.ApplyValidationSet("", "bar", opts) + _, err = cs.cli.ApplyValidationSet("", "bar", opts) c.Assert(err, check.ErrorMatches, `cannot apply validation set without account ID and name`) } diff -Nru snapd-2.55.5+20.04/cmd/autogen.sh snapd-2.57.5+20.04/cmd/autogen.sh --- snapd-2.55.5+20.04/cmd/autogen.sh 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/autogen.sh 2022-10-17 16:25:18.000000000 +0000 @@ -14,7 +14,7 @@ ( cd .. && ./mkversion.sh ) fi -# Sanity check, are we in the right directory? +# Precondition check, are we in the right directory? test -f configure.ac # Regenerate the build system diff -Nru snapd-2.55.5+20.04/cmd/libsnap-confine-private/apparmor-support.c snapd-2.57.5+20.04/cmd/libsnap-confine-private/apparmor-support.c --- snapd-2.55.5+20.04/cmd/libsnap-confine-private/apparmor-support.c 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/libsnap-confine-private/apparmor-support.c 2022-10-17 16:25:18.000000000 +0000 @@ -124,8 +124,18 @@ debug("requesting changing of apparmor profile on next exec to %s", profile); if (aa_change_onexec(profile) < 0) { + /* Save errno because secure_getenv() can overwrite it */ + int aa_change_onexec_errno = errno; if (secure_getenv("SNAPPY_LAUNCHER_INSIDE_TESTS") == NULL) { - die("cannot change profile for the next exec call"); + errno = aa_change_onexec_errno; + if (errno == ENOENT) { + fprintf(stderr, "missing profile %s.\n" + "Please make sure that the snapd.apparmor service is enabled and started\n", + profile); + exit(1); + } else { + die("cannot change profile for the next exec call"); + } } } #endif // ifdef HAVE_APPARMOR diff -Nru snapd-2.55.5+20.04/cmd/libsnap-confine-private/string-utils-test.c snapd-2.57.5+20.04/cmd/libsnap-confine-private/string-utils-test.c --- snapd-2.55.5+20.04/cmd/libsnap-confine-private/string-utils-test.c 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/libsnap-confine-private/string-utils-test.c 2022-10-17 16:25:18.000000000 +0000 @@ -164,10 +164,11 @@ { if (g_test_subprocess()) { char buf[4] = { 0 }; - +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" // Try to append a string that's one character too long. sc_string_append(buf, sizeof buf, "1234"); - +#pragma GCC diagnostic pop g_test_message("expected sc_string_append not to return"); g_test_fail(); return; @@ -304,8 +305,10 @@ { if (g_test_subprocess()) { char buf[1] = { 0 }; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" sc_string_append_char(buf, sizeof buf, 'a'); - +#pragma GCC diagnostic pop g_test_message("expected sc_string_append_char not to return"); g_test_fail(); return; @@ -392,8 +395,10 @@ { if (g_test_subprocess()) { char buf[2] = { 0 }; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Warray-bounds" sc_string_append_char_pair(buf, sizeof buf, 'a', 'b'); - +#pragma GCC diagnostic pop g_test_message ("expected sc_string_append_char_pair not to return"); g_test_fail(); diff -Nru snapd-2.55.5+20.04/cmd/Makefile.am snapd-2.57.5+20.04/cmd/Makefile.am --- snapd-2.55.5+20.04/cmd/Makefile.am 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/Makefile.am 2022-10-17 16:25:18.000000000 +0000 @@ -100,7 +100,7 @@ # The hack target helps developers work on snap-confine on their live system by # installing a fresh copy of snap confine and the appropriate apparmor profile. .PHONY: hack -hack: snap-confine/snap-confine-debug snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp snap-discard-ns/snap-discard-ns snap-device-helper/snap-device-helper +hack: snap-confine/snap-confine-debug snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp snap-discard-ns/snap-discard-ns snap-device-helper/snap-device-helper snapd-apparmor/snapd-apparmor sudo install -D -m 4755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine if [ -d /etc/apparmor.d ]; then sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real; fi sudo install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/ @@ -109,6 +109,7 @@ sudo install -m 755 snap-discard-ns/snap-discard-ns $(DESTDIR)$(libexecdir)/snap-discard-ns sudo install -m 755 snap-seccomp/snap-seccomp $(DESTDIR)$(libexecdir)/snap-seccomp sudo install -m 755 snap-device-helper/snap-device-helper $(DESTDIR)$(libexecdir)/snap-device-helper + sudo install -m 755 snapd-apparmor/snapd-apparmor $(DESTDIR)$(libexecdir)/snapd-apparmor if [ "$$(command -v restorecon)" != "" ]; then sudo restorecon -R -v $(DESTDIR)$(libexecdir)/; fi # for the hack target also: @@ -117,6 +118,8 @@ -ldflags='-extldflags=-static -linkmode=external' -v snap-seccomp/snap-seccomp: snap-seccomp/*.go cd snap-seccomp && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -v +snapd-apparmor/snapd-apparmor: snapd-apparmor/*.go + cd snapd-apparmor && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -v ## ## libsnap-confine-private.a @@ -571,14 +574,5 @@ CLEANFILES += snapd-env-generator/snapd-env-generator.8 endif -## -## snapd-apparmor -## - -EXTRA_DIST += snapd-apparmor/snapd-apparmor - install-exec-local:: install -d -m 755 $(DESTDIR)$(libexecdir) -if APPARMOR - install -m 755 $(srcdir)/snapd-apparmor/snapd-apparmor $(DESTDIR)$(libexecdir) -endif diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_booted.go snapd-2.57.5+20.04/cmd/snap/cmd_booted.go --- snapd-2.55.5+20.04/cmd/snap/cmd_booted.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_booted.go 2022-10-17 16:25:18.000000000 +0000 @@ -39,7 +39,7 @@ // WARNING: do not remove this command, older systems may still have // a systemd snapd.firstboot.service job in /etc/systemd/system -// that we did not cleanup. so we need this dummy command or +// that we did not cleanup. so we need this sample command or // those units will start failing. func (x *cmdBooted) Execute(args []string) error { if len(args) > 0 { diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_changes.go snapd-2.57.5+20.04/cmd/snap/cmd_changes.go --- snapd-2.55.5+20.04/cmd/snap/cmd_changes.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_changes.go 2022-10-17 16:25:18.000000000 +0000 @@ -200,7 +200,7 @@ func warnMaintenance(cli *client.Client) error { if maintErr := cli.Maintenance(); maintErr != nil { - msg, err := errorToCmdMessage("", maintErr, nil) + msg, err := errorToCmdMessage("", "", maintErr, nil) if err != nil { return err } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_create_key.go snapd-2.57.5+20.04/cmd/snap/cmd_create_key.go --- snapd-2.55.5+20.04/cmd/snap/cmd_create_key.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_create_key.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,6 +25,7 @@ "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/signtool" "github.com/snapcore/snapd/i18n" ) @@ -66,9 +67,9 @@ return fmt.Errorf(i18n.G("key name %q is not valid; only ASCII letters, digits, and hyphens are allowed"), keyName) } - keypairMgr, err := getKeypairManager() + keypairMgr, err := signtool.GetKeypairManager() if err != nil { return err } - return generateKey(keypairMgr, keyName) + return signtool.GenerateKey(keypairMgr, keyName) } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_debug_bootvars.go snapd-2.57.5+20.04/cmd/snap/cmd_debug_bootvars.go --- snapd-2.55.5+20.04/cmd/snap/cmd_debug_bootvars.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_debug_bootvars.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019 Canonical Ltd + * Copyright (C) 2022 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 @@ -49,7 +49,7 @@ func() flags.Commander { return &cmdBootvarsGet{} }, map[string]string{ - "uc20": i18n.G("Whether to use uc20 boot vars or not"), + "uc20": i18n.G("Whether to use UC20+ boot vars or not"), "root-dir": i18n.G("Root directory to look for boot variables in"), }, nil) @@ -59,8 +59,8 @@ func() flags.Commander { return &cmdBootvarsSet{} }, map[string]string{ - "root-dir": i18n.G("Root directory to look for boot variables in (implies UC20)"), - "recovery": i18n.G("Manipulate the recovery bootloader (implies UC20)"), + "root-dir": i18n.G("Root directory to look for boot variables in (implies UC20+)"), + "recovery": i18n.G("Manipulate the recovery bootloader (implies UC20+)"), }, nil) if release.OnClassic { diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_debug_bootvars_test.go snapd-2.57.5+20.04/cmd/snap/cmd_debug_bootvars_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_debug_bootvars_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_debug_bootvars_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -151,7 +151,7 @@ "foo": "recovery", }) - // but basic sanity checks are still done + // but basic validity checks are still done _, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "set-boot-vars", "--recovery", "--root-dir", boot.InitramfsUbuntuBootDir, "foo=recovery"}) c.Assert(err, check.ErrorMatches, "cannot use run bootloader root-dir with a recovery flag") } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_debug_migrate.go snapd-2.57.5+20.04/cmd/snap/cmd_debug_migrate.go --- snapd-2.55.5+20.04/cmd/snap/cmd_debug_migrate.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_debug_migrate.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,80 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 main + +import ( + "errors" + "fmt" + + "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/strutil" +) + +type cmdMigrateHome struct { + waitMixin + + Positional struct { + Snaps []string `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func init() { + addDebugCommand("migrate-home", + "Migrate snaps' directory to ~/Snap.", + "Migrate snaps' directory to ~/Snap.", + func() flags.Commander { + return &cmdMigrateHome{} + }, nil, nil) +} + +func (x *cmdMigrateHome) Execute(args []string) error { + chgID, err := x.client.MigrateSnapHome(x.Positional.Snaps) + if err != nil { + msg, err := errorToCmdMessage("", "migrate-home", err, nil) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil + } + + chg, err := x.wait(chgID) + if err != nil { + return err + } + + var snaps []string + if err := chg.Get("snap-names", &snaps); err != nil { + return errors.New(`cannot get "snap-names" from change`) + } + + if len(snaps) == 0 { + return errors.New(`expected "migrate-home" change to have non-empty "snap-names"`) + } + + msg := fmt.Sprintf("%s's home directory was migrated to ~/Snap\n", snaps[0]) + if len(snaps) > 1 { + msg = fmt.Sprintf(i18n.G("%s migrated their home directories to ~/Snap\n"), strutil.Quoted(snaps)) + } + + fmt.Fprintf(Stdout, msg) + return nil +} diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_debug_migrate_test.go snapd-2.57.5+20.04/cmd/snap/cmd_debug_migrate_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_debug_migrate_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_debug_migrate_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,160 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 main_test + +import ( + "fmt" + "net/http" + + snap "github.com/snapcore/snapd/cmd/snap" + "gopkg.in/check.v1" + . "gopkg.in/check.v1" +) + +type MigrateHomeSuite struct { + BaseSnapSuite +} + +var _ = check.Suite(&MigrateHomeSuite{}) + +// failRequest logs an error message, fails the test and returns a proper error +// to the client. Use this instead of panic() or c.Fatal() because those crash +// the server and leave the client hanging/retrying. +func failRequest(msg string, w http.ResponseWriter, c *C) { + c.Error(msg) + w.WriteHeader(400) + fmt.Fprintf(w, `{"type": "error", "status-code": 400, "result": {"message": %q}}`, msg) +} + +func serverWithChange(chgRsp string, c *C) func(w http.ResponseWriter, r *http.Request) { + var n int + return func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/debug") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "migrate-home", + "snaps": []interface{}{"foo"}, + }) + w.WriteHeader(202) + fmt.Fprintln(w, `{"type": "async", "status-code": 202, "result": {}, "change": "12"}`) + + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/12") + fmt.Fprintf(w, chgRsp) + + default: + failRequest(fmt.Sprintf("server expected to get 2 requests, now on %d", n+1), w, c) + } + + n++ + } +} + +func (s *MigrateHomeSuite) TestMigrateHome(c *C) { + rsp := serverWithChange(`{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["foo"]}}}\n`, c) + s.RedirectClientToTestServer(rsp) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "migrate-home", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "foo's home directory was migrated to ~/Snap\n") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *MigrateHomeSuite) TestMigrateHomeManySnaps(c *C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/debug") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "migrate-home", + "snaps": []interface{}{"foo", "bar"}, + }) + w.WriteHeader(202) + fmt.Fprintln(w, `{"type": "async", "status-code": 202, "result": {}, "change": "12"}`) + + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/changes/12") + fmt.Fprintf(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": ["foo", "bar"]}}}\n`) + + default: + failRequest(fmt.Sprintf("server expected to get 2 requests, now on %d", n+1), w, c) + } + + n++ + }) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "migrate-home", "foo", "bar"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "\"foo\", \"bar\" migrated their home directories to ~/Snap\n") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *MigrateHomeSuite) TestMigrateHomeNoSnaps(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + failRequest("unexpected request on server", w, c) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "migrate-home"}) + c.Assert(err, check.ErrorMatches, "the required argument .* was not provided") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *MigrateHomeSuite) TestMigrateHomeServerError(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + fmt.Fprintf(w, `{"type": "error", "status-code": 500, "result": {"message": "boom"}}`) + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "migrate-home", "foo"}) + c.Assert(err, check.ErrorMatches, "boom") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *MigrateHomeSuite) TestMigrateHomeBadChangeNoSnaps(c *C) { + // broken change response: missing required "snap-names" + srv := serverWithChange(`{"type": "sync", "result": {"ready": true, "status": "Done", "data": {"snap-names": []}}}\n`, c) + s.RedirectClientToTestServer(srv) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "migrate-home", "foo"}) + c.Assert(err, check.ErrorMatches, `expected "migrate-home" change to have non-empty "snap-names"`) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *MigrateHomeSuite) TestMigrateHomeBadChangeNoData(c *C) { + // broken change response: missing data + srv := serverWithChange(`{"type": "sync", "result": {"ready": true, "status": "Done"}}\n`, c) + s.RedirectClientToTestServer(srv) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "migrate-home", "foo"}) + c.Assert(err, check.ErrorMatches, `cannot get "snap-names" from change`) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_debug_state.go snapd-2.57.5+20.04/cmd/snap/cmd_debug_state.go --- snapd-2.55.5+20.04/cmd/snap/cmd_debug_state.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_debug_state.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package main import ( + "errors" "fmt" "os" "sort" @@ -27,10 +28,15 @@ "strings" "text/tabwriter" + "gopkg.in/yaml.v2" + "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/overlord/ifacestate/schema" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/strutil" ) type cmdDebugState struct { @@ -39,6 +45,10 @@ Changes bool `long:"changes"` TaskID string `long:"task"` ChangeID string `long:"change"` + Check bool `long:"check"` + + Connections bool `long:"connections"` + Connection string `long:"connection"` IsSeeded bool `long:"is-seeded"` @@ -55,11 +65,11 @@ var cmdDebugStateShortHelp = i18n.G("Inspect a snapd state file.") var cmdDebugStateLongHelp = i18n.G("Inspect a snapd state file, bypassing snapd API.") -type byChangeID []*state.Change +type byChangeSpawnTime []*state.Change -func (c byChangeID) Len() int { return len(c) } -func (c byChangeID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func (c byChangeID) Less(i, j int) bool { return c[i].ID() < c[j].ID() } +func (c byChangeSpawnTime) Len() int { return len(c) } +func (c byChangeSpawnTime) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c byChangeSpawnTime) Less(i, j int) bool { return c[i].SpawnTime().Before(c[j].SpawnTime()) } func loadState(path string) (*state.State, error) { if path == "" { @@ -79,12 +89,15 @@ return &cmdDebugState{} }, timeDescs.also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. - "change": i18n.G("ID of the change to inspect"), - "task": i18n.G("ID of the task to inspect"), - "dot": i18n.G("Dot (graphviz) output"), - "no-hold": i18n.G("Omit tasks in 'Hold' state in the change output"), - "changes": i18n.G("List all changes"), - "is-seeded": i18n.G("Output seeding status (true or false)"), + "change": i18n.G("ID of the change to inspect"), + "task": i18n.G("ID of the task to inspect"), + "dot": i18n.G("Dot (graphviz) output"), + "no-hold": i18n.G("Omit tasks in 'Hold' state in the change output"), + "changes": i18n.G("List all changes"), + "connections": i18n.G("List all connections"), + "connection": i18n.G("Show details of the matching connections (snap or snap:plug,snap:slot or snap:plug-or-slot"), + "is-seeded": i18n.G("Output seeding status (true or false)"), + "check": i18n.G("Check change consistency"), }), nil) } @@ -167,12 +180,8 @@ if c.NoHoldState && t.Status() == state.HoldStatus { continue } - var lanes []string - for _, lane := range t.Lanes() { - lanes = append(lanes, fmt.Sprintf("%d", lane)) - } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", - strings.Join(lanes, ","), + strutil.IntsToCommaSeparated(t.Lanes()), t.ID(), t.Status().String(), c.fmtTime(t.SpawnTime()), @@ -197,12 +206,71 @@ return nil } +func (c *cmdDebugState) checkTasks(st *state.State, changeID string) error { + st.Lock() + defer st.Unlock() + + showAtMostTasks := 3 + formatAtMostTaskIDs := func(tasks []*state.Task) string { + var b strings.Builder + b.WriteRune('[') + atMostTasks := tasks + trimmed := false + if len(atMostTasks) > showAtMostTasks { + atMostTasks = tasks[:showAtMostTasks] + trimmed = true + } + for i, t := range atMostTasks { + b.WriteString(t.ID()) + if i < len(atMostTasks)-1 { + b.WriteRune(',') + } + } + if trimmed { + b.WriteString(",...") + } + b.WriteRune(']') + return b.String() + } + + chg := st.Change(changeID) + if chg == nil { + return fmt.Errorf("no such change: %s", changeID) + } + err := chg.CheckTaskDependencies() + if err != nil { + if tdcErr, ok := err.(*state.TaskDependencyCycleError); ok { + fmt.Fprintf(Stdout, "Detected task dependency cycle involving tasks:\n") + w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0) + fmt.Fprintf(w, "Lanes\tID\tStatus\tSpawn\tReady\tKind\tSummary\tAfter\tBefore\n") + for _, tid := range tdcErr.IDs { + t := st.Task(tid) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%v\t%v\n", + strutil.IntsToCommaSeparated(t.Lanes()), + t.ID(), + t.Status().String(), + c.fmtTime(t.SpawnTime()), + c.fmtTime(t.ReadyTime()), + t.Kind(), + t.Summary(), + formatAtMostTaskIDs(t.WaitTasks()), + formatAtMostTaskIDs(t.HaltTasks()), + ) + } + w.Flush() + } else { + return err + } + } + return nil +} + func (c *cmdDebugState) showChanges(st *state.State) error { st.Lock() defer st.Unlock() changes := st.Changes() - sort.Sort(byChangeID(changes)) + sort.Sort(byChangeSpawnTime(changes)) w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0) fmt.Fprintf(w, "ID\tStatus\tSpawn\tReady\tLabel\tSummary\n") @@ -226,7 +294,7 @@ var isSeeded bool err := st.Get("seeded", &isSeeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } fmt.Fprintf(Stdout, "%v\n", isSeeded) @@ -234,6 +302,156 @@ return nil } +type connectionInfo struct { + PlugSnap string + PlugName string + SlotSnap string + SlotName string + + schema.ConnState +} + +type byPlug []*connectionInfo + +func (c byPlug) Len() int { return len(c) } +func (c byPlug) Swap(i, j int) { c[i], c[j] = c[j], c[i] } +func (c byPlug) Less(i, j int) bool { + a, b := c[i], c[j] + return a.PlugSnap < b.PlugSnap || (a.PlugSnap == b.PlugSnap && a.PlugName < b.PlugName) +} + +func (c *cmdDebugState) showConnectionDetails(st *state.State, connArg string) error { + st.Lock() + defer st.Unlock() + + p := strings.FieldsFunc(connArg, func(r rune) bool { + return r == ' ' || r == ',' + }) + + var plugMatch, slotMatch SnapAndName + if err := plugMatch.UnmarshalFlag(p[0]); err != nil { + return err + } + + if len(p) > 1 { + if err := slotMatch.UnmarshalFlag(p[1]); err != nil { + return err + } + } + + var conns map[string]*schema.ConnState + if err := st.Get("conns", &conns); err != nil && !errors.Is(err, state.ErrNoState) { + return err + } + + // sort by connection ID + connIDs := make([]string, 0, len(conns)) + for connID := range conns { + connIDs = append(connIDs, connID) + } + sort.Strings(connIDs) + + for _, connID := range connIDs { + connRef, err := interfaces.ParseConnRef(connID) + if err != nil { + return err + } + + refMatch := func(x SnapAndName, y interface{ String() string }) bool { + parts := strings.Split(y.String(), ":") + return len(parts) == 2 && x.Snap == parts[0] && x.Name == parts[1] + } + plug, slot := connRef.PlugRef, connRef.SlotRef + + switch { + // command invoked with 'snap:plug,snap:slot' + case slotMatch.Name != "" && slotMatch.Snap != "" && plugMatch.Snap != "" && plugMatch.Name != "": + // should match the connection exactly + if !refMatch(plugMatch, plug) || !refMatch(slotMatch, slot) { + continue + } + + // command invoked with 'snap:plug-or-slot' + case plugMatch.Snap != "" && plugMatch.Name != "" && slotMatch.Snap == "" && slotMatch.Name == "": + // should match either the connection's slot or plug + if !refMatch(plugMatch, plug) && !refMatch(plugMatch, slot) { + continue + } + + // command invoked with 'snap' only + case plugMatch.Snap != "" && plugMatch.Name == "" && slotMatch.Snap == "" && slotMatch.Name == "": + // should match one of the snap names + if plugMatch.Snap != slot.Snap && plugMatch.Snap != plug.Snap { + continue + } + + default: + return fmt.Errorf("invalid command with connection args: %s", connArg) + } + + conn := conns[connID] + + // the output of 'debug connection' is yaml + fmt.Fprintf(Stdout, "id: %s\n", connID) + out, err := yaml.Marshal(conn) + if err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", out) + } + return nil +} + +func (c *cmdDebugState) showConnections(st *state.State) error { + st.Lock() + defer st.Unlock() + + var conns map[string]*schema.ConnState + if err := st.Get("conns", &conns); err != nil && !errors.Is(err, state.ErrNoState) { + return err + } + + all := make([]*connectionInfo, 0, len(conns)) + for connID, conn := range conns { + p := strings.Split(connID, " ") + if len(p) != 2 { + return fmt.Errorf("cannot parse connection ID %q", connID) + } + plug := strings.Split(p[0], ":") + slot := strings.Split(p[1], ":") + + c := &connectionInfo{ + PlugSnap: plug[0], + PlugName: plug[1], + SlotSnap: slot[0], + SlotName: slot[1], + ConnState: *conn, + } + all = append(all, c) + } + + sort.Sort(byPlug(all)) + + w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0) + fmt.Fprintf(w, "Interface\tPlug\tSlot\tNotes\n") + for _, conn := range all { + var notes []string + if conn.Auto { + notes = append(notes, "auto") + } + if conn.Undesired { + notes = append(notes, "undesired") + } + if conn.ByGadget { + notes = append(notes, "by-gadget") + } + fmt.Fprintf(w, "%s\t%s:%s\t%s:%s\t%s\n", conn.Interface, conn.PlugSnap, conn.PlugName, conn.SlotSnap, conn.SlotName, strings.Join(notes, ",")) + } + w.Flush() + + return nil +} + func (c *cmdDebugState) showTask(st *state.State, taskID string) error { st.Lock() defer st.Unlock() @@ -259,7 +477,7 @@ if len(log) > 0 { fmt.Fprintf(Stdout, "log: |\n") for _, msg := range log { - if err := wrapLine(Stdout, []rune(msg), " ", termWidth); err != nil { + if err := strutil.WordWrapPadded(Stdout, []rune(msg), " ", termWidth); err != nil { break } } @@ -299,6 +517,9 @@ if c.IsSeeded { cmds = append(cmds, "--is-seeded") } + if c.Connections { + cmds = append(cmds, "--connections") + } if len(cmds) > 1 { return fmt.Errorf("cannot use %s and %s together", cmds[0], cmds[1]) } @@ -313,6 +534,9 @@ if c.NoHoldState && c.ChangeID == "" { return fmt.Errorf("--no-hold can only be used with --change=") } + if c.Check && c.ChangeID == "" { + return fmt.Errorf("--check can only be used with --change") + } if c.Changes { return c.showChanges(st) @@ -326,6 +550,9 @@ if c.DotOutput { return c.writeDotOutput(st, c.ChangeID) } + if c.Check { + return c.checkTasks(st, c.ChangeID) + } return c.showTasks(st, c.ChangeID) } @@ -337,6 +564,14 @@ return c.showTask(st, c.TaskID) } + if c.Connections { + return c.showConnections(st) + } + + if c.Connection != "" { + return c.showConnectionDetails(st, c.Connection) + } + // show changes by default return c.showChanges(st) } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_debug_state_test.go snapd-2.57.5+20.04/cmd/snap/cmd_debug_state_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_debug_state_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_debug_state_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,8 +20,10 @@ package main_test import ( + "fmt" "io/ioutil" "path/filepath" + "time" . "gopkg.in/check.v1" @@ -31,34 +33,37 @@ var stateJSON = []byte(` { "last-task-id": 31, - "last-change-id": 2, + "last-change-id": 10, "data": { "snaps": {}, "seeded": true }, "changes": { - "1": { - "id": "1", + "9": { + "id": "9", "kind": "install-snap", "summary": "install a snap", "status": 0, "data": {"snap-names": ["a"]}, - "task-ids": ["11","12"] + "task-ids": ["11","12"], + "spawn-time": "2009-11-10T23:00:00Z" }, - "2": { - "id": "2", + "10": { + "id": "10", "kind": "revert-snap", "summary": "revert c snap", "status": 0, "data": {"snap-names": ["c"]}, - "task-ids": ["21","31"] + "task-ids": ["21","31"], + "spawn-time": "2009-11-10T23:00:10Z", + "ready-time": "2009-11-10T23:00:30Z" } }, "tasks": { "11": { "id": "11", - "change": "1", + "change": "9", "kind": "download-snap", "summary": "Download snap a from channel edge", "status": 4, @@ -68,10 +73,10 @@ }}, "halt-tasks": ["12"] }, - "12": {"id": "12", "change": "1", "kind": "some-other-task"}, + "12": {"id": "12", "change": "9", "kind": "some-other-task"}, "21": { "id": "21", - "change": "2", + "change": "10", "kind": "download-snap", "summary": "Download snap b from channel beta", "status": 4, @@ -83,7 +88,7 @@ }, "31": { "id": "31", - "change": "2", + "change": "10", "kind": "prepare-snap", "summary": "Prepare snap c", "status": 4, @@ -98,6 +103,76 @@ } `) +var stateConnsJSON = []byte(` +{ + "data": { + "conns": { + "gnome-calculator:desktop-legacy core:desktop-legacy": { + "auto": true, + "interface": "desktop-legacy" + }, + "gnome-calculator:gtk-3-themes gtk-common-themes:gtk-3-themes": { + "auto": true, + "interface": "content", + "plug-static": { + "content": "gtk-3-themes", + "default-provider": "gtk-common-themes", + "target": "$SNAP/data-dir/themes" + }, + "slot-static": { + "content": "gtk-3-themes", + "source": { + "read": [ + "$SNAP/share/themes/Adwaita", + "$SNAP/share/themes/Materia-light-compact" + ] + } + } + }, + "gnome-calculator:icon-themes gtk-common-themes:icon-themes": { + "auto": true, + "interface": "content", + "plug-static": { + "content": "icon-themes", + "default-provider": "gtk-common-themes", + "target": "$SNAP/data-dir/icons" + }, + "slot-static": { + "content": "icon-themes", + "source": { + "read": [ + "$SNAP/share/icons/Adwaita", + "$SNAP/share/icons/elementary-xfce-darkest" + ] + } + } + }, + "gnome-calculator:network core:network": { + "auto": true, + "interface": "network" + }, + "gnome-calculator:x11 core:x11": { + "auto": true, + "interface": "x11" + }, + "vlc:x11 core:x11": { + "auto": true, + "interface": "x11" + }, + "vlc:network core:network": { + "auto": true, + "undesired": true, + "interface": "network" + }, + "some-snap:network core:network": { + "auto": true, + "by-gadget": true, + "interface": "network" + } + } + } +}`) + var stateCyclesJSON = []byte(` { "last-task-id": 14, @@ -123,21 +198,24 @@ "kind": "foo", "summary": "Foo task", "status": 4, - "halt-tasks": ["13"] + "halt-tasks": ["13"], + "lanes": [1,2] }, "12": { "id": "12", "change": "1", "kind": "bar", "summary": "Bar task", - "halt-tasks": ["13"] + "halt-tasks": ["13"], + "lanes": [1] }, "13": { "id": "13", "change": "1", "kind": "bar", "summary": "Bar task", - "halt-tasks": ["11","12"] + "halt-tasks": ["11","12"], + "lanes": [2] } } } @@ -153,8 +231,8 @@ c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Matches, "ID Status Spawn Ready Label Summary\n"+ - "1 Do 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z install-snap install a snap\n"+ - "2 Done 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z revert-snap revert c snap\n") + "9 Do 2009-11-10T23:00:00Z 0001-01-01T00:00:00Z install-snap install a snap\n"+ + "10 Done 2009-11-10T23:00:10Z 2009-11-10T23:00:30Z revert-snap revert c snap\n") c.Check(s.Stderr(), Equals, "") } @@ -237,7 +315,7 @@ stateFile := filepath.Join(dir, "test-state.json") c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil) - rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--abs-time", "--change=1", stateFile}) + rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--abs-time", "--change=9", stateFile}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Matches, @@ -256,10 +334,37 @@ c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Check(s.Stdout(), Matches, - "Lanes ID Status Spawn Ready Kind Summary\n"+ - "0 13 Do 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z bar Bar task\n"+ - "0 12 Do 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z bar Bar task\n"+ - "0 11 Done 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z foo Foo task\n") + ""+ + "Lanes ID Status Spawn Ready Kind Summary\n"+ + "1 12 Do 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z bar Bar task\n"+ + "1,2 11 Done 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z foo Foo task\n"+ + "2 13 Do 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z bar Bar task\n") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDebugCheckForCycles(c *C) { + // we use local time when printing times in a human-friendly format, which can + // break the comparison below + oldLoc := time.Local + time.Local = time.UTC + defer func() { + time.Local = oldLoc + }() + + dir := c.MkDir() + stateFile := filepath.Join(dir, "test-state.json") + c.Assert(ioutil.WriteFile(stateFile, stateCyclesJSON, 0644), IsNil) + + rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--check", "--change=1", stateFile}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, ``+ + `Detected task dependency cycle involving tasks: +Lanes ID Status Spawn Ready Kind Summary After Before +1,2 11 Done 0001-01-01 0001-01-01 foo Foo task [] [13] +1 12 Do 0001-01-01 0001-01-01 bar Bar task [] [13] +2 13 Do 0001-01-01 0001-01-01 bar Bar task [] [11,12] +`) c.Check(s.Stderr(), Equals, "") } @@ -291,3 +396,157 @@ c.Check(s.Stdout(), Matches, "false\n") c.Check(s.Stderr(), Equals, "") } + +func (s *SnapSuite) TestDebugConnections(c *C) { + dir := c.MkDir() + stateFile := filepath.Join(dir, "test-state.json") + c.Assert(ioutil.WriteFile(stateFile, stateConnsJSON, 0644), IsNil) + + rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--connections", stateFile}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, + "Interface Plug Slot Notes\n"+ + "desktop-legacy gnome-calculator:desktop-legacy core:desktop-legacy auto\n"+ + "content gnome-calculator:gtk-3-themes gtk-common-themes:gtk-3-themes auto\n"+ + "content gnome-calculator:icon-themes gtk-common-themes:icon-themes auto\n"+ + "network gnome-calculator:network core:network auto\n"+ + "x11 gnome-calculator:x11 core:x11 auto\n"+ + "network some-snap:network core:network auto,by-gadget\n"+ + "network vlc:network core:network auto,undesired\n"+ + "x11 vlc:x11 core:x11 auto\n") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDebugConnectionDetails(c *C) { + dir := c.MkDir() + stateFile := filepath.Join(dir, "test-state.json") + c.Assert(ioutil.WriteFile(stateFile, stateConnsJSON, 0644), IsNil) + + for i, connArg := range []string{"gnome-calculator:gtk-3-themes", ",gtk-common-themes:gtk-3-themes"} { + s.ResetStdStreams() + rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", fmt.Sprintf("--connection=%s", connArg), stateFile}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, + "id: gnome-calculator:gtk-3-themes gtk-common-themes:gtk-3-themes\n"+ + "auto: true\n"+ + "by-gadget: false\n"+ + "interface: content\n"+ + "undesired: false\n"+ + "plug-static:\n"+ + " content: gtk-3-themes\n"+ + " default-provider: gtk-common-themes\n"+ + " target: \\$SNAP/data-dir/themes\n"+ + "slot-static:\n"+ + " content: gtk-3-themes\n"+ + " source:\n"+ + " read:\n"+ + " - \\$SNAP/share/themes/Adwaita\n"+ + " - \\$SNAP/share/themes/Materia-light-compact\n"+ + "\n", Commentf("#%d: %s", i, connArg)) + c.Check(s.Stderr(), Equals, "") + } +} + +func (s *SnapSuite) TestDebugConnectionPlugAndSlot(c *C) { + dir := c.MkDir() + stateFile := filepath.Join(dir, "test-state.json") + c.Assert(ioutil.WriteFile(stateFile, stateConnsJSON, 0644), IsNil) + + connArg := "gnome-calculator:network,core:network" + rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", fmt.Sprintf("--connection=%s", connArg), stateFile}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, + "id: gnome-calculator:network core:network\n"+ + "auto: true\n"+ + "by-gadget: false\n"+ + "interface: network\n"+ + "undesired: false\n"+ + "\n", Commentf("#0: %s", connArg)) + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDebugConnectionInvalidCombination(c *C) { + dir := c.MkDir() + stateFile := filepath.Join(dir, "test-state.json") + c.Assert(ioutil.WriteFile(stateFile, stateConnsJSON, 0644), IsNil) + + connArg := "gnome-calculator,core:network" + _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", fmt.Sprintf("--connection=%s", connArg), stateFile}) + c.Assert(err, ErrorMatches, fmt.Sprintf("invalid command with connection args: %s", connArg)) + c.Check(s.Stdout(), Equals, "") +} + +func (s *SnapSuite) TestDebugConnectionDetailsMany(c *C) { + dir := c.MkDir() + stateFile := filepath.Join(dir, "test-state.json") + c.Assert(ioutil.WriteFile(stateFile, stateConnsJSON, 0644), IsNil) + + rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--connection=core", stateFile}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, + "id: gnome-calculator:desktop-legacy core:desktop-legacy\n"+ + "auto: true\n"+ + "by-gadget: false\n"+ + "interface: desktop-legacy\n"+ + "undesired: false\n"+ + "\n"+ + "id: gnome-calculator:network core:network\n"+ + "auto: true\n"+ + "by-gadget: false\n"+ + "interface: network\n"+ + "undesired: false\n"+ + "\n"+ + "id: gnome-calculator:x11 core:x11\n"+ + "auto: true\n"+ + "by-gadget: false\n"+ + "interface: x11\n"+ + "undesired: false\n"+ + "\n"+ + "id: some-snap:network core:network\n"+ + "auto: true\n"+ + "by-gadget: true\n"+ + "interface: network\n"+ + "undesired: false\n"+ + "\n"+ + "id: vlc:network core:network\n"+ + "auto: true\n"+ + "by-gadget: false\n"+ + "interface: network\n"+ + "undesired: true\n"+ + "\n"+ + "id: vlc:x11 core:x11\n"+ + "auto: true\n"+ + "by-gadget: false\n"+ + "interface: x11\n"+ + "undesired: false\n"+ + "\n") + + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDebugConnectionDetailsManySlotSide(c *C) { + dir := c.MkDir() + stateFile := filepath.Join(dir, "test-state.json") + c.Assert(ioutil.WriteFile(stateFile, stateConnsJSON, 0644), IsNil) + + rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--connection=core:x11", stateFile}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, + "id: gnome-calculator:x11 core:x11\n"+ + "auto: true\n"+ + "by-gadget: false\n"+ + "interface: x11\n"+ + "undesired: false\n"+ + "\n"+ + "id: vlc:x11 core:x11\n"+ + "auto: true\n"+ + "by-gadget: false\n"+ + "interface: x11\n"+ + "undesired: false\n"+ + "\n") +} diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_delete_key.go snapd-2.57.5+20.04/cmd/snap/cmd_delete_key.go --- snapd-2.55.5+20.04/cmd/snap/cmd_delete_key.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_delete_key.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,6 +25,7 @@ "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/signtool" "github.com/snapcore/snapd/i18n" ) @@ -58,7 +59,7 @@ return ErrExtraArgs } - keypairMgr, err := getKeypairManager() + keypairMgr, err := signtool.GetKeypairManager() if err != nil { return err } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_delete_key_test.go snapd-2.57.5+20.04/cmd/snap/cmd_delete_key_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_delete_key_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_delete_key_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,12 +21,32 @@ import ( "encoding/json" + "os" . "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/testutil" ) +// XXX: share this helper with signtool tests? +func mockNopExtKeyMgr(c *C) (pgm *testutil.MockCmd, restore func()) { + os.Setenv("SNAPD_EXT_KEYMGR", "keymgr") + pgm = testutil.MockCommand(c, "keymgr", ` +if [ "$1" == "features" ]; then + echo '{"signing":["RSA-PKCS"] , "public-keys":["DER"]}' + exit 0 +fi +exit 1 +`) + r := func() { + pgm.Restore() + os.Unsetenv("SNAPD_EXT_KEYMGR") + } + + return pgm, r +} + func (s *SnapKeysSuite) TestDeleteKeyRequiresName(c *C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"delete-key"}) c.Assert(err, NotNil) diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_download.go snapd-2.57.5+20.04/cmd/snap/cmd_download.go --- snapd-2.55.5+20.04/cmd/snap/cmd_download.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_download.go 2022-10-17 16:25:18.000000000 +0000 @@ -32,6 +32,7 @@ "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/image" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/store/tooling" ) type cmdDownload struct { @@ -71,7 +72,7 @@ }}) } -func fetchSnapAssertionsDirect(tsto *image.ToolingStore, snapPath string, snapInfo *snap.Info) (string, error) { +func fetchSnapAssertionsDirect(tsto *tooling.ToolingStore, snapPath string, snapInfo *snap.Info) (string, error) { db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ Backstore: asserts.NewMemoryBackstore(), Trusted: sysdb.Trusted(), @@ -93,7 +94,7 @@ } f := tsto.AssertionFetcher(db, save) - _, err = image.FetchAndCheckSnapAssertions(snapPath, snapInfo, f, db) + _, err = image.FetchAndCheckSnapAssertions(snapPath, snapInfo, nil, f, db) return assertPath, err } @@ -116,11 +117,12 @@ // for testing var downloadDirect = downloadDirectImpl -func downloadDirectImpl(snapName string, revision snap.Revision, dlOpts image.DownloadSnapOptions) error { - tsto, err := image.NewToolingStore() +func downloadDirectImpl(snapName string, revision snap.Revision, dlOpts tooling.DownloadSnapOptions) error { + tsto, err := tooling.NewToolingStore() if err != nil { return err } + tsto.Stdout = Stdout fmt.Fprintf(Stdout, i18n.G("Fetching snap %q\n"), snapName) dlSnap, err := tsto.DownloadSnap(snapName, dlOpts) @@ -138,7 +140,7 @@ } func (x *cmdDownload) downloadFromStore(snapName string, revision snap.Revision) error { - dlOpts := image.DownloadSnapOptions{ + dlOpts := tooling.DownloadSnapOptions{ TargetDir: x.TargetDir, Basename: x.Basename, Channel: x.Channel, diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_download_test.go snapd-2.57.5+20.04/cmd/snap/cmd_download_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_download_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_download_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -27,8 +27,8 @@ "gopkg.in/check.v1" snapCmd "github.com/snapcore/snapd/cmd/snap" - "github.com/snapcore/snapd/image" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/store/tooling" ) // these only cover errors that happen before hitting the network, @@ -87,7 +87,7 @@ func (s *SnapSuite) TestDownloadDirect(c *check.C) { var n int - restore := snapCmd.MockDownloadDirect(func(snapName string, revision snap.Revision, dlOpts image.DownloadSnapOptions) error { + restore := snapCmd.MockDownloadDirect(func(snapName string, revision snap.Revision, dlOpts tooling.DownloadSnapOptions) error { c.Check(snapName, check.Equals, "a-snap") c.Check(revision, check.Equals, snap.R(0)) c.Check(dlOpts.Basename, check.Equals, "some-base-name") @@ -114,7 +114,7 @@ func (s *SnapSuite) TestDownloadDirectErrors(c *check.C) { var n int - restore := snapCmd.MockDownloadDirect(func(snapName string, revision snap.Revision, dlOpts image.DownloadSnapOptions) error { + restore := snapCmd.MockDownloadDirect(func(snapName string, revision snap.Revision, dlOpts tooling.DownloadSnapOptions) error { n++ return fmt.Errorf("some-error") }) diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_export_key.go snapd-2.57.5+20.04/cmd/snap/cmd_export_key.go --- snapd-2.55.5+20.04/cmd/snap/cmd_export_key.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_export_key.go 2022-10-17 16:25:18.000000000 +0000 @@ -26,6 +26,7 @@ "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/signtool" "github.com/snapcore/snapd/i18n" ) @@ -66,7 +67,7 @@ keyName = "default" } - keypairMgr, err := getKeypairManager() + keypairMgr, err := signtool.GetKeypairManager() if err != nil { return err } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_find.go snapd-2.57.5+20.04/cmd/snap/cmd_find.go --- snapd-2.55.5+20.04/cmd/snap/cmd_find.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_find.go 2022-10-17 16:25:18.000000000 +0000 @@ -155,7 +155,7 @@ Narrow bool `long:"narrow"` Section SectionName `long:"section" optional:"true" optional-value:"show-all-sections-please" default:"no-section-specified" default-mask:"-"` Positional struct { - Query string + Query []string } `positional-args:"yes"` colorMixin } @@ -183,8 +183,9 @@ } // LP: 1740605 - if strings.TrimSpace(x.Positional.Query) == "" { - x.Positional.Query = "" + query := strings.Join(x.Positional.Query, " ") + if strings.TrimSpace(query) == "" { + query = "" } // section will be: @@ -200,7 +201,7 @@ } // magic! `snap find` returns the featured snaps - showFeatured := (x.Positional.Query == "" && x.Section == "") + showFeatured := (query == "" && x.Section == "") if showFeatured { x.Section = "featured" } @@ -224,7 +225,7 @@ } opts := &client.FindOptions{ - Query: x.Positional.Query, + Query: query, Section: string(x.Section), Private: x.Private, } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_find_test.go snapd-2.57.5+20.04/cmd/snap/cmd_find_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_find_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_find_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -23,6 +23,7 @@ "fmt" "io/ioutil" "net/http" + "net/url" "os" "path" @@ -125,14 +126,13 @@ c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/find") q := r.URL.Query() - if q.Get("q") == "" { - v, ok := q["section"] - c.Check(ok, check.Equals, true) - c.Check(v, check.DeepEquals, []string{""}) - } + c.Check(q, check.DeepEquals, url.Values{ + "q": {"hello"}, + "scope": {"wide"}, + }) fmt.Fprint(w, findJSON) default: - c.Fatalf("expected to get 2 requests, now on %d", n+1) + c.Fatalf("expected to get 1 request, now on %d", n+1) } n++ }) @@ -143,8 +143,8 @@ c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical\* +- +GNU Hello, the "hello world" snap -hello-world +6.1 +canonical\* +- +Hello world example +hello +2.10 +canonical\*\* +- +GNU Hello, the "hello world" snap +hello-world +6.1 +canonical\*\* +- +Hello world example hello-huge +1.0 +noise +- +a really big snap `) c.Check(s.Stderr(), check.Equals, "") @@ -152,6 +152,112 @@ s.ResetStdStreams() } +const findHelloWorldJSON = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + }, + { + "channel": "stable", + "confinement": "strict", + "description": "This is a simple hello world example.", + "developer": "canonical", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "download-size": 20480, + "icon": "", + "id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "private": false, + "resource": "/v2/snaps/hello-world", + "revision": "26", + "status": "available", + "summary": "Hello world example", + "type": "app", + "version": "6.1" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestFindSnapNameAggregateTerms(c *check.C) { + n := 0 + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0, 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + q := r.URL.Query() + c.Check(q, check.DeepEquals, url.Values{ + "q": {"hello world"}, + "scope": {"wide"}, + }) + fmt.Fprint(w, findHelloWorldJSON) + default: + c.Fatalf("expected to get 2 requests, now on %d", n+1) + } + n++ + }) + + // search terms will become one string + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"find", "hello", "world"}) + + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + + stdout := s.Stdout() + c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary +hello +2.10 +canonical\*\* +- +GNU Hello, the "hello world" snap +hello-world +6.1 +canonical\*\* +- +Hello world example +`) + c.Check(s.Stderr(), check.Equals, "") + + s.ResetStdStreams() + + // search terms are already joined in the command line + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"find", "hello world"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + // with same output + c.Check(s.Stdout(), check.Equals, stdout) + c.Check(s.Stderr(), check.Equals, "") + + s.ResetStdStreams() +} + const findHelloJSON = ` { "type": "sync", @@ -234,7 +340,7 @@ c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical\* +- +GNU Hello, the "hello world" snap +hello +2.10 +canonical\*\* +- +GNU Hello, the "hello world" snap hello-huge +1.0 +noise +- +a really big snap `) c.Check(s.Stderr(), check.Equals, "") @@ -261,7 +367,7 @@ c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical\* +- +GNU Hello, the "hello world" snap +hello +2.10 +canonical\*\* +- +GNU Hello, the "hello world" snap hello-huge +1.0 +noise +- +a really big snap `) c.Check(s.Stderr(), check.Equals, "") @@ -324,7 +430,7 @@ c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical\* +1.99GBP +GNU Hello, the "hello world" snap +hello +2.10 +canonical\*\* +1.99GBP +GNU Hello, the "hello world" snap `) c.Check(s.Stderr(), check.Equals, "") } @@ -385,7 +491,7 @@ c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical\* +bought +GNU Hello, the "hello world" snap +hello +2.10 +canonical\*\* +bought +GNU Hello, the "hello world" snap `) c.Check(s.Stderr(), check.Equals, "") } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_first_boot.go snapd-2.57.5+20.04/cmd/snap/cmd_first_boot.go --- snapd-2.55.5+20.04/cmd/snap/cmd_first_boot.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_first_boot.go 2022-10-17 16:25:18.000000000 +0000 @@ -39,7 +39,7 @@ // WARNING: do not remove this command, older systems may still have // a systemd snapd.firstboot.service job in /etc/systemd/system -// that we did not cleanup. so we need this dummy command or +// that we did not cleanup. so we need this sample command or // those units will start failing. func (x *cmdInternalFirstBoot) Execute(args []string) error { if len(args) > 0 { diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_info.go snapd-2.57.5+20.04/cmd/snap/cmd_info.go --- snapd-2.55.5+20.04/cmd/snap/cmd_info.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_info.go 2022-10-17 16:25:18.000000000 +0000 @@ -28,7 +28,6 @@ "text/tabwriter" "time" "unicode" - "unicode/utf8" "github.com/jessevdk/go-flags" "gopkg.in/yaml.v2" @@ -98,7 +97,7 @@ fmt.Fprintln(iw, "health:") fmt.Fprintf(iw, " status:\t%s\n", health.Status) if health.Message != "" { - wrapGeneric(iw, quotedIfNeeded(health.Message), " message:\t", " ", iw.termWidth) + strutil.WordWrap(iw, quotedIfNeeded(health.Message), " message:\t", " ", iw.termWidth) } if health.Code != "" { fmt.Fprintf(iw, " code:\t%s\n", health.Code) @@ -139,98 +138,10 @@ return path } -// runesTrimRightSpace returns text, with any trailing whitespace dropped. -func runesTrimRightSpace(text []rune) []rune { - j := len(text) - for j > 0 && unicode.IsSpace(text[j-1]) { - j-- - } - return text[:j] -} - -// runesLastIndexSpace returns the index of the last whitespace rune -// in the text. If the text has no whitespace, returns -1. -func runesLastIndexSpace(text []rune) int { - for i := len(text) - 1; i >= 0; i-- { - if unicode.IsSpace(text[i]) { - return i - } - } - return -1 -} - -// wrapLine wraps a line, assumed to be part of a block-style yaml -// string, to fit into termWidth, preserving the line's indent, and -// writes it out prepending padding to each line. -func wrapLine(out io.Writer, text []rune, pad string, termWidth int) error { - // discard any trailing whitespace - text = runesTrimRightSpace(text) - // establish the indent of the whole block - idx := 0 - for idx < len(text) && unicode.IsSpace(text[idx]) { - idx++ - } - indent := pad + string(text[:idx]) - text = text[idx:] - if len(indent) > termWidth/2 { - // If indent is too big there's not enough space for the actual - // text, in the pathological case the indent can even be bigger - // than the terminal which leads to lp:1828425. - // Rather than let that happen, give up. - indent = pad + " " - } - return wrapGeneric(out, text, indent, indent, termWidth) -} - // wrapFlow wraps the text using yaml's flow style, allowing indent // characters for the first line. func wrapFlow(out io.Writer, text []rune, indent string, termWidth int) error { - return wrapGeneric(out, text, indent, " ", termWidth) -} - -// wrapGeneric wraps the given text to the given width, prefixing the -// first line with indent and the remaining lines with indent2 -func wrapGeneric(out io.Writer, text []rune, indent, indent2 string, termWidth int) error { - // Note: this is _wrong_ for much of unicode (because the width of a rune on - // the terminal is anything between 0 and 2, not always 1 as this code - // assumes) but fixing that is Hard. Long story short, you can get close - // using a couple of big unicode tables (which is what wcwidth - // does). Getting it 100% requires a terminfo-alike of unicode behaviour. - // However, before this we'd count bytes instead of runes, so we'd be - // even more broken. Think of it as successive approximations... at least - // with this work we share tabwriter's opinion on the width of things! - - // This (and possibly printDescr below) should move to strutil once - // we're happy with it getting wider (heh heh) use. - - indentWidth := utf8.RuneCountInString(indent) - delta := indentWidth - utf8.RuneCountInString(indent2) - width := termWidth - indentWidth - - // establish the indent of the whole block - var err error - for len(text) > width && err == nil { - // find a good place to chop the text - idx := runesLastIndexSpace(text[:width+1]) - if idx < 0 { - // there's no whitespace; just chop at line width - idx = width - } - _, err = fmt.Fprint(out, indent, string(text[:idx]), "\n") - // prune any remaining whitespace before the start of the next line - for idx < len(text) && unicode.IsSpace(text[idx]) { - idx++ - } - text = text[idx:] - width += delta - indent = indent2 - delta = 0 - } - if err != nil { - return err - } - _, err = fmt.Fprint(out, indent, string(text), "\n") - return err + return strutil.WordWrap(out, text, indent, " ", termWidth) } func quotedIfNeeded(raw string) []rune { @@ -258,7 +169,7 @@ var err error descr = strings.TrimRightFunc(descr, unicode.IsSpace) for _, line := range strings.Split(descr, "\n") { - err = wrapLine(w, []rune(line), " ", termWidth) + err = strutil.WordWrapPadded(w, []rune(line), " ", termWidth) if err != nil { break } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_info_test.go snapd-2.57.5+20.04/cmd/snap/cmd_info_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_info_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_info_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -436,7 +436,7 @@ name: hello summary: GNU Hello, the "hello world" snap -publisher: Canonical* +publisher: Canonical** license: Proprietary price: 1.99GBP description: | @@ -471,7 +471,7 @@ c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: GNU Hello, the "hello world" snap -publisher: Canonical* +publisher: Canonical** license: Proprietary price: 1.99GBP description: | @@ -593,7 +593,7 @@ c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: Canonical* +publisher: Canonical** license: MIT description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -699,7 +699,7 @@ message: please configure the grawflit checked: 2019-05-13T16:27:01+01:00 revision: 1 -publisher: Canonical* +publisher: Canonical** license: BSD-3 description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -757,7 +757,7 @@ c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: Canonical* +publisher: Canonical** license: unset description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -793,7 +793,7 @@ c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: Canonical* +publisher: Canonical** store-url: https://snapcraft.io/hello license: unset description: | @@ -820,7 +820,7 @@ refreshDate := isoDateTimeToLocalDate(c, "2006-01-02T22:04:07.123456789Z") c.Check(s.Stdout(), check.Equals, fmt.Sprintf(`name: hello summary: The GNU Hello snap -publisher: Canonical* +publisher: Canonical** store-url: https://snapcraft.io/hello license: unset description: | @@ -893,7 +893,7 @@ c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: Canonical* +publisher: Canonical** license: unset description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -1040,20 +1040,6 @@ } } -func (infoSuite) TestWrapCornerCase(c *check.C) { - // this particular corner case isn't currently reachable from - // printDescr nor printSummary, but best to have it covered - var buf bytes.Buffer - const s = "This is a paragraph indented with leading spaces that are encoded as multiple bytes. All hail EN SPACE." - snap.WrapFlow(&buf, []rune(s), "\u2002\u2002", 30) - c.Check(buf.String(), check.Equals, ` -  This is a paragraph indented - with leading spaces that are - encoded as multiple bytes. - All hail EN SPACE. -`[1:]) -} - func (infoSuite) TestBug1828425(c *check.C) { const s = `This is a description that has @@ -1132,7 +1118,7 @@ // make sure local and remote info is combined in the output c.Check(s.Stdout(), check.Equals, fmt.Sprintf(`name: hello_foo summary: The GNU Hello snap -publisher: Canonical* +publisher: Canonical** store-url: https://snapcraft.io/hello license: unset description: | @@ -1212,7 +1198,7 @@ // make sure local and remote info is combined in the output c.Check(s.Stdout(), check.Equals, fmt.Sprintf(`name: hello summary: The GNU Hello snap -publisher: Canonical* +publisher: Canonical** store-url: https://snapcraft.io/hello license: unset description: | diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_keys.go snapd-2.57.5+20.04/cmd/snap/cmd_keys.go --- snapd-2.55.5+20.04/cmd/snap/cmd_keys.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_keys.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,6 +25,7 @@ "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/asserts/signtool" "github.com/snapcore/snapd/i18n" ) @@ -85,7 +86,7 @@ return ErrExtraArgs } - keypairMgr, err := getKeypairManager() + keypairMgr, err := signtool.GetKeypairManager() if err != nil { return err } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_model.go snapd-2.57.5+20.04/cmd/snap/cmd_model.go --- snapd-2.55.5+20.04/cmd/snap/cmd_model.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_model.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,13 +22,11 @@ import ( "errors" "fmt" - "strings" - "time" "github.com/jessevdk/go-flags" - "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/client/clientutil" "github.com/snapcore/snapd/i18n" ) @@ -48,36 +46,33 @@ model assertion. `) - invalidTypeMessage = i18n.G("invalid type for %q header") errNoMainAssertion = errors.New(i18n.G("device not ready yet (no assertions found)")) errNoSerial = errors.New(i18n.G("device not registered yet (no serial assertion found)")) errNoVerboseAssertion = errors.New(i18n.G("cannot use --verbose with --assertion")) - - // this list is a "nice" "human" "readable" "ordering" of headers to print - // off, sorted in lexical order with meta headers and primary key headers - // removed, and big nasty keys such as device-key-sha3-384 and - // device-key at the bottom - // it also contains both serial and model assertion headers, but we - // follow the same code path for both assertion types and some of the - // headers are shared between the two, so it still works out correctly - niceOrdering = [...]string{ - "architecture", - "base", - "classic", - "display-name", - "gadget", - "kernel", - "revision", - "store", - "system-user-authority", - "timestamp", - "required-snaps", // for uc16 and uc18 models - "snaps", // for uc20 models - "device-key-sha3-384", - "device-key", - } ) +// cmdModelFormatter implements the interface required by clientutil.Print* +// functions, as it formats the output it requires some extra information from +// environment its called from. +type cmdModelFormatter struct { + client *client.Client + esc *escapes +} + +func (mf cmdModelFormatter) GetEscapedDash() string { + return mf.esc.dash +} + +func (mf cmdModelFormatter) LongPublisher(storeAccountID string) string { + storeAccount, err := mf.client.StoreAccount(storeAccountID) + if err != nil { + return "" + } + // use the longPublisher helper to format the brand store account + // like we do in `snap info` + return longPublisher(mf.esc, storeAccount) +} + type cmdModel struct { clientMixin timeMixin @@ -110,7 +105,6 @@ return errNoVerboseAssertion } - var mainAssertion asserts.Assertion serialAssertion, serialErr := x.client.CurrentSerialAssertion() modelAssertion, modelErr := x.client.CurrentModelAssertion() @@ -130,21 +124,12 @@ return serialErr } - if x.Serial { - mainAssertion = serialAssertion - } else { - mainAssertion = modelAssertion - } - if x.Assertion { // if we are using the serial assertion and we specifically didn't find the // serial assertion, bail with specific error if x.Serial && client.IsAssertionNotFoundError(serialErr) { return errNoMainAssertion } - - _, err := Stdout.Write(asserts.Encode(mainAssertion)) - return err } termWidth, _ := termSize() @@ -154,8 +139,6 @@ termWidth = 100 } - esc := x.getEscapes() - w := tabWriter() if x.Serial && client.IsAssertionNotFoundError(serialErr) { @@ -169,241 +152,24 @@ return errNoSerial } - // the rest of this function is the main flow for outputting either the - // model or serial assertion in normal or verbose mode - - // for the `snap model` case with no options, we don't want colons, we want - // to be like `snap version` - separator := ":" - if !x.Verbose && !x.Serial { - separator = "" + modelFormatter := cmdModelFormatter{ + esc: x.getEscapes(), + client: x.client, + } + opts := clientutil.PrintModelAssertionOptions{ + TermWidth: termWidth, + AbsTime: x.AbsTime, + Verbose: x.Verbose, + Assertion: x.Assertion, } - - // ordering of the primary keys for model: brand, model, serial - // ordering of primary keys for serial is brand-id, model, serial - - // output brand/brand-id - brandIDHeader := mainAssertion.HeaderString("brand-id") - modelHeader := mainAssertion.HeaderString("model") - // for the serial header, if there's no serial yet, it's not an error for - // model (and we already handled the serial error above) but need to add a - // parenthetical about the device not being registered yet - var serial string - if client.IsAssertionNotFoundError(serialErr) { - if x.Verbose || x.Serial { - // verbose and serial are yamlish, so we need to escape the dash - serial = esc.dash - } else { - serial = "-" + if x.Serial { + if err := clientutil.PrintSerialAssertionYAML(w, *serialAssertion, modelFormatter, opts); err != nil { + return err } - serial += " (device not registered yet)" - } else { - serial = serialAssertion.HeaderString("serial") - } - - // handle brand/brand-id and model/model + display-name differently on just - // `snap model` w/o opts - if x.Serial || x.Verbose { - fmt.Fprintf(w, "brand-id:\t%s\n", brandIDHeader) - fmt.Fprintf(w, "model:\t%s\n", modelHeader) } else { - // for the model command (not --serial) we want to show a publisher - // style display of "brand" instead of just "brand-id" - storeAccount, err := x.client.StoreAccount(brandIDHeader) - if err != nil { + if err := clientutil.PrintModelAssertion(w, *modelAssertion, serialAssertion, modelFormatter, opts); err != nil { return err } - // use the longPublisher helper to format the brand store account - // like we do in `snap info` - fmt.Fprintf(w, "brand%s\t%s\n", separator, longPublisher(x.getEscapes(), storeAccount)) - - // for model, if there's a display-name, we show that first with the - // real model in parenthesis - if displayName := modelAssertion.HeaderString("display-name"); displayName != "" { - modelHeader = fmt.Sprintf("%s (%s)", displayName, modelHeader) - } - fmt.Fprintf(w, "model%s\t%s\n", separator, modelHeader) - } - - // only output the grade if it is non-empty, either it is not in the model - // assertion for all non-uc20 model assertions, or it is non-empty and - // required for uc20 model assertions - grade := modelAssertion.HeaderString("grade") - if grade != "" { - fmt.Fprintf(w, "grade%s\t%s\n", separator, grade) - } - - storageSafety := modelAssertion.HeaderString("storage-safety") - if storageSafety != "" { - fmt.Fprintf(w, "storage-safety%s\t%s\n", separator, storageSafety) - } - - // serial is same for all variants - fmt.Fprintf(w, "serial%s\t%s\n", separator, serial) - - // --verbose means output more information - if x.Verbose { - allHeadersMap := mainAssertion.Headers() - - for _, headerName := range niceOrdering { - invalidTypeErr := fmt.Errorf(invalidTypeMessage, headerName) - - headerValue, ok := allHeadersMap[headerName] - // make sure the header is in the map - if !ok { - continue - } - - // switch on which header it is to handle some special cases - switch headerName { - // list of scalars - case "required-snaps", "system-user-authority": - headerIfaceList, ok := headerValue.([]interface{}) - if !ok { - return invalidTypeErr - } - if len(headerIfaceList) == 0 { - continue - } - fmt.Fprintf(w, "%s:\t\n", headerName) - for _, elem := range headerIfaceList { - headerStringElem, ok := elem.(string) - if !ok { - return invalidTypeErr - } - // note we don't wrap these, since for now this is - // specifically just required-snaps and so all of these - // will be snap names which are required to be short - fmt.Fprintf(w, " - %s\n", headerStringElem) - } - - //timestamp needs to be formatted with fmtTime from the timeMixin - case "timestamp": - timestamp, ok := headerValue.(string) - if !ok { - return invalidTypeErr - } - - // parse the time string as RFC3339, which is what the format is - // always in for assertions - t, err := time.Parse(time.RFC3339, timestamp) - if err != nil { - return err - } - fmt.Fprintf(w, "timestamp:\t%s\n", x.fmtTime(t)) - - // long string key we don't want to rewrap but can safely handle - // on "reasonable" width terminals - case "device-key-sha3-384": - // also flush the writer before continuing so the previous keys - // don't try to align with this key - w.Flush() - headerString, ok := headerValue.(string) - if !ok { - return invalidTypeErr - } - - switch { - case termWidth > 86: - fmt.Fprintf(w, "device-key-sha3-384: %s\n", headerString) - case termWidth <= 86 && termWidth > 66: - fmt.Fprintln(w, "device-key-sha3-384: |") - wrapLine(w, []rune(headerString), " ", termWidth) - } - case "snaps": - // also flush the writer before continuing so the previous keys - // don't try to align with this key - w.Flush() - snapsHeader, ok := headerValue.([]interface{}) - if !ok { - return invalidTypeErr - } - if len(snapsHeader) == 0 { - // unexpected why this is an empty list, but just ignore for - // now - continue - } - fmt.Fprintf(w, "snaps:\n") - for _, sn := range snapsHeader { - snMap, ok := sn.(map[string]interface{}) - if !ok { - return invalidTypeErr - } - // iterate over all keys in the map in a stable, visually - // appealing ordering - // first do snap name, which will always be present since we - // parsed a valid assertion - name := snMap["name"].(string) - fmt.Fprintf(w, " - name:\t%s\n", name) - - // the rest of these may be absent, but they are all still - // simple strings - for _, snKey := range []string{"id", "type", "default-channel", "presence"} { - snValue, ok := snMap[snKey] - if !ok { - continue - } - snStrValue, ok := snValue.(string) - if !ok { - return invalidTypeErr - } - if snStrValue != "" { - fmt.Fprintf(w, " %s:\t%s\n", snKey, snStrValue) - } - } - - // finally handle "modes" which is a list - modes, ok := snMap["modes"] - if !ok { - continue - } - modesSlice, ok := modes.([]interface{}) - if !ok { - return invalidTypeErr - } - if len(modesSlice) == 0 { - continue - } - - modeStrSlice := make([]string, 0, len(modesSlice)) - for _, mode := range modesSlice { - modeStr, ok := mode.(string) - if !ok { - return invalidTypeErr - } - modeStrSlice = append(modeStrSlice, modeStr) - } - modesSliceYamlStr := "[" + strings.Join(modeStrSlice, ", ") + "]" - fmt.Fprintf(w, " modes:\t%s\n", modesSliceYamlStr) - } - - // long base64 key we can rewrap safely - case "device-key": - headerString, ok := headerValue.(string) - if !ok { - return invalidTypeErr - } - // the string value here has newlines inserted as part of the - // raw assertion, but base64 doesn't care about whitespace, so - // it's safe to split by newlines and re-wrap to make it - // prettier - headerString = strings.Join( - strings.Split(headerString, "\n"), - "") - fmt.Fprintln(w, "device-key: |") - wrapLine(w, []rune(headerString), " ", termWidth) - - // the default is all the rest of short scalar values, which all - // should be strings - default: - headerString, ok := headerValue.(string) - if !ok { - return invalidTypeErr - } - fmt.Fprintf(w, "%s:\t%s\n", headerName, headerString) - } - } } - return w.Flush() } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_model_test.go snapd-2.57.5+20.04/cmd/snap/cmd_model_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_model_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_model_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -352,7 +352,7 @@ modelF: simpleHappyResponder(happyModelAssertionResponse), serialF: simpleHappyResponder(happySerialAssertionResponse), outText: ` -brand MeMeMe (meuser*) +brand MeMeMe (meuser**) model test-model serial serialserial `[1:], @@ -362,7 +362,7 @@ modelF: simpleHappyResponder(happyUC20ModelAssertionResponse), serialF: simpleHappyResponder(happySerialUC20AssertionResponse), outText: ` -brand MeMeMe (meuser*) +brand MeMeMe (meuser**) model test-snapd-core-20-amd64 grade dangerous storage-safety prefer-encrypted @@ -374,7 +374,7 @@ modelF: simpleHappyResponder(happyModelWithDisplayNameAssertionResponse), serialF: simpleHappyResponder(happySerialAssertionResponse), outText: ` -brand MeMeMe (meuser*) +brand MeMeMe (meuser**) model Model Name (test-model) serial serialserial `[1:], @@ -384,7 +384,7 @@ modelF: simpleHappyResponder(happyModelAssertionResponse), serialF: simpleUnhappyResponder(noSerialAssertionYetResponse), outText: ` -brand MeMeMe (meuser*) +brand MeMeMe (meuser**) model test-model serial - (device not registered yet) `[1:], diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_prepare_image.go snapd-2.57.5+20.04/cmd/snap/cmd_prepare_image.go --- snapd-2.55.5+20.04/cmd/snap/cmd_prepare_image.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_prepare_image.go 2022-10-17 16:25:18.000000000 +0000 @@ -32,8 +32,12 @@ ) type cmdPrepareImage struct { - Classic bool `long:"classic"` - Architecture string `long:"arch"` + Classic bool `long:"classic"` + Preseed bool `long:"preseed"` + PreseedSignKey string `long:"preseed-sign-key"` + // optional path to AppArmor kernel features directory + AppArmorKernelFeaturesDir string `long:"apparmor-features-dir"` + Architecture string `long:"arch"` Positional struct { ModelAssertionFn string @@ -65,6 +69,12 @@ // TRANSLATORS: This should not start with a lowercase letter. "classic": i18n.G("Enable classic mode to prepare a classic model image"), // TRANSLATORS: This should not start with a lowercase letter. + "preseed": i18n.G("Preseed (UC20+ only)"), + // TRANSLATORS: This should not start with a lowercase letter. + "preseed-sign-key": i18n.G("Name of the key to use to sign preseed assertion, otherwise use the default key"), + // TRANSLATORS: This should not start with a lowercase letter. + "apparmor-features-dir": i18n.G("Optional path to apparmor kernel features directory (UC20+ only)"), + // TRANSLATORS: This should not start with a lowercase letter. "arch": i18n.G("Specify an architecture for snaps for --classic when the model does not"), // TRANSLATORS: This should not start with a lowercase letter. "snap": i18n.G("Include the given snap from the store or a local file and/or specify the channel to track for the given snap"), @@ -132,6 +142,13 @@ opts.PrepareDir = x.Positional.TargetDir opts.Classic = x.Classic + if x.PreseedSignKey != "" && !x.Preseed { + return fmt.Errorf("--preseed-sign-key cannot be used without --preseed") + } + opts.Preseed = x.Preseed + opts.PreseedSignKey = x.PreseedSignKey + opts.AppArmorKernelFeaturesDir = x.AppArmorKernelFeaturesDir + return imagePrepare(opts) } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_prepare_image_test.go snapd-2.57.5+20.04/cmd/snap/cmd_prepare_image_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_prepare_image_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_prepare_image_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -173,3 +173,30 @@ }, }) } + +func (s *SnapPrepareImageSuite) TestPrepareImagePreseedArgError(c *C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "--preseed-sign-key", "key", "model", "prepare-dir"}) + c.Assert(err, ErrorMatches, `--preseed-sign-key cannot be used without --preseed`) +} + +func (s *SnapPrepareImageSuite) TestPrepareImagePreseed(c *C) { + var opts *image.Options + prep := func(o *image.Options) error { + opts = o + return nil + } + r := snap.MockImagePrepare(prep) + defer r() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "--preseed", "--preseed-sign-key", "key", "--apparmor-features-dir", "aafeatures-dir", "model", "prepare-dir"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + c.Check(opts, DeepEquals, &image.Options{ + ModelFile: "model", + PrepareDir: "prepare-dir", + Preseed: true, + PreseedSignKey: "key", + AppArmorKernelFeaturesDir: "aafeatures-dir", + }) +} diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_quota.go snapd-2.57.5+20.04/cmd/snap/cmd_quota.go --- snapd-2.55.5+20.04/cmd/snap/cmd_quota.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_quota.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,6 +25,7 @@ "sort" "strconv" "strings" + "time" "github.com/jessevdk/go-flags" @@ -90,6 +91,10 @@ decrease the threads limit for a quota group, the entire group must be removed with the remove-quota command and recreated with a lower limit. +The journal limits can be increased and decreased after being set on a group. +Setting a journal limit will cause the snaps in the group to be put into the same +journal namespace. This will affect the behaviour of the log command. + New quotas can be set on existing quota groups, but existing quotas cannot be removed from a quota group, without removing and recreating the entire group. @@ -101,7 +106,17 @@ func init() { // TODO: unhide the commands when non-experimental - cmd := addCommand("set-quota", shortSetQuotaHelp, longSetQuotaHelp, func() flags.Commander { return &cmdSetQuota{} }, nil, nil) + cmd := addCommand("set-quota", shortSetQuotaHelp, longSetQuotaHelp, + func() flags.Commander { return &cmdSetQuota{} }, + waitDescs.also(map[string]string{ + "memory": i18n.G("Memory quota"), + "cpu": i18n.G("CPU quota"), + "cpu-set": i18n.G("CPU set quota"), + "threads": i18n.G("Threads quota"), + "journal-size": i18n.G("Journal size quota"), + "journal-rate-limit": i18n.G("Journal rate limit as /"), + "parent": i18n.G("Parent quota group"), + }), nil) cmd.hidden = true cmd = addCommand("quota", shortQuotaHelp, longQuotaHelp, func() flags.Commander { return &cmdQuota{} }, nil, nil) @@ -117,12 +132,14 @@ type cmdSetQuota struct { waitMixin - MemoryMax string `long:"memory" optional:"true"` - CPUMax string `long:"cpu" optional:"true"` - CPUSet string `long:"cpu-set" optional:"true"` - ThreadsMax string `long:"threads" optional:"true"` - Parent string `long:"parent" optional:"true"` - Positional struct { + MemoryMax string `long:"memory" optional:"true"` + CPUMax string `long:"cpu" optional:"true"` + CPUSet string `long:"cpu-set" optional:"true"` + ThreadsMax string `long:"threads" optional:"true"` + JournalSizeMax string `long:"journal-size" optional:"true"` + JournalRateLimit string `long:"journal-rate-limit" optional:"true"` + Parent string `long:"parent" optional:"true"` + Positional struct { GroupName string `positional-arg-name:"" required:"true"` Snaps []installedSnapName `positional-arg-name:"" optional:"true"` } `positional-args:"yes"` @@ -157,23 +174,39 @@ return count, percentage, nil } -func parseQuotas(maxMemory string, cpuMax string, cpuSet string, threadsMax string) (*client.QuotaValues, error) { - var mem int64 - var cpuCount int - var cpuPercentage int - var cpus []int - var threads int +func parseJournalRateQuota(journalRateLimit string) (count int, period time.Duration, err error) { + // the rate limit is a string of the form N/P, where N is the number of + // messages and P is the period as a time string (e.g 5s) + parts := strings.Split(journalRateLimit, "/") + if len(parts) != 2 { + return 0, 0, fmt.Errorf("rate limit must be of the form /") + } + + count, err = strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, fmt.Errorf("cannot parse message count: %v", err) + } + + period, err = time.ParseDuration(parts[1]) + if err != nil { + return 0, 0, fmt.Errorf("cannot parse period: %v", err) + } + return count, period, nil +} + +func (x *cmdSetQuota) parseQuotas() (*client.QuotaValues, error) { + var quotaValues client.QuotaValues - if maxMemory != "" { - value, err := strutil.ParseByteSize(maxMemory) + if x.MemoryMax != "" { + value, err := strutil.ParseByteSize(x.MemoryMax) if err != nil { return nil, err } - mem = value + quotaValues.Memory = quantity.Size(value) } - if cpuMax != "" { - countValue, percentageValue, err := parseCpuQuota(cpuMax) + if x.CPUMax != "" { + countValue, percentageValue, err := parseCpuQuota(x.CPUMax) if err != nil { return nil, err } @@ -181,12 +214,15 @@ return nil, fmt.Errorf("cannot use value %v: cpu quota percentage must be between 1 and 100", percentageValue) } - cpuCount = countValue - cpuPercentage = percentageValue + quotaValues.CPU = &client.QuotaCPUValues{ + Count: countValue, + Percentage: percentageValue, + } } - if cpuSet != "" { - cpuTokens := strutil.CommaSeparatedList(cpuSet) + if x.CPUSet != "" { + var cpus []int + cpuTokens := strutil.CommaSeparatedList(x.CPUSet) for _, cpuToken := range cpuTokens { cpu, err := strconv.ParseUint(cpuToken, 10, 32) if err != nil { @@ -194,31 +230,52 @@ } cpus = append(cpus, int(cpu)) } + + quotaValues.CPUSet = &client.QuotaCPUSetValues{ + CPUs: cpus, + } } - if threadsMax != "" { - value, err := strconv.ParseUint(threadsMax, 10, 32) + if x.ThreadsMax != "" { + value, err := strconv.ParseUint(x.ThreadsMax, 10, 32) if err != nil { - return nil, fmt.Errorf("cannot use threads value %q", threadsMax) + return nil, fmt.Errorf("cannot use threads value %q", x.ThreadsMax) } - threads = int(value) + quotaValues.Threads = int(value) } - return &client.QuotaValues{ - Memory: quantity.Size(mem), - CPU: &client.QuotaCPUValues{ - Count: cpuCount, - Percentage: cpuPercentage, - }, - CPUSet: &client.QuotaCPUSetValues{ - CPUs: cpus, - }, - Threads: threads, - }, nil + if x.JournalSizeMax != "" || x.JournalRateLimit != "" { + quotaValues.Journal = &client.QuotaJournalValues{} + if x.JournalSizeMax != "" { + value, err := strutil.ParseByteSize(x.JournalSizeMax) + if err != nil { + return nil, fmt.Errorf("cannot parse journal size %q: %v", x.JournalSizeMax, err) + } + quotaValues.Journal.Size = quantity.Size(value) + } + + if x.JournalRateLimit != "" { + count, period, err := parseJournalRateQuota(x.JournalRateLimit) + if err != nil { + return nil, fmt.Errorf("cannot parse journal rate limit %q: %v", x.JournalRateLimit, err) + } + quotaValues.Journal.QuotaJournalRate = &client.QuotaJournalRate{ + RateCount: count, + RatePeriod: period, + } + } + } + + return "aValues, nil +} + +func (x *cmdSetQuota) hasQuotaSet() bool { + return x.MemoryMax != "" || x.CPUMax != "" || x.CPUSet != "" || + x.ThreadsMax != "" || x.JournalSizeMax != "" || x.JournalRateLimit != "" } func (x *cmdSetQuota) Execute(args []string) (err error) { - quotaProvided := x.MemoryMax != "" || x.CPUMax != "" || x.CPUSet != "" || x.ThreadsMax != "" + quotaProvided := x.hasQuotaSet() names := installedSnapNames(x.Positional.Snaps) @@ -259,7 +316,7 @@ // we have a limits to set for this group, so specify that along // with whatever snaps may have been provided and whatever parent may // have been specified - quotaValues, err := parseQuotas(x.MemoryMax, x.CPUMax, x.CPUSet, x.ThreadsMax) + quotaValues, err := x.parseQuotas() if err != nil { return err } @@ -351,6 +408,17 @@ if group.Constraints.Threads != 0 { fmt.Fprintf(w, " threads:\t%d\n", group.Constraints.Threads) } + if group.Constraints.Journal != nil { + if group.Constraints.Journal.Size != 0 { + val := strings.TrimSpace(fmtSize(int64(group.Constraints.Journal.Size))) + fmt.Fprintf(w, " journal-size:\t%s\n", val) + } + if group.Constraints.Journal.QuotaJournalRate != nil { + fmt.Fprintf(w, " journal-rate:\t%d/%s\n", + group.Constraints.Journal.RateCount, + group.Constraints.Journal.RatePeriod) + } + } memoryUsage := "0B" currentThreads := 0 @@ -438,9 +506,8 @@ // format cpu constraint as cpu=NxM%,cpu-set=x,y,z if q.Constraints.CPU != nil { if q.Constraints.CPU.Count != 0 { - grpConstraints = append(grpConstraints, fmt.Sprintf("cpu=%dx", q.Constraints.CPU.Count)) - } - if q.Constraints.CPU.Percentage != 0 { + grpConstraints = append(grpConstraints, fmt.Sprintf("cpu=%dx%d%%", q.Constraints.CPU.Count, q.Constraints.CPU.Percentage)) + } else { grpConstraints = append(grpConstraints, fmt.Sprintf("cpu=%d%%", q.Constraints.CPU.Percentage)) } } @@ -455,6 +522,19 @@ grpConstraints = append(grpConstraints, "threads="+strconv.Itoa(q.Constraints.Threads)) } + // format journal constraint as journal-size=xMB,journal-rate=x/y + if q.Constraints.Journal != nil { + if q.Constraints.Journal.Size != 0 { + grpConstraints = append(grpConstraints, "journal-size="+strings.TrimSpace(fmtSize(int64(q.Constraints.Journal.Size)))) + } + + if q.Constraints.Journal.QuotaJournalRate != nil { + grpConstraints = append(grpConstraints, + fmt.Sprintf("journal-rate=%d/%s", + q.Constraints.Journal.RateCount, q.Constraints.Journal.RatePeriod)) + } + } + // format current resource values as memory=N,threads=N var grpCurrent []string if q.Current != nil { diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_quota_test.go snapd-2.57.5+20.04/cmd/snap/cmd_quota_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_quota_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_quota_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -35,12 +35,23 @@ type quotaSuite struct { BaseSnapSuite + quotaGetGroupHandlerCalls int + quotaGetGroupsHandlerCalls int + quotaPostHandlerCalls int } var _ = check.Suite("aSuite{}) -func makeFakeGetQuotaGroupNotFoundHandler(c *check.C, group string) func(w http.ResponseWriter, r *http.Request) { +func (s *quotaSuite) SetUpTest(c *check.C) { + s.BaseSnapSuite.SetUpTest(c) + s.quotaGetGroupHandlerCalls = 0 + s.quotaGetGroupsHandlerCalls = 0 + s.quotaPostHandlerCalls = 0 +} + +func (s *quotaSuite) makeFakeGetQuotaGroupNotFoundHandler(c *check.C, group string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { + s.quotaGetGroupHandlerCalls++ c.Check(r.URL.Path, check.Equals, "/v2/quotas/"+group) c.Check(r.Method, check.Equals, "GET") w.WriteHeader(404) @@ -56,13 +67,9 @@ } -func makeFakeGetQuotaGroupHandler(c *check.C, body string) func(w http.ResponseWriter, r *http.Request) { - var called bool +func (s *quotaSuite) makeFakeGetQuotaGroupHandler(c *check.C, body string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if called { - c.Fatalf("expected a single request") - } - called = true + s.quotaGetGroupHandlerCalls++ c.Check(r.URL.Path, check.Equals, "/v2/quotas/foo") c.Check(r.Method, check.Equals, "GET") w.WriteHeader(200) @@ -70,13 +77,9 @@ } } -func makeFakeGetQuotaGroupsHandler(c *check.C, body string) func(w http.ResponseWriter, r *http.Request) { - var called bool +func (s *quotaSuite) makeFakeGetQuotaGroupsHandler(c *check.C, body string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if called { - c.Fatalf("expected a single request") - } - called = true + s.quotaGetGroupsHandlerCalls++ c.Check(r.URL.Path, check.Equals, "/v2/quotas") c.Check(r.Method, check.Equals, "GET") w.WriteHeader(200) @@ -131,13 +134,9 @@ Constraints quotasEnsureBodyConstraints `json:"constraints,omitempty"` } -func makeFakeQuotaPostHandler(c *check.C, opts fakeQuotaGroupPostHandlerOpts) func(w http.ResponseWriter, r *http.Request) { - var called bool +func (s *quotaSuite) makeFakeQuotaPostHandler(c *check.C, opts fakeQuotaGroupPostHandlerOpts) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if called { - c.Fatalf("expected a single request") - } - called = true + s.quotaPostHandlerCalls++ c.Check(r.URL.Path, check.Equals, "/v2/quotas") c.Check(r.Method, check.Equals, "POST") @@ -204,20 +203,28 @@ func (s *quotaSuite) TestParseQuotas(c *check.C) { for _, testData := range []struct { - maxMemory string - cpuMax string - cpuSet string - threadsMax string + maxMemory string + cpuMax string + cpuSet string + threadsMax string + journalSizeMax string + journalRateLimit string // Use the JSON representation of the quota, as it's easier to handle in the test data quotas string err string }{ - {maxMemory: "12KB", quotas: `{"memory":12000,"cpu":{},"cpu-set":{}}`}, - {cpuMax: "12x40%", quotas: `{"cpu":{"count":12,"percentage":40},"cpu-set":{}}`}, - {cpuMax: "40%", quotas: `{"cpu":{"percentage":40},"cpu-set":{}}`}, - {cpuSet: "1,3", quotas: `{"cpu":{},"cpu-set":{"cpus":[1,3]}}`}, - {threadsMax: "2", quotas: `{"cpu":{},"cpu-set":{},"threads":2}`}, + {maxMemory: "12KB", quotas: `{"memory":12000}`}, + {cpuMax: "12x40%", quotas: `{"cpu":{"count":12,"percentage":40}}`}, + {cpuMax: "40%", quotas: `{"cpu":{"percentage":40}}`}, + {cpuSet: "1,3", quotas: `{"cpu-set":{"cpus":[1,3]}}`}, + {threadsMax: "2", quotas: `{"threads":2}`}, + {journalSizeMax: "16MB", quotas: `{"journal":{"size":16000000}}`}, + {journalRateLimit: "10/15s", quotas: `{"journal":{"rate-count":10,"rate-period":15000000000}}`}, + {journalRateLimit: "1500/15ms", quotas: `{"journal":{"rate-count":1500,"rate-period":15000000}}`}, + {journalRateLimit: "1/15us", quotas: `{"journal":{"rate-count":1,"rate-period":15000}}`}, + {journalRateLimit: "0/0s", quotas: `{"journal":{"rate-count":0,"rate-period":0}}`}, + // Error cases {cpuMax: "ASD", err: `cannot parse cpu quota string "ASD"`}, {cpuMax: "0x100%", err: `cannot parse cpu quota string "0x100%"`}, @@ -230,8 +237,12 @@ {cpuSet: "0,-2", err: `cannot parse CPU set value "-2"`}, {threadsMax: "xxx", err: `cannot use threads value "xxx"`}, {threadsMax: "-3", err: `cannot use threads value "-3"`}, + {journalRateLimit: "0", err: `cannot parse journal rate limit "0": rate limit must be of the form /`}, + {journalRateLimit: "x/5m", err: `cannot parse journal rate limit "x/5m": cannot parse message count: strconv.Atoi: parsing "x": invalid syntax`}, + {journalRateLimit: "1/wow", err: `cannot parse journal rate limit "1/wow": cannot parse period: time: invalid duration ["]?wow["]?`}, } { - quotas, err := main.ParseQuotas(testData.maxMemory, testData.cpuMax, testData.cpuSet, testData.threadsMax) + quotas, err := main.ParseQuotaValues(testData.maxMemory, testData.cpuMax, + testData.cpuSet, testData.threadsMax, testData.journalSizeMax, testData.journalRateLimit) testLabel := check.Commentf("%v", testData) if testData.err == "" { c.Check(err, check.IsNil, testLabel) @@ -285,18 +296,20 @@ } }` routes := map[string]http.HandlerFunc{ - "/v2/quotas": makeFakeQuotaPostHandler( + "/v2/quotas": s.makeFakeQuotaPostHandler( c, fakeHandlerOpts, ), - "/v2/quotas/foo": makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(getJsonTemplate, 1000)), + "/v2/quotas/foo": s.makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(getJsonTemplate, 1000)), "/v2/changes/42": makeChangesHandler(c), } s.RedirectClientToTestServer(dispatchFakeHandlers(c, routes)) // ensure that --cpu still works with cgroup version 1 _, err := main.Parser(main.Client()).ParseArgs([]string{"set-quota", "--cpu=2x50%", "foo"}) - c.Assert(err, check.IsNil) + c.Check(err, check.IsNil) + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 1) + c.Check(s.quotaPostHandlerCalls, check.Equals, 1) } func (s *quotaSuite) TestGetQuotaGroup(c *check.C) { @@ -316,7 +329,7 @@ } }` - s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, json)) + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupHandler(c, json)) rest, err := main.Parser(main.Client()).ParseArgs([]string{"quota", "foo"}) c.Assert(err, check.IsNil) @@ -335,6 +348,8 @@ - snap-a - snap-b `[1:]) + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 1) + c.Check(s.quotaPostHandlerCalls, check.Equals, 0) } func (s *quotaSuite) TestGetMemoryQuotaGroupSimple(c *check.C) { @@ -348,7 +363,7 @@ } }` - s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 0))) + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 0))) outputTemplate := ` name: foo @@ -363,17 +378,20 @@ c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, fmt.Sprintf(outputTemplate, 0)) + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 1) + c.Check(s.quotaPostHandlerCalls, check.Equals, 0) s.stdout.Reset() s.stderr.Reset() - s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 500))) + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 500))) rest, err = main.Parser(main.Client()).ParseArgs([]string{"quota", "foo"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, fmt.Sprintf(outputTemplate, 500)) + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 2) } func (s *quotaSuite) TestGetCpuQuotaGroupSimple(c *check.C) { @@ -387,7 +405,7 @@ } }` - s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 16))) + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 16))) outputTemplate := ` name: foo @@ -405,17 +423,47 @@ c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, fmt.Sprintf(outputTemplate, 16)) + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 1) s.stdout.Reset() s.stderr.Reset() - s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 500))) + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(jsonTemplate, 500))) rest, err = main.Parser(main.Client()).ParseArgs([]string{"quota", "foo"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, fmt.Sprintf(outputTemplate, 500)) + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 2) +} + +func (s *quotaSuite) TestJournalQuotaGroupSimple(c *check.C) { + const jsonTemplate = `{ + "type": "sync", + "status-code": 200, + "result": { + "group-name": "foo", + "constraints": {"journal":{"size":1048576,"rate-count":50,"rate-period":60000000000}} + } + }` + + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupHandler(c, jsonTemplate)) + + outputTemplate := ` +name: foo +constraints: + journal-size: 1.05MB + journal-rate: 50/1m0s +current: +`[1:] + + rest, err := main.Parser(main.Client()).ParseArgs([]string{"quota", "foo"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, outputTemplate) + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 1) } func (s *quotaSuite) TestSetQuotaGroupCreateNew(c *check.C) { @@ -430,12 +478,12 @@ } routes := map[string]http.HandlerFunc{ - "/v2/quotas": makeFakeQuotaPostHandler( + "/v2/quotas": s.makeFakeQuotaPostHandler( c, fakeHandlerOpts, ), // the foo quota group is not found since it doesn't exist yet - "/v2/quotas/foo": makeFakeGetQuotaGroupNotFoundHandler(c, "foo"), + "/v2/quotas/foo": s.makeFakeGetQuotaGroupNotFoundHandler(c, "foo"), "/v2/changes/42": makeChangesHandler(c), } @@ -447,6 +495,8 @@ c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, "") + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 1) + c.Check(s.quotaPostHandlerCalls, check.Equals, 1) } func (s *quotaSuite) TestSetQuotaGroupUpdateExistingUnhappy(c *check.C) { @@ -486,15 +536,16 @@ } }` - s.RedirectClientToTestServer(makeFakeGetQuotaGroupHandler(c, getJson)) + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupHandler(c, getJson)) } else { - s.RedirectClientToTestServer(makeFakeGetQuotaGroupNotFoundHandler(c, "foo")) + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupNotFoundHandler(c, "foo")) } cmdArgs := append([]string{"set-quota", "foo"}, args...) _, err := main.Parser(main.Client()).ParseArgs(cmdArgs) c.Assert(err, check.ErrorMatches, errPattern) c.Check(s.Stdout(), check.Equals, "") + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 1) } func (s *quotaSuite) TestSetQuotaGroupUpdateExisting(c *check.C) { @@ -517,11 +568,11 @@ }` routes := map[string]http.HandlerFunc{ - "/v2/quotas": makeFakeQuotaPostHandler( + "/v2/quotas": s.makeFakeQuotaPostHandler( c, fakeHandlerOpts, ), - "/v2/quotas/foo": makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(getJsonTemplate, 1000)), + "/v2/quotas/foo": s.makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(getJsonTemplate, 1000)), "/v2/changes/42": makeChangesHandler(c), } @@ -533,6 +584,8 @@ c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, "") + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 1) + c.Check(s.quotaPostHandlerCalls, check.Equals, 1) s.stdout.Reset() s.stderr.Reset() @@ -545,12 +598,12 @@ } routes = map[string]http.HandlerFunc{ - "/v2/quotas": makeFakeQuotaPostHandler( + "/v2/quotas": s.makeFakeQuotaPostHandler( c, fakeHandlerOpts2, ), // the group was updated to have a 2000 memory limit now - "/v2/quotas/foo": makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(getJsonTemplate, 2000)), + "/v2/quotas/foo": s.makeFakeGetQuotaGroupHandler(c, fmt.Sprintf(getJsonTemplate, 2000)), "/v2/changes/42": makeChangesHandler(c), } @@ -563,6 +616,8 @@ c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, "") + c.Check(s.quotaGetGroupHandlerCalls, check.Equals, 2) + c.Check(s.quotaPostHandlerCalls, check.Equals, 2) } func (s *quotaSuite) TestRemoveQuotaGroup(c *check.C) { @@ -574,7 +629,7 @@ } routes := map[string]http.HandlerFunc{ - "/v2/quotas": makeFakeQuotaPostHandler(c, fakeHandlerOpts), + "/v2/quotas": s.makeFakeQuotaPostHandler(c, fakeHandlerOpts), "/v2/changes/42": makeChangesHandler(c), } @@ -586,13 +641,14 @@ c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, "") + c.Check(s.quotaPostHandlerCalls, check.Equals, 1) } func (s *quotaSuite) TestGetAllQuotaGroups(c *check.C) { restore := main.MockIsStdinTTY(true) defer restore() - s.RedirectClientToTestServer(makeFakeGetQuotaGroupsHandler(c, + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupsHandler(c, `{"type": "sync", "status-code": 200, "result": [ {"group-name":"aaa","subgroups":["ccc","ddd","fff"],"parent":"zzz","constraints":{"memory":1000}}, {"group-name":"ddd","parent":"aaa","constraints":{"memory":400}}, @@ -605,10 +661,12 @@ {"group-name":"fff","parent":"aaa","constraints":{"memory":1000},"current":{"memory":0}}, {"group-name":"xxx","constraints":{"memory":9900},"current":{"memory":10000}}, {"group-name":"cp0","constraints":{"memory":9900, "cpu":{"percentage":90}},"current":{"memory":10000}}, - {"group-name":"cp1","subgroups":["cps0"],"constraints":{"cpu":{"count":2, "percentage":90}}}, + {"group-name":"cp1","subgroups":["cps0","js0","js1"],"constraints":{"cpu":{"count":2, "percentage":90}}}, {"group-name":"cps0","parent":"cp1","constraints":{"cpu":{"percentage":40}}}, {"group-name":"cp2","subgroups":["cps1"],"constraints":{"cpu":{"count":2,"percentage":100},"cpu-set":{"cpus":[0,1]}}}, - {"group-name":"cps1","parent":"cp2","constraints":{"memory":9900,"cpu":{"percentage":50},"cpu-set":{"cpus":[1]}},"current":{"memory":10000}} + {"group-name":"cps1","parent":"cp2","constraints":{"memory":9900,"cpu":{"percentage":50},"cpu-set":{"cpus":[1]}},"current":{"memory":10000}}, + {"group-name":"js0","parent":"cp1","constraints":{"journal":{"size":1048576,"rate-count":50,"rate-period":60000000000}}}, + {"group-name":"js1","parent":"cp1","constraints":{"journal":{"rate-count":0,"rate-period":0}}} ]}`)) rest, err := main.Parser(main.Client()).ParseArgs([]string{"quotas"}) @@ -616,42 +674,46 @@ c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, ` -Quota Parent Constraints Current -cp0 memory=9.9kB,cpu=90% memory=10.0kB -cp1 cpu=2x,cpu=90% -cps0 cp1 cpu=40% -cp2 cpu=2x,cpu=100%,cpu-set=0,1 -cps1 cp2 memory=9.9kB,cpu=50%,cpu-set=1 memory=10.0kB -ggg memory=1000B,threads=100 memory=3000B -hhh threads=100 -xxx memory=9.9kB memory=10.0kB -yyyyyyy memory=1000B -zzz memory=5000B -aaa zzz memory=1000B -ccc aaa memory=400B -ddd aaa memory=400B -fff aaa memory=1000B -bbb zzz memory=1000B memory=400B +Quota Parent Constraints Current +cp0 memory=9.9kB,cpu=90% memory=10.0kB +cp1 cpu=2x90% +cps0 cp1 cpu=40% +js0 cp1 journal-size=1.05MB,journal-rate=50/1m0s +js1 cp1 journal-rate=0/0s +cp2 cpu=2x100%,cpu-set=0,1 +cps1 cp2 memory=9.9kB,cpu=50%,cpu-set=1 memory=10.0kB +ggg memory=1000B,threads=100 memory=3000B +hhh threads=100 +xxx memory=9.9kB memory=10.0kB +yyyyyyy memory=1000B +zzz memory=5000B +aaa zzz memory=1000B +ccc aaa memory=400B +ddd aaa memory=400B +fff aaa memory=1000B +bbb zzz memory=1000B memory=400B `[1:]) + c.Check(s.quotaGetGroupsHandlerCalls, check.Equals, 1) } func (s *quotaSuite) TestGetAllQuotaGroupsInconsistencyError(c *check.C) { restore := main.MockIsStdinTTY(true) defer restore() - s.RedirectClientToTestServer(makeFakeGetQuotaGroupsHandler(c, + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupsHandler(c, `{"type": "sync", "status-code": 200, "result": [ {"group-name":"aaa","subgroups":["ccc"],"max-memory":1000}]}`)) _, err := main.Parser(main.Client()).ParseArgs([]string{"quotas"}) c.Assert(err, check.ErrorMatches, `internal error: inconsistent groups received, unknown subgroup "ccc"`) + c.Check(s.quotaGetGroupsHandlerCalls, check.Equals, 1) } func (s *quotaSuite) TestNoQuotaGroups(c *check.C) { restore := main.MockIsStdinTTY(true) defer restore() - s.RedirectClientToTestServer(makeFakeGetQuotaGroupsHandler(c, + s.RedirectClientToTestServer(s.makeFakeGetQuotaGroupsHandler(c, `{"type": "sync", "status-code": 200, "result": []}`)) rest, err := main.Parser(main.Client()).ParseArgs([]string{"quotas"}) @@ -659,4 +721,5 @@ c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, "No quota groups defined.\n") + c.Check(s.quotaGetGroupsHandlerCalls, check.Equals, 1) } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_recovery.go snapd-2.57.5+20.04/cmd/snap/cmd_recovery.go --- snapd-2.55.5+20.04/cmd/snap/cmd_recovery.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_recovery.go 2022-10-17 16:25:18.000000000 +0000 @@ -74,7 +74,9 @@ return err } fmt.Fprintf(w, "recovery:\t%s\n", srk.RecoveryKey) - fmt.Fprintf(w, "reinstall:\t%s\n", srk.ReinstallKey) + if srk.ReinstallKey != "" { + fmt.Fprintf(w, "reinstall:\t%s\n", srk.ReinstallKey) + } return nil } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_recovery_test.go snapd-2.57.5+20.04/cmd/snap/cmd_recovery_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_recovery_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_recovery_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -192,3 +192,30 @@ c.Check(s.Stderr(), Equals, "") c.Check(n, Equals, 1) } + +func (s *SnapSuite) TestRecoveryShowRecoveryKeyAloneHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/system-recovery-keys") + c.Check(r.URL.RawQuery, Equals, "") + fmt.Fprintln(w, `{"type": "sync", "result": {"recovery-key": "61665-00531-54469-09783-47273-19035-40077-28287"}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"recovery", "--show-keys"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, `recovery: 61665-00531-54469-09783-47273-19035-40077-28287 +`) + c.Check(s.Stderr(), Equals, "") + c.Check(n, Equals, 1) +} diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_routine_portal_info.go snapd-2.57.5+20.04/cmd/snap/cmd_routine_portal_info.go --- snapd-2.55.5+20.04/cmd/snap/cmd_routine_portal_info.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_routine_portal_info.go 2022-10-17 16:25:18.000000000 +0000 @@ -102,6 +102,11 @@ desktopFile = filepath.Base(app.DesktopFile) } + var commonID string + if app != nil { + commonID = app.CommonID + } + // Determine whether the snap has access to the network status // TODO: use direct API for asking about interface being connected if // that becomes available @@ -131,6 +136,9 @@ {{- if .DesktopFile}} DesktopFile={{.DesktopFile}} {{- end}} +{{- if .CommonID}} +CommonID={{.CommonID}} +{{- end}} HasNetworkStatus={{.HasNetworkStatus}} ` t := template.Must(template.New("portal-info").Parse(portalInfoTemplate)) @@ -138,11 +146,13 @@ Snap *client.Snap App *client.AppInfo DesktopFile string + CommonID string HasNetworkStatus bool }{ Snap: snap, App: app, DesktopFile: desktopFile, + CommonID: commonID, HasNetworkStatus: hasNetworkStatus, } if err := t.Execute(Stdout, data); err != nil { diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_routine_portal_info_test.go snapd-2.57.5+20.04/cmd/snap/cmd_routine_portal_info_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_routine_portal_info_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_routine_portal_info_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -72,6 +72,12 @@ "snap": "hello", "name": "universe", "desktop-file": "/path/to/hello_universe.desktop" + }, + { + "snap": "hello", + "name": "common-id", + "desktop-file": "/path/to/hello_common-id.desktop", + "common-id": "io.snapcraft.hello.common-id" } ], "contact": "mailto:snaps@canonical.com", @@ -140,6 +146,53 @@ `) c.Check(s.Stderr(), Equals, "") } + +func (s *SnapSuite) TestPortalInfoCommonID(c *C) { + restore := snap.MockCgroupSnapNameFromPid(func(pid int) (string, error) { + c.Check(pid, Equals, 42) + return "hello", nil + }) + defer restore() + restore = snap.MockApparmorSnapAppFromPid(func(pid int) (string, string, string, error) { + c.Check(pid, Equals, 42) + return "hello", "common-id", "", nil + }) + defer restore() + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/snaps/hello") + fmt.Fprint(w, mockInfoJSONWithApps) + case 1: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, url.Values{ + "snap": []string{"hello"}, + "interface": []string{"network-status"}, + }) + result := client.Connections{} + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + default: + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) + } + n++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "portal-info", "42"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, `[Snap Info] +InstanceName=hello +AppName=common-id +DesktopFile=hello_common-id.desktop +CommonID=io.snapcraft.hello.common-id +HasNetworkStatus=false +`) + c.Check(s.Stderr(), Equals, "") +} func (s *SnapSuite) TestPortalInfoNoAppInfo(c *C) { restore := snap.MockCgroupSnapNameFromPid(func(pid int) (string, error) { diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_run.go snapd-2.57.5+20.04/cmd/snap/cmd_run.go --- snapd-2.55.5+20.04/cmd/snap/cmd_run.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_run.go 2022-10-17 16:25:18.000000000 +0000 @@ -237,6 +237,8 @@ return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.HookName, strings.Join(args, " ")) } + logger.StartupStageTimestamp("start") + if err := maybeWaitForSecurityProfileRegeneration(x.client); err != nil { return err } @@ -1223,6 +1225,7 @@ logger.Debugf("snap refreshes will not be postponed by this process") } } + logger.StartupStageTimestamp("snap to snap-confine") if x.TraceExec { return x.runCmdWithTraceExec(cmd, envForExec) } else if x.Gdb { diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_services.go snapd-2.57.5+20.04/cmd/snap/cmd_services.go --- snapd-2.55.5+20.04/cmd/snap/cmd_services.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_services.go 2022-10-17 16:25:18.000000000 +0000 @@ -208,7 +208,7 @@ return err } - fmt.Fprintf(Stdout, i18n.G("Started.\n")) + fmt.Fprintln(Stdout, i18n.G("Started.")) return nil } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_sign_build.go snapd-2.57.5+20.04/cmd/snap/cmd_sign_build.go --- snapd-2.55.5+20.04/cmd/snap/cmd_sign_build.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_sign_build.go 2022-10-17 16:25:18.000000000 +0000 @@ -29,6 +29,7 @@ _ "golang.org/x/crypto/sha3" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/signtool" "github.com/snapcore/snapd/i18n" ) @@ -84,7 +85,7 @@ return err } - keypairMgr, err := getKeypairManager() + keypairMgr, err := signtool.GetKeypairManager() if err != nil { return err } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_sign.go snapd-2.57.5+20.04/cmd/snap/cmd_sign.go --- snapd-2.55.5+20.04/cmd/snap/cmd_sign.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_sign.go 2022-10-17 16:25:18.000000000 +0000 @@ -80,7 +80,7 @@ return fmt.Errorf(i18n.G("cannot read assertion input: %v"), err) } - keypairMgr, err := getKeypairManager() + keypairMgr, err := signtool.GetKeypairManager() if err != nil { return err } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_snap_op.go snapd-2.57.5+20.04/cmd/snap/cmd_snap_op.go --- snapd-2.55.5+20.04/cmd/snap/cmd_snap_op.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_snap_op.go 2022-10-17 16:25:18.000000000 +0000 @@ -132,7 +132,7 @@ changeID, err := x.client.Remove(name, opts) if err != nil { - msg, err := errorToCmdMessage(name, err, opts) + msg, err := errorToCmdMessage(name, "remove", err, opts) if err != nil { return err } @@ -159,7 +159,19 @@ names := installedSnapNames(x.Positional.Snaps) changeID, err := x.client.RemoveMany(names, opts) if err != nil { - return err + var name string + if cerr, ok := err.(*client.Error); ok { + if snapName, ok := cerr.Value.(string); ok { + name = snapName + } + } + + msg, err := errorToCmdMessage(name, "remove", err, opts) + if err != nil { + return err + } + fmt.Fprintln(Stderr, msg) + return nil } chg, err := x.wait(changeID) @@ -502,7 +514,7 @@ changeID, err = x.client.Install(snapName, opts) } if err != nil { - msg, err := errorToCmdMessage(nameOrPath, err, opts) + msg, err := errorToCmdMessage(nameOrPath, "install", err, opts) if err != nil { return err } @@ -559,7 +571,7 @@ if err, ok := err.(*client.Error); ok { snapName, _ = err.Value.(string) } - msg, err := errorToCmdMessage(snapName, err, opts) + msg, err := errorToCmdMessage(snapName, "install", err, opts) if err != nil { return err } @@ -704,7 +716,7 @@ func (x *cmdRefresh) refreshOne(name string, opts *client.SnapOptions) error { changeID, err := x.client.Refresh(name, opts) if err != nil { - msg, err := errorToCmdMessage(name, err, opts) + msg, err := errorToCmdMessage(name, "refresh", err, opts) if err != nil { return err } @@ -912,7 +924,7 @@ changeID, err := x.client.Try(path, opts) if err != nil { - msg, err := errorToCmdMessage(name, err, opts) + msg, err := errorToCmdMessage(name, "try", err, opts) if err != nil { return err } diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_snap_op_test.go snapd-2.57.5+20.04/cmd/snap/cmd_snap_op_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_snap_op_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_snap_op_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1825,6 +1825,48 @@ c.Assert(err, check.ErrorMatches, "Please specify a single channel") } +func (s *SnapOpSuite) TestNotInstalledError(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{ + "type": "error", + "result": { + "message": "Snap was not installed", + "kind": "snap-not-installed", + "status-code": 400 + }}`) + }) + + for _, t := range []struct { + cmd string + err bool + }{ + {cmd: "refresh foo", err: true}, + {cmd: "refresh foo bar", err: true}, + {cmd: "install foo", err: true}, + {cmd: "install foo bar", err: true}, + {cmd: "revert foo", err: true}, + {cmd: "switch --channel stable foo", err: true}, + {cmd: "switch --channel stable foo bar", err: true}, + {cmd: "enable foo", err: true}, + {cmd: "enable foo bar", err: true}, + {cmd: "disable foo", err: true}, + {cmd: "disable foo bar", err: true}, + {cmd: "list foo", err: true}, + {cmd: "list foo bar", err: true}, + {cmd: "save foo", err: true}, + {cmd: "save foo bar", err: true}, + {cmd: "remove foo", err: false}, + {cmd: "remove foo bar", err: false}, + } { + _, err := snap.Parser(snap.Client()).ParseArgs(strings.Fields(t.cmd)) + if t.err { + c.Check(err, check.ErrorMatches, "Snap was not installed") + } else { + c.Check(err, check.IsNil) + } + } +} + func (s *SnapOpSuite) TestInstallFromChannel(c *check.C) { s.srv.checker = func(r *http.Request) { c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") @@ -2246,6 +2288,7 @@ {"disable", "foo"}, {"try", "."}, {"switch", "--channel=foo", "bar"}, + {"debug", "migrate-home", "foo"}, // commands that use waitMixin from elsewhere {"start", "foo"}, {"stop", "foo"}, diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_validate.go snapd-2.57.5+20.04/cmd/snap/cmd_validate.go --- snapd-2.55.5+20.04/cmd/snap/cmd_validate.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_validate.go 2022-10-17 16:25:18.000000000 +0000 @@ -33,9 +33,8 @@ type cmdValidate struct { clientMixin - Monitor bool `long:"monitor"` - // XXX: enforce mode is not supported yet - Enforce bool `long:"enforce" hidden:"yes"` + Monitor bool `long:"monitor"` + Enforce bool `long:"enforce"` Forget bool `long:"forget"` Positional struct { ValidationSet string `positional-arg-name:""` @@ -152,7 +151,16 @@ Mode: action, Sequence: seq, } - return cmd.client.ApplyValidationSet(accountID, name, opts) + res, err := cmd.client.ApplyValidationSet(accountID, name, opts) + if err != nil { + return err + } + // only print valid/invalid status for monitor mode; enforce fails with an error if invalid + // and otherwise has no output. + if action == "monitor" { + fmt.Fprintln(Stdout, fmtValid(res)) + } + return nil } // no validation set argument, print list with extended info diff -Nru snapd-2.55.5+20.04/cmd/snap/cmd_validate_test.go snapd-2.57.5+20.04/cmd/snap/cmd_validate_test.go --- snapd-2.55.5+20.04/cmd/snap/cmd_validate_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/cmd_validate_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -117,27 +117,27 @@ } func (s *validateSuite) TestValidateMonitor(c *check.C) { - s.RedirectClientToTestServer(makeFakeValidationSetPostHandler(c, `{"type": "sync", "status-code": 200, "result": []}`, "monitor", 0)) + s.RedirectClientToTestServer(makeFakeValidationSetPostHandler(c, `{"type": "sync", "status-code": 200, "result": {"account-id":"foo","name":"bar","mode":"monitor","sequence":3,"valid":false}}`, "monitor", 0)) rest, err := main.Parser(main.Client()).ParseArgs([]string{"validate", "--monitor", "foo/bar"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") - c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, "invalid\n") } func (s *validateSuite) TestValidateMonitorPinned(c *check.C) { - s.RedirectClientToTestServer(makeFakeValidationSetPostHandler(c, `{"type": "sync", "status-code": 200, "result": []}`, "monitor", 3)) + s.RedirectClientToTestServer(makeFakeValidationSetPostHandler(c, `{"type": "sync", "status-code": 200, "result": {"account-id":"foo","name":"bar","mode":"monitor","sequence":3,"valid":true}}}`, "monitor", 3)) rest, err := main.Parser(main.Client()).ParseArgs([]string{"validate", "--monitor", "foo/bar=3"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") - c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, "valid\n") } func (s *validateSuite) TestValidateEnforce(c *check.C) { - s.RedirectClientToTestServer(makeFakeValidationSetPostHandler(c, `{"type": "sync", "status-code": 200, "result": []}`, "enforce", 0)) + s.RedirectClientToTestServer(makeFakeValidationSetPostHandler(c, `{"type": "sync", "status-code": 200, "result": {"account-id":"foo","name":"bar","mode":"enforce","sequence":3,"valid":true}}}`, "enforce", 0)) rest, err := main.Parser(main.Client()).ParseArgs([]string{"validate", "--enforce", "foo/bar"}) c.Assert(err, check.IsNil) @@ -147,7 +147,7 @@ } func (s *validateSuite) TestValidateEnforcePinned(c *check.C) { - s.RedirectClientToTestServer(makeFakeValidationSetPostHandler(c, `{"type": "sync", "status-code": 200, "result": []}`, "enforce", 5)) + s.RedirectClientToTestServer(makeFakeValidationSetPostHandler(c, `{"type": "sync", "status-code": 200, "result": {"account-id":"foo","name":"bar","mode":"enforce","sequence":3,"valid":true}}}`, "enforce", 5)) rest, err := main.Parser(main.Client()).ParseArgs([]string{"validate", "--enforce", "foo/bar=5"}) c.Assert(err, check.IsNil) diff -Nru snapd-2.55.5+20.04/cmd/snap/color.go snapd-2.57.5+20.04/cmd/snap/color.go --- snapd-2.55.5+20.04/cmd/snap/color.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/color.go 2022-10-17 16:25:18.000000000 +0000 @@ -39,10 +39,12 @@ esc.dash = "–" // that's an en dash (so yaml is happy) esc.uparrow = "↑" esc.tick = "✓" + esc.star = "✪" } else { esc.dash = "--" // two dashes keeps yaml happy also esc.uparrow = "^" - esc.tick = "*" + esc.tick = "**" + esc.star = "*" } } @@ -126,24 +128,27 @@ } type escapes struct { - green string - bold string - end string + green string + brightYellow string + bold string + end string - tick, dash, uparrow string + tick, dash, uparrow, star string } var ( color = escapes{ - green: "\033[32m", - bold: "\033[1m", - end: "\033[0m", + green: "\033[32m", + brightYellow: "\033[93m", + bold: "\033[1m", + end: "\033[0m", } mono = escapes{ - green: "\033[1m", - bold: "\033[1m", - end: "\033[0m", + green: "\033[1m", // bold + brightYellow: "\033[2m", // dim + bold: "\033[1m", + end: "\033[0m", } noesc = escapes{} @@ -161,23 +166,33 @@ // * if the publisher's username and display name match, it's just the display // name; otherwise, it'll include the username in parentheses // -// * if the publisher is verified, it'll include a green check mark; otherwise, +// * if the publisher is "starred" it'll include a yellow star; if the +// publisher is "verified", it'll include a green check mark; otherwise, // it'll include a no-op escape sequence of the same length as the escape -// sequence used to make it green (this so that tabwriter gets things right). +// sequence used to make it colorful (this so that tabwriter gets things +// right). func longPublisher(esc *escapes, storeAccount *snap.StoreAccount) string { if storeAccount == nil { return esc.dash + esc.green + esc.end } - badge := "" - if storeAccount.Validation == "verified" { + var badge, color string + switch storeAccount.Validation { + case "verified": badge = esc.tick + color = esc.green + case "starred": + badge = esc.star + color = esc.brightYellow + default: + // no-op escape sequence so that things line-up + color = esc.green } // NOTE this makes e.g. 'Potato' == 'potato', and 'Potato Team' == 'potato-team', // but 'Potato Team' != 'potatoteam', 'Potato Inc.' != 'potato' (in fact 'Potato Inc.' != 'potato-inc') if strings.EqualFold(strings.Replace(storeAccount.Username, "-", " ", -1), storeAccount.DisplayName) { - return storeAccount.DisplayName + esc.green + badge + esc.end + return storeAccount.DisplayName + color + badge + esc.end } - return fmt.Sprintf("%s (%s%s%s%s)", storeAccount.DisplayName, storeAccount.Username, esc.green, badge, esc.end) + return fmt.Sprintf("%s (%s%s%s%s)", storeAccount.DisplayName, storeAccount.Username, color, badge, esc.end) } // shortPublisher returns a string that'll present the publisher of a snap to the @@ -185,17 +200,27 @@ // // * it'll always be just the username // -// * if the publisher is verified, it'll include a green check mark; otherwise, +// * if the publisher is "starred" it'll include a yellow star; if the +// publisher is "verified", it'll include a green check mark; otherwise, // it'll include a no-op escape sequence of the same length as the escape -// sequence used to make it green (this so that tabwriter gets things right). +// sequence used to make it colorful (this so that tabwriter gets things +// right). func shortPublisher(esc *escapes, storeAccount *snap.StoreAccount) string { if storeAccount == nil { return "-" + esc.green + esc.end } - badge := "" - if storeAccount.Validation == "verified" { + var badge, color string + switch storeAccount.Validation { + case "verified": badge = esc.tick + color = esc.green + case "starred": + badge = esc.star + color = esc.brightYellow + default: + // no-op escape sequence so that things line-up + color = esc.green } - return storeAccount.Username + esc.green + badge + esc.end + return storeAccount.Username + color + badge + esc.end } diff -Nru snapd-2.55.5+20.04/cmd/snap/color_test.go snapd-2.57.5+20.04/cmd/snap/color_test.go --- snapd-2.55.5+20.04/cmd/snap/color_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/color_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -126,6 +126,7 @@ color, unicode bool username, display string verified bool + starred bool short, long, fill string } for _, t := range []T{ @@ -140,13 +141,22 @@ short: "potato\x1b[32m\x1b[0m", long: "Potato\x1b[32m\x1b[0m", fill: "\x1b[32m\x1b[0m"}, // verified equal under fold: {color: false, unicode: false, username: "potato", display: "Potato", verified: true, - short: "potato*", long: "Potato*", fill: ""}, + short: "potato**", long: "Potato**", fill: ""}, {color: false, unicode: true, username: "potato", display: "Potato", verified: true, short: "potato✓", long: "Potato✓", fill: ""}, {color: true, unicode: false, username: "potato", display: "Potato", verified: true, - short: "potato\x1b[32m*\x1b[0m", long: "Potato\x1b[32m*\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + short: "potato\x1b[32m**\x1b[0m", long: "Potato\x1b[32m**\x1b[0m", fill: "\x1b[32m\x1b[0m"}, {color: true, unicode: true, username: "potato", display: "Potato", verified: true, short: "potato\x1b[32m✓\x1b[0m", long: "Potato\x1b[32m✓\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + // starred equal under fold: + {color: false, unicode: false, username: "potato", display: "Potato", starred: true, + short: "potato*", long: "Potato*", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Potato", starred: true, + short: "potato✪", long: "Potato✪", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Potato", starred: true, + short: "potato\x1b[93m*\x1b[0m", long: "Potato\x1b[93m*\x1b[0m", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Potato", starred: true, + short: "potato\x1b[93m✪\x1b[0m", long: "Potato\x1b[93m✪\x1b[0m", fill: "\x1b[32m\x1b[0m"}, // non-verified, different {color: false, unicode: false, username: "potato", display: "Carrot", short: "potato", long: "Carrot (potato)", fill: ""}, @@ -158,13 +168,22 @@ short: "potato\x1b[32m\x1b[0m", long: "Carrot (potato\x1b[32m\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, // verified, different {color: false, unicode: false, username: "potato", display: "Carrot", verified: true, - short: "potato*", long: "Carrot (potato*)", fill: ""}, + short: "potato**", long: "Carrot (potato**)", fill: ""}, {color: false, unicode: true, username: "potato", display: "Carrot", verified: true, short: "potato✓", long: "Carrot (potato✓)", fill: ""}, {color: true, unicode: false, username: "potato", display: "Carrot", verified: true, - short: "potato\x1b[32m*\x1b[0m", long: "Carrot (potato\x1b[32m*\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + short: "potato\x1b[32m**\x1b[0m", long: "Carrot (potato\x1b[32m**\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, {color: true, unicode: true, username: "potato", display: "Carrot", verified: true, short: "potato\x1b[32m✓\x1b[0m", long: "Carrot (potato\x1b[32m✓\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + // starred, different + {color: false, unicode: false, username: "potato", display: "Carrot", starred: true, + short: "potato*", long: "Carrot (potato*)", fill: ""}, + {color: false, unicode: true, username: "potato", display: "Carrot", starred: true, + short: "potato✪", long: "Carrot (potato✪)", fill: ""}, + {color: true, unicode: false, username: "potato", display: "Carrot", starred: true, + short: "potato\x1b[93m*\x1b[0m", long: "Carrot (potato\x1b[93m*\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, + {color: true, unicode: true, username: "potato", display: "Carrot", starred: true, + short: "potato\x1b[93m✪\x1b[0m", long: "Carrot (potato\x1b[93m✪\x1b[0m)", fill: "\x1b[32m\x1b[0m"}, // some interesting equal-under-folds: {color: false, unicode: false, username: "potato", display: "PoTaTo", short: "potato", long: "PoTaTo", fill: ""}, @@ -172,8 +191,13 @@ short: "potato-team", long: "Potato Team", fill: ""}, } { pub := &snap.StoreAccount{Username: t.username, DisplayName: t.display} - if t.verified { + switch { + case t.verified && t.starred: + panic("invalid test setup: cannot be starred and validated at the same time") + case t.verified: pub.Validation = "verified" + case t.starred: + pub.Validation = "starred" } color := "never" if t.color { diff -Nru snapd-2.55.5+20.04/cmd/snap/complete.go snapd-2.57.5+20.04/cmd/snap/complete.go --- snapd-2.55.5+20.04/cmd/snap/complete.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/complete.go 2022-10-17 16:25:18.000000000 +0000 @@ -28,6 +28,7 @@ "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/asserts/signtool" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" @@ -184,7 +185,7 @@ type keyName string func (s keyName) Complete(match string) []flags.Completion { - keypairManager, err := getKeypairManager() + keypairManager, err := signtool.GetKeypairManager() if err != nil { return nil } diff -Nru snapd-2.55.5+20.04/cmd/snap/error.go snapd-2.57.5+20.04/cmd/snap/error.go --- snapd-2.55.5+20.04/cmd/snap/error.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/error.go 2022-10-17 16:25:18.000000000 +0000 @@ -87,7 +87,10 @@ return strings.TrimSpace(buf.String()) } -func errorToCmdMessage(snapName string, e error, opts *client.SnapOptions) (string, error) { +// errorToCmdMessage returns the appropriate error message and value based on the +// client error and some context information. The opName is the lowercase name +// of the failed operation (e.g., "refresh"). +func errorToCmdMessage(snapName string, opName string, e error, opts *client.SnapOptions) (string, error) { // do this here instead of in the caller for more DRY err, ok := e.(*client.Error) if !ok { @@ -211,7 +214,12 @@ isError = false msg = i18n.G("snap %q has no updates available") case client.ErrorKindSnapNotInstalled: - isError = false + isError = true + // if the snap isn't installed, then remove can ignore this error + if opName == "remove" { + isError = false + } + usesSnapName = false msg = err.Message case client.ErrorKindNetworkTimeout: diff -Nru snapd-2.55.5+20.04/cmd/snap/export_test.go snapd-2.57.5+20.04/cmd/snap/export_test.go --- snapd-2.55.5+20.04/cmd/snap/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -33,6 +33,7 @@ "github.com/snapcore/snapd/sandbox/selinux" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/store/tooling" usersessionclient "github.com/snapcore/snapd/usersession/client" ) @@ -51,7 +52,6 @@ Antialias = antialias FormatChannel = fmtChannel PrintDescr = printDescr - WrapFlow = wrapFlow TrueishJSON = trueishJSON CompletionHandler = completionHandler MarkForNoCompletion = markForNoCompletion @@ -94,12 +94,7 @@ IsStopping = isStopping - GetKeypairManager = getKeypairManager - GenerateKey = generateKey - GetSnapDirOptions = getSnapDirOptions - - ParseQuotas = parseQuotas ) func HiddenCmd(descr string, completeHidden bool) *cmdInfo { @@ -381,7 +376,7 @@ } } -func MockDownloadDirect(f func(snapName string, revision snap.Revision, dlOpts image.DownloadSnapOptions) error) (restore func()) { +func MockDownloadDirect(f func(snapName string, revision snap.Revision, dlOpts tooling.DownloadSnapOptions) error) (restore func()) { old := downloadDirect downloadDirect = f return func() { @@ -462,3 +457,16 @@ autostartSessionApps = old } } + +func ParseQuotaValues(maxMemory, cpuMax, cpuSet, threadsMax, journalSizeMax, journalRateLimit string) (*client.QuotaValues, error) { + var quotas cmdSetQuota + + quotas.MemoryMax = maxMemory + quotas.CPUMax = cpuMax + quotas.CPUSet = cpuSet + quotas.ThreadsMax = threadsMax + quotas.JournalSizeMax = journalSizeMax + quotas.JournalRateLimit = journalRateLimit + + return quotas.parseQuotas() +} diff -Nru snapd-2.55.5+20.04/cmd/snap/inhibit.go snapd-2.57.5+20.04/cmd/snap/inhibit.go --- snapd-2.55.5+20.04/cmd/snap/inhibit.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/inhibit.go 2022-10-17 16:25:18.000000000 +0000 @@ -110,7 +110,7 @@ func textFlow(snapName string, hint runinhibit.Hint) error { fmt.Fprintf(Stdout, "%s\n", inhibitMessage(snapName, hint)) - pb := progress.MakeProgressBar() + pb := progress.MakeProgressBar(Stdout) pb.Spin(i18n.G("please wait...")) _, err := waitInhibitUnlock(snapName, runinhibit.HintNotInhibited) pb.Finished() diff -Nru snapd-2.55.5+20.04/cmd/snap/keymgr.go snapd-2.57.5+20.04/cmd/snap/keymgr.go --- snapd-2.55.5+20.04/cmd/snap/keymgr.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/keymgr.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,96 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2021 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 main - -import ( - "errors" - "fmt" - "os" - - "golang.org/x/crypto/ssh/terminal" - - "github.com/snapcore/snapd/asserts" - "github.com/snapcore/snapd/i18n" -) - -type KeypairManager interface { - asserts.KeypairManager - - GetByName(keyNname string) (asserts.PrivateKey, error) - Export(keyName string) ([]byte, error) - List() ([]asserts.ExternalKeyInfo, error) - DeleteByName(keyName string) error -} - -func getKeypairManager() (KeypairManager, error) { - keymgrPath := os.Getenv("SNAPD_EXT_KEYMGR") - if keymgrPath != "" { - keypairMgr, err := asserts.NewExternalKeypairManager(keymgrPath) - if err != nil { - return nil, fmt.Errorf(i18n.G("cannot setup external keypair manager: %v"), err) - } - return keypairMgr, nil - } - keypairMgr := asserts.NewGPGKeypairManager() - return keypairMgr, nil -} - -type takingPassKeyGen interface { - Generate(passphrase string, keyName string) error -} - -type ownSecuringKeyGen interface { - Generate(keyName string) error -} - -func generateKey(keypairMgr KeypairManager, keyName string) error { - switch keyGen := keypairMgr.(type) { - case takingPassKeyGen: - return takePassGenKey(keyGen, keyName) - case ownSecuringKeyGen: - err := keyGen.Generate(keyName) - if _, ok := err.(*asserts.ExternalUnsupportedOpError); ok { - return fmt.Errorf(i18n.G("cannot generate external keypair manager key via snap command, use the appropriate external procedure to create a 4096-bit RSA key under the name/label %q"), keyName) - } - return err - default: - return fmt.Errorf("internal error: unsupported keypair manager %T", keypairMgr) - } -} - -func takePassGenKey(keyGen takingPassKeyGen, keyName string) error { - fmt.Fprint(Stdout, i18n.G("Passphrase: ")) - passphrase, err := terminal.ReadPassword(0) - fmt.Fprint(Stdout, "\n") - if err != nil { - return err - } - fmt.Fprint(Stdout, i18n.G("Confirm passphrase: ")) - confirmPassphrase, err := terminal.ReadPassword(0) - fmt.Fprint(Stdout, "\n") - if err != nil { - return err - } - if string(passphrase) != string(confirmPassphrase) { - return errors.New(i18n.G("passphrases do not match")) - } - - return keyGen.Generate(string(passphrase), keyName) -} diff -Nru snapd-2.55.5+20.04/cmd/snap/keymgr_test.go snapd-2.57.5+20.04/cmd/snap/keymgr_test.go --- snapd-2.55.5+20.04/cmd/snap/keymgr_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/keymgr_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,91 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2021 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 main_test - -import ( - "os" - - "gopkg.in/check.v1" - - "github.com/snapcore/snapd/asserts" - snap "github.com/snapcore/snapd/cmd/snap" - "github.com/snapcore/snapd/testutil" -) - -type keymgrSuite struct{} - -var _ = check.Suite(&keymgrSuite{}) - -func (keymgrSuite) TestGPGKeypairManager(c *check.C) { - keypairMgr, err := snap.GetKeypairManager() - c.Check(err, check.IsNil) - c.Check(keypairMgr, check.FitsTypeOf, &asserts.GPGKeypairManager{}) -} - -func mockNopExtKeyMgr(c *check.C) (pgm *testutil.MockCmd, restore func()) { - os.Setenv("SNAPD_EXT_KEYMGR", "keymgr") - pgm = testutil.MockCommand(c, "keymgr", ` -if [ "$1" == "features" ]; then - echo '{"signing":["RSA-PKCS"] , "public-keys":["DER"]}' - exit 0 -fi -exit 1 -`) - r := func() { - pgm.Restore() - os.Unsetenv("SNAPD_EXT_KEYMGR") - } - - return pgm, r -} - -func (keymgrSuite) TestExternalKeypairManager(c *check.C) { - pgm, restore := mockNopExtKeyMgr(c) - defer restore() - - keypairMgr, err := snap.GetKeypairManager() - c.Check(err, check.IsNil) - c.Check(keypairMgr, check.FitsTypeOf, &asserts.ExternalKeypairManager{}) - c.Check(pgm.Calls(), check.HasLen, 1) -} - -func (keymgrSuite) TestExternalKeypairManagerError(c *check.C) { - os.Setenv("SNAPD_EXT_KEYMGR", "keymgr") - defer os.Unsetenv("SNAPD_EXT_KEYMGR") - - pgm := testutil.MockCommand(c, "keymgr", ` -exit 1 -`) - defer pgm.Restore() - - _, err := snap.GetKeypairManager() - c.Check(err, check.ErrorMatches, `cannot setup external keypair manager: external keypair manager "keymgr" \[features\] failed: exit status 1.*`) -} - -func (keymgrSuite) TestExternalKeypairManagerGenerateKey(c *check.C) { - _, restore := mockNopExtKeyMgr(c) - defer restore() - - keypairMgr, err := snap.GetKeypairManager() - c.Check(err, check.IsNil) - - err = snap.GenerateKey(keypairMgr, "key") - c.Check(err, check.ErrorMatches, `cannot generate external keypair manager key via snap command, use the appropriate external procedure to create a 4096-bit RSA key under the name/label "key"`) -} diff -Nru snapd-2.55.5+20.04/cmd/snap/main.go snapd-2.57.5+20.04/cmd/snap/main.go --- snapd-2.55.5+20.04/cmd/snap/main.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -384,8 +384,8 @@ cli := client.New(cfg) goos := runtime.GOOS - if release.OnWSL { - goos = "Windows Subsystem for Linux" + if release.WSLVersion == 1 { + goos = "Windows Subsystem for Linux 1" } if goos != "linux" { cli.Hijack(func(*http.Request) (*http.Response, error) { @@ -556,7 +556,7 @@ } } - msg, err := errorToCmdMessage("", err, nil) + msg, err := errorToCmdMessage("", strings.ToLower(parser.Active.Name), err, nil) if cmdline := strings.Join(os.Args, " "); strings.ContainsAny(cmdline, wrongDashes) { // TRANSLATORS: the %+q is the commandline (+q means quoted, with any non-ascii character called out). Please keep the lines to at most 80 characters. diff -Nru snapd-2.55.5+20.04/cmd/snap/wait.go snapd-2.57.5+20.04/cmd/snap/wait.go --- snapd-2.55.5+20.04/cmd/snap/wait.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap/wait.go 2022-10-17 16:25:18.000000000 +0000 @@ -70,7 +70,7 @@ } }() - pb := progress.MakeProgressBar() + pb := progress.MakeProgressBar(Stdout) defer func() { pb.Finished() // next two not strictly needed for CLI, but without diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts.go snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,6 +22,7 @@ import ( "crypto/subtle" "encoding/json" + "errors" "fmt" "io/ioutil" "os" @@ -35,6 +36,8 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" @@ -74,10 +77,12 @@ snapTypeToMountDir = map[snap.Type]string{ snap.TypeBase: "base", + snap.TypeGadget: "gadget", snap.TypeKernel: "kernel", snap.TypeSnapd: "snapd", } + secbootProvisionForCVM func(initramfsUbuntuSeedDir string) error secbootMeasureSnapSystemEpochWhenPossible func() error secbootMeasureSnapModelWhenPossible func(findModel func() (*asserts.Model, error)) error secbootUnlockVolumeUsingSealedKeyIfEncrypted func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) @@ -89,6 +94,7 @@ mountReadOnlyOptions = &systemdMountOptions{ ReadOnly: true, + Private: true, } ) @@ -139,6 +145,8 @@ recoverySystem: recoverySystem, } + // rootfs is different in UC vs classic with modes + rootfsDir := boot.InitramfsWritableDir switch mode { case "recover": err = generateMountsModeRecover(mst) @@ -147,7 +155,9 @@ case "factory-reset": err = generateMountsModeFactoryReset(mst) case "run": - err = generateMountsModeRun(mst) + rootfsDir, err = generateMountsModeRun(mst) + case "cloudimg-rootfs": + err = generateMountsModeRunCVM(mst) default: // this should never be reached, ModeAndRecoverySystemFromKernelCommandLine // will have returned a non-nill error above if there was another mode @@ -162,7 +172,7 @@ // finally, the initramfs is responsible for reading the boot flags and // copying them to /run, so that userspace has an unambiguous place to read // the boot flags for the current boot from - flags, err := boot.InitramfsActiveBootFlags(mode) + flags, err := boot.InitramfsActiveBootFlags(mode, rootfsDir) if err != nil { // We don't die on failing to read boot flags, we just log the error and // don't set any flags, this is because the boot flags in the case of @@ -286,7 +296,7 @@ srcState := filepath.Join(src, "system-data/var/lib/snapd/state.json") dstState := filepath.Join(dst, "system-data/var/lib/snapd/state.json") err := state.CopyState(srcState, dstState, []string{"auth.users", "auth.macaroon-key", "auth.last-id"}) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return fmt.Errorf("cannot copy user state: %v", err) } @@ -834,10 +844,11 @@ // (u-boot for example) ubuntu-boot is vfat and it could have been unmounted // dirtily, and we need to fsck it to ensure it is mounted safely before // reading keys from it - fsckSystemdOpts := &systemdMountOptions{ + systemdOpts := &systemdMountOptions{ NeedsFsck: true, + Private: true, } - mountErr := doSystemdMount(part.fsDevice, boot.InitramfsUbuntuBootDir, fsckSystemdOpts) + mountErr := doSystemdMount(part.fsDevice, boot.InitramfsUbuntuBootDir, systemdOpts) if err := m.setMountState("ubuntu-boot", boot.InitramfsUbuntuBootDir, mountErr); err != nil { return nil, err } @@ -857,7 +868,7 @@ // - failed to find data at all -> try to unlock save // - unlocked data with run key -> mount data func (m *recoverModeStateMachine) unlockDataRunKey() (stateFunc, error) { - runModeKey := filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key") + runModeKey := device.DataSealedKeyUnder(boot.InitramfsBootEncryptionKeyDir) unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ // don't allow using the recovery key to unlock, we only try using the // recovery key after we first try the fallback object @@ -908,7 +919,7 @@ // TODO: we should somehow customize the prompt to mention what key we need // the user to enter, and what we are unlocking (as currently the prompt // says "recovery key" and the partition UUID for what is being unlocked) - dataFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key") + dataFallbackKey := device.FallbackDataSealedKeyUnder(boot.InitramfsSeedEncryptionKeyDir) unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", dataFallbackKey, unlockOpts) if err := m.setUnlockStateWithFallbackKey("ubuntu-data", unlockRes, unlockErr); err != nil { return nil, err @@ -928,10 +939,11 @@ // don't do fsck on the data partition, it could be corrupted // however, data should always be mounted nosuid to prevent snaps from // extracting suid executables there and trying to circumvent the sandbox - nosuidMountOpts := &systemdMountOptions{ - NoSuid: true, + mountOpts := &systemdMountOptions{ + NoSuid: true, + Private: true, } - mountErr := doSystemdMount(data.fsDevice, boot.InitramfsHostUbuntuDataDir, nosuidMountOpts) + mountErr := doSystemdMount(data.fsDevice, boot.InitramfsHostUbuntuDataDir, mountOpts) if err := m.setMountState("ubuntu-data", boot.InitramfsHostUbuntuDataDir, mountErr); err != nil { return nil, err } @@ -956,7 +968,7 @@ func (m *recoverModeStateMachine) unlockEncryptedSaveRunKey() (stateFunc, error) { // to get to this state, we needed to have mounted ubuntu-data on host, so // if encrypted, we can try to read the run key from host ubuntu-data - saveKey := filepath.Join(dirs.SnapFDEDirUnder(boot.InitramfsHostWritableDir), "ubuntu-save.key") + saveKey := device.SaveKeyUnder(dirs.SnapFDEDirUnder(boot.InitramfsHostWritableDir)) key, err := ioutil.ReadFile(saveKey) if err != nil { // log the error and skip to trying the fallback key @@ -1037,7 +1049,7 @@ AllowRecoveryKey: true, WhichModel: m.whichModel, } - saveFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key") + saveFallbackKey := device.FallbackSaveSealedKeyUnder(boot.InitramfsSeedEncryptionKeyDir) // TODO: this prompts again for a recover key, but really this is the // reinstall key we will prompt for // TODO: we should somehow customize the prompt to mention what key we need @@ -1059,7 +1071,10 @@ func (m *recoverModeStateMachine) mountSave() (stateFunc, error) { save := m.degradedState.partition("ubuntu-save") // TODO: should we fsck ubuntu-save ? - mountErr := doSystemdMount(save.fsDevice, boot.InitramfsUbuntuSaveDir, nil) + mountOpts := &systemdMountOptions{ + Private: true, + } + mountErr := doSystemdMount(save.fsDevice, boot.InitramfsUbuntuSaveDir, mountOpts) if err := m.setMountState("ubuntu-save", boot.InitramfsUbuntuSaveDir, mountErr); err != nil { return nil, err } @@ -1275,15 +1290,7 @@ // checkDataAndSavePairing make sure that ubuntu-data and ubuntu-save // come from the same install by comparing secret markers in them func checkDataAndSavePairing(rootdir string) (bool, error) { - // read the secret marker file from ubuntu-data - markerFile1 := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "marker") - marker1, err := ioutil.ReadFile(markerFile1) - if err != nil { - return false, err - } - // read the secret marker file from ubuntu-save - markerFile2 := filepath.Join(dirs.SnapFDEDirUnderSave(boot.InitramfsUbuntuSaveDir), "marker") - marker2, err := ioutil.ReadFile(markerFile2) + marker1, marker2, err := device.ReadEncryptionMarkers(dirs.SnapFDEDirUnder(rootdir), dirs.SnapFDEDirUnderSave(boot.InitramfsUbuntuSaveDir)) if err != nil { return false, err } @@ -1336,6 +1343,7 @@ NeedsFsck: true, // don't need nosuid option here, since this function is only used // for ubuntu-boot and ubuntu-seed, never ubuntu-data + Private: true, } return doSystemdMount(partSrc, dir, opts) } @@ -1376,13 +1384,7 @@ systemSnaps := make(map[snap.Type]snap.PlaceInfo) for _, essentialSnap := range essSnaps { - if essentialSnap.EssentialType == snap.TypeGadget { - // don't need to mount the gadget anywhere, but we use the snap - // later hence it is loaded - continue - } systemSnaps[essentialSnap.EssentialType] = essentialSnap.PlaceInfo() - dir := snapTypeToMountDir[essentialSnap.EssentialType] // TODO:UC20: we need to cross-check the kernel path with snapd_recovery_kernel used by grub if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), mountReadOnlyOptions); err != nil { @@ -1407,8 +1409,9 @@ // snaps from being able to bypass the sandbox by creating suid root files // there and try to escape the sandbox mntOpts := &systemdMountOptions{ - Tmpfs: true, - NoSuid: true, + Tmpfs: true, + NoSuid: true, + Private: true, } err = doSystemdMount("tmpfs", boot.InitramfsDataDir, mntOpts) if err != nil { @@ -1448,7 +1451,7 @@ func maybeMountSave(disk disks.Disk, rootdir string, encrypted bool, mountOpts *systemdMountOptions) (haveSave bool, err error) { var saveDevice string if encrypted { - saveKey := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "ubuntu-save.key") + saveKey := device.SaveKeyUnder(dirs.SnapFDEDirUnder(rootdir)) // if ubuntu-save exists and is encrypted, the key has been created during install if !osutil.FileExists(saveKey) { // ubuntu-data is encrypted, but we appear to be missing @@ -1483,51 +1486,138 @@ return true, nil } -func generateMountsModeRun(mst *initramfsMountsState) error { +func generateMountsModeRunCVM(mst *initramfsMountsState) error { + // Mount ESP as UbuntuSeedDir which has UEFI label + if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "UEFI"); err != nil { + return err + } + + // get the disk that we mounted the ESP from as a reference + // point for future mounts + disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil) + if err != nil { + return err + } + + // Mount rootfs + if err := secbootProvisionForCVM(boot.InitramfsUbuntuSeedDir); err != nil { + return err + } + runModeCVMKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "cloudimg-rootfs.sealed-key") + opts := &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + } + unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "cloudimg-rootfs", runModeCVMKey, opts) + if err != nil { + return err + } + fsckSystemdOpts := &systemdMountOptions{ + NeedsFsck: true, + Ephemeral: true, + } + if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, fsckSystemdOpts); err != nil { + return err + } + + // Verify that cloudimg-rootfs comes from where we expect it to + diskOpts := &disks.Options{} + if unlockRes.IsEncrypted { + // then we need to specify that the data mountpoint is + // expected to be a decrypted device + diskOpts.IsDecryptedDevice = true + } + + matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts) + if err != nil { + return err + } + if !matches { + // failed to verify that cloudimg-rootfs mountpoint + // comes from the same disk as ESP + return fmt.Errorf("cannot validate boot: cloudimg-rootfs mountpoint is expected to be from disk %s but is not", disk.Dev()) + } + + // Unmount ESP because otherwise unmounting is racy and results in booted systems without ESP + if err := doSystemdMount("", boot.InitramfsUbuntuSeedDir, &systemdMountOptions{Umount: true, Ephemeral: true}); err != nil { + return err + } + + return nil +} + +func generateMountsModeRun(mst *initramfsMountsState) (string, error) { // 1. mount ubuntu-boot if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuBootDir, "ubuntu-boot"); err != nil { - return err + return "", err } // get the disk that we mounted the ubuntu-boot partition from as a // reference point for future mounts disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuBootDir, nil) if err != nil { - return err + return "", err + } + + // 1.1. measure model + err = stampedAction("run-model-measured", func() error { + return secbootMeasureSnapModelWhenPossible(mst.UnverifiedBootModel) + }) + if err != nil { + return "", err } - // 2. mount ubuntu-seed + // The model is now measured, use it to check if this is a classic install + model, err := mst.UnverifiedBootModel() + if err != nil { + return "", err + } + isClassic := model.Classic() + var rootfsDir string + if isClassic { + logger.Noticef("generating mounts for classic system, run mode") + rootfsDir = boot.InitramfsDataDir + } else { + logger.Noticef("generating mounts for Ubuntu Core system, run mode") + rootfsDir = boot.InitramfsWritableDir + } + + // 2. mount ubuntu-seed (optional for classic) + systemdOpts := &systemdMountOptions{ + NeedsFsck: true, + Private: true, + } // use the disk we mounted ubuntu-boot from as a reference to find // ubuntu-seed and mount it + hasSeedPart := true partUUID, err := disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-seed") if err != nil { - return err + if isClassic { + // If there is no ubuntu-seed on classic, that's fine + if _, ok := err.(disks.PartitionNotFoundError); !ok { + return "", err + } + hasSeedPart = false + } else { + return "", err + } } - // fsck is safe to run on ubuntu-seed as per the manpage, it should not // meaningfully contribute to corruption if we fsck it every time we boot, // and it is important to fsck it because it is vfat and mounted writable // TODO:UC20: mount it as read-only here and remount as writable when we // need it to be writable for i.e. transitioning to recover mode - fsckSystemdOpts := &systemdMountOptions{ - NeedsFsck: true, - } - if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID), boot.InitramfsUbuntuSeedDir, fsckSystemdOpts); err != nil { - return err + if partUUID != "" { + if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID), + boot.InitramfsUbuntuSeedDir, systemdOpts); err != nil { + return "", err + } } // 2.1 Update bootloader variables now that boot/seed are mounted if err := boot.InitramfsRunModeUpdateBootloaderVars(); err != nil { - return err + return "", err } - // 3.1. measure model - err = stampedAction("run-model-measured", func() error { - return secbootMeasureSnapModelWhenPossible(mst.UnverifiedBootModel) - }) - if err != nil { - return err - } // at this point on a system with TPM-based encryption // data can be open only if the measured model matches the actual // run model. @@ -1535,35 +1625,39 @@ // we need other ways to make sure that the disk is opened // and we continue booting only for expected models - // 3.2. mount Data - runModeKey := filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key") + // 3.1. mount Data + runModeKey := device.DataSealedKeyUnder(boot.InitramfsBootEncryptionKeyDir) opts := &secboot.UnlockVolumeUsingSealedKeyOptions{ AllowRecoveryKey: true, WhichModel: mst.UnverifiedBootModel, } unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "ubuntu-data", runModeKey, opts) if err != nil { - return err + return "", err } // TODO: do we actually need fsck if we are mounting a mapper device? // probably not? - // fsck and mount with nosuid to prevent snaps from being able to bypass - // the sandbox by creating suid root files there and trying to escape the - // sandbox dataMountOpts := &systemdMountOptions{ NeedsFsck: true, - NoSuid: true, + } + if !isClassic { + // fsck and mount with nosuid to prevent snaps from being able to bypass + // the sandbox by creating suid root files there and trying to escape the + // sandbox + dataMountOpts.NoSuid = true + // Note that on classic the default is to allow mount propagation + dataMountOpts.Private = true } if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, dataMountOpts); err != nil { - return err + return "", err } isEncryptedDev := unlockRes.IsEncrypted - // 3.3. mount ubuntu-save (if present) - haveSave, err := maybeMountSave(disk, boot.InitramfsWritableDir, isEncryptedDev, fsckSystemdOpts) + // 3.2. mount ubuntu-save (if present) + haveSave, err := maybeMountSave(disk, rootfsDir, isEncryptedDev, systemdOpts) if err != nil { - return err + return "", err } // 4.1 verify that ubuntu-data comes from where we expect it to @@ -1576,21 +1670,21 @@ matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts) if err != nil { - return err + return "", err } if !matches { // failed to verify that ubuntu-data mountpoint comes from the same disk // as ubuntu-boot - return fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev()) + return "", fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev()) } if haveSave { // 4.1a we have ubuntu-save, verify it as well matches, err = disk.MountPointIsFromDisk(boot.InitramfsUbuntuSaveDir, diskOpts) if err != nil { - return err + return "", err } if !matches { - return fmt.Errorf("cannot validate boot: ubuntu-save mountpoint is expected to be from disk %s but is not", disk.Dev()) + return "", fmt.Errorf("cannot validate boot: ubuntu-save mountpoint is expected to be from disk %s but is not", disk.Dev()) } if isEncryptedDev { @@ -1602,29 +1696,33 @@ // be locked. // for symmetry with recover code and extra paranoia // though also check that the markers match. - paired, err := checkDataAndSavePairing(boot.InitramfsWritableDir) + paired, err := checkDataAndSavePairing(rootfsDir) if err != nil { - return err + return "", err } if !paired { - return fmt.Errorf("cannot validate boot: ubuntu-save and ubuntu-data are not marked as from the same install") + return "", fmt.Errorf("cannot validate boot: ubuntu-save and ubuntu-data are not marked as from the same install") } } } // 4.2. read modeenv - modeEnv, err := boot.ReadModeenv(boot.InitramfsWritableDir) + modeEnv, err := boot.ReadModeenv(rootfsDir) if err != nil { - return err + return "", err } - typs := []snap.Type{snap.TypeBase, snap.TypeKernel} + // order in the list must not change as it determines the mount order + typs := []snap.Type{snap.TypeGadget, snap.TypeKernel} + if !isClassic { + typs = append([]snap.Type{snap.TypeBase}, typs...) + } - // 4.2 choose base and kernel snaps (this includes updating modeenv if - // needed to try the base snap) - mounts, err := boot.InitramfsRunModeSelectSnapsToMount(typs, modeEnv) + // 4.2 choose base, gadget and kernel snaps (this includes updating + // modeenv if needed to try the base snap) + mounts, err := boot.InitramfsRunModeSelectSnapsToMount(typs, modeEnv, rootfsDir) if err != nil { - return err + return "", err } // TODO:UC20: with grade > dangerous, verify the kernel snap hash against @@ -1632,29 +1730,55 @@ // to the function above to make decisions there, or perhaps this // code actually belongs in the bootloader implementation itself - // 4.3 mount base and kernel snaps - // make sure this is a deterministic order - for _, typ := range []snap.Type{snap.TypeBase, snap.TypeKernel} { + // 4.3 mount base (if UC), gadget and kernel snaps + for _, typ := range typs { if sn, ok := mounts[typ]; ok { dir := snapTypeToMountDir[typ] - snapPath := filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), sn.Filename()) + snapPath := filepath.Join(dirs.SnapBlobDirUnder(rootfsDir), sn.Filename()) if err := doSystemdMount(snapPath, filepath.Join(boot.InitramfsRunMntDir, dir), mountReadOnlyOptions); err != nil { - return err + return "", err + } + } + } + + // 4.4 check if we expected a ubuntu-seed partition from the gadget data + if isClassic { + gadgetDir := filepath.Join(boot.InitramfsRunMntDir, snapTypeToMountDir[snap.TypeGadget]) + gadgetInfo, err := gadget.ReadInfo(gadgetDir, model) + if err != nil { + return "", err + } + seedDefinedInGadget := false + volLoop: + for _, vol := range gadgetInfo.Volumes { + for _, part := range vol.Structure { + if part.Role == gadget.SystemSeed { + seedDefinedInGadget = true + break volLoop + } } } + if hasSeedPart && !seedDefinedInGadget { + return "", fmt.Errorf("seed partition found but not defined in the gadget") + } + if !hasSeedPart && seedDefinedInGadget { + return "", fmt.Errorf("seed partition not found but defined in the gadget") + } } - // 4.4 mount snapd snap only on first boot - if modeEnv.RecoverySystem != "" { + // 4.5 mount snapd snap only on first boot + if modeEnv.RecoverySystem != "" && !isClassic { // load the recovery system and generate mount for snapd _, essSnaps, err := mst.ReadEssential(modeEnv.RecoverySystem, []snap.Type{snap.TypeSnapd}) if err != nil { - return fmt.Errorf("cannot load metadata and verify snapd snap: %v", err) + return "", fmt.Errorf("cannot load metadata and verify snapd snap: %v", err) + } + if err := doSystemdMount(essSnaps[0].Path, filepath.Join(boot.InitramfsRunMntDir, "snapd"), mountReadOnlyOptions); err != nil { + return "", fmt.Errorf("cannot mount snapd snap: %v", err) } - return doSystemdMount(essSnaps[0].Path, filepath.Join(boot.InitramfsRunMntDir, "snapd"), mountReadOnlyOptions) } - return nil + return rootfsDir, nil } var tryRecoverySystemHealthCheck = func() error { diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,6 +34,9 @@ ) func init() { + secbootProvisionForCVM = func(_ string) error { + return errNotImplemented + } secbootMeasureSnapSystemEpochWhenPossible = func() error { return errNotImplemented } diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go 2022-10-17 16:25:18.000000000 +0000 @@ -26,6 +26,7 @@ ) func init() { + secbootProvisionForCVM = secboot.ProvisionForCVM secbootMeasureSnapSystemEpochWhenPossible = secboot.MeasureSnapSystemEpochWhenPossible secbootMeasureSnapModelWhenPossible = secboot.MeasureSnapModelWhenPossible secbootUnlockVolumeUsingSealedKeyIfEncrypted = secboot.UnlockVolumeUsingSealedKeyIfEncrypted diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -52,9 +52,11 @@ var brandPrivKey, _ = assertstest.GenerateKey(752) -type initramfsMountsSuite struct { +type baseInitramfsMountsSuite struct { testutil.BaseTest + isClassic bool + // makes available a bunch of helper (like MakeAssertedSnap) *seedtest.TestingSeed20 @@ -72,28 +74,44 @@ kernelr2 snap.PlaceInfo core20 snap.PlaceInfo core20r2 snap.PlaceInfo + gadget snap.PlaceInfo snapd snap.PlaceInfo } +type initramfsMountsSuite struct { + baseInitramfsMountsSuite +} + var _ = Suite(&initramfsMountsSuite{}) var ( tmpfsMountOpts = &main.SystemdMountOptions{ - Tmpfs: true, - NoSuid: true, + Tmpfs: true, + NoSuid: true, + Private: true, + } + needsFsckNoPrivateDiskMountOpts = &main.SystemdMountOptions{ + NeedsFsck: true, } needsFsckDiskMountOpts = &main.SystemdMountOptions{ NeedsFsck: true, + Private: true, } needsFsckAndNoSuidDiskMountOpts = &main.SystemdMountOptions{ NeedsFsck: true, NoSuid: true, + Private: true, } needsNoSuidDiskMountOpts = &main.SystemdMountOptions{ - NoSuid: true, + NoSuid: true, + Private: true, } snapMountOpts = &main.SystemdMountOptions{ ReadOnly: true, + Private: true, + } + mountOpts = &main.SystemdMountOptions{ + Private: true, } seedPart = disks.Partition{ @@ -132,6 +150,12 @@ KernelDeviceNode: "/dev/sda5", } + cvmEncPart = disks.Partition{ + FilesystemLabel: "cloudimg-rootfs-enc", + PartitionUUID: "cloudimg-rootfs-enc-partuuid", + KernelDeviceNode: "/dev/sda1", + } + // a boot disk without ubuntu-save defaultBootDisk = &disks.MockDiskMapping{ Structure: []disks.Partition{ @@ -165,10 +189,31 @@ DevNum: "defaultEncDev", } + defaultCVMDisk = &disks.MockDiskMapping{ + Structure: []disks.Partition{ + seedPart, + cvmEncPart, + }, + DiskHasPartitions: true, + DevNum: "defaultCVMDev", + } + + // a boot disk without ubuntu-seed, which can happen for classic + defaultNoSeedWithSaveDisk = &disks.MockDiskMapping{ + Structure: []disks.Partition{ + bootPart, + dataPart, + savePart, + }, + DiskHasPartitions: true, + DevNum: "default-no-seed-with-save", + } + mockStateContent = `{"data":{"auth":{"users":[{"id":1,"name":"mvo"}],"macaroon-key":"not-a-cookie","last-id":1}},"some":{"other":"stuff"}}` ) -func (s *initramfsMountsSuite) setupSeed(c *C, modelAssertTime time.Time, gadgetSnapFiles [][]string) { +func (s *baseInitramfsMountsSuite) setupSeed(c *C, modelAssertTime time.Time, gadgetSnapFiles [][]string) { + // pretend /run/mnt/ubuntu-seed has a valid seed s.seedDir = boot.InitramfsUbuntuSeedDir @@ -199,7 +244,7 @@ } s.sysLabel = "20191118" - s.model = seed20.MakeSeed(c, s.sysLabel, "my-brand", "my-model", map[string]interface{}{ + model := map[string]interface{}{ "display-name": "my model", "architecture": "amd64", "base": "core20", @@ -217,10 +262,15 @@ "type": "gadget", "default-channel": "20", }}, - }, nil) + } + if s.isClassic { + model["classic"] = "true" + model["distribution"] = "ubuntu" + } + s.model = seed20.MakeSeed(c, s.sysLabel, "my-brand", "my-model", model, nil) } -func (s *initramfsMountsSuite) SetUpTest(c *C) { +func (s *baseInitramfsMountsSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) s.Stdout = bytes.NewBuffer(nil) @@ -249,8 +299,17 @@ // setup the seed s.setupSeed(c, time.Time{}, nil) - // make test snap PlaceInfo's for various boot functionality + // Make sure we have a model assertion in the ubuntu-boot partition var err error + err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755) + c.Assert(err, IsNil) + mf, err := os.Create(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model")) + c.Assert(err, IsNil) + defer mf.Close() + err = asserts.NewEncoder(mf).Encode(s.model) + c.Assert(err, IsNil) + + // make test snap PlaceInfo's for various boot functionality s.kernel, err = snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") c.Assert(err, IsNil) @@ -263,12 +322,18 @@ s.core20r2, err = snap.ParsePlaceInfoFromSnapFileName("core20_2.snap") c.Assert(err, IsNil) + s.gadget, err = snap.ParsePlaceInfoFromSnapFileName("pc_1.snap") + c.Assert(err, IsNil) + s.snapd, err = snap.ParsePlaceInfoFromSnapFileName("snapd_1.snap") c.Assert(err, IsNil) // by default mock that we don't have UEFI vars, etc. to get the booted // kernel partition partition uuid s.AddCleanup(main.MockPartitionUUIDForBootedKernelDisk("")) + s.AddCleanup(main.MockSecbootProvisionForCVM(func(_ string) error { + return nil + })) s.AddCleanup(main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { return nil })) @@ -389,8 +454,11 @@ // makeSnapFilesOnEarlyBootUbuntuData creates the snap files on ubuntu-data as // we -func makeSnapFilesOnEarlyBootUbuntuData(c *C, snaps ...snap.PlaceInfo) { +func (s *baseInitramfsMountsSuite) makeSnapFilesOnEarlyBootUbuntuData(c *C, snaps ...snap.PlaceInfo) { snapDir := dirs.SnapBlobDirUnder(boot.InitramfsWritableDir) + if s.isClassic { + snapDir = dirs.SnapBlobDirUnder(boot.InitramfsDataDir) + } err := os.MkdirAll(snapDir, 0755) c.Assert(err, IsNil) for _, sn := range snaps { @@ -400,7 +468,7 @@ } } -func (s *initramfsMountsSuite) mockProcCmdlineContent(c *C, newContent string) { +func (s *baseInitramfsMountsSuite) mockProcCmdlineContent(c *C, newContent string) { mockProcCmdline := filepath.Join(c.MkDir(), "proc-cmdline") err := ioutil.WriteFile(mockProcCmdline, []byte(newContent), 0644) c.Assert(err, IsNil) @@ -408,7 +476,7 @@ s.AddCleanup(restore) } -func (s *initramfsMountsSuite) mockUbuntuSaveKeyAndMarker(c *C, rootDir, key, marker string) { +func (s *baseInitramfsMountsSuite) mockUbuntuSaveKeyAndMarker(c *C, rootDir, key, marker string) { keyPath := filepath.Join(dirs.SnapFDEDirUnder(rootDir), "ubuntu-save.key") c.Assert(os.MkdirAll(filepath.Dir(keyPath), 0700), IsNil) c.Assert(ioutil.WriteFile(keyPath, []byte(key), 0600), IsNil) @@ -419,7 +487,7 @@ } } -func (s *initramfsMountsSuite) mockUbuntuSaveMarker(c *C, rootDir, marker string) { +func (s *baseInitramfsMountsSuite) mockUbuntuSaveMarker(c *C, rootDir, marker string) { markerPath := filepath.Join(rootDir, "device/fde", "marker") c.Assert(os.MkdirAll(filepath.Dir(markerPath), 0700), IsNil) c.Assert(ioutil.WriteFile(markerPath, []byte(marker), 0600), IsNil) @@ -448,7 +516,7 @@ // this is a function so we evaluate InitramfsUbuntuBootDir, etc at the time of // the test to pick up test-specific dirs.GlobalRootDir -func ubuntuLabelMount(label string, mode string) systemdMount { +func (s *baseInitramfsMountsSuite) ubuntuLabelMount(label string, mode string) systemdMount { mnt := systemdMount{ opts: needsFsckDiskMountOpts, } @@ -466,7 +534,11 @@ case "ubuntu-data": mnt.what = "/dev/disk/by-label/ubuntu-data" mnt.where = boot.InitramfsDataDir - mnt.opts = needsFsckAndNoSuidDiskMountOpts + if s.isClassic { + mnt.opts = needsFsckNoPrivateDiskMountOpts + } else { + mnt.opts = needsFsckAndNoSuidDiskMountOpts + } } return mnt @@ -474,7 +546,7 @@ // ubuntuPartUUIDMount returns a systemdMount for the partuuid disk, expecting // that the partuuid contains in it the expected label for easier coding -func ubuntuPartUUIDMount(partuuid string, mode string) systemdMount { +func (s *baseInitramfsMountsSuite) ubuntuPartUUIDMount(partuuid string, mode string) systemdMount { // all partitions are expected to be mounted with fsck on mnt := systemdMount{ opts: needsFsckDiskMountOpts, @@ -487,7 +559,11 @@ mnt.where = boot.InitramfsUbuntuSeedDir case strings.Contains(partuuid, "ubuntu-data"): mnt.where = boot.InitramfsDataDir - mnt.opts = needsFsckAndNoSuidDiskMountOpts + if s.isClassic { + mnt.opts = needsFsckNoPrivateDiskMountOpts + } else { + mnt.opts = needsFsckAndNoSuidDiskMountOpts + } case strings.Contains(partuuid, "ubuntu-save"): mnt.where = boot.InitramfsUbuntuSaveDir } @@ -495,7 +571,7 @@ return mnt } -func (s *initramfsMountsSuite) makeSeedSnapSystemdMount(typ snap.Type) systemdMount { +func (s *baseInitramfsMountsSuite) makeSeedSnapSystemdMount(typ snap.Type) systemdMount { mnt := systemdMount{} var name, dir string switch typ { @@ -505,6 +581,9 @@ case snap.TypeBase: name = "core20" dir = "base" + case snap.TypeGadget: + name = "pc" + dir = "gadget" case snap.TypeKernel: name = "pc-kernel" dir = "kernel" @@ -516,7 +595,7 @@ return mnt } -func (s *initramfsMountsSuite) makeRunSnapSystemdMount(typ snap.Type, sn snap.PlaceInfo) systemdMount { +func (s *baseInitramfsMountsSuite) makeRunSnapSystemdMount(typ snap.Type, sn snap.PlaceInfo) systemdMount { mnt := systemdMount{} var dir string switch typ { @@ -524,18 +603,24 @@ dir = "snapd" case snap.TypeBase: dir = "base" + case snap.TypeGadget: + dir = "gadget" case snap.TypeKernel: dir = "kernel" } - mnt.what = filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), sn.Filename()) + snapDir := boot.InitramfsWritableDir + if s.isClassic { + snapDir = boot.InitramfsDataDir + } + mnt.what = filepath.Join(dirs.SnapBlobDirUnder(snapDir), sn.Filename()) mnt.where = filepath.Join(boot.InitramfsRunMntDir, dir) mnt.opts = snapMountOpts return mnt } -func (s *initramfsMountsSuite) mockSystemdMountSequence(c *C, mounts []systemdMount, comment CommentInterface) (restore func()) { +func (s *baseInitramfsMountsSuite) mockSystemdMountSequence(c *C, mounts []systemdMount, comment CommentInterface) (restore func()) { n := 0 if comment == nil { comment = Commentf("") @@ -570,10 +655,11 @@ })() restore := s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "install"), + s.ubuntuLabelMount("ubuntu-seed", "install"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -590,6 +676,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=install recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -626,10 +713,11 @@ for _, t := range tt { restore := s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "install"), + s.ubuntuLabelMount("ubuntu-seed", "install"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -692,11 +780,12 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), }, nil) defer restore() @@ -710,12 +799,13 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) // write modeenv with boot flags modeEnv := boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, BootFlags: t.bootFlags, } @@ -763,10 +853,11 @@ cleanups = append(cleanups, restore) restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "install"), + s.ubuntuLabelMount("ubuntu-seed", "install"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -800,17 +891,17 @@ ` c.Assert(os.RemoveAll(s.seedDir), IsNil) - s.setupSeed(c, time.Time{}, [][]string{ - {"meta/gadget.yaml", gadgetYamlDefaults}, - }) + s.setupSeed(c, time.Time{}, + [][]string{{"meta/gadget.yaml", gadgetYamlDefaults}}) s.mockProcCmdlineContent(c, "snapd_recovery_mode=install snapd_recovery_system="+s.sysLabel) restore := s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "install"), + s.ubuntuLabelMount("ubuntu-seed", "install"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -836,6 +927,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=install recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -869,6 +961,7 @@ s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -885,6 +978,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=install recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -905,11 +999,12 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), }, nil) defer restore() @@ -923,12 +1018,60 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) // write modeenv modeEnv := boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsWritableDir) + c.Assert(err, IsNil) + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeHappyNoGadgetMount(c *C) { + // M + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) + + // write modeenv, with no gadget field so the gadget is not mounted + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), CurrentKernels: []string{s.kernel.Filename()}, } err := modeEnv.WriteTo(boot.InitramfsWritableDir) @@ -981,10 +1124,11 @@ cleanups = append(cleanups, restore) mnts := []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), } @@ -1004,12 +1148,13 @@ restore = bloader.SetEnabledKernel(s.kernel) cleanups = append(cleanups, restore) - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) // write modeenv modeEnv := boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, } @@ -1054,10 +1199,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), }, nil) defer restore() @@ -1071,12 +1217,13 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) // write modeenv modeEnv := boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, } err := modeEnv.WriteTo(boot.InitramfsWritableDir) @@ -1161,6 +1308,7 @@ s.mockProcCmdlineContent(c, "snapd_recovery_mode=install snapd_recovery_system="+s.sysLabel) baseMnt := filepath.Join(boot.InitramfsRunMntDir, "base") + gadgetMnt := filepath.Join(boot.InitramfsRunMntDir, "gadget") kernelMnt := filepath.Join(boot.InitramfsRunMntDir, "kernel") snapdMnt := filepath.Join(boot.InitramfsRunMntDir, "snapd") @@ -1180,23 +1328,21 @@ // aren't mounted by the time systemd-mount returns case 1, 2: c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir) - return n%2 == 0, nil case 3, 4: c.Assert(where, Equals, snapdMnt) - return n%2 == 0, nil case 5, 6: c.Assert(where, Equals, kernelMnt) - return n%2 == 0, nil case 7, 8: c.Assert(where, Equals, baseMnt) - return n%2 == 0, nil case 9, 10: + c.Assert(where, Equals, gadgetMnt) + case 11, 12: c.Assert(where, Equals, boot.InitramfsDataDir) - return n%2 == 0, nil default: c.Errorf("unexpected IsMounted check on %s", where) return false, fmt.Errorf("unexpected IsMounted check on %s", where) } + return n%2 == 0, nil }) defer restore() @@ -1209,7 +1355,7 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) // write modeenv modeEnv := boot.Modeenv{ @@ -1234,6 +1380,7 @@ systemd.EscapeUnitNamePath(snapdMnt), systemd.EscapeUnitNamePath(kernelMnt), systemd.EscapeUnitNamePath(baseMnt), + systemd.EscapeUnitNamePath(gadgetMnt), systemd.EscapeUnitNamePath(boot.InitramfsDataDir), } { fname := fmt.Sprintf("snap_bootstrap_%s.conf", mountUnit) @@ -1245,7 +1392,7 @@ } // 2 IsMounted calls per mount point, so 10 total IsMounted calls - c.Assert(n, Equals, 10) + c.Assert(n, Equals, 12) c.Assert(cmd.Calls(), DeepEquals, [][]string{ { @@ -1255,6 +1402,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1263,7 +1411,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1272,7 +1420,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1281,7 +1429,16 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", + "--property=Before=initrd-fs.target", + }, { + "systemd-mount", + filepath.Join(s.seedDir, "snaps", s.gadget.Filename()), + gadgetMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1291,7 +1448,7 @@ "--no-ask-password", "--type=tmpfs", "--fsck=no", - "--options=nosuid", + "--options=nosuid,private", "--property=Before=initrd-fs.target", }, }) @@ -1301,6 +1458,7 @@ s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) baseMnt := filepath.Join(boot.InitramfsRunMntDir, "base") + gadgetMnt := filepath.Join(boot.InitramfsRunMntDir, "gadget") kernelMnt := filepath.Join(boot.InitramfsRunMntDir, "kernel") snapdMnt := filepath.Join(boot.InitramfsRunMntDir, "snapd") @@ -1329,29 +1487,25 @@ // aren't mounted by the time systemd-mount returns case 1, 2: c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir) - return n%2 == 0, nil case 3, 4: c.Assert(where, Equals, snapdMnt) - return n%2 == 0, nil case 5, 6: c.Assert(where, Equals, kernelMnt) - return n%2 == 0, nil case 7, 8: c.Assert(where, Equals, baseMnt) - return n%2 == 0, nil case 9, 10: - c.Assert(where, Equals, boot.InitramfsDataDir) - return n%2 == 0, nil + c.Assert(where, Equals, gadgetMnt) case 11, 12: - c.Assert(where, Equals, boot.InitramfsUbuntuBootDir) - return n%2 == 0, nil + c.Assert(where, Equals, boot.InitramfsDataDir) case 13, 14: + c.Assert(where, Equals, boot.InitramfsUbuntuBootDir) + case 15, 16: c.Assert(where, Equals, boot.InitramfsHostUbuntuDataDir) - return n%2 == 0, nil default: c.Errorf("unexpected IsMounted check on %s", where) return false, fmt.Errorf("unexpected IsMounted check on %s", where) } + return n%2 == 0, nil }) defer restore() @@ -1381,7 +1535,7 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) // write modeenv modeEnv := boot.Modeenv{ @@ -1406,6 +1560,7 @@ systemd.EscapeUnitNamePath(snapdMnt), systemd.EscapeUnitNamePath(kernelMnt), systemd.EscapeUnitNamePath(baseMnt), + systemd.EscapeUnitNamePath(gadgetMnt), systemd.EscapeUnitNamePath(boot.InitramfsDataDir), systemd.EscapeUnitNamePath(boot.InitramfsHostUbuntuDataDir), } { @@ -1418,7 +1573,7 @@ } // 2 IsMounted calls per mount point, so 14 total IsMounted calls - c.Assert(n, Equals, 14) + c.Assert(n, Equals, 16) c.Assert(cmd.Calls(), DeepEquals, [][]string{ { @@ -1428,6 +1583,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1436,7 +1592,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1445,7 +1601,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1454,7 +1610,16 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", + "--property=Before=initrd-fs.target", + }, { + "systemd-mount", + filepath.Join(s.seedDir, "snaps", s.gadget.Filename()), + gadgetMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1464,7 +1629,7 @@ "--no-ask-password", "--type=tmpfs", "--fsck=no", - "--options=nosuid", + "--options=nosuid,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1473,6 +1638,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1481,7 +1647,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=nosuid", + "--options=nosuid,private", "--property=Before=initrd-fs.target", }, }) @@ -1510,6 +1676,7 @@ defer restore() baseMnt := filepath.Join(boot.InitramfsRunMntDir, "base") + gadgetMnt := filepath.Join(boot.InitramfsRunMntDir, "gadget") kernelMnt := filepath.Join(boot.InitramfsRunMntDir, "kernel") snapdMnt := filepath.Join(boot.InitramfsRunMntDir, "snapd") @@ -1534,7 +1701,7 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) // write modeenv modeEnv := boot.Modeenv{ @@ -1568,6 +1735,7 @@ snapdMnt, kernelMnt, baseMnt, + gadgetMnt, boot.InitramfsDataDir, boot.InitramfsUbuntuBootDir, boot.InitramfsHostUbuntuDataDir, @@ -1581,6 +1749,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1589,7 +1758,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1598,7 +1767,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1607,7 +1776,16 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", + "--property=Before=initrd-fs.target", + }, { + "systemd-mount", + filepath.Join(s.seedDir, "snaps", s.gadget.Filename()), + gadgetMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1617,7 +1795,7 @@ "--no-ask-password", "--type=tmpfs", "--fsck=no", - "--options=nosuid", + "--options=nosuid,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1626,6 +1804,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1634,7 +1813,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=nosuid", + "--options=nosuid,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1643,6 +1822,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=private", "--property=Before=initrd-fs.target", }, }) @@ -1666,6 +1846,7 @@ defer restore() baseMnt := filepath.Join(boot.InitramfsRunMntDir, "base") + gadgetMnt := filepath.Join(boot.InitramfsRunMntDir, "gadget") kernelMnt := filepath.Join(boot.InitramfsRunMntDir, "kernel") // don't do anything from systemd-mount, we verify the arguments passed at @@ -1684,23 +1865,21 @@ // aren't mounted by the time systemd-mount returns case 1, 2: c.Assert(where, Equals, boot.InitramfsUbuntuBootDir) - return n%2 == 0, nil case 3, 4: c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir) - return n%2 == 0, nil case 5, 6: c.Assert(where, Equals, boot.InitramfsDataDir) - return n%2 == 0, nil case 7, 8: c.Assert(where, Equals, baseMnt) - return n%2 == 0, nil case 9, 10: + c.Assert(where, Equals, gadgetMnt) + case 11, 12: c.Assert(where, Equals, kernelMnt) - return n%2 == 0, nil default: c.Errorf("unexpected IsMounted check on %s", where) return false, fmt.Errorf("unexpected IsMounted check on %s", where) } + return n%2 == 0, nil }) defer restore() @@ -1713,12 +1892,13 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) // write modeenv modeEnv := boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, } err := modeEnv.WriteTo(boot.InitramfsWritableDir) @@ -1738,6 +1918,7 @@ systemd.EscapeUnitNamePath(boot.InitramfsUbuntuSeedDir), systemd.EscapeUnitNamePath(boot.InitramfsDataDir), systemd.EscapeUnitNamePath(baseMnt), + systemd.EscapeUnitNamePath(gadgetMnt), systemd.EscapeUnitNamePath(kernelMnt), } { fname := fmt.Sprintf("snap_bootstrap_%s.conf", mountUnit) @@ -1749,7 +1930,7 @@ } // 2 IsMounted calls per mount point, so 10 total IsMounted calls - c.Assert(n, Equals, 10) + c.Assert(n, Equals, 12) c.Assert(cmd.Calls(), DeepEquals, [][]string{ { @@ -1759,6 +1940,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1767,6 +1949,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1775,7 +1958,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", - "--options=nosuid", + "--options=nosuid,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1784,7 +1967,16 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", + "--property=Before=initrd-fs.target", + }, { + "systemd-mount", + filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.gadget.Filename()), + gadgetMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1793,7 +1985,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", "--property=Before=initrd-fs.target", }, }) @@ -1812,6 +2004,7 @@ defer restore() baseMnt := filepath.Join(boot.InitramfsRunMntDir, "base") + gadgetMnt := filepath.Join(boot.InitramfsRunMntDir, "gadget") kernelMnt := filepath.Join(boot.InitramfsRunMntDir, "kernel") // don't do anything from systemd-mount, we verify the arguments passed at @@ -1835,12 +2028,13 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) // write modeenv modeEnv := boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, } err := modeEnv.WriteTo(boot.InitramfsWritableDir) @@ -1870,6 +2064,7 @@ boot.InitramfsDataDir, boot.InitramfsUbuntuSaveDir, baseMnt, + gadgetMnt, kernelMnt, }) c.Check(cmd.Calls(), DeepEquals, [][]string{ @@ -1880,6 +2075,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1888,6 +2084,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1896,7 +2093,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", - "--options=nosuid", + "--options=nosuid,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1905,6 +2102,7 @@ "--no-pager", "--no-ask-password", "--fsck=yes", + "--options=private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1913,7 +2111,16 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", + "--property=Before=initrd-fs.target", + }, { + "systemd-mount", + filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.gadget.Filename()), + gadgetMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + "--options=ro,private", "--property=Before=initrd-fs.target", }, { "systemd-mount", @@ -1922,7 +2129,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", - "--options=ro", + "--options=ro,private", "--property=Before=initrd-fs.target", }, }) @@ -1941,11 +2148,12 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), // RecoverySystem set makes us mount the snapd snap here s.makeSeedSnapSystemdMount(snap.TypeSnapd), @@ -1961,13 +2169,14 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) // write modeenv modeEnv := boot.Modeenv{ Mode: "run", RecoverySystem: "20191118", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, } err := modeEnv.WriteTo(boot.InitramfsWritableDir) @@ -2001,9 +2210,10 @@ needsFsckDiskMountOpts, nil, }, - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), }, nil) defer restore() @@ -2017,12 +2227,13 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) // write modeenv modeEnv := boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, } err := modeEnv.WriteTo(boot.InitramfsWritableDir) @@ -2052,8 +2263,8 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), { "/dev/mapper/ubuntu-data-random", boot.InitramfsDataDir, @@ -2067,6 +2278,7 @@ nil, }, s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), }, nil) defer restore() @@ -2138,12 +2350,13 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) // write modeenv modeEnv := boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, } err = modeEnv.WriteTo(boot.InitramfsWritableDir) @@ -2162,6 +2375,116 @@ c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "run-model-measured"), testutil.FilePresent) } +func (s *initramfsMountsSuite) TestInitramfsMountsRunCVMModeHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=cloudimg-rootfs") + + restore := main.MockPartitionUUIDForBootedKernelDisk("specific-ubuntu-seed-partuuid") + defer restore() + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultCVMDisk, + {Mountpoint: boot.InitramfsDataDir, IsDecryptedDevice: true}: defaultCVMDisk, + }, + ) + defer restore() + + // don't do anything from systemd-mount, we verify the arguments passed at + // the end with cmd.Calls + cmd := testutil.MockCommand(c, "systemd-mount", ``) + defer cmd.Restore() + + // mock that in turn, /run/mnt/ubuntu-boot, /run/mnt/ubuntu-seed, etc. are + // mounted + n := 0 + restore = main.MockOsutilIsMounted(func(where string) (bool, error) { + n++ + switch n { + // first call for each mount returns false, then returns true, this + // tests in the case where systemd is racy / inconsistent and things + // aren't mounted by the time systemd-mount returns + case 1, 2: + c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir) + case 3, 4: + c.Assert(where, Equals, boot.InitramfsDataDir) + case 5, 6: + c.Assert(where, Equals, boot.InitramfsUbuntuSeedDir) + default: + c.Errorf("unexpected IsMounted check on %s", where) + return false, fmt.Errorf("unexpected IsMounted check on %s", where) + } + return n%2 == 0, nil + }) + defer restore() + + // Mock the call to TPMCVM, to ensure that TPM provisioning is + // done before unlock attempt + provisionTPMCVMCalled := false + restore = main.MockSecbootProvisionForCVM(func(_ string) error { + // Ensure this function is only called once + c.Assert(provisionTPMCVMCalled, Equals, false) + provisionTPMCVMCalled = true + return nil + }) + defer restore() + + cloudimgActivated := false + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + c.Assert(provisionTPMCVMCalled, Equals, true) + c.Assert(name, Equals, "cloudimg-rootfs") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/cloudimg-rootfs.sealed-key")) + c.Assert(opts.AllowRecoveryKey, Equals, true) + c.Assert(opts.WhichModel, IsNil) + + cloudimgActivated = true + // return true because we are using an encrypted device + return happyUnlocked("cloudimg-rootfs", secboot.UnlockedWithSealedKey), nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + c.Check(s.Stdout.String(), Equals, "") + + // 2 per mountpoint + 1 more for cross check + c.Assert(n, Equals, 5) + + // failed to use mockSystemdMountSequence way of asserting this + // note that other test cases also mix & match using + // mockSystemdMountSequence & DeepEquals + c.Assert(cmd.Calls(), DeepEquals, [][]string{ + { + "systemd-mount", + "/dev/disk/by-partuuid/specific-ubuntu-seed-partuuid", + boot.InitramfsUbuntuSeedDir, + "--no-pager", + "--no-ask-password", + "--fsck=yes", + "--options=private", + "--property=Before=initrd-fs.target", + }, + { + "systemd-mount", + "/dev/mapper/cloudimg-rootfs-random", + boot.InitramfsDataDir, + "--no-pager", + "--no-ask-password", + "--fsck=yes", + }, + { + "systemd-mount", + boot.InitramfsUbuntuSeedDir, + "--umount", + "--no-pager", + "--no-ask-password", + "--fsck=no", + }, + }) + + c.Check(provisionTPMCVMCalled, Equals, true) + c.Check(cloudimgActivated, Equals, true) +} + func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataUnhappyNoSave(c *C) { s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") @@ -2185,8 +2508,8 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), { "/dev/mapper/ubuntu-data-random", boot.InitramfsDataDir, @@ -2229,7 +2552,7 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) // write modeenv modeEnv := boot.Modeenv{ @@ -2264,8 +2587,8 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), { "/dev/mapper/ubuntu-data-random", boot.InitramfsDataDir, @@ -2307,7 +2630,7 @@ restore = bloader.SetEnabledKernel(s.kernel) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) // write modeenv modeEnv := boot.Modeenv{ @@ -2340,6 +2663,10 @@ func (s *initramfsMountsSuite) testInitramfsMountsEncryptedNoModel(c *C, mode, label string, expectedMeasureModelCalls int) { s.mockProcCmdlineContent(c, fmt.Sprintf("snapd_recovery_mode=%s", mode)) + // Make sure there is no model for this test + err := os.Remove(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model")) + c.Assert(err, IsNil) + // ensure that we check that access to sealed keys were locked sealedKeysLocked := false defer main.MockSecbootLockSealedKeys(func() error { @@ -2347,13 +2674,11 @@ return fmt.Errorf("blocking keys failed") })() - // install and recover mounts are just ubuntu-seed before we fail var restore func() if mode == "run" { - // run mode will mount ubuntu-boot and ubuntu-seed + // run mode will mount ubuntu-boot only before failing restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", mode), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", mode), + s.ubuntuLabelMount("ubuntu-boot", mode), }, nil) restore2 := disks.MockMountPointDisksToPartitionMapping( map[disks.Mountpoint]*disks.MockDiskMapping{ @@ -2362,8 +2687,9 @@ ) defer restore2() } else { + // install and recover mounts are just ubuntu-seed before we fail restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", mode), + s.ubuntuLabelMount("ubuntu-seed", mode), }, nil) // in install / recover mode the code doesn't make it far enough to do @@ -2397,7 +2723,7 @@ }) defer restore() - _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) where := "/run/mnt/ubuntu-boot/device/model" if mode != "run" { where = fmt.Sprintf("/run/mnt/ubuntu-seed/systems/%s/model", label) @@ -2434,16 +2760,18 @@ modeenv: &boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, }, additionalMountsFunc: func() []systemdMount { return []systemdMount{ s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), } }, enableKernel: s.kernel, - snapFiles: []snap.PlaceInfo{s.core20, s.kernel}, + snapFiles: []snap.PlaceInfo{s.core20, s.gadget, s.kernel}, comment: "happy default no upgrades", }, @@ -2452,18 +2780,20 @@ modeenv: &boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename(), s.kernelr2.Filename()}, }, additionalMountsFunc: func() []systemdMount { return []systemdMount{ s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernelr2), } }, kernelStatus: boot.TryingStatus, enableKernel: s.kernel, enableTryKernel: s.kernelr2, - snapFiles: []snap.PlaceInfo{s.core20, s.kernel, s.kernelr2}, + snapFiles: []snap.PlaceInfo{s.core20, s.gadget, s.kernel, s.kernelr2}, comment: "happy kernel snap upgrade", }, { @@ -2472,21 +2802,24 @@ Base: s.core20.Filename(), TryBase: s.core20r2.Filename(), BaseStatus: boot.TryStatus, + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, }, additionalMountsFunc: func() []systemdMount { return []systemdMount{ s.makeRunSnapSystemdMount(snap.TypeBase, s.core20r2), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), } }, enableKernel: s.kernel, - snapFiles: []snap.PlaceInfo{s.kernel, s.core20, s.core20r2}, + snapFiles: []snap.PlaceInfo{s.kernel, s.gadget, s.core20, s.core20r2}, expModeenv: &boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), TryBase: s.core20r2.Filename(), BaseStatus: boot.TryingStatus, + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, }, comment: "happy base snap upgrade", @@ -2497,23 +2830,26 @@ Base: s.core20.Filename(), TryBase: s.core20r2.Filename(), BaseStatus: boot.TryStatus, + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename(), s.kernelr2.Filename()}, }, additionalMountsFunc: func() []systemdMount { return []systemdMount{ s.makeRunSnapSystemdMount(snap.TypeBase, s.core20r2), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernelr2), } }, enableKernel: s.kernel, enableTryKernel: s.kernelr2, - snapFiles: []snap.PlaceInfo{s.kernel, s.kernelr2, s.core20, s.core20r2}, + snapFiles: []snap.PlaceInfo{s.kernel, s.kernelr2, s.core20, s.core20r2, s.gadget}, kernelStatus: boot.TryingStatus, expModeenv: &boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), TryBase: s.core20r2.Filename(), BaseStatus: boot.TryingStatus, + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename(), s.kernelr2.Filename()}, }, comment: "happy simultaneous base snap and kernel snap upgrade", @@ -2525,17 +2861,19 @@ Mode: "run", Base: s.core20.Filename(), TryBase: s.core20r2.Filename(), + Gadget: s.gadget.Filename(), BaseStatus: boot.TryStatus, CurrentKernels: []string{s.kernel.Filename()}, }, additionalMountsFunc: func() []systemdMount { return []systemdMount{ s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), } }, enableKernel: s.kernel, - snapFiles: []snap.PlaceInfo{s.kernel, s.core20}, + snapFiles: []snap.PlaceInfo{s.kernel, s.core20, s.gadget}, comment: "happy fallback try base not existing", }, { @@ -2544,16 +2882,18 @@ Base: s.core20.Filename(), BaseStatus: boot.TryStatus, TryBase: "", + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, }, additionalMountsFunc: func() []systemdMount { return []systemdMount{ s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), } }, enableKernel: s.kernel, - snapFiles: []snap.PlaceInfo{s.kernel, s.core20}, + snapFiles: []snap.PlaceInfo{s.kernel, s.core20, s.gadget}, comment: "happy fallback base_status try, empty try_base", }, { @@ -2562,21 +2902,24 @@ Base: s.core20.Filename(), TryBase: s.core20r2.Filename(), BaseStatus: boot.TryingStatus, + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, }, additionalMountsFunc: func() []systemdMount { return []systemdMount{ s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), } }, enableKernel: s.kernel, - snapFiles: []snap.PlaceInfo{s.kernel, s.core20, s.core20r2}, + snapFiles: []snap.PlaceInfo{s.kernel, s.core20, s.core20r2, s.gadget}, expModeenv: &boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), TryBase: s.core20r2.Filename(), BaseStatus: boot.DefaultStatus, + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, }, comment: "happy fallback failed boot with try snap", @@ -2585,11 +2928,12 @@ modeenv: &boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, }, enableKernel: s.kernel, enableTryKernel: s.kernelr2, - snapFiles: []snap.PlaceInfo{s.core20, s.kernel, s.kernelr2}, + snapFiles: []snap.PlaceInfo{s.core20, s.gadget, s.kernel, s.kernelr2}, kernelStatus: boot.TryingStatus, expRebootPanic: "reboot due to untrusted try kernel snap", comment: "happy fallback untrusted try kernel snap", @@ -2603,11 +2947,12 @@ modeenv: &boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, }, kernelStatus: boot.TryingStatus, enableKernel: s.kernel, - snapFiles: []snap.PlaceInfo{s.core20, s.kernel}, + snapFiles: []snap.PlaceInfo{s.core20, s.kernel, s.gadget}, expRebootPanic: "reboot due to no try kernel snap", comment: "happy fallback kernel_status trying no try kernel", }, @@ -2624,10 +2969,11 @@ modeenv: &boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename()}, }, enableKernel: s.kernelr2, - snapFiles: []snap.PlaceInfo{s.core20, s.kernelr2}, + snapFiles: []snap.PlaceInfo{s.core20, s.kernelr2, s.gadget}, expError: fmt.Sprintf("fallback kernel snap %q is not trusted in the modeenv", s.kernelr2.Filename()), comment: "unhappy untrusted main kernel snap", }, @@ -2660,13 +3006,23 @@ ) cleanups = append(cleanups, restore) + // Make sure we have a model + var err error + err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755) + c.Assert(err, IsNil) + mf, err := os.Create(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model")) + c.Assert(err, IsNil) + defer mf.Close() + err = asserts.NewEncoder(mf).Encode(s.model) + c.Assert(err, IsNil) + // setup expected systemd-mount calls - every test case has ubuntu-boot, // ubuntu-seed and ubuntu-data mounts because all those mounts happen // before any boot logic mnts := []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), } if t.additionalMountsFunc != nil { mnts = append(mnts, t.additionalMountsFunc()...) @@ -2688,7 +3044,7 @@ } // set the kernel_status boot var - err := bloader.SetBootVars(map[string]string{"kernel_status": t.kernelStatus}) + err = bloader.SetBootVars(map[string]string{"kernel_status": t.kernelStatus}) c.Assert(err, IsNil, comment) // write the initial modeenv @@ -2697,7 +3053,7 @@ // make the snap files - no restore needed because we use a unique root // dir for each test case - makeSnapFilesOnEarlyBootUbuntuData(c, t.snapFiles...) + s.makeSnapFilesOnEarlyBootUbuntuData(c, t.snapFiles...) if t.expRebootPanic != "" { f := func() { main.Parser().ParseArgs([]string{"initramfs-mounts"}) } @@ -2743,11 +3099,12 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), s.makeRunSnapSystemdMount(snap.TypeKernel, *finalKernel), }, nil) defer restore() @@ -2764,12 +3121,13 @@ restore = bloader.SetEnabledTryKernel(s.kernelr2) defer restore() - makeSnapFilesOnEarlyBootUbuntuData(c, s.core20, s.kernel, s.kernelr2) + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.core20, s.gadget, s.kernel, s.kernelr2) // write modeenv modeEnv := boot.Modeenv{ Mode: "run", Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), CurrentKernels: []string{s.kernel.Filename(), s.kernelr2.Filename()}, } err := modeEnv.WriteTo(boot.InitramfsWritableDir) @@ -2867,6 +3225,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=recover recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -2922,10 +3281,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -2947,7 +3307,7 @@ { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -3010,10 +3370,11 @@ cleanups = append(cleanups, restore) restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -3035,7 +3396,7 @@ { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -3093,10 +3454,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -3118,7 +3480,7 @@ { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -3181,6 +3543,7 @@ s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -3202,7 +3565,7 @@ { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -3296,10 +3659,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -3321,7 +3685,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -3454,10 +3818,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -3479,7 +3844,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -3631,10 +3996,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -3656,7 +4022,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -3800,10 +4166,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -3820,7 +4187,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -3962,10 +4329,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -3982,7 +4350,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -4130,10 +4498,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -4149,7 +4518,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -4173,6 +4542,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=recover recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -4318,10 +4688,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -4337,7 +4708,7 @@ { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -4361,6 +4732,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=recover recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -4507,10 +4879,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -4652,10 +5025,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -4768,10 +5142,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -4793,7 +5168,7 @@ { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -4913,10 +5288,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -4932,7 +5308,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -4956,6 +5332,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=recover recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -5117,10 +5494,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -5154,6 +5532,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=recover recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -5284,10 +5663,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -5309,7 +5689,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -5333,6 +5713,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=recover recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -5494,10 +5875,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -5519,7 +5901,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -5540,10 +5922,11 @@ s.mockProcCmdlineContent(c, fmt.Sprintf("snapd_recovery_mode=%s snapd_recovery_system=%s", mode, s.sysLabel)) modeMnts := []systemdMount{ - ubuntuLabelMount("ubuntu-seed", mode), + s.ubuntuLabelMount("ubuntu-seed", mode), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -5584,7 +5967,7 @@ systemdMount{ "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }) @@ -5637,6 +6020,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=install recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -5681,10 +6065,11 @@ ) defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -5706,7 +6091,7 @@ { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -5782,10 +6167,11 @@ ) defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -5896,6 +6282,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=recover recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -5938,10 +6325,11 @@ }: defaultEncBootDisk, } mountSequence := []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -6324,10 +6712,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -6337,7 +6726,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -6366,6 +6755,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=factory-reset recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -6431,10 +6821,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -6444,7 +6835,7 @@ { "/dev/disk/by-partuuid/ubuntu-save-partuuid", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, nil, }, }, nil) @@ -6462,6 +6853,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=factory-reset recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -6515,10 +6907,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -6540,6 +6933,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=factory-reset recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -6614,10 +7008,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -6650,6 +7045,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=factory-reset recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -6717,10 +7113,11 @@ defer restore() restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-seed", "recover"), + s.ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), + s.makeSeedSnapSystemdMount(snap.TypeGadget), { "tmpfs", boot.InitramfsDataDir, @@ -6730,7 +7127,7 @@ { "/dev/mapper/ubuntu-save-random", boot.InitramfsUbuntuSaveDir, - nil, + mountOpts, fmt.Errorf("mount failed"), }, }, nil) @@ -6759,6 +7156,7 @@ c.Check(modeEnv, testutil.FileEquals, `mode=factory-reset recovery_system=20191118 base=core20_1.snap +gadget=pc_1.snap model=my-brand/my-model grade=signed `) @@ -6783,3 +7181,438 @@ c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) } + +type initramfsClassicMountsSuite struct { + baseInitramfsMountsSuite +} + +var _ = Suite(&initramfsClassicMountsSuite{}) + +func (s *initramfsClassicMountsSuite) SetUpTest(c *C) { + s.isClassic = true + s.baseInitramfsMountsSuite.SetUpTest(c) +} + +func writeGadget(c *C, espName, espRole string) { + gadgetYaml := ` +volumes: + pc: + bootloader: grub + structure: + - name: ` + espName + + if espRole != "" { + gadgetYaml += ` + role: ` + espRole + } + + gadgetYaml += ` + filesystem: vfat + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + size: 99M + - name: ubuntu-boot + role: system-boot + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + offset: 1202M + size: 750M + - name: ubuntu-save + role: system-save + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 16M + - name: ubuntu-data + role: system-data + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 4312776192 +` + var err error + gadgetDir := filepath.Join(boot.InitramfsRunMntDir, "gadget", "meta") + err = os.MkdirAll(gadgetDir, 0755) + c.Assert(err, IsNil) + err = osutil.AtomicWriteFile(filepath.Join(gadgetDir, "gadget.yaml"), []byte(gadgetYaml), 0644, 0) + c.Assert(err, IsNil) +} + +func (s *initramfsClassicMountsSuite) TestInitramfsMountsRunModeUnencryptedWithSaveHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsDataDir) + c.Assert(err, IsNil) + + // write gadget.yaml, which is checked for classic + writeGadget(c, "ubuntu-seed", "system-seed") + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) +} + +func (s *initramfsClassicMountsSuite) TestInitramfsMountsRunModeUnencryptedSeedPartNotInGadget(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsDataDir) + c.Assert(err, IsNil) + + // write gadget.yaml with no ubuntu-seed label + writeGadget(c, "EFI System partition", "") + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, ErrorMatches, "seed partition found but not defined in the gadget") +} + +func (s *initramfsClassicMountsSuite) TestInitramfsMountsRunModeUnencryptedSeedInGadgetNotInVolume(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultNoSeedWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultNoSeedWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultNoSeedWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsDataDir) + c.Assert(err, IsNil) + + // write gadget.yaml, which is checked for classic + writeGadget(c, "ubuntu-seed", "system-seed") + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, ErrorMatches, "seed partition not found but defined in the gadget") +} + +func (s *initramfsClassicMountsSuite) TestInitramfsMountsRunModeUnencryptedNoSeedHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultNoSeedWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultNoSeedWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultNoSeedWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsDataDir) + c.Assert(err, IsNil) + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) +} + +func (s *initramfsClassicMountsSuite) TestInitramfsMountsRunModeHappyNoGadgetMount(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + s.ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) + + // write modeenv, with no gadget field so the gadget is not mounted + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsDataDir) + c.Assert(err, IsNil) + + // write gadget.yaml, which is checked for classic + writeGadget(c, "ubuntu-seed", "system-seed") + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) +} + +func (s *initramfsClassicMountsSuite) TestInitramfsMountsRunModeEncryptedDataHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + defer main.MockSecbootLockSealedKeys(func() error { + sealedKeysLocked = true + return nil + })() + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsDataDir, IsDecryptedDevice: true}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir, IsDecryptedDevice: true}: defaultEncBootDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + s.ubuntuLabelMount("ubuntu-boot", "run"), + s.ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + { + "/dev/mapper/ubuntu-data-random", + boot.InitramfsDataDir, + needsFsckNoPrivateDiskMountOpts, + nil, + }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + needsFsckDiskMountOpts, + nil, + }, + s.makeRunSnapSystemdMount(snap.TypeGadget, s.gadget), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // write the installed model like makebootable does it + err := os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755) + c.Assert(err, IsNil) + mf, err := os.Create(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model")) + c.Assert(err, IsNil) + defer mf.Close() + err = asserts.NewEncoder(mf).Encode(s.model) + c.Assert(err, IsNil) + + // write gadget.yaml, which is checked for classic + writeGadget(c, "ubuntu-seed", "system-seed") + + dataActivated := false + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted( + func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + c.Assert(opts.AllowRecoveryKey, Equals, true) + c.Assert(opts.WhichModel, NotNil) + mod, err := opts.WhichModel() + c.Assert(err, IsNil) + c.Check(mod.Model(), Equals, "my-model") + + dataActivated = true + // return true because we are using an encrypted device + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsDataDir, "foo", "marker") + s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker") + + saveActivated := false + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + saveActivated = true + c.Assert(name, Equals, "ubuntu-save") + c.Assert(key, DeepEquals, []byte("foo")) + return happyUnlocked("ubuntu-save", secboot.UnlockedWithKey), nil + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + s.makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20, s.gadget) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + Gadget: s.gadget.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err = modeEnv.WriteTo(boot.InitramfsDataDir) + c.Assert(err, IsNil) + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + c.Check(dataActivated, Equals, true) + c.Check(saveActivated, Equals, true) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + c.Check(sealedKeysLocked, Equals, true) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "run-model-measured"), testutil.FilePresent) +} diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_recovery_chooser_trigger.go snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_recovery_chooser_trigger.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_recovery_chooser_trigger.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_recovery_chooser_trigger.go 2022-10-17 16:25:18.000000000 +0000 @@ -47,15 +47,17 @@ triggerwatchWait = triggerwatch.Wait // default trigger wait timeout - defaultTimeout = 10 * time.Second + defaultTimeout = 10 * time.Second + defaultDeviceTimeout = 2 * time.Second // default marker file location defaultMarkerFile = "/run/snapd-recovery-chooser-triggered" ) type cmdRecoveryChooserTrigger struct { - MarkerFile string `long:"marker-file" value-name:"filename" description:"trigger marker file location"` - WaitTimeout string `long:"wait-timeout" value-name:"duration" description:"trigger wait timeout"` + MarkerFile string `long:"marker-file" value-name:"filename" description:"trigger marker file location"` + WaitTimeout string `long:"wait-timeout" value-name:"duration" description:"trigger wait timeout"` + DeviceTimeout string `long:"device-timeout" value-name:"duration" description:"timeout for devices to appear"` } func (c *cmdRecoveryChooserTrigger) Execute(args []string) error { @@ -64,6 +66,7 @@ // and also thinking if/how such a hook can be confined. timeout := defaultTimeout + deviceTimeout := defaultDeviceTimeout markerFile := defaultMarkerFile if c.WaitTimeout != "" { @@ -74,10 +77,19 @@ timeout = userTimeout } } + if c.DeviceTimeout != "" { + userTimeout, err := time.ParseDuration(c.DeviceTimeout) + if err != nil { + logger.Noticef("cannot parse duration %q, using default", c.DeviceTimeout) + } else { + deviceTimeout = userTimeout + } + } if c.MarkerFile != "" { markerFile = c.MarkerFile } logger.Noticef("trigger wait timeout %v", timeout) + logger.Noticef("device timeout %v", deviceTimeout) logger.Noticef("marker file %v", markerFile) _, err := os.Stat(markerFile) @@ -86,7 +98,7 @@ return nil } - err = triggerwatchWait(timeout) + err = triggerwatchWait(timeout, deviceTimeout) if err != nil { switch err { case triggerwatch.ErrTriggerNotDetected: diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_recovery_chooser_trigger_test.go snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_recovery_chooser_trigger_test.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/cmd_recovery_chooser_trigger_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/cmd_recovery_chooser_trigger_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -36,11 +36,13 @@ n := 0 marker := filepath.Join(c.MkDir(), "marker") passedTimeout := time.Duration(0) + passedDeviceTimeout := time.Duration(0) restore := main.MockDefaultMarkerFile(marker) defer restore() - restore = main.MockTriggerwatchWait(func(timeout time.Duration) error { + restore = main.MockTriggerwatchWait(func(timeout time.Duration, deviceTimeout time.Duration) error { passedTimeout = timeout + passedDeviceTimeout = deviceTimeout n++ // trigger happened return nil @@ -52,6 +54,7 @@ c.Assert(rest, HasLen, 0) c.Check(n, Equals, 1) c.Check(passedTimeout, Equals, main.DefaultTimeout) + c.Check(passedDeviceTimeout, Equals, main.DefaultDeviceTimeout) c.Check(marker, testutil.FilePresent) } @@ -61,7 +64,7 @@ restore := main.MockDefaultMarkerFile(marker) defer restore() - restore = main.MockTriggerwatchWait(func(_ time.Duration) error { + restore = main.MockTriggerwatchWait(func(_ time.Duration, _ time.Duration) error { n++ // trigger did not happen return triggerwatch.ErrTriggerNotDetected @@ -78,9 +81,11 @@ marker := filepath.Join(c.MkDir(), "foobar") n := 0 passedTimeout := time.Duration(0) + passedDeviceTimeout := time.Duration(0) - restore := main.MockTriggerwatchWait(func(timeout time.Duration) error { + restore := main.MockTriggerwatchWait(func(timeout time.Duration, deviceTimeout time.Duration) error { passedTimeout = timeout + passedDeviceTimeout = deviceTimeout n++ // trigger happened return nil @@ -89,6 +94,7 @@ rest, err := main.Parser().ParseArgs([]string{ "recovery-chooser-trigger", + "--device-timeout", "1m", "--wait-timeout", "2m", "--marker-file", marker, }) @@ -96,13 +102,14 @@ c.Assert(rest, HasLen, 0) c.Check(n, Equals, 1) c.Check(passedTimeout, Equals, 2*time.Minute) + c.Check(passedDeviceTimeout, Equals, 1*time.Minute) c.Check(marker, testutil.FilePresent) } func (s *cmdSuite) TestRecoveryChooserTriggerDoesNothingWhenMarkerPresent(c *C) { marker := filepath.Join(c.MkDir(), "foobar") n := 0 - restore := main.MockTriggerwatchWait(func(_ time.Duration) error { + restore := main.MockTriggerwatchWait(func(_ time.Duration, _ time.Duration) error { n++ return errors.New("unexpected call") }) @@ -127,7 +134,7 @@ restore := main.MockDefaultMarkerFile(filepath.Join(c.MkDir(), "marker")) defer restore() - restore = main.MockTriggerwatchWait(func(timeout time.Duration) error { + restore = main.MockTriggerwatchWait(func(timeout time.Duration, _ time.Duration) error { passedTimeout = timeout n++ // trigger happened @@ -144,13 +151,36 @@ c.Check(passedTimeout, Equals, main.DefaultTimeout) } +func (s *cmdSuite) TestRecoveryChooserTriggerBadDeviceDurationFallback(c *C) { + n := 0 + passedTimeout := time.Duration(0) + restore := main.MockDefaultMarkerFile(filepath.Join(c.MkDir(), "marker")) + defer restore() + + restore = main.MockTriggerwatchWait(func(_ time.Duration, timeout time.Duration) error { + passedTimeout = timeout + n++ + // trigger happened + return triggerwatch.ErrTriggerNotDetected + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{ + "recovery-chooser-trigger", + "--device-timeout=foobar", + }) + c.Assert(err, IsNil) + c.Check(n, Equals, 1) + c.Check(passedTimeout, Equals, main.DefaultDeviceTimeout) +} + func (s *cmdSuite) TestRecoveryChooserTriggerNoInputDevsNoError(c *C) { n := 0 marker := filepath.Join(c.MkDir(), "marker") restore := main.MockDefaultMarkerFile(marker) defer restore() - restore = main.MockTriggerwatchWait(func(_ time.Duration) error { + restore = main.MockTriggerwatchWait(func(_ time.Duration, _ time.Duration) error { n++ // no input devices return triggerwatch.ErrNoMatchingInputDevices diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/export_test.go snapd-2.57.5+20.04/cmd/snap-bootstrap/export_test.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -82,7 +82,7 @@ } } -func MockTriggerwatchWait(f func(_ time.Duration) error) (restore func()) { +func MockTriggerwatchWait(f func(_ time.Duration, _ time.Duration) error) (restore func()) { oldTriggerwatchWait := triggerwatchWait triggerwatchWait = f return func() { @@ -91,6 +91,7 @@ } var DefaultTimeout = defaultTimeout +var DefaultDeviceTimeout = defaultDeviceTimeout func MockDefaultMarkerFile(p string) (restore func()) { old := defaultMarkerFile @@ -116,6 +117,14 @@ } } +func MockSecbootProvisionForCVM(f func(_ string) error) (restore func()) { + old := secbootProvisionForCVM + secbootProvisionForCVM = f + return func() { + secbootProvisionForCVM = old + } +} + func MockSecbootMeasureSnapSystemEpochWhenPossible(f func() error) (restore func()) { old := secbootMeasureSnapSystemEpochWhenPossible secbootMeasureSnapSystemEpochWhenPossible = f diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/initramfs_mounts_state.go snapd-2.57.5+20.04/cmd/snap-bootstrap/initramfs_mounts_state.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/initramfs_mounts_state.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/initramfs_mounts_state.go 2022-10-17 16:25:18.000000000 +0000 @@ -24,6 +24,7 @@ "fmt" "os" "path/filepath" + "runtime" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" @@ -36,6 +37,7 @@ var ( osutilSetTime = osutil.SetTime + runtimeNumCPU = runtime.NumCPU ) // initramfsMountsState helps tracking the state and progress @@ -70,7 +72,11 @@ // the RTC does not have a battery or is otherwise unreliable, etc. now := timeNow() - model, snaps, newTrustedEarliestTime, err := seed.ReadSystemEssentialAndBetterEarliestTime(boot.InitramfsUbuntuSeedDir, recoverySystem, essentialTypes, now, perf) + jobs := 1 + if runtimeNumCPU() > 1 { + jobs = 2 + } + model, snaps, newTrustedEarliestTime, err := seed.ReadSystemEssentialAndBetterEarliestTime(boot.InitramfsUbuntuSeedDir, recoverySystem, essentialTypes, now, jobs, perf) if err != nil { return nil, nil, err } @@ -123,6 +129,7 @@ Mode: mst.mode, RecoverySystem: mst.recoverySystem, Base: snaps[snap.TypeBase].Filename(), + Gadget: snaps[snap.TypeGadget].Filename(), Model: model.Model(), BrandID: model.BrandID(), Grade: string(model.Grade()), diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/initramfs_systemd_mount.go snapd-2.57.5+20.04/cmd/snap-bootstrap/initramfs_systemd_mount.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/initramfs_systemd_mount.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/initramfs_systemd_mount.go 2022-10-17 16:25:18.000000000 +0000 @@ -73,6 +73,10 @@ Bind bool // Read-only mount ReadOnly bool + // Private mount + Private bool + // Umount the mountpoint + Umount bool } // doSystemdMount will mount "what" at "where" using systemd-mount(1) with @@ -94,6 +98,11 @@ unitName := whereEscaped + ".mount" args := []string{what, where, "--no-pager", "--no-ask-password"} + + if opts.Umount { + args = []string{where, "--umount", "--no-pager", "--no-ask-password"} + } + if opts.Tmpfs { args = append(args, "--type=tmpfs") } @@ -139,6 +148,9 @@ if opts.ReadOnly { options = append(options, "ro") } + if opts.Private { + options = append(options, "private") + } if len(options) > 0 { args = append(args, "--options="+strings.Join(options, ",")) } @@ -195,7 +207,7 @@ var now time.Time for now = timeNow(); now.Sub(start) < defaultMountUnitWaitTimeout; now = timeNow() { mounted, err := osutilIsMounted(where) - if mounted { + if mounted == !opts.Umount { break } if err != nil { diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/initramfs_systemd_mount_test.go snapd-2.57.5+20.04/cmd/snap-bootstrap/initramfs_systemd_mount_test.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/initramfs_systemd_mount_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/initramfs_systemd_mount_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -155,6 +155,16 @@ what: "tmpfs", where: "/run/mnt/data", opts: &main.SystemdMountOptions{ + Umount: true, + }, + timeNowTimes: []time.Time{testStart, testStart}, + isMountedReturns: []bool{false}, + comment: "happy umount", + }, + { + what: "tmpfs", + where: "/run/mnt/data", + opts: &main.SystemdMountOptions{ NoSuid: true, Bind: true, }, @@ -237,6 +247,11 @@ args := []string{ "systemd-mount", t.what, t.where, "--no-pager", "--no-ask-password", } + if opts.Umount { + args = []string{ + "systemd-mount", t.where, "--umount", "--no-pager", "--no-ask-password", + } + } c.Assert(call[:len(args)], DeepEquals, args) foundTypeTmpfs := false @@ -247,6 +262,7 @@ foundNoSuid := false foundBind := false foundReadOnly := false + foundPrivate := false for _, arg := range call[len(args):] { switch { @@ -269,6 +285,8 @@ foundBind = true case "ro": foundReadOnly = true + case "private": + foundPrivate = true default: c.Logf("Option '%s' unexpected", opt) c.Fail() @@ -287,6 +305,7 @@ c.Assert(foundNoSuid, Equals, opts.NoSuid) c.Assert(foundBind, Equals, opts.Bind) c.Assert(foundReadOnly, Equals, opts.ReadOnly) + c.Assert(foundPrivate, Equals, opts.Private) // check that the overrides are present if opts.Ephemeral is false, // or check the overrides are not present if opts.Ephemeral is true diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/main.go snapd-2.57.5+20.04/cmd/snap-bootstrap/main.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/main.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -66,6 +66,9 @@ p := parser() _, err := p.ParseArgs(args) + if err != nil { + logger.Noticef("execution error: %v", err) + } return err } diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/triggerwatch/evdev.go snapd-2.57.5+20.04/cmd/snap-bootstrap/triggerwatch/evdev.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/triggerwatch/evdev.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/triggerwatch/evdev.go 2022-10-17 16:25:18.000000000 +0000 @@ -205,6 +205,38 @@ type evdevInput struct{} +func getCapabilityCode(Key string) (evdev.CapabilityCode, error) { + keyCode, ok := strToKey[Key] + if !ok { + return evdev.CapabilityCode{}, fmt.Errorf("cannot find a key matching the filter %q", Key) + } + return evdev.CapabilityCode{Code: keyCode, Name: Key}, nil +} + +func matchDevice(cap evdev.CapabilityCode, dev *evdev.InputDevice) triggerDevice { + for _, cc := range dev.Capabilities[evKeyCapability] { + if cc == cap { + return &evdevKeyboardInputDevice{ + dev: dev, + keyCode: uint16(cap.Code), + } + } + } + return nil +} + +func (e *evdevInput) Open(filter triggerEventFilter, node string) (triggerDevice, error) { + evdevDevice, err := evdev.Open(node) + if err != nil { + return nil, err + } + cap, err := getCapabilityCode(filter.Key) + if err != nil { + return nil, err + } + return matchDevice(cap, evdevDevice), nil +} + func (e *evdevInput) FindMatchingDevices(filter triggerEventFilter) ([]triggerDevice, error) { devices, err := evdev.ListInputDevices() if err != nil { @@ -212,28 +244,15 @@ } // NOTE: this supports so far only key input devices - - kc, ok := strToKey[filter.Key] - if !ok { - return nil, fmt.Errorf("cannot find a key matching the filter %q", filter.Key) + cap, err := getCapabilityCode(filter.Key) + if err != nil { + return nil, err } - cap := evdev.CapabilityCode{Code: kc, Name: filter.Key} - match := func(dev *evdev.InputDevice) triggerDevice { - for _, cc := range dev.Capabilities[evKeyCapability] { - if cc == cap { - return &evdevKeyboardInputDevice{ - dev: dev, - keyCode: uint16(cap.Code), - } - } - } - return nil - } // collect all input devices that can emit the trigger key var devs []triggerDevice for _, dev := range devices { - idev := match(dev) + idev := matchDevice(cap, dev) if idev != nil { devs = append(devs, idev) } else { diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/triggerwatch/export_test.go snapd-2.57.5+20.04/cmd/snap-bootstrap/triggerwatch/export_test.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/triggerwatch/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/triggerwatch/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -18,6 +18,12 @@ */ package triggerwatch +import ( + "time" + + "github.com/snapcore/snapd/osutil/udev/netlink" +) + func MockInput(newInput TriggerProvider) (restore func()) { oldInput := trigger trigger = newInput @@ -30,3 +36,37 @@ type TriggerDevice = triggerDevice type TriggerCapabilityFilter = triggerEventFilter type KeyEvent = keyEvent + +type mockUEventConnection struct { + events []netlink.UEvent +} + +func (m *mockUEventConnection) Connect(mode netlink.Mode) error { + return nil +} + +func (m *mockUEventConnection) Close() error { + return nil +} + +func (m *mockUEventConnection) Monitor(queue chan netlink.UEvent, errors chan error, matcher netlink.Matcher) func(time.Duration) bool { + go func() { + for _, event := range m.events { + queue <- event + } + }() + return func(time.Duration) bool { + return true + } +} + +func MockUEvent(events []netlink.UEvent) (restore func()) { + oldGetUEventConn := getUEventConn + getUEventConn = func() ueventConnection { + return &mockUEventConnection{events} + } + + return func() { + getUEventConn = oldGetUEventConn + } +} diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/triggerwatch/triggerwatch.go snapd-2.57.5+20.04/cmd/snap-bootstrap/triggerwatch/triggerwatch.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/triggerwatch/triggerwatch.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/triggerwatch/triggerwatch.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,12 +22,17 @@ import ( "errors" "fmt" + "os" + "os/signal" + "syscall" "time" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil/udev/netlink" ) type triggerProvider interface { + Open(filter triggerEventFilter, node string) (triggerDevice, error) FindMatchingDevices(filter triggerEventFilter) ([]triggerDevice, error) } @@ -37,9 +42,18 @@ Close() } +type ueventConnection interface { + Connect(mode netlink.Mode) error + Close() error + Monitor(queue chan netlink.UEvent, errors chan error, matcher netlink.Matcher) func(time.Duration) bool +} + var ( // trigger mechanism - trigger triggerProvider + trigger triggerProvider + getUEventConn = func() ueventConnection { + return &netlink.UEventConn{} + } // wait for '1' to be pressed triggerFilter = triggerEventFilter{Key: "KEY_1"} @@ -51,7 +65,33 @@ // Wait waits for a trigger on the available trigger devices for a given amount // of time. Returns nil if one was detected, ErrTriggerNotDetected if timeout // was hit, or other non-nil error. -func Wait(timeout time.Duration) error { +func Wait(timeout time.Duration, deviceTimeout time.Duration) error { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGUSR1) + conn := getUEventConn() + if err := conn.Connect(netlink.UdevEvent); err != nil { + logger.Panicf("Unable to connect to Netlink Kobject UEvent socket") + } + defer conn.Close() + + add := "add" + matcher := &netlink.RuleDefinitions{ + Rules: []netlink.RuleDefinition{ + { + Action: &add, + Env: map[string]string{ + "SUBSYSTEM": "input", + "ID_INPUT_KEYBOARD": "1", + "DEVNAME": ".*", + }, + }, + }, + } + + ueventQueue := make(chan netlink.UEvent) + ueventErrors := make(chan error) + conn.Monitor(ueventQueue, ueventErrors, matcher) + if trigger == nil { logger.Panicf("trigger is unset") } @@ -60,8 +100,9 @@ if err != nil { return fmt.Errorf("cannot list trigger devices: %v", err) } + if devices == nil { - return ErrNoMatchingInputDevices + devices = make([]triggerDevice, 0) } logger.Noticef("waiting for trigger key: %v", triggerFilter.Key) @@ -71,17 +112,47 @@ go dev.WaitForTrigger(detectKeyCh) defer dev.Close() } + foundDevice := len(devices) != 0 - select { - case kev := <-detectKeyCh: - if kev.Err != nil { - return kev.Err + start := time.Now() + for { + timePassed := time.Now().Sub(start) + relTimeout := timeout - timePassed + relDeviceTimeout := deviceTimeout - timePassed + select { + case kev := <-detectKeyCh: + if kev.Err != nil { + return kev.Err + } + // channel got closed without an error + logger.Noticef("%s: + got trigger key %v", kev.Dev, triggerFilter.Key) + return nil + case <-time.After(relTimeout): + return ErrTriggerNotDetected + case <-time.After(relDeviceTimeout): + if !foundDevice { + return ErrNoMatchingInputDevices + } + case uevent := <-ueventQueue: + dev, err := trigger.Open(triggerFilter, uevent.Env["DEVNAME"]) + if err != nil { + logger.Noticef("ignoring device %s that cannot be opened: %v", uevent.Env["DEVNAME"], err) + } else if dev != nil { + foundDevice = true + defer dev.Close() + go dev.WaitForTrigger(detectKeyCh) + } + case <-sigs: + logger.Noticef("Switching root") + if err := syscall.Chdir("/sysroot"); err != nil { + return fmt.Errorf("Cannot change directory: %w", err) + } + if err := syscall.Chroot("/sysroot"); err != nil { + return fmt.Errorf("Cannot change root: %w", err) + } + if err := syscall.Chdir("/"); err != nil { + return fmt.Errorf("Cannot change directory: %w", err) + } } - // channel got closed without an error - logger.Noticef("%s: + got trigger key %v", kev.Dev, triggerFilter.Key) - case <-time.After(timeout): - return ErrTriggerNotDetected } - - return nil } diff -Nru snapd-2.55.5+20.04/cmd/snap-bootstrap/triggerwatch/triggerwatch_test.go snapd-2.57.5+20.04/cmd/snap-bootstrap/triggerwatch/triggerwatch_test.go --- snapd-2.55.5+20.04/cmd/snap-bootstrap/triggerwatch/triggerwatch_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-bootstrap/triggerwatch/triggerwatch_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package triggerwatch_test import ( + "errors" "fmt" "testing" "time" @@ -27,6 +28,7 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/cmd/snap-bootstrap/triggerwatch" + "github.com/snapcore/snapd/osutil/udev/netlink" ) // Hook up check.v1 into the "go test" runner @@ -55,11 +57,14 @@ func (m *mockTriggerDevice) Close() { m.closeCalls++ } type mockTrigger struct { - f triggerwatch.TriggerCapabilityFilter - d *mockTriggerDevice + f triggerwatch.TriggerCapabilityFilter + d *mockTriggerDevice + unlistedDevices map[string]*mockTriggerDevice + err error findMatchingCalls int + openCalls int } func (m *mockTrigger) FindMatchingDevices(f triggerwatch.TriggerCapabilityFilter) ([]triggerwatch.TriggerDevice, error) { @@ -75,7 +80,18 @@ return nil, nil } +func (m *mockTrigger) Open(filter triggerwatch.TriggerCapabilityFilter, node string) (triggerwatch.TriggerDevice, error) { + m.openCalls++ + device, ok := m.unlistedDevices[node] + if !ok { + return nil, errors.New("Not found") + } else { + return device, nil + } +} + const testTriggerTimeout = 5 * time.Millisecond +const testDeviceTimeout = 2 * time.Millisecond func (s *triggerwatchSuite) TestNoDevsWaitKey(c *C) { md := &mockTriggerDevice{ev: &triggerwatch.KeyEvent{}} @@ -83,7 +99,7 @@ restore := triggerwatch.MockInput(mi) defer restore() - err := triggerwatch.Wait(testTriggerTimeout) + err := triggerwatch.Wait(testTriggerTimeout, testDeviceTimeout) c.Assert(err, IsNil) c.Assert(mi.findMatchingCalls, Equals, 1) c.Assert(md.waitForTriggerCalls, Equals, 1) @@ -96,7 +112,7 @@ restore := triggerwatch.MockInput(mi) defer restore() - err := triggerwatch.Wait(testTriggerTimeout) + err := triggerwatch.Wait(testTriggerTimeout, testDeviceTimeout) c.Assert(err, Equals, triggerwatch.ErrTriggerNotDetected) c.Assert(mi.findMatchingCalls, Equals, 1) c.Assert(md.waitForTriggerCalls, Equals, 1) @@ -108,7 +124,7 @@ restore := triggerwatch.MockInput(mi) defer restore() - err := triggerwatch.Wait(testTriggerTimeout) + err := triggerwatch.Wait(testTriggerTimeout, testDeviceTimeout) c.Assert(err, Equals, triggerwatch.ErrNoMatchingInputDevices) } @@ -117,7 +133,7 @@ restore := triggerwatch.MockInput(mi) defer restore() - err := triggerwatch.Wait(testTriggerTimeout) + err := triggerwatch.Wait(testTriggerTimeout, testDeviceTimeout) c.Assert(err, ErrorMatches, "cannot list trigger devices: failed") } @@ -125,6 +141,78 @@ restore := triggerwatch.MockInput(nil) defer restore() - c.Assert(func() { triggerwatch.Wait(testTriggerTimeout) }, + c.Assert(func() { triggerwatch.Wait(testTriggerTimeout, testDeviceTimeout) }, Panics, "trigger is unset") } + +func (s *triggerwatchSuite) TestUdevEvent(c *C) { + nodepath := "/dev/input/event0" + devpath := "/devices/SOMEBUS/input/input0/event0" + + md := &mockTriggerDevice{ev: &triggerwatch.KeyEvent{}} + mi := &mockTrigger{ + unlistedDevices: map[string]*mockTriggerDevice{ + "/dev/input/event0": md, + }, + } + restore := triggerwatch.MockInput(mi) + defer restore() + + events := []netlink.UEvent{ + { + Action: netlink.ADD, + KObj: devpath, + Env: map[string]string{ + "SUBSYSTEM": "input", + "DEVNAME": nodepath, + "DEVPATH": devpath, + }, + }, + } + restoreUevents := triggerwatch.MockUEvent(events) + defer restoreUevents() + + err := triggerwatch.Wait(testTriggerTimeout, testDeviceTimeout) + c.Assert(err, IsNil) + c.Assert(mi.findMatchingCalls, Equals, 1) + + c.Assert(mi.openCalls, Equals, 1) + c.Assert(md.waitForTriggerCalls, Equals, 1) + c.Assert(md.closeCalls, Equals, 1) +} + +func (s *triggerwatchSuite) TestUdevEventNoKeyEvent(c *C) { + nodepath := "/dev/input/event0" + devpath := "/devices/SOMEBUS/input/input0/event0" + + md := &mockTriggerDevice{} + mi := &mockTrigger{ + unlistedDevices: map[string]*mockTriggerDevice{ + "/dev/input/event0": md, + }, + } + restore := triggerwatch.MockInput(mi) + defer restore() + + events := []netlink.UEvent{ + { + Action: netlink.ADD, + KObj: devpath, + Env: map[string]string{ + "SUBSYSTEM": "input", + "DEVNAME": nodepath, + "DEVPATH": devpath, + }, + }, + } + restoreUevents := triggerwatch.MockUEvent(events) + defer restoreUevents() + + err := triggerwatch.Wait(testTriggerTimeout, testDeviceTimeout) + c.Assert(err, Equals, triggerwatch.ErrTriggerNotDetected) + c.Assert(mi.findMatchingCalls, Equals, 1) + + c.Assert(mi.openCalls, Equals, 1) + c.Assert(md.waitForTriggerCalls, Equals, 1) + c.Assert(md.closeCalls, Equals, 1) +} diff -Nru snapd-2.55.5+20.04/cmd/snap-confine/mount-support.h snapd-2.57.5+20.04/cmd/snap-confine/mount-support.h --- snapd-2.55.5+20.04/cmd/snap-confine/mount-support.h 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-confine/mount-support.h 2022-10-17 16:25:18.000000000 +0000 @@ -22,6 +22,16 @@ #include "snap-confine-invocation.h" #include +/* Base location where extra libraries might be made available to the snap. + * This is currently used for graphics drivers, but could pontentially be used + * for other goals as well. + * + * NOTE: do not bind-mount anything directly onto this directory! This is only + * a *base* directory: for exposing drivers and libraries, create a + * sub-directory in SC_EXTRA_LIB_DIR and use that one as the bind mount target. + */ +#define SC_EXTRA_LIB_DIR "/var/lib/snapd/lib" + /** * Assuming a new mountspace, populate it accordingly. * diff -Nru snapd-2.55.5+20.04/cmd/snap-confine/mount-support-nvidia.c snapd-2.57.5+20.04/cmd/snap-confine/mount-support-nvidia.c --- snapd-2.55.5+20.04/cmd/snap-confine/mount-support-nvidia.c 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-confine/mount-support-nvidia.c 2022-10-17 16:25:18.000000000 +0000 @@ -35,17 +35,14 @@ #include "../libsnap-confine-private/cleanup-funcs.h" #include "../libsnap-confine-private/string-utils.h" #include "../libsnap-confine-private/utils.h" +#include "mount-support.h" #define SC_NVIDIA_DRIVER_VERSION_FILE "/sys/module/nvidia/version" -// note: if the parent dir changes to something other than -// the current /var/lib/snapd/lib then sc_mkdir_and_mount_and_bind -// and sc_mkdir_and_mount_and_bind need updating. -#define SC_LIB "/var/lib/snapd/lib" -#define SC_LIBGL_DIR SC_LIB "/gl" -#define SC_LIBGL32_DIR SC_LIB "/gl32" -#define SC_VULKAN_DIR SC_LIB "/vulkan" -#define SC_GLVND_DIR SC_LIB "/glvnd" +#define SC_LIBGL_DIR SC_EXTRA_LIB_DIR "/gl" +#define SC_LIBGL32_DIR SC_EXTRA_LIB_DIR "/gl32" +#define SC_VULKAN_DIR SC_EXTRA_LIB_DIR "/vulkan" +#define SC_GLVND_DIR SC_EXTRA_LIB_DIR "/glvnd" #define SC_VULKAN_SOURCE_DIR "/usr/share/vulkan" #define SC_EGL_VENDOR_SOURCE_DIR "/usr/share/glvnd" @@ -590,13 +587,13 @@ } sc_identity old = sc_set_effective_identity(sc_root_group_identity()); - int res = mkdir(SC_LIB, 0755); - if (res != 0 && errno != EEXIST) { - die("cannot create " SC_LIB); + int res = sc_nonfatal_mkpath(SC_EXTRA_LIB_DIR, 0755); + if (res != 0) { + die("cannot create " SC_EXTRA_LIB_DIR); } - if (res == 0 && (chown(SC_LIB, 0, 0) < 0)) { + if (res == 0 && (chown(SC_EXTRA_LIB_DIR, 0, 0) < 0)) { // Adjust the ownership only if we created the directory. - die("cannot change ownership of " SC_LIB); + die("cannot change ownership of " SC_EXTRA_LIB_DIR); } (void)sc_set_effective_identity(old); diff -Nru snapd-2.55.5+20.04/cmd/snap-confine/ns-support.c snapd-2.57.5+20.04/cmd/snap-confine/ns-support.c --- snapd-2.55.5+20.04/cmd/snap-confine/ns-support.c 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-confine/ns-support.c 2022-10-17 16:25:18.000000000 +0000 @@ -287,20 +287,36 @@ if (mi == NULL) { die("cannot parse mountinfo of the current process"); } + /* We are looking for a mount entry matching the device ID of the base + * snap. We need to take these cases into account: + * 1) In the typical case, this will be mounted on the "/" directory. + * 2) If the root directory is a tmpfs, the base snap would be mounted + * under /usr. + * 3) If the snap has a layout that adds directories or files directly + * under /usr, a writable mimic will be created: /usr will be a tmpfs, + * with all of the original directory entries inside of /usr being + * bind-mounted onto mount-points created into the tmpfs. + * In light of the above, we do ignore all tmpfs entries and accept that + * our base snap might be mounted under /, /usr, or anywhere under /usr. + */ for (mie = sc_first_mountinfo_entry(mi); mie != NULL; mie = sc_next_mountinfo_entry(mie)) { - if (!sc_streq(mie->mount_dir, "/")) { + if (sc_streq(mie->fs_type, "tmpfs")) { continue; } - // NOTE: we want the initial rootfs just in case overmount - // was used to do something weird. The initial rootfs was - // set up by snap-confine and that is the one we want to - // measure. - debug("block device of the root filesystem is %d:%d", - mie->dev_major, mie->dev_minor); - return base_snap_dev != makedev(mie->dev_major, mie->dev_minor); + + if (base_snap_dev == makedev(mie->dev_major, mie->dev_minor) && + (sc_streq(mie->mount_dir, "/") || + sc_streq(mie->mount_dir, "/usr") || + sc_startswith(mie->mount_dir, "/usr/"))) { + debug("found base snap device %d:%d on %s", + mie->dev_major, mie->dev_minor, mie->mount_dir); + return false; + } } - die("cannot find mount entry of the root filesystem"); + debug("base snap device %d:%d not found in existing mount ns", + major(base_snap_dev), minor(base_snap_dev)); + return true; } enum sc_discard_vote { diff -Nru snapd-2.55.5+20.04/cmd/snap-confine/ns-support.h snapd-2.57.5+20.04/cmd/snap-confine/ns-support.h --- snapd-2.55.5+20.04/cmd/snap-confine/ns-support.h 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-confine/ns-support.h 2022-10-17 16:25:18.000000000 +0000 @@ -90,8 +90,8 @@ * use setns() with the obtained file descriptor. * * If the preserved mount namespace does not exist or exists but is stale and - * was discarded and returns ESRCH. If the mount namespace was joined the - * function returns zero. + * was discarded the function returns ESRCH. If the mount namespace was joined + * it returns zero. **/ int sc_join_preserved_ns(struct sc_mount_ns *group, struct sc_apparmor *apparmor, const sc_invocation * inv, diff -Nru snapd-2.55.5+20.04/cmd/snap-confine/snap-confine.c snapd-2.57.5+20.04/cmd/snap-confine/snap-confine.c --- snapd-2.55.5+20.04/cmd/snap-confine/snap-confine.c 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-confine/snap-confine.c 2022-10-17 16:25:18.000000000 +0000 @@ -30,6 +30,7 @@ #include #include #include +#include #include #include "../libsnap-confine-private/apparmor-support.h" @@ -286,6 +287,17 @@ debug("the process has been placed in the special void directory"); } +static void log_startup_stage(const char *stage) +{ + if (!sc_is_debug_enabled()) { + return; + } + struct timeval tv; + gettimeofday(&tv, NULL); + debug("-- snap startup {\"stage\":\"%s\", \"time\":\"%lu.%06lu\"}", + stage, tv.tv_sec, tv.tv_usec); +} + /** * sc_cleanup_preserved_process_state releases system resources. **/ @@ -306,6 +318,7 @@ int main(int argc, char **argv) { + log_startup_stage("snap-confine enter"); // Use our super-defensive parser to figure out what we've been asked to do. sc_error *err = NULL; struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL; @@ -380,11 +393,15 @@ // id is non-root. This protects against, for example, unprivileged // users trying to leverage the snap-confine in the core snap to // escalate privileges. + errno = 0; // errno is insignificant here die("snap-confine has elevated permissions and is not confined" " but should be. Refusing to continue to avoid" - " permission escalation attacks"); + " permission escalation attacks\n" + "Please make sure that the snapd.apparmor service is enabled and started."); } + log_startup_stage("snap-confine mount namespace start"); + /* perform global initialization of mount namespace support for non-classic * snaps or both classic and non-classic when parallel-instances feature is * enabled */ @@ -427,6 +444,9 @@ real_uid, real_gid, saved_gid); } + + log_startup_stage("snap-confine mount namespace finish"); + // Temporarily drop privileges back to the calling user until we can // permanently drop (which we can't do just yet due to seccomp, see // below). @@ -539,6 +559,7 @@ } // Restore process state that was recorded earlier. sc_restore_process_state(&proc_state); + log_startup_stage("snap-confine to snap-exec"); execv(invocation.executable, (char *const *)&argv[0]); perror("execv failed"); return 1; diff -Nru snapd-2.55.5+20.04/cmd/snapd/export_test.go snapd-2.57.5+20.04/cmd/snapd/export_test.go --- snapd-2.55.5+20.04/cmd/snapd/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snapd/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -27,11 +27,11 @@ Run = run ) -func MockSanityCheck(f func() error) (restore func()) { - oldSanityCheck := sanityCheck - sanityCheck = f +func MockSyscheckCheckSystem(f func() error) (restore func()) { + oldSyscheckCheckSystem := syscheckCheckSystem + syscheckCheckSystem = f return func() { - sanityCheck = oldSanityCheck + syscheckCheckSystem = oldSyscheckCheckSystem } } diff -Nru snapd-2.55.5+20.04/cmd/snapd/main.go snapd-2.57.5+20.04/cmd/snapd/main.go --- snapd-2.55.5+20.04/cmd/snapd/main.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snapd/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -31,14 +31,14 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/sandbox" - "github.com/snapcore/snapd/sanity" "github.com/snapcore/snapd/snapdenv" "github.com/snapcore/snapd/snapdtool" + "github.com/snapcore/snapd/syscheck" "github.com/snapcore/snapd/systemd" ) var ( - sanityCheck = sanity.Check + syscheckCheckSystem = syscheck.CheckSystem ) func init() { @@ -119,12 +119,12 @@ return err } - // Run sanity check now, if anything goes wrong with the + // Run syscheck check now, if anything goes wrong with the // check we go into "degraded" mode where we always report // the given error to any snap client. var checkTicker <-chan time.Time var tic *time.Ticker - if err := sanityCheck(); err != nil { + if err := syscheckCheckSystem(); err != nil { degradedErr := fmt.Errorf("system does not fully support snapd: %s", err) logger.Noticef("%s", degradedErr) d.SetDegradedMode(degradedErr) @@ -158,7 +158,7 @@ // something called Stop() break out case <-checkTicker: - if err := sanityCheck(); err == nil { + if err := syscheckCheckSystem(); err == nil { d.SetDegradedMode(nil) tic.Stop() } diff -Nru snapd-2.55.5+20.04/cmd/snapd/main_test.go snapd-2.57.5+20.04/cmd/snapd/main_test.go --- snapd-2.55.5+20.04/cmd/snapd/main_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snapd/main_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -61,7 +61,7 @@ s.AddCleanup(restore) } -func (s *snapdSuite) TestSanityFailGoesIntoDegradedMode(c *C) { +func (s *snapdSuite) TestSyscheckFailGoesIntoDegradedMode(c *C) { logbuf, restore := logger.MockLogger() defer restore() restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil }) @@ -69,21 +69,21 @@ restore = seccomp.MockSnapSeccompVersionInfo("abcdef 1.2.3 1234abcd -") defer restore() - sanityErr := fmt.Errorf("foo failed") - sanityCalled := make(chan bool) - sanityRan := 0 - restore = snapd.MockSanityCheck(func() error { - sanityRan++ + syscheckErr := fmt.Errorf("foo failed") + syscheckCalled := make(chan bool) + syscheckRan := 0 + restore = snapd.MockSyscheckCheckSystem(func() error { + syscheckRan++ // Ensure this ran at least *twice* to avoid a race here: // If we close the channel and this wakes up the "select" // below immediately and stops this go-routine then the // check that the logbuf contains the error will fail. // By running this at least twice we know the error made // it to the log. - if sanityRan == 2 { - close(sanityCalled) + if syscheckRan == 2 { + close(syscheckCalled) } - return sanityErr + return syscheckErr }) defer restore() @@ -100,17 +100,17 @@ c.Check(err, IsNil) }() - sanityCheckWasRun := false + syscheckCheckWasRun := false select { case <-time.After(5 * time.Second): - case _, stillOpen := <-sanityCalled: + case _, stillOpen := <-syscheckCalled: c.Assert(stillOpen, Equals, false) - sanityCheckWasRun = true + syscheckCheckWasRun = true } - c.Check(sanityCheckWasRun, Equals, true) + c.Check(syscheckCheckWasRun, Equals, true) c.Check(logbuf.String(), testutil.Contains, "system does not fully support snapd: foo failed") - // verify that talking to the daemon yields the sanity error + // verify that talking to the daemon yields the syscheck error // message // disable keepliave as it would sometimes cause the daemon to be // blocked when closing connections during graceful shutdown diff -Nru snapd-2.55.5+20.04/cmd/snapd-aa-prompt-listener/main.go snapd-2.57.5+20.04/cmd/snapd-aa-prompt-listener/main.go --- snapd-2.55.5+20.04/cmd/snapd-aa-prompt-listener/main.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snapd-aa-prompt-listener/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,41 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 main + +import ( + "fmt" + "os" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/snapdtool" +) + +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: failed to activate logging: %v\n", err) + } +} + +func main() { + snapdtool.ExecInSnapdOrCoreSnap() + // This point is only reached if reexec did not happen + fmt.Fprintln(os.Stderr, "AA Prompt listener not implemented") +} diff -Nru snapd-2.55.5+20.04/cmd/snapd-aa-prompt-ui/main.go snapd-2.57.5+20.04/cmd/snapd-aa-prompt-ui/main.go --- snapd-2.55.5+20.04/cmd/snapd-aa-prompt-ui/main.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snapd-aa-prompt-ui/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,41 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 main + +import ( + "fmt" + "os" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/snapdtool" +) + +func init() { + err := logger.SimpleSetup() + if err != nil { + fmt.Fprintf(os.Stderr, "WARNING: failed to activate logging: %v\n", err) + } +} + +func main() { + snapdtool.ExecInSnapdOrCoreSnap() + // This point is only reached if reexec did not happen + fmt.Fprintln(os.Stderr, "AA Prompt UI not implemented") +} diff -Nru snapd-2.55.5+20.04/cmd/snapd-apparmor/export_test.go snapd-2.57.5+20.04/cmd/snapd-apparmor/export_test.go --- snapd-2.55.5+20.04/cmd/snapd-apparmor/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snapd-apparmor/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 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 main + +var ( + Run = run + ValidateArgs = validateArgs + IsContainer = isContainer + IsContainerWithInternalPolicy = isContainerWithInternalPolicy + LoadAppArmorProfiles = loadAppArmorProfiles +) diff -Nru snapd-2.55.5+20.04/cmd/snapd-apparmor/main.go snapd-2.57.5+20.04/cmd/snapd-apparmor/main.go --- snapd-2.55.5+20.04/cmd/snapd-apparmor/main.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snapd-apparmor/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,167 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 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 . + * + */ + +// This tool is provided for integration with systemd on distributions where +// apparmor profiles generated and managed by snapd are not loaded by the +// system-wide apparmor systemd integration on early boot-up. +// +// Only the start operation is provided as all other activity is managed by +// snapd as a part of the life-cycle of particular snaps. +// +// In addition this tool assumes that the system-wide apparmor service has +// already executed, initializing apparmor file-systems as necessary. +// +// NOTE: This tool ignores failures in some scenarios as the intent is to +// simply load application profiles ahead of time, as many as we can (for +// performance reasons), even if for whatever reason some of those fail. + +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/release" + apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" + "github.com/snapcore/snapd/snapdtool" +) + +// Checks to see if the current container is capable of having internal AppArmor +// profiles that should be loaded. +// +// The only known container environments capable of supporting internal policy +// are LXD and LXC environment. +// +// Returns true if the container environment is capable of having its own internal +// policy and false otherwise. +// +// IMPORTANT: This function will return true in the case of a non-LXD/non-LXC +// system container technology being nested inside of a LXD/LXC container that +// utilized an AppArmor namespace and profile stacking. The reason true will be +// returned is because .ns_stacked will be "yes" and .ns_name will still match +// "lx[dc]-*" since the nested system container technology will not have set up +// a new AppArmor profile namespace. This will result in the nested system +// container's boot process to experience failed policy loads but the boot +// process should continue without any loss of functionality. This is an +// unsupported configuration that cannot be properly handled by this function. +// +func isContainerWithInternalPolicy() bool { + if release.OnWSL { + return true + } + + var appArmorSecurityFSPath = filepath.Join(dirs.GlobalRootDir, "/sys/kernel/security/apparmor") + var nsStackedPath = filepath.Join(appArmorSecurityFSPath, ".ns_stacked") + var nsNamePath = filepath.Join(appArmorSecurityFSPath, ".ns_name") + + contents, err := ioutil.ReadFile(nsStackedPath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + logger.Noticef("Failed to read %s: %v", nsStackedPath, err) + return false + } + + if strings.TrimSpace(string(contents)) != "yes" { + return false + } + + contents, err = ioutil.ReadFile(nsNamePath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + logger.Noticef("Failed to read %s: %v", nsNamePath, err) + return false + } + + // LXD and LXC set up AppArmor namespaces starting with "lxd-" and + // "lxc-", respectively. Return false for all other namespace + // identifiers. + name := strings.TrimSpace(string(contents)) + if !strings.HasPrefix(name, "lxd-") && !strings.HasPrefix(name, "lxc-") { + return false + } + return true +} + +func loadAppArmorProfiles() error { + candidates, err := filepath.Glob(dirs.SnapAppArmorDir + "/*") + if err != nil { + return fmt.Errorf("Failed to glob profiles from snap apparmor dir %s: %v", dirs.SnapAppArmorDir, err) + } + + profiles := make([]string, 0, len(candidates)) + for _, profile := range candidates { + // Filter out profiles with names ending with ~, those are + // temporary files created by snapd. + if strings.HasSuffix(profile, "~") { + continue + } + profiles = append(profiles, profile) + } + if len(profiles) == 0 { + logger.Noticef("No profiles to load") + return nil + } + logger.Noticef("Loading profiles %v", profiles) + return apparmor_sandbox.LoadProfiles(profiles, apparmor_sandbox.SystemCacheDir, 0) +} + +func isContainer() bool { + // systemd's implementation may fail on WSL2 with custom kernels + return release.OnWSL || (exec.Command("systemd-detect-virt", "--quiet", "--container").Run() == nil) +} + +func validateArgs(args []string) error { + if len(args) != 1 || args[0] != "start" { + return errors.New("Expected to be called with a single 'start' argument.") + } + return nil +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + snapdtool.ExecInSnapdOrCoreSnap() + + if err := validateArgs(os.Args[1:]); err != nil { + return err + } + + if isContainer() { + logger.Debugf("inside container environment") + // in container environment - see if container has own + // policy that we need to manage otherwise get out of the + // way + if !isContainerWithInternalPolicy() { + logger.Noticef("Inside container environment without internal policy") + return nil + } + } + + return loadAppArmorProfiles() +} diff -Nru snapd-2.55.5+20.04/cmd/snapd-apparmor/main_test.go snapd-2.57.5+20.04/cmd/snapd-apparmor/main_test.go --- snapd-2.55.5+20.04/cmd/snapd-apparmor/main_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snapd-apparmor/main_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,274 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 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 main_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + snapd_apparmor "github.com/snapcore/snapd/cmd/snapd-apparmor" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type mainSuite struct { + testutil.BaseTest +} + +var _ = Suite(&mainSuite{}) + +func (s *mainSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) +} + +func (s *mainSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") +} + +// Mocks WSL check. Values: +// - 0 to mock not being on WSL. +// - 1 to mock being on WSL 1. +// - 2 to mock being on WSL 2. +func mockWSL(version int) (restore func()) { + restoreOnWSL := testutil.Backup(&release.OnWSL) + restoreWSLVersion := testutil.Backup(&release.WSLVersion) + + release.OnWSL = version != 0 + release.WSLVersion = version + + return func() { + restoreOnWSL() + restoreWSLVersion() + } +} + +func (s *mainSuite) TestIsContainerWithInternalPolicy(c *C) { + // since "apparmorfs" is not present within our test root dir setup + // we expect this to return false + c.Assert(snapd_apparmor.IsContainerWithInternalPolicy(), Equals, false) + + appArmorSecurityFSPath := filepath.Join(dirs.GlobalRootDir, "/sys/kernel/security/apparmor/") + err := os.MkdirAll(appArmorSecurityFSPath, 0755) + c.Assert(err, IsNil) + + c.Assert(snapd_apparmor.IsContainerWithInternalPolicy(), Equals, false) + + // simulate being inside WSL + restore := mockWSL(1) + c.Assert(snapd_apparmor.IsContainerWithInternalPolicy(), Equals, true) + restore() + + restore = mockWSL(2) + c.Assert(snapd_apparmor.IsContainerWithInternalPolicy(), Equals, true) + restore() + + // simulate being inside a container environment + testutil.MockCommand(c, "systemd-detect-virt", "echo lxc") + c.Assert(snapd_apparmor.IsContainerWithInternalPolicy(), Equals, false) + + err = ioutil.WriteFile(filepath.Join(appArmorSecurityFSPath, ".ns_stacked"), []byte("yes"), 0644) + c.Assert(err, IsNil) + c.Assert(snapd_apparmor.IsContainerWithInternalPolicy(), Equals, false) + + err = ioutil.WriteFile(filepath.Join(appArmorSecurityFSPath, ".ns_name"), nil, 0644) + c.Assert(err, IsNil) + c.Assert(snapd_apparmor.IsContainerWithInternalPolicy(), Equals, false) + + err = ioutil.WriteFile(filepath.Join(appArmorSecurityFSPath, ".ns_name"), []byte("foo"), 0644) + c.Assert(err, IsNil) + c.Assert(snapd_apparmor.IsContainerWithInternalPolicy(), Equals, false) + // lxc/lxd name should result in a container with internal policy + err = ioutil.WriteFile(filepath.Join(appArmorSecurityFSPath, ".ns_name"), []byte("lxc-foo"), 0644) + c.Assert(err, IsNil) + c.Assert(snapd_apparmor.IsContainerWithInternalPolicy(), Equals, true) +} + +func (s *mainSuite) TestLoadAppArmorProfiles(c *C) { + parserCmd := testutil.MockCommand(c, "apparmor_parser", "") + defer parserCmd.Restore() + err := snapd_apparmor.LoadAppArmorProfiles() + c.Assert(err, IsNil) + // since no profiles to load the parser should not have been called + c.Assert(parserCmd.Calls(), HasLen, 0) + + // mock a profile + err = os.MkdirAll(dirs.SnapAppArmorDir, 0755) + c.Assert(err, IsNil) + + profile := filepath.Join(dirs.SnapAppArmorDir, "foo") + err = ioutil.WriteFile(profile, nil, 0644) + c.Assert(err, IsNil) + + // ensure SNAPD_DEBUG is set in the environment so then --quiet + // will *not* be included in the apparmor_parser arguments (since + // when these test are run in via CI SNAPD_DEBUG is set) + os.Setenv("SNAPD_DEBUG", "1") + err = snapd_apparmor.LoadAppArmorProfiles() + c.Assert(err, IsNil) + + // check arguments to the parser are as expected + c.Assert(parserCmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "--replace", "--write-cache", + "-O", "no-expr-simplify", + fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", dirs.GlobalRootDir), + profile}}) + + // test error case + testutil.MockCommand(c, "apparmor_parser", "echo mocked parser failed > /dev/stderr; exit 1") + err = snapd_apparmor.LoadAppArmorProfiles() + c.Check(err.Error(), Equals, fmt.Sprintf("cannot load apparmor profiles: exit status 1\napparmor_parser output:\nmocked parser failed\n")) + + // rename so file is ignored + err = os.Rename(profile, profile+"~") + c.Assert(err, IsNil) + // forget previous calls so we can check below that as a result of + // having no profiles again that no invocation of the parser occurs + parserCmd.ForgetCalls() + err = snapd_apparmor.LoadAppArmorProfiles() + c.Assert(err, IsNil) + c.Assert(parserCmd.Calls(), HasLen, 0) +} + +func (s *mainSuite) TestIsContainer(c *C) { + detectCmd := testutil.MockCommand(c, "systemd-detect-virt", "exit 1") + defer detectCmd.Restore() + c.Check(snapd_apparmor.IsContainer(), Equals, false) + c.Assert(detectCmd.Calls(), DeepEquals, [][]string{ + {"systemd-detect-virt", "--quiet", "--container"}}) + + detectCmd = testutil.MockCommand(c, "systemd-detect-virt", "") + c.Check(snapd_apparmor.IsContainer(), Equals, true) + c.Assert(detectCmd.Calls(), DeepEquals, [][]string{ + {"systemd-detect-virt", "--quiet", "--container"}}) + + // test error cases too + detectCmd = testutil.MockCommand(c, "systemd-detect-virt", "echo failed > /dev/stderr; exit 1") + c.Check(snapd_apparmor.IsContainer(), Equals, false) + c.Assert(detectCmd.Calls(), DeepEquals, [][]string{ + {"systemd-detect-virt", "--quiet", "--container"}}) + + // Test WSL2 with custom kernel + // systemd-detect-virt may return a non-zero exit code as it fails to recognize it as WSL + // This will happen when the kernel name includes neither "WSL" not "Microsoft" + detectCmd = testutil.MockCommand(c, "systemd-detect-virt", "echo none; exit 1") + defer mockWSL(2)() + c.Check(snapd_apparmor.IsContainer(), Equals, true) + c.Assert(detectCmd.Calls(), DeepEquals, [][]string(nil)) +} + +func (s *mainSuite) TestValidateArgs(c *C) { + testCases := []struct { + args []string + errMsg string + }{ + { + args: []string{"start"}, + errMsg: "", + }, + { + args: []string{"foo"}, + errMsg: "Expected to be called with a single 'start' argument.", + }, + { + args: []string{"start", "foo"}, + errMsg: "Expected to be called with a single 'start' argument.", + }, + } + for _, tc := range testCases { + err := snapd_apparmor.ValidateArgs(tc.args) + if err != nil { + c.Check(err.Error(), Equals, tc.errMsg) + } else { + c.Check(tc.errMsg, Equals, "") + } + } +} + +type integrationSuite struct { + testutil.BaseTest + + logBuf *bytes.Buffer + parserCmd *testutil.MockCmd +} + +var _ = Suite(&integrationSuite{}) + +func (s *integrationSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + s.AddCleanup(func() { dirs.SetRootDir("/") }) + + logBuf, r := logger.MockLogger() + s.AddCleanup(r) + s.logBuf = logBuf + + // simulate a single profile to load + s.parserCmd = testutil.MockCommand(c, "apparmor_parser", "") + s.AddCleanup(s.parserCmd.Restore) + err := os.MkdirAll(dirs.SnapAppArmorDir, 0755) + c.Assert(err, IsNil) + profile := filepath.Join(dirs.SnapAppArmorDir, "foo") + err = ioutil.WriteFile(profile, nil, 0644) + c.Assert(err, IsNil) + + os.Args = []string{"snapd-apparmor", "start"} +} + +func (s *integrationSuite) TestRunInContainerSkipsLoading(c *C) { + testutil.MockCommand(c, "systemd-detect-virt", "exit 0") + + err := snapd_apparmor.Run() + c.Assert(err, IsNil) + c.Check(s.logBuf.String(), testutil.Contains, "DEBUG: inside container environment") + c.Check(s.logBuf.String(), testutil.Contains, "Inside container environment without internal policy") + c.Assert(s.parserCmd.Calls(), HasLen, 0) +} + +func (s *integrationSuite) TestRunInContainerWithInternalPolicyLoadsProfiles(c *C) { + defer mockWSL(1)() + err := snapd_apparmor.Run() + c.Assert(err, IsNil) + c.Check(s.logBuf.String(), testutil.Contains, "DEBUG: inside container environment") + c.Check(s.logBuf.String(), Not(testutil.Contains), "Inside container environment without internal policy") + c.Assert(s.parserCmd.Calls(), HasLen, 1) +} + +func (s *integrationSuite) TestRunNormalLoadsProfiles(c *C) { + // simulate a normal system (not a container) + testutil.MockCommand(c, "systemd-detect-virt", "exit 1") + + detectCmd := testutil.MockCommand(c, "systemd-detect-virt", "exit 1") + defer detectCmd.Restore() + + err := snapd_apparmor.Run() + c.Assert(err, IsNil) + c.Assert(s.parserCmd.Calls(), HasLen, 1) + c.Check(s.logBuf.String(), Matches, `(?s).* main.go:[0-9]+: Loading profiles \[.*/var/lib/snapd/apparmor/profiles/foo\].*`) +} diff -Nru snapd-2.55.5+20.04/cmd/snapd-apparmor/snapd-apparmor snapd-2.57.5+20.04/cmd/snapd-apparmor/snapd-apparmor --- snapd-2.55.5+20.04/cmd/snapd-apparmor/snapd-apparmor 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snapd-apparmor/snapd-apparmor 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +0,0 @@ -#!/bin/sh -# This script is provided for integration with systemd on distributions where -# apparmor profiles generated and managed by snapd are not loaded by the -# system-wide apparmor systemd integration on early boot-up. -# -# Only the start operation is provided as all other activity is managed by -# snapd as a part of the life-cycle of particular snaps. -# -# In addition the script assumes that the system-wide apparmor service has -# already executed, initializing apparmor file-systems as necessary. - -# NOTE: This script doesn't set -e as it contains code copied from apparmor -# init script that also does not set it. In addition the intent is to simply -# load application profiles, as many as we can, even if for whatever reason -# some of those fail. - -# The following portion is copied from /lib/apparmor/functions as shipped by Ubuntu -# - -SECURITYFS="/sys/kernel/security" -export AA_SFS="$SECURITYFS/apparmor" - - -# Checks to see if the current container is capable of having internal AppArmor -# profiles that should be loaded. Callers of this function should have already -# verified that they're running inside of a container environment with -# something like `systemd-detect-virt --container`. -# -# The only known container environments capable of supporting internal policy -# are LXD and LXC environment. -# -# Returns 0 if the container environment is capable of having its own internal -# policy and non-zero otherwise. -# -# IMPORTANT: This function will return 0 in the case of a non-LXD/non-LXC -# system container technology being nested inside of a LXD/LXC container that -# utilized an AppArmor namespace and profile stacking. The reason 0 will be -# returned is because .ns_stacked will be "yes" and .ns_name will still match -# "lx[dc]-*" since the nested system container technology will not have set up -# a new AppArmor profile namespace. This will result in the nested system -# container's boot process to experience failed policy loads but the boot -# process should continue without any loss of functionality. This is an -# unsupported configuration that cannot be properly handled by this function. -is_container_with_internal_policy() { - ns_stacked_path="${AA_SFS}/.ns_stacked" - ns_name_path="${AA_SFS}/.ns_name" - # shellcheck disable=SC3043,SC2039 - local ns_stacked - # shellcheck disable=SC3043,SC2039 - local ns_name - - if ! [ -f "$ns_stacked_path" ] || ! [ -f "$ns_name_path" ]; then - return 1 - fi - - read -r ns_stacked < "$ns_stacked_path" - if [ "$ns_stacked" != "yes" ]; then - return 1 - fi - - # LXD and LXC set up AppArmor namespaces starting with "lxd-" and - # "lxc-", respectively. Return non-zero for all other namespace - # identifiers. - read -r ns_name < "$ns_name_path" - if [ "${ns_name#lxd-*}" = "$ns_name" ] && \ - [ "${ns_name#lxc-*}" = "$ns_name" ]; then - return 1 - fi - - return 0 -} - -# This terminates code copied from /lib/apparmor/functions on Ubuntu -# - -case "$1" in - start) - # - if [ -x /usr/bin/systemd-detect-virt ] && \ - systemd-detect-virt --quiet --container && \ - ! is_container_with_internal_policy; then - exit 0 - fi - # - - if [ "$(find /var/lib/snapd/apparmor/profiles/ -type f | wc -l)" -eq 0 ]; then - exit 0 - fi - for profile in /var/lib/snapd/apparmor/profiles/*; do - # Filter out profiles with names ending with ~, those are temporary files created by snapd. - test "${profile%\~}" != "${profile}" && continue - echo "$profile" - done | xargs \ - -P"$(getconf _NPROCESSORS_ONLN)" \ - apparmor_parser \ - --replace \ - --write-cache \ - --cache-loc=/var/cache/apparmor \ - -O no-expr-simplify \ - --quiet - ;; -esac diff -Nru snapd-2.55.5+20.04/cmd/snap-exec/main.go snapd-2.57.5+20.04/cmd/snap-exec/main.go --- snapd-2.55.5+20.04/cmd/snap-exec/main.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-exec/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -29,6 +29,7 @@ "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapenv" @@ -48,6 +49,7 @@ func init() { // plug/slot sanitization not used nor possible from snap-exec, make it no-op snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {} + logger.SimpleSetup() } func main() { @@ -245,6 +247,7 @@ fullCmd = append(absoluteCommandChain(app.Snap, app.CommandChain), fullCmd...) + logger.StartupStageTimestamp("snap-exec to app") if err := syscallExec(fullCmd[0], fullCmd, env.ForExec()); err != nil { return fmt.Errorf("cannot exec %q: %s", fullCmd[0], err) } diff -Nru snapd-2.55.5+20.04/cmd/snap-fde-keymgr/export_test.go snapd-2.57.5+20.04/cmd/snap-fde-keymgr/export_test.go --- snapd-2.55.5+20.04/cmd/snap-fde-keymgr/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-fde-keymgr/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,70 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 main + +import ( + "io" + + "github.com/snapcore/snapd/secboot/keys" + "github.com/snapcore/snapd/testutil" +) + +var Run = run + +func MockAddRecoveryKeyToLUKS(f func(recoveryKey keys.RecoveryKey, dev string) error) (restore func()) { + restore = testutil.Backup(&keymgrAddRecoveryKeyToLUKSDevice) + keymgrAddRecoveryKeyToLUKSDevice = f + return restore +} + +func MockAddRecoveryKeyToLUKSUsingKey(f func(recoveryKey keys.RecoveryKey, key keys.EncryptionKey, dev string) error) (restore func()) { + restore = testutil.Backup(&keymgrAddRecoveryKeyToLUKSDeviceUsingKey) + keymgrAddRecoveryKeyToLUKSDeviceUsingKey = f + return restore +} + +func MockRemoveRecoveryKeyFromLUKS(f func(dev string) error) (restore func()) { + restore = testutil.Backup(&keymgrRemoveRecoveryKeyFromLUKSDevice) + keymgrRemoveRecoveryKeyFromLUKSDevice = f + return restore +} + +func MockRemoveRecoveryKeyFromLUKSUsingKey(f func(key keys.EncryptionKey, dev string) error) (restore func()) { + restore = testutil.Backup(&keymgrRemoveRecoveryKeyFromLUKSDeviceUsingKey) + keymgrRemoveRecoveryKeyFromLUKSDeviceUsingKey = f + return restore +} + +func MockStageLUKSEncryptionKeyChange(f func(newKey keys.EncryptionKey, dev string) error) (restore func()) { + restore = testutil.Backup(&keymgrStageLUKSDeviceEncryptionKeyChange) + keymgrStageLUKSDeviceEncryptionKeyChange = f + return restore +} + +func MockTransitionLUKSEncryptionKeyChange(f func(newKey keys.EncryptionKey, dev string) error) (restore func()) { + restore = testutil.Backup(&keymgrTransitionLUKSDeviceEncryptionKeyChange) + keymgrTransitionLUKSDeviceEncryptionKeyChange = f + return restore +} + +func MockOsStdin(r io.Reader) (restore func()) { + restore = testutil.Backup(&osStdin) + osStdin = r + return restore +} diff -Nru snapd-2.55.5+20.04/cmd/snap-fde-keymgr/main.go snapd-2.57.5+20.04/cmd/snap-fde-keymgr/main.go --- snapd-2.55.5+20.04/cmd/snap-fde-keymgr/main.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-fde-keymgr/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,251 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 main + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/secboot/keymgr" + "github.com/snapcore/snapd/secboot/keys" +) + +var osStdin io.Reader = os.Stdin + +type commonMultiDeviceMixin struct { + Devices []string `long:"devices" description:"encrypted devices (can be more than one)" required:"yes"` + Authorizations []string `long:"authorizations" description:"authorization sources (one for each device, either 'keyring' or 'file:')" required:"yes"` +} + +type cmdAddRecoveryKey struct { + commonMultiDeviceMixin + KeyFile string `long:"key-file" description:"path for generated recovery key file" required:"yes"` +} + +type cmdRemoveRecoveryKey struct { + commonMultiDeviceMixin + KeyFiles []string `long:"key-files" description:"path to recovery key files to be removed" required:"yes"` +} + +type cmdChangeEncryptionKey struct { + Device string `long:"device" description:"encrypted device" required:"yes"` + Stage bool `long:"stage" description:"stage the new key"` + Transition bool `long:"transition" description:"replace the old key, unstage the new"` +} + +type options struct { + CmdAddRecoveryKey cmdAddRecoveryKey `command:"add-recovery-key"` + CmdRemoveRecoveryKey cmdRemoveRecoveryKey `command:"remove-recovery-key"` + CmdChangeEncryptionKey cmdChangeEncryptionKey `command:"change-encryption-key"` +} + +var ( + keymgrAddRecoveryKeyToLUKSDevice = keymgr.AddRecoveryKeyToLUKSDevice + keymgrAddRecoveryKeyToLUKSDeviceUsingKey = keymgr.AddRecoveryKeyToLUKSDeviceUsingKey + keymgrRemoveRecoveryKeyFromLUKSDevice = keymgr.RemoveRecoveryKeyFromLUKSDevice + keymgrRemoveRecoveryKeyFromLUKSDeviceUsingKey = keymgr.RemoveRecoveryKeyFromLUKSDeviceUsingKey + keymgrStageLUKSDeviceEncryptionKeyChange = keymgr.StageLUKSDeviceEncryptionKeyChange + keymgrTransitionLUKSDeviceEncryptionKeyChange = keymgr.TransitionLUKSDeviceEncryptionKeyChange +) + +func validateAuthorizations(authorizations []string) error { + for _, authz := range authorizations { + switch { + case authz == "keyring": + // happy + case strings.HasPrefix(authz, "file:"): + // file must exist + kf := authz[len("file:"):] + if !osutil.FileExists(kf) { + return fmt.Errorf("authorization file %v does not exist", kf) + } + default: + return fmt.Errorf("unknown authorization method %q", authz) + } + } + return nil +} + +func writeIfNotExists(p string, data []byte) (alreadyExists bool, err error) { + f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) + if err != nil { + if os.IsExist(err) { + return true, nil + } + return false, err + } + if _, err := f.Write(data); err != nil { + f.Close() + return false, err + } + return false, f.Close() +} + +func (c *cmdAddRecoveryKey) Execute(args []string) error { + recoveryKey, err := keys.NewRecoveryKey() + if err != nil { + return fmt.Errorf("cannot create recovery key: %v", err) + } + if len(c.Authorizations) != len(c.Devices) { + return fmt.Errorf("cannot add recovery keys: mismatch in the number of devices and authorizations") + } + if err := validateAuthorizations(c.Authorizations); err != nil { + return fmt.Errorf("cannot add recovery keys with invalid authorizations: %v", err) + } + // write the key to the file, if the file already exists it is possible + // that we are being called again after an unexpected reboot or a + // similar event + alreadyExists, err := writeIfNotExists(c.KeyFile, recoveryKey[:]) + if err != nil { + return fmt.Errorf("cannot write recovery key to file: %v", err) + } + if alreadyExists { + // we already have the recovery key, read it back + maybeKey, err := ioutil.ReadFile(c.KeyFile) + if err != nil { + return fmt.Errorf("cannot read existing recovery key file: %v", err) + } + // TODO: verify that the size if non 0 and try again otherwise? + if len(maybeKey) != len(recoveryKey) { + return fmt.Errorf("cannot use existing recovery key of size %v", len(maybeKey)) + } + copy(recoveryKey[:], maybeKey[:]) + } + // add the recovery key to each device; keys are always added to the + // same keyslot, so when the key existed on disk, assume that the key + // was already added to the device in case we hit an error with keyslot + // being already used + for i, dev := range c.Devices { + authz := c.Authorizations[i] + switch { + case authz == "keyring": + if err := keymgrAddRecoveryKeyToLUKSDevice(recoveryKey, dev); err != nil { + if !alreadyExists || !keymgr.IsKeyslotAlreadyUsed(err) { + return fmt.Errorf("cannot add recovery key to LUKS device: %v", err) + } + } + case strings.HasPrefix(authz, "file:"): + authzKey, err := ioutil.ReadFile(authz[len("file:"):]) + if err != nil { + return fmt.Errorf("cannot load authorization key: %v", err) + } + if err := keymgrAddRecoveryKeyToLUKSDeviceUsingKey(recoveryKey, authzKey, dev); err != nil { + if !alreadyExists || !keymgr.IsKeyslotAlreadyUsed(err) { + return fmt.Errorf("cannot add recovery key to LUKS device using authorization key: %v", err) + } + } + } + } + return nil +} + +func (c *cmdRemoveRecoveryKey) Execute(args []string) error { + if len(c.Authorizations) != len(c.Devices) { + return fmt.Errorf("cannot remove recovery keys: mismatch in the number of devices and authorizations") + } + if err := validateAuthorizations(c.Authorizations); err != nil { + return fmt.Errorf("cannot remove recovery keys with invalid authorizations: %v", err) + } + for i, dev := range c.Devices { + authz := c.Authorizations[i] + switch { + case authz == "keyring": + if err := keymgrRemoveRecoveryKeyFromLUKSDevice(dev); err != nil { + return fmt.Errorf("cannot remove recovery key from LUKS device: %v", err) + } + case strings.HasPrefix(authz, "file:"): + authzKey, err := ioutil.ReadFile(authz[len("file:"):]) + if err != nil { + return fmt.Errorf("cannot load authorization key: %v", err) + } + if err := keymgrRemoveRecoveryKeyFromLUKSDeviceUsingKey(authzKey, dev); err != nil { + return fmt.Errorf("cannot remove recovery key from device using authorization key: %v", err) + } + } + } + var rmErrors []string + for _, kf := range c.KeyFiles { + if err := os.Remove(kf); err != nil && !os.IsNotExist(err) { + rmErrors = append(rmErrors, err.Error()) + } + } + if len(rmErrors) != 0 { + return fmt.Errorf("cannot remove key files:\n%s", strings.Join(rmErrors, "\n")) + } + return nil +} + +type newKey struct { + Key []byte `json:"key"` +} + +func (c *cmdChangeEncryptionKey) Execute(args []string) error { + if c.Stage && c.Transition { + return fmt.Errorf("cannot both stage and transition the encryption key change") + } + if !c.Stage && !c.Transition { + return fmt.Errorf("cannot change encryption key without stage or transition request") + } + + var newEncryptionKeyData newKey + dec := json.NewDecoder(osStdin) + if err := dec.Decode(&newEncryptionKeyData); err != nil { + return fmt.Errorf("cannot obtain new encryption key: %v", err) + } + switch { + case c.Stage: + // staging the key change authorizes the operation using a key + // from the keyring + if err := keymgrStageLUKSDeviceEncryptionKeyChange(newEncryptionKeyData.Key, c.Device); err != nil { + return fmt.Errorf("cannot stage LUKS device encryption key change: %v", err) + } + case c.Transition: + // transitioning the key change authorizes the operation using + // the currently provided key (which must have been staged + // before hence the op will be authorized successfully) + if err := keymgrTransitionLUKSDeviceEncryptionKeyChange(newEncryptionKeyData.Key, c.Device); err != nil { + return fmt.Errorf("cannot transition LUKS device encryption key change: %v", err) + } + } + return nil +} + +func run(osArgs1 []string) error { + var opts options + p := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash) + if _, err := p.ParseArgs(osArgs1); err != nil { + return err + } + return nil +} + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} diff -Nru snapd-2.55.5+20.04/cmd/snap-fde-keymgr/main_test.go snapd-2.57.5+20.04/cmd/snap-fde-keymgr/main_test.go --- snapd-2.55.5+20.04/cmd/snap-fde-keymgr/main_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-fde-keymgr/main_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,452 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 main_test + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + main "github.com/snapcore/snapd/cmd/snap-fde-keymgr" + "github.com/snapcore/snapd/secboot/keys" + "github.com/snapcore/snapd/testutil" +) + +type mainSuite struct{} + +var _ = Suite(&mainSuite{}) + +func TestT(t *testing.T) { + TestingT(t) +} + +func (s *mainSuite) TestAddKey(c *C) { + d := c.MkDir() + dev := "" + rkey := keys.RecoveryKey{} + addCalls := 0 + restore := main.MockAddRecoveryKeyToLUKS(func(recoveryKey keys.RecoveryKey, luksDev string) error { + addCalls++ + dev = luksDev + rkey = recoveryKey + // recovery key is already written to a file + c.Assert(filepath.Join(d, "recovery.key"), testutil.FileEquals, rkey[:]) + return nil + }) + defer restore() + devUsingKey := "" + addUsingKeyCalls := 0 + var authzKey keys.EncryptionKey + restore = main.MockAddRecoveryKeyToLUKSUsingKey(func(recoveryKey keys.RecoveryKey, key keys.EncryptionKey, luksDev string) error { + addUsingKeyCalls++ + devUsingKey = luksDev + authzKey = key + // recovery key is already written to a file + c.Assert(filepath.Join(d, "recovery.key"), testutil.FileEquals, rkey[:]) + return nil + }) + defer restore() + c.Assert(ioutil.WriteFile(filepath.Join(d, "authz.key"), []byte{1, 1, 1}, 0644), IsNil) + err := main.Run([]string{ + "add-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "keyring", + "--devices", "/dev/vda5", + "--authorizations", "file:" + filepath.Join(d, "authz.key"), + "--key-file", filepath.Join(d, "recovery.key"), + }) + c.Assert(err, IsNil) + c.Check(addCalls, Equals, 1) + c.Check(dev, Equals, "/dev/vda4") + c.Check(addUsingKeyCalls, Equals, 1) + c.Check(devUsingKey, Equals, "/dev/vda5") + c.Check(rkey, Not(DeepEquals), keys.RecoveryKey{}) + c.Assert(filepath.Join(d, "recovery.key"), testutil.FileEquals, rkey[:]) + + oldKey := rkey + // add again, in which case already existing key is read back + err = main.Run([]string{ + "add-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "keyring", + "--devices", "/dev/vda5", + "--authorizations", "file:" + filepath.Join(d, "authz.key"), + "--key-file", filepath.Join(d, "recovery.key"), + }) + c.Assert(err, IsNil) + c.Check(addCalls, Equals, 2) + c.Check(dev, Equals, "/dev/vda4") + c.Check(addUsingKeyCalls, Equals, 2) + c.Check(devUsingKey, Equals, "/dev/vda5") + c.Assert(authzKey, DeepEquals, keys.EncryptionKey([]byte{1, 1, 1})) + c.Check(rkey, DeepEquals, oldKey) + // file was overwritten + c.Assert(filepath.Join(d, "recovery.key"), testutil.FileEquals, rkey[:]) +} + +func (s *mainSuite) TestAddKeyRequiresAuthz(c *C) { + restore := main.MockAddRecoveryKeyToLUKS(func(recoveryKey keys.RecoveryKey, luksDev string) error { + c.Fail() + return fmt.Errorf("unexpected call") + }) + defer restore() + restore = main.MockAddRecoveryKeyToLUKSUsingKey(func(recoveryKey keys.RecoveryKey, key keys.EncryptionKey, luksDev string) error { + c.Fail() + return fmt.Errorf("unexpected call") + }) + defer restore() + d := c.MkDir() + err := main.Run([]string{ + "add-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "keyring", + "--devices", "/dev/vda5", + "--key-file", filepath.Join(d, "recovery.key"), + }) + c.Assert(err, ErrorMatches, "cannot add recovery keys: mismatch in the number of devices and authorizations") + + // --authorization=invalid + err = main.Run([]string{ + "add-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "invalid", + "--devices", "/dev/vda5", + "--authorizations", "file:" + filepath.Join(d, "authz.key"), + "--key-file", filepath.Join(d, "recovery.key"), + }) + c.Assert(err, ErrorMatches, `cannot add recovery keys with invalid authorizations: unknown authorization method "invalid"`) + + // authorization key file does not exist + err = main.Run([]string{ + "add-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "keyring", + "--devices", "/dev/vda5", + "--authorizations", "file:" + filepath.Join(d, "authz.key"), + "--key-file", filepath.Join(d, "recovery.key"), + }) + c.Assert(err, ErrorMatches, `cannot add recovery keys with invalid authorizations: authorization file .*/authz.key does not exist`) +} + +type addKeyTestCase struct { + errAddToLUKS error + addCalls int + errAddToLUKSUsingKey error + addUsingKeyCalls int + expErr string +} + +func (s *mainSuite) testAddKeyIdempotent(c *C, tc addKeyTestCase) { + d := c.MkDir() + c.Assert(ioutil.WriteFile(filepath.Join(d, "authz.key"), []byte{1, 1, 1}, 0644), IsNil) + rkey := keys.RecoveryKey{'r', 'e', 'c', 'o', 'v', 'e', 'r', 'y'} + c.Assert(ioutil.WriteFile(filepath.Join(d, "recovery.key"), rkey[:], 0600), IsNil) + + addCalls := 0 + restore := main.MockAddRecoveryKeyToLUKS(func(recoveryKey keys.RecoveryKey, luksDev string) error { + addCalls++ + c.Check(luksDev, Equals, "/dev/vda4") + c.Check(recoveryKey, DeepEquals, rkey) + return tc.errAddToLUKS + }) + defer restore() + addUsingKeyCalls := 0 + restore = main.MockAddRecoveryKeyToLUKSUsingKey(func(recoveryKey keys.RecoveryKey, key keys.EncryptionKey, luksDev string) error { + addUsingKeyCalls++ + c.Check(luksDev, Equals, "/dev/vda5") + c.Check(recoveryKey, DeepEquals, rkey) + return tc.errAddToLUKSUsingKey + }) + defer restore() + + err := main.Run([]string{ + "add-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "keyring", + "--devices", "/dev/vda5", + "--authorizations", "file:" + filepath.Join(d, "authz.key"), + "--key-file", filepath.Join(d, "recovery.key"), + }) + if tc.expErr != "" { + c.Assert(err, ErrorMatches, tc.expErr) + } else { + c.Assert(err, IsNil) + } + c.Check(addCalls, Equals, tc.addCalls) + c.Check(addUsingKeyCalls, Equals, tc.addUsingKeyCalls) + // file was not overwritten + c.Assert(filepath.Join(d, "recovery.key"), testutil.FileEquals, rkey[:]) +} + +func (s *mainSuite) TestAddKeyIdempotentBothEmpty(c *C) { + s.testAddKeyIdempotent(c, addKeyTestCase{ + addCalls: 1, + addUsingKeyCalls: 1, + }) +} + +func (s *mainSuite) TestAddKeyIdempotentOneErr(c *C) { + s.testAddKeyIdempotent(c, addKeyTestCase{ + addCalls: 1, + errAddToLUKS: errors.New("mock error"), + expErr: "cannot add recovery key to LUKS device: mock error", + }) +} + +func (s *mainSuite) TestAddKeyIdempotentOtherErr(c *C) { + s.testAddKeyIdempotent(c, addKeyTestCase{ + addCalls: 1, + addUsingKeyCalls: 1, + errAddToLUKSUsingKey: errors.New("mock error"), + expErr: "cannot add recovery key to LUKS device using authorization key: mock error", + }) +} + +func (s *mainSuite) TestAddKeyIdempotentBothPresent(c *C) { + s.testAddKeyIdempotent(c, addKeyTestCase{ + addCalls: 1, + addUsingKeyCalls: 1, + errAddToLUKS: errors.New("mock error: cryptsetup failed with: Key slot 1 is full, please select another one."), + errAddToLUKSUsingKey: errors.New("mock error: cryptsetup failed with: Key slot 1 is full, please select another one."), + }) +} + +func (s *mainSuite) TestAddKeyIdempotentOnePresent(c *C) { + s.testAddKeyIdempotent(c, addKeyTestCase{ + addCalls: 1, + addUsingKeyCalls: 1, + errAddToLUKS: errors.New("mock error: cryptsetup failed with: Key slot 1 is full, please select another one."), + }) +} + +func (s *mainSuite) TestRemoveKey(c *C) { + dev := "" + removeCalls := 0 + restore := main.MockRemoveRecoveryKeyFromLUKS(func(luksDev string) error { + removeCalls++ + dev = luksDev + return nil + }) + defer restore() + removeUsingKeyCalls := 0 + devUsingKey := "" + var authzKey keys.EncryptionKey + restore = main.MockRemoveRecoveryKeyFromLUKSUsingKey(func(key keys.EncryptionKey, luksDev string) error { + authzKey = key + removeUsingKeyCalls++ + devUsingKey = luksDev + return nil + }) + defer restore() + d := c.MkDir() + // key which will be removed + c.Assert(ioutil.WriteFile(filepath.Join(d, "recovery.key"), []byte{0, 0, 0}, 0644), IsNil) + + c.Assert(ioutil.WriteFile(filepath.Join(d, "authz.key"), []byte{1, 1, 1}, 0644), IsNil) + err := main.Run([]string{ + "remove-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "keyring", + "--devices", "/dev/vda5", + "--authorizations", "file:" + filepath.Join(d, "authz.key"), + "--key-files", filepath.Join(d, "recovery.key"), + }) + c.Assert(err, IsNil) + c.Check(removeCalls, Equals, 1) + c.Check(dev, Equals, "/dev/vda4") + c.Check(removeUsingKeyCalls, Equals, 1) + c.Check(devUsingKey, Equals, "/dev/vda5") + c.Assert(authzKey, DeepEquals, keys.EncryptionKey([]byte{1, 1, 1})) + c.Assert(filepath.Join(d, "recovery.key"), testutil.FileAbsent) + // again when the recover key file is gone already + err = main.Run([]string{ + "remove-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "keyring", + "--devices", "/dev/vda5", + "--authorizations", "file:" + filepath.Join(d, "authz.key"), + "--key-files", filepath.Join(d, "recovery.key"), + }) + c.Check(removeCalls, Equals, 2) + c.Check(removeUsingKeyCalls, Equals, 2) + c.Assert(err, IsNil) +} + +func (s *mainSuite) TestRemoveKeyRequiresAuthz(c *C) { + restore := main.MockRemoveRecoveryKeyFromLUKS(func(luksDev string) error { + c.Fail() + return fmt.Errorf("unexpected call") + }) + defer restore() + restore = main.MockRemoveRecoveryKeyFromLUKSUsingKey(func(key keys.EncryptionKey, luksDev string) error { + c.Fail() + return fmt.Errorf("unexpected call") + }) + defer restore() + d := c.MkDir() + + err := main.Run([]string{ + "remove-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "keyring", + "--devices", "/dev/vda5", + "--key-files", filepath.Join(d, "recovery.key"), + }) + c.Assert(err, ErrorMatches, "cannot remove recovery keys: mismatch in the number of devices and authorizations") + + // --authorization=invalid + err = main.Run([]string{ + "remove-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "invalid", + "--devices", "/dev/vda5", + "--authorizations", "file:" + filepath.Join(d, "authz.key"), + "--key-files", filepath.Join(d, "recovery.key"), + }) + c.Assert(err, ErrorMatches, `cannot remove recovery keys with invalid authorizations: unknown authorization method "invalid"`) + + // authorization key file does not exist + err = main.Run([]string{ + "remove-recovery-key", + "--devices", "/dev/vda4", + "--authorizations", "keyring", + "--devices", "/dev/vda5", + "--authorizations", "file:" + filepath.Join(d, "authz.key"), + "--key-files", filepath.Join(d, "recovery.key"), + }) + c.Assert(err, ErrorMatches, `cannot remove recovery keys with invalid authorizations: authorization file .*/authz.key does not exist`) +} + +// 1 in ASCII repeated 32 times +const all1sKey = `{"key":"MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTE="}` + +func (s *mainSuite) TestChangeEncryptionKey(c *C) { + b := bytes.NewBufferString(all1sKey) + restore := main.MockOsStdin(b) + defer restore() + unexpectedCall := func(newKey keys.EncryptionKey, luksDev string) error { + c.Errorf("unexpected call") + return fmt.Errorf("unexpected call") + } + defer main.MockStageLUKSEncryptionKeyChange(unexpectedCall) + defer main.MockTransitionLUKSEncryptionKeyChange(unexpectedCall) + + err := main.Run([]string{ + "change-encryption-key", + "--device", "/dev/vda4", + }) + c.Assert(err, ErrorMatches, "cannot change encryption key without stage or transition request") + + err = main.Run([]string{ + "change-encryption-key", + "--device", "/dev/vda4", + "--stage", "--transition", + }) + c.Assert(err, ErrorMatches, "cannot both stage and transition the encryption key change") +} + +func (s *mainSuite) TestStageEncryptionKey(c *C) { + b := bytes.NewBufferString(all1sKey) + restore := main.MockOsStdin(b) + defer restore() + dev := "" + stageCalls := 0 + var key []byte + var stageErr error + restore = main.MockStageLUKSEncryptionKeyChange(func(newKey keys.EncryptionKey, luksDev string) error { + stageCalls++ + dev = luksDev + key = newKey + return stageErr + }) + defer restore() + restore = main.MockTransitionLUKSEncryptionKeyChange(func(newKey keys.EncryptionKey, luksDev string) error { + c.Errorf("unexpected call") + return fmt.Errorf("unexpected call") + }) + defer restore() + err := main.Run([]string{ + "change-encryption-key", + "--device", "/dev/vda4", + "--stage", + }) + c.Assert(err, IsNil) + c.Check(stageCalls, Equals, 1) + c.Check(dev, Equals, "/dev/vda4") + // secboot encryption key size + c.Check(key, DeepEquals, bytes.Repeat([]byte("1"), 32)) + + restore = main.MockOsStdin(bytes.NewBufferString(all1sKey)) + defer restore() + stageErr = fmt.Errorf("mock stage error") + err = main.Run([]string{ + "change-encryption-key", + "--device", "/dev/vda4", + "--stage", + }) + c.Assert(err, ErrorMatches, "cannot stage LUKS device encryption key change: mock stage error") +} + +func (s *mainSuite) TestTransitionEncryptionKey(c *C) { + b := bytes.NewBufferString(all1sKey) + restore := main.MockOsStdin(b) + defer restore() + dev := "" + transitionCalls := 0 + var key []byte + var transitionErr error + restore = main.MockStageLUKSEncryptionKeyChange(func(newKey keys.EncryptionKey, luksDev string) error { + c.Errorf("unexpected call") + return fmt.Errorf("unexpected call") + }) + defer restore() + restore = main.MockTransitionLUKSEncryptionKeyChange(func(newKey keys.EncryptionKey, luksDev string) error { + transitionCalls++ + dev = luksDev + key = newKey + return transitionErr + }) + defer restore() + defer restore() + err := main.Run([]string{ + "change-encryption-key", + "--device", "/dev/vda4", + "--transition", + }) + c.Assert(err, IsNil) + c.Check(transitionCalls, Equals, 1) + c.Check(dev, Equals, "/dev/vda4") + // secboot encryption key size + c.Check(key, DeepEquals, bytes.Repeat([]byte("1"), 32)) + + restore = main.MockOsStdin(bytes.NewBufferString(all1sKey)) + defer restore() + transitionErr = fmt.Errorf("mock transition error") + err = main.Run([]string{ + "change-encryption-key", + "--device", "/dev/vda4", + "--transition", + }) + c.Assert(err, ErrorMatches, "cannot transition LUKS device encryption key change: mock transition error") +} diff -Nru snapd-2.55.5+20.04/cmd/snap-preseed/export_test.go snapd-2.57.5+20.04/cmd/snap-preseed/export_test.go --- snapd-2.55.5+20.04/cmd/snap-preseed/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-preseed/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -19,12 +19,32 @@ package main +import "github.com/snapcore/snapd/testutil" + var ( Run = run ) func MockOsGetuid(f func() int) (restore func()) { - oldOsGetuid := osGetuid + r := testutil.Backup(&osGetuid) osGetuid = f - return func() { osGetuid = oldOsGetuid } + return r +} + +func MockPreseedCore20(f func(dir, key, aaDir string) error) (restore func()) { + r := testutil.Backup(&preseedCore20) + preseedCore20 = f + return r +} + +func MockPreseedClassic(f func(dir string) error) (restore func()) { + r := testutil.Backup(&preseedClassic) + preseedClassic = f + return r +} + +func MockResetPreseededChroot(f func(dir string) error) (restore func()) { + r := testutil.Backup(&preseedResetPreseededChroot) + preseedResetPreseededChroot = f + return r } diff -Nru snapd-2.55.5+20.04/cmd/snap-preseed/main.go snapd-2.57.5+20.04/cmd/snap-preseed/main.go --- snapd-2.55.5+20.04/cmd/snap-preseed/main.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-preseed/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -47,13 +47,20 @@ ) type options struct { - Reset bool `long:"reset"` + Reset bool `long:"reset"` + PreseedSignKey string `long:"preseed-sign-key"` + AppArmorFeaturesDir string `long:"apparmor-features-dir"` } var ( - osGetuid = os.Getuid - Stdout io.Writer = os.Stdout - Stderr io.Writer = os.Stderr + osGetuid = os.Getuid + // unused currently, left in place for consistency for when it is needed + // Stdout io.Writer = os.Stdout + Stderr io.Writer = os.Stderr + + preseedCore20 = preseed.Core20 + preseedClassic = preseed.Classic + preseedResetPreseededChroot = preseed.ResetPreseededChroot opts options ) @@ -109,11 +116,11 @@ } if opts.Reset { - return preseed.ResetPreseededChroot(chrootDir) + return preseedResetPreseededChroot(chrootDir) } if probeCore20ImageDir(chrootDir) { - return preseed.Core20(chrootDir) + return preseedCore20(chrootDir, opts.PreseedSignKey, opts.AppArmorFeaturesDir) } - return preseed.Classic(chrootDir) + return preseedClassic(chrootDir) } diff -Nru snapd-2.55.5+20.04/cmd/snap-preseed/preseed_classic_test.go snapd-2.57.5+20.04/cmd/snap-preseed/preseed_classic_test.go --- snapd-2.55.5+20.04/cmd/snap-preseed/preseed_classic_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-preseed/preseed_classic_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,23 +20,14 @@ package main_test import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" "testing" "github.com/jessevdk/go-flags" . "gopkg.in/check.v1" "github.com/snapcore/snapd/cmd/snap-preseed" - "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/image/preseed" - "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/squashfs" - apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" ) @@ -67,26 +58,6 @@ return parser } -func mockVersionFiles(c *C, rootDir1, version1, rootDir2, version2 string) { - versions := []string{version1, version2} - for i, root := range []string{rootDir1, rootDir2} { - c.Assert(os.MkdirAll(filepath.Join(root, dirs.CoreLibExecDir), 0755), IsNil) - infoFile := filepath.Join(root, dirs.CoreLibExecDir, "info") - c.Assert(ioutil.WriteFile(infoFile, []byte(fmt.Sprintf("VERSION=%s", versions[i])), 0644), IsNil) - } -} - -func mockChrootDirs(c *C, rootDir string, apparmorDir bool) func() { - if apparmorDir { - c.Assert(os.MkdirAll(filepath.Join(rootDir, "/sys/kernel/security/apparmor"), 0755), IsNil) - } - mockMountInfo := `912 920 0:57 / ${rootDir}/proc rw,nosuid,nodev,noexec,relatime - proc proc rw -914 913 0:7 / ${rootDir}/sys/kernel/security rw,nosuid,nodev,noexec,relatime master:8 - securityfs securityfs rw -915 920 0:58 / ${rootDir}/dev rw,relatime - tmpfs none rw,size=492k,mode=755,uid=100000,gid=100000 -` - return osutil.MockMountInfo(strings.Replace(mockMountInfo, "${rootDir}", rootDir, -1)) -} - func (s *startPreseedSuite) TestRequiresRoot(c *C) { restore := main.MockOsGetuid(func() int { return 1000 @@ -107,368 +78,49 @@ c.Check(main.Run(parser, nil), ErrorMatches, `need chroot path as argument`) } -func (s *startPreseedSuite) TestChrootDoesntExist(c *C) { - restore := main.MockOsGetuid(func() int { return 0 }) - defer restore() - - parser := testParser(c) - c.Check(main.Run(parser, []string{"/non-existing-dir"}), ErrorMatches, `cannot verify "/non-existing-dir": is not a directory`) -} - -func (s *startPreseedSuite) TestChrootValidationUnhappy(c *C) { - restore := main.MockOsGetuid(func() int { return 0 }) - defer restore() - - tmpDir := c.MkDir() - defer osutil.MockMountInfo("")() - - parser := testParser(c) - c.Check(main.Run(parser, []string{tmpDir}), ErrorMatches, "cannot preseed without the following mountpoints:\n - .*/dev\n - .*/proc\n - .*/sys/kernel/security") -} - -func (s *startPreseedSuite) TestRunPreseedMountUnhappy(c *C) { - tmpDir := c.MkDir() - dirs.SetRootDir(tmpDir) - defer mockChrootDirs(c, tmpDir, true)() - - restoreOsGuid := main.MockOsGetuid(func() int { return 0 }) - defer restoreOsGuid() - - restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { return nil }) - defer restoreSyscallChroot() - - mockMountCmd := testutil.MockCommand(c, "mount", `echo "something went wrong" -exit 32 -`) - defer mockMountCmd.Restore() - - targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") - restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) - defer restoreMountPath() - - restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/core.snap", "", nil }) - defer restoreSystemSnapFromSeed() - - parser := testParser(c) - c.Check(main.Run(parser, []string{tmpDir}), ErrorMatches, `cannot mount .+ at .+ in preseed mode: exit status 32\n'mount -t squashfs -o ro,x-gdu.hide,x-gvfs-hide /a/core.snap .*/target-core-mounted-here' failed with: something went wrong\n`) -} - -func (s *startPreseedSuite) TestChrootValidationUnhappyNoApparmor(c *C) { +func (s *startPreseedSuite) TestRunPreseedAgainstFilesystemRoot(c *C) { restore := main.MockOsGetuid(func() int { return 0 }) defer restore() - tmpDir := c.MkDir() - defer mockChrootDirs(c, tmpDir, false)() - parser := testParser(c) - c.Check(main.Run(parser, []string{tmpDir}), ErrorMatches, `cannot preseed without access to ".*sys/kernel/security/apparmor"`) + c.Assert(main.Run(parser, []string{"/"}), ErrorMatches, `cannot run snap-preseed against /`) } -func (s *startPreseedSuite) TestChrootValidationAlreadyPreseeded(c *C) { - restore := main.MockOsGetuid(func() int { return 0 }) +func (s *startPreseedSuite) TestRunPreseedClassicHappy(c *C) { + restore := main.MockOsGetuid(func() int { + return 0 + }) defer restore() - tmpDir := c.MkDir() - snapdDir := filepath.Dir(dirs.SnapStateFile) - c.Assert(os.MkdirAll(filepath.Join(tmpDir, snapdDir), 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(tmpDir, dirs.SnapStateFile), nil, os.ModePerm), IsNil) - - parser := testParser(c) - c.Check(main.Run(parser, []string{tmpDir}), ErrorMatches, fmt.Sprintf("the system at %q appears to be preseeded, pass --reset flag to clean it up", tmpDir)) -} - -func (s *startPreseedSuite) TestChrootFailure(c *C) { - restoreOsGuid := main.MockOsGetuid(func() int { return 0 }) - defer restoreOsGuid() - - restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { - return fmt.Errorf("FAIL: %s", path) + var called bool + restorePreseed := main.MockPreseedClassic(func(dir string) error { + c.Check(dir, Equals, "/a/dir") + called = true + return nil }) - defer restoreSyscallChroot() - - tmpDir := c.MkDir() - defer mockChrootDirs(c, tmpDir, true)() + defer restorePreseed() parser := testParser(c) - c.Check(main.Run(parser, []string{tmpDir}), ErrorMatches, fmt.Sprintf("cannot chroot into %s: FAIL: %s", tmpDir, tmpDir)) -} - -func (s *startPreseedSuite) TestRunPreseedHappy(c *C) { - tmpDir := c.MkDir() - dirs.SetRootDir(tmpDir) - defer mockChrootDirs(c, tmpDir, true)() - - restoreOsGuid := main.MockOsGetuid(func() int { return 0 }) - defer restoreOsGuid() - - restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { return nil }) - defer restoreSyscallChroot() - - mockMountCmd := testutil.MockCommand(c, "mount", "") - defer mockMountCmd.Restore() - - mockUmountCmd := testutil.MockCommand(c, "umount", "") - defer mockUmountCmd.Restore() - - targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") - restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) - defer restoreMountPath() - - restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/core.snap", "", nil }) - defer restoreSystemSnapFromSeed() - - mockTargetSnapd := testutil.MockCommand(c, filepath.Join(targetSnapdRoot, "usr/lib/snapd/snapd"), `#!/bin/sh - if [ "$SNAPD_PRESEED" != "1" ]; then - exit 1 - fi -`) - defer mockTargetSnapd.Restore() - - mockSnapdFromDeb := testutil.MockCommand(c, filepath.Join(tmpDir, "usr/lib/snapd/snapd"), `#!/bin/sh - exit 1 -`) - defer mockSnapdFromDeb.Restore() - - // snapd from the snap is newer than deb - mockVersionFiles(c, targetSnapdRoot, "2.44.0", tmpDir, "2.41.0") - - parser := testParser(c) - c.Check(main.Run(parser, []string{tmpDir}), IsNil) - - c.Assert(mockMountCmd.Calls(), HasLen, 1) - // note, tmpDir, targetSnapdRoot are contactenated again cause we're not really chrooting in the test - // and mocking dirs.RootDir - c.Check(mockMountCmd.Calls()[0], DeepEquals, []string{"mount", "-t", "squashfs", "-o", "ro,x-gdu.hide,x-gvfs-hide", "/a/core.snap", filepath.Join(tmpDir, targetSnapdRoot)}) - - c.Assert(mockTargetSnapd.Calls(), HasLen, 1) - c.Check(mockTargetSnapd.Calls()[0], DeepEquals, []string{"snapd"}) - - c.Assert(mockSnapdFromDeb.Calls(), HasLen, 0) - - // relative chroot path works too - tmpDirPath, relativeChroot := filepath.Split(tmpDir) - pwd, err := os.Getwd() - c.Assert(err, IsNil) - defer func() { - os.Chdir(pwd) - }() - c.Assert(os.Chdir(tmpDirPath), IsNil) - c.Check(main.Run(parser, []string{relativeChroot}), IsNil) -} - -func (s *startPreseedSuite) TestRunPreseedHappyDebVersionIsNewer(c *C) { - tmpDir := c.MkDir() - dirs.SetRootDir(tmpDir) - defer mockChrootDirs(c, tmpDir, true)() - - restoreOsGuid := main.MockOsGetuid(func() int { return 0 }) - defer restoreOsGuid() - - restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { return nil }) - defer restoreSyscallChroot() - - mockMountCmd := testutil.MockCommand(c, "mount", "") - defer mockMountCmd.Restore() - - mockUmountCmd := testutil.MockCommand(c, "umount", "") - defer mockUmountCmd.Restore() - - targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") - restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) - defer restoreMountPath() - - restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/core.snap", "", nil }) - defer restoreSystemSnapFromSeed() - - c.Assert(os.MkdirAll(filepath.Join(targetSnapdRoot, "usr/lib/snapd/"), 0755), IsNil) - mockSnapdFromSnap := testutil.MockCommand(c, filepath.Join(targetSnapdRoot, "usr/lib/snapd/snapd"), `#!/bin/sh - exit 1 -`) - defer mockSnapdFromSnap.Restore() - - mockSnapdFromDeb := testutil.MockCommand(c, filepath.Join(tmpDir, "usr/lib/snapd/snapd"), `#!/bin/sh - if [ "$SNAPD_PRESEED" != "1" ]; then - exit 1 - fi -`) - defer mockSnapdFromDeb.Restore() - - // snapd from the deb is newer than snap - mockVersionFiles(c, targetSnapdRoot, "2.44.0", tmpDir, "2.45.0") - - parser := testParser(c) - c.Check(main.Run(parser, []string{tmpDir}), IsNil) - - c.Assert(mockMountCmd.Calls(), HasLen, 1) - // note, tmpDir, targetSnapdRoot are contactenated again cause we're not really chrooting in the test - // and mocking dirs.RootDir - c.Check(mockMountCmd.Calls()[0], DeepEquals, []string{"mount", "-t", "squashfs", "-o", "ro,x-gdu.hide,x-gvfs-hide", "/a/core.snap", filepath.Join(tmpDir, targetSnapdRoot)}) - - c.Assert(mockSnapdFromDeb.Calls(), HasLen, 1) - c.Check(mockSnapdFromDeb.Calls()[0], DeepEquals, []string{"snapd"}) - c.Assert(mockSnapdFromSnap.Calls(), HasLen, 0) -} - -func (s *startPreseedSuite) TestRunPreseedUnsupportedVersion(c *C) { - tmpDir := c.MkDir() - dirs.SetRootDir(tmpDir) - c.Assert(os.MkdirAll(filepath.Join(tmpDir, "usr/lib/snapd/"), 0755), IsNil) - defer mockChrootDirs(c, tmpDir, true)() - - restoreOsGuid := main.MockOsGetuid(func() int { return 0 }) - defer restoreOsGuid() - - restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { return nil }) - defer restoreSyscallChroot() - - mockMountCmd := testutil.MockCommand(c, "mount", "") - defer mockMountCmd.Restore() - - targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") - restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) - defer restoreMountPath() - - restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/core.snap", "", nil }) - defer restoreSystemSnapFromSeed() - - c.Assert(os.MkdirAll(filepath.Join(targetSnapdRoot, "usr/lib/snapd/"), 0755), IsNil) - mockTargetSnapd := testutil.MockCommand(c, filepath.Join(targetSnapdRoot, "usr/lib/snapd/snapd"), "") - defer mockTargetSnapd.Restore() - - infoFile := filepath.Join(targetSnapdRoot, dirs.CoreLibExecDir, "info") - c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=2.43.0"), 0644), IsNil) - - // simulate snapd version from the deb - infoFile = filepath.Join(filepath.Join(tmpDir, dirs.CoreLibExecDir, "info")) - c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=2.41.0"), 0644), IsNil) - - parser := testParser(c) - c.Check(main.Run(parser, []string{tmpDir}), ErrorMatches, - `snapd 2.43.0 from the target system does not support preseeding, the minimum required version is 2.43.3\+`) -} - -func (s *startPreseedSuite) TestRunPreseedAgainstFilesystemRoot(c *C) { - restore := main.MockOsGetuid(func() int { return 0 }) - defer restore() - - parser := testParser(c) - c.Assert(main.Run(parser, []string{"/"}), ErrorMatches, `cannot run snap-preseed against /`) + c.Assert(main.Run(parser, []string{"/a/dir"}), IsNil) + c.Check(called, Equals, true) } func (s *startPreseedSuite) TestReset(c *C) { - restore := main.MockOsGetuid(func() int { return 0 }) + restore := main.MockOsGetuid(func() int { + return 0 + }) defer restore() - startDir, err := os.Getwd() - c.Assert(err, IsNil) - defer func() { - os.Chdir(startDir) - }() - - for _, isRelative := range []bool{false, true} { - tmpDir := c.MkDir() - resetDirArg := tmpDir - if isRelative { - var parentDir string - parentDir, resetDirArg = filepath.Split(tmpDir) - os.Chdir(parentDir) - } - - // mock some preseeding artifacts - artifacts := []struct { - path string - // if symlinkTarget is not empty, then a path -> symlinkTarget symlink - // will be created instead of a regular file. - symlinkTarget string - }{ - {dirs.SnapStateFile, ""}, - {dirs.SnapSystemKeyFile, ""}, - {filepath.Join(dirs.SnapDesktopFilesDir, "foo.desktop"), ""}, - {filepath.Join(dirs.SnapDesktopIconsDir, "foo.png"), ""}, - {filepath.Join(dirs.SnapMountPolicyDir, "foo.fstab"), ""}, - {filepath.Join(dirs.SnapBlobDir, "foo.snap"), ""}, - {filepath.Join(dirs.SnapUdevRulesDir, "foo-snap.bar.rules"), ""}, - {filepath.Join(dirs.SnapDBusSystemPolicyDir, "snap.foo.bar.conf"), ""}, - {filepath.Join(dirs.SnapDBusSessionServicesDir, "org.example.Session.service"), ""}, - {filepath.Join(dirs.SnapDBusSystemServicesDir, "org.example.System.service"), ""}, - {filepath.Join(dirs.SnapServicesDir, "snap.foo.service"), ""}, - {filepath.Join(dirs.SnapServicesDir, "snap.foo.timer"), ""}, - {filepath.Join(dirs.SnapServicesDir, "snap.foo.socket"), ""}, - {filepath.Join(dirs.SnapServicesDir, "snap-foo.mount"), ""}, - {filepath.Join(dirs.SnapServicesDir, "multi-user.target.wants", "snap-foo.mount"), ""}, - {filepath.Join(dirs.SnapDataDir, "foo", "bar"), ""}, - {filepath.Join(dirs.SnapCacheDir, "foocache", "bar"), ""}, - {filepath.Join(apparmor_sandbox.CacheDir, "foo", "bar"), ""}, - {filepath.Join(dirs.SnapAppArmorDir, "foo"), ""}, - {filepath.Join(dirs.SnapAssertsDBDir, "foo"), ""}, - {filepath.Join(dirs.FeaturesDir, "foo"), ""}, - {filepath.Join(dirs.SnapDeviceDir, "foo-1", "bar"), ""}, - {filepath.Join(dirs.SnapCookieDir, "foo"), ""}, - {filepath.Join(dirs.SnapSeqDir, "foo.json"), ""}, - {filepath.Join(dirs.SnapMountDir, "foo", "bin"), ""}, - {filepath.Join(dirs.SnapSeccompDir, "foo.bin"), ""}, - {filepath.Join(runinhibit.InhibitDir, "foo.lock"), ""}, - // bash-completion symlinks - {filepath.Join(dirs.CompletersDir, "foo.bar"), "/a/snapd/complete.sh"}, - {filepath.Join(dirs.CompletersDir, "foo"), "foo.bar"}, - } - - for _, art := range artifacts { - fullPath := filepath.Join(tmpDir, art.path) - // create parent dir - c.Assert(os.MkdirAll(filepath.Dir(fullPath), 0755), IsNil) - if art.symlinkTarget != "" { - // note, symlinkTarget is not relative to tmpDir - c.Assert(os.Symlink(art.symlinkTarget, fullPath), IsNil) - } else { - c.Assert(ioutil.WriteFile(fullPath, nil, os.ModePerm), IsNil) - } - } - - checkArtifacts := func(exists bool) { - for _, art := range artifacts { - fullPath := filepath.Join(tmpDir, art.path) - if art.symlinkTarget != "" { - c.Check(osutil.IsSymlink(fullPath), Equals, exists, Commentf("offending symlink: %s", fullPath)) - } else { - c.Check(osutil.FileExists(fullPath), Equals, exists, Commentf("offending file: %s", fullPath)) - } - } - } - - // validity - checkArtifacts(true) - - snapdDir := filepath.Dir(dirs.SnapStateFile) - c.Assert(os.MkdirAll(filepath.Join(tmpDir, snapdDir), 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(tmpDir, dirs.SnapStateFile), nil, os.ModePerm), IsNil) - - parser := testParser(c) - c.Assert(main.Run(parser, []string{"--reset", resetDirArg}), IsNil) - - checkArtifacts(false) - - // running reset again is ok - parser = testParser(c) - c.Assert(main.Run(parser, []string{"--reset", resetDirArg}), IsNil) - - // reset complains if target directory doesn't exist - c.Assert(main.Run(parser, []string{"--reset", "/non/existing/chrootpath"}), ErrorMatches, `cannot reset non-existing directory "/non/existing/chrootpath"`) - - // reset complains if target is not a directory - dummyFile := filepath.Join(resetDirArg, "foo") - c.Assert(ioutil.WriteFile(dummyFile, nil, os.ModePerm), IsNil) - err = main.Run(parser, []string{"--reset", dummyFile}) - // the error message is always with an absolute file, so make the path - // absolute if we are running the relative test to properly match - if isRelative { - var err2 error - dummyFile, err2 = filepath.Abs(dummyFile) - c.Assert(err2, IsNil) - } - c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot reset %q, it is not a directory`, dummyFile)) - } + var called bool + main.MockResetPreseededChroot(func(dir string) error { + c.Check(dir, Equals, "/a/dir") + called = true + return nil + }) + parser := testParser(c) + c.Assert(main.Run(parser, []string{"--reset", "/a/dir"}), IsNil) + c.Check(called, Equals, true) } func (s *startPreseedSuite) TestReadInfoValidity(c *C) { @@ -481,7 +133,7 @@ }, } - // set a dummy sanitize method. + // set an empty sanitize method. snap.SanitizePlugsSlots = func(*snap.Info) { called = true } parser := testParser(c) diff -Nru snapd-2.55.5+20.04/cmd/snap-preseed/preseed_uc20_test.go snapd-2.57.5+20.04/cmd/snap-preseed/preseed_uc20_test.go --- snapd-2.55.5+20.04/cmd/snap-preseed/preseed_uc20_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-preseed/preseed_uc20_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,7 +20,6 @@ package main_test import ( - "fmt" "io/ioutil" "os" "path/filepath" @@ -29,129 +28,61 @@ "github.com/snapcore/snapd/cmd/snap-preseed" "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/image/preseed" - "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/testutil" ) func (s *startPreseedSuite) TestRunPreseedUC20Happy(c *C) { tmpDir := c.MkDir() dirs.SetRootDir(tmpDir) - defer mockChrootDirs(c, tmpDir, true)() - restoreOsGuid := main.MockOsGetuid(func() int { return 0 }) - defer restoreOsGuid() - - mockChootCmd := testutil.MockCommand(c, "chroot", "") - defer mockChootCmd.Restore() - - mockMountCmd := testutil.MockCommand(c, "mount", "") - defer mockMountCmd.Restore() - - mockUmountCmd := testutil.MockCommand(c, "umount", "") - defer mockUmountCmd.Restore() - - preseedTmpDir := filepath.Join(tmpDir, "preseed-tmp") - restoreMakePreseedTmpDir := preseed.MockMakePreseedTempDir(func() (string, error) { - return preseedTmpDir, nil + restore := main.MockOsGetuid(func() int { + return 0 }) - defer restoreMakePreseedTmpDir() - - writableTmpDir := filepath.Join(tmpDir, "writable-tmp") - restoreMakeWritableTempDir := preseed.MockMakeWritableTempDir(func() (string, error) { - return writableTmpDir, nil - }) - defer restoreMakeWritableTempDir() - - c.Assert(os.MkdirAll(filepath.Join(writableTmpDir, "system-data/etc/bar"), 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(writableTmpDir, "system-data/etc/bar/a"), nil, 0644), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(writableTmpDir, "system-data/etc/bar/b"), nil, 0644), IsNil) - - mockTar := testutil.MockCommand(c, "tar", "") - defer mockTar.Restore() - - const exportFileContents = `{ -"exclude": ["foo"], -"include": ["/etc/bar/a", "/etc/bar/b"] -}` - - c.Assert(os.MkdirAll(filepath.Join(writableTmpDir, "system-data/var/lib/snapd"), 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(writableTmpDir, "system-data/var/lib/snapd/preseed-export.json"), []byte(exportFileContents), 0644), IsNil) - - mockWritablePaths := testutil.MockCommand(c, filepath.Join(preseedTmpDir, "/usr/lib/core/handle-writable-paths"), "") - defer mockWritablePaths.Restore() - - restore := osutil.MockMountInfo(fmt.Sprintf(`130 30 42:1 / %s/somepath rw,relatime shared:54 - ext4 /some/path rw -`, preseedTmpDir)) defer restore() - targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") - restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) - defer restoreMountPath() - - restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/snapd.snap", "/a/base.snap", nil }) - defer restoreSystemSnapFromSeed() - + // for UC20 probing c.Assert(os.MkdirAll(filepath.Join(tmpDir, "system-seed/systems/20220203"), 0755), IsNil) + // we don't run tar, so create a fake artifact to make FileDigest happy + c.Assert(ioutil.WriteFile(filepath.Join(tmpDir, "system-seed/systems/20220203/preseed.tgz"), nil, 0644), IsNil) + + var called bool + restorePreseed := main.MockPreseedCore20(func(dir, key, aaDir string) error { + c.Check(dir, Equals, tmpDir) + c.Check(key, Equals, "key") + c.Check(aaDir, Equals, "/custom/aa/features") + called = true + return nil + }) + defer restorePreseed() parser := testParser(c) - c.Assert(main.Run(parser, []string{tmpDir}), IsNil) + c.Assert(main.Run(parser, []string{"--preseed-sign-key", "key", "--apparmor-features-dir", "/custom/aa/features", tmpDir}), IsNil) + c.Check(called, Equals, true) +} - c.Check(mockChootCmd.Calls()[0], DeepEquals, []string{"chroot", preseedTmpDir, "/usr/lib/snapd/snapd"}) +func (s *startPreseedSuite) TestRunPreseedUC20HappyNoArgs(c *C) { + tmpDir := c.MkDir() + dirs.SetRootDir(tmpDir) - c.Check(mockMountCmd.Calls(), DeepEquals, [][]string{ - {"mount", "-o", "loop", "/a/base.snap", preseedTmpDir}, - {"mount", "-o", "loop", "/a/snapd.snap", targetSnapdRoot}, - {"mount", "-t", "tmpfs", "tmpfs", filepath.Join(preseedTmpDir, "run")}, - {"mount", "-t", "tmpfs", "tmpfs", filepath.Join(preseedTmpDir, "var/tmp")}, - {"mount", "--bind", filepath.Join(preseedTmpDir, "/var/tmp"), filepath.Join(preseedTmpDir, "tmp")}, - {"mount", "-t", "proc", "proc", filepath.Join(preseedTmpDir, "proc")}, - {"mount", "-t", "sysfs", "sysfs", filepath.Join(preseedTmpDir, "sys")}, - {"mount", "-t", "devtmpfs", "udev", filepath.Join(preseedTmpDir, "dev")}, - {"mount", "-t", "securityfs", "securityfs", filepath.Join(preseedTmpDir, "sys/kernel/security")}, - {"mount", "--bind", writableTmpDir, filepath.Join(preseedTmpDir, "writable")}, - {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/var/lib/snapd"), filepath.Join(preseedTmpDir, "var/lib/snapd")}, - {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/var/cache/snapd"), filepath.Join(preseedTmpDir, "var/cache/snapd")}, - {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/var/cache/apparmor"), filepath.Join(preseedTmpDir, "var/cache/apparmor")}, - {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/var/snap"), filepath.Join(preseedTmpDir, "var/snap")}, - {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/snap"), filepath.Join(preseedTmpDir, "snap")}, - {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/etc/systemd"), filepath.Join(preseedTmpDir, "etc/systemd")}, - {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/etc/dbus-1"), filepath.Join(preseedTmpDir, "etc/dbus-1")}, - {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/etc/udev/rules.d"), filepath.Join(preseedTmpDir, "etc/udev/rules.d")}, - {"mount", "--bind", filepath.Join(targetSnapdRoot, "/usr/lib/snapd"), filepath.Join(preseedTmpDir, "usr/lib/snapd")}, - {"mount", "--bind", filepath.Join(tmpDir, "system-seed"), filepath.Join(preseedTmpDir, "var/lib/snapd/seed")}, + restore := main.MockOsGetuid(func() int { + return 0 }) + defer restore() - c.Check(mockTar.Calls(), DeepEquals, [][]string{ - {"tar", "-czf", filepath.Join(tmpDir, "system-seed/systems/20220203/preseed.tgz"), "-p", "-C", - filepath.Join(writableTmpDir, "system-data"), "--exclude", "foo", "etc/bar/a", "etc/bar/b"}, - }) + // for UC20 probing + c.Assert(os.MkdirAll(filepath.Join(tmpDir, "system-seed/systems/20220203"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(tmpDir, "system-seed/systems/20220203/preseed.tgz"), nil, 0644), IsNil) - c.Check(mockUmountCmd.Calls(), DeepEquals, [][]string{ - {"umount", filepath.Join(preseedTmpDir, "var/lib/snapd/seed")}, - {"umount", filepath.Join(preseedTmpDir, "usr/lib/snapd")}, - {"umount", filepath.Join(preseedTmpDir, "etc/udev/rules.d")}, - {"umount", filepath.Join(preseedTmpDir, "etc/dbus-1")}, - {"umount", filepath.Join(preseedTmpDir, "etc/systemd")}, - {"umount", filepath.Join(preseedTmpDir, "snap")}, - {"umount", filepath.Join(preseedTmpDir, "var/snap")}, - {"umount", filepath.Join(preseedTmpDir, "var/cache/apparmor")}, - {"umount", filepath.Join(preseedTmpDir, "var/cache/snapd")}, - {"umount", filepath.Join(preseedTmpDir, "var/lib/snapd")}, - {"umount", filepath.Join(preseedTmpDir, "writable")}, - {"umount", filepath.Join(preseedTmpDir, "sys/kernel/security")}, - {"umount", filepath.Join(preseedTmpDir, "dev")}, - {"umount", filepath.Join(preseedTmpDir, "sys")}, - {"umount", filepath.Join(preseedTmpDir, "proc")}, - {"umount", filepath.Join(preseedTmpDir, "tmp")}, - {"umount", filepath.Join(preseedTmpDir, "var/tmp")}, - {"umount", filepath.Join(preseedTmpDir, "run")}, - {"umount", filepath.Join(tmpDir, "target-core-mounted-here")}, - {"umount", preseedTmpDir}, - // from handle-writable-paths - {"umount", filepath.Join(preseedTmpDir, "somepath")}, + var called bool + restorePreseed := main.MockPreseedCore20(func(dir, key, aaDir string) error { + c.Check(dir, Equals, tmpDir) + c.Check(key, Equals, "") + c.Check(aaDir, Equals, "") + called = true + return nil }) + defer restorePreseed() - // validity check; -1 to account for handle-writable-paths mock which doesn’t trigger mount in the test - c.Check(len(mockMountCmd.Calls()), Equals, len(mockUmountCmd.Calls())-1) + parser := testParser(c) + c.Assert(main.Run(parser, []string{tmpDir}), IsNil) + c.Check(called, Equals, true) } diff -Nru snapd-2.55.5+20.04/cmd/snap-repair/runner_test.go snapd-2.57.5+20.04/cmd/snap-repair/runner_test.go --- snapd-2.55.5+20.04/cmd/snap-repair/runner_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-repair/runner_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -868,7 +868,7 @@ var mockServer *httptest.Server mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ua := r.Header.Get("User-Agent") - c.Check(strings.Contains(ua, "snap-repair"), Equals, true) + c.Check(ua, testutil.Contains, "snap-repair") urlPath := r.URL.Path if redirectFirst && r.Header.Get("Accept") == asserts.MediaType { @@ -1881,18 +1881,24 @@ func (s *runScriptSuite) SetUpTest(c *C) { s.baseRunnerSuite.SetUpTest(c) + s.runDir = filepath.Join(dirs.SnapRepairRunDir, "canonical", "1") + + s.AddCleanup(snapdenv.SetUserAgentFromVersion("1", nil, "snap-repair")) + + restoreErrTrackerReportRepair := repair.MockErrtrackerReportRepair(s.errtrackerReportRepair) + s.AddCleanup(restoreErrTrackerReportRepair) +} +// setupRunner must be called from the tests so that the *C passed into contains +// the tests' state and not the SetUpTest's state (otherwise, assertion failures +// in the mock server go unreported). +func (s *runScriptSuite) setupRunner(c *C) { s.mockServer = makeMockServer(c, &s.seqRepairs, false) s.AddCleanup(func() { s.mockServer.Close() }) s.runner = repair.NewRunner() s.runner.BaseURL = mustParseURL(s.mockServer.URL) s.runner.LoadState() - - s.runDir = filepath.Join(dirs.SnapRepairRunDir, "canonical", "1") - - restoreErrTrackerReportRepair := repair.MockErrtrackerReportRepair(s.errtrackerReportRepair) - s.AddCleanup(restoreErrTrackerReportRepair) } func (s *runScriptSuite) errtrackerReportRepair(repair, errMsg, dupSig string, extra map[string]string) (string, error) { @@ -1941,6 +1947,7 @@ } func (s *runScriptSuite) TestRepairBasicRunHappy(c *C) { + s.setupRunner(c) script := `#!/bin/sh echo "happy output" echo "done" >&$SNAP_REPAIR_STATUS_FD @@ -1964,6 +1971,7 @@ } func (s *runScriptSuite) TestRepairBasicRunUnhappy(c *C) { + s.setupRunner(c) script := `#!/bin/sh echo "unhappy output" exit 1 @@ -2005,6 +2013,7 @@ } func (s *runScriptSuite) TestRepairBasicSkip(c *C) { + s.setupRunner(c) script := `#!/bin/sh echo "other output" echo "skip" >&$SNAP_REPAIR_STATUS_FD @@ -2028,6 +2037,7 @@ } func (s *runScriptSuite) TestRepairBasicRunUnhappyThenHappy(c *C) { + s.setupRunner(c) script := `#!/bin/sh if [ -f zzz-ran-once ]; then echo "happy now" @@ -2074,6 +2084,7 @@ } func (s *runScriptSuite) TestRepairHitsTimeout(c *C) { + s.setupRunner(c) r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) @@ -2111,6 +2122,7 @@ } func (s *runScriptSuite) TestRepairHasCorrectPath(c *C) { + s.setupRunner(c) r1 := sysdb.InjectTrusted(s.storeSigning.Trusted) defer r1() r2 := repair.MockTrustedRepairRootKeys([]*asserts.AccountKey{s.repairRootAcctKey}) @@ -2207,7 +2219,7 @@ s.seedAssertsDir = filepath.Join(dirs.SnapSeedDir, "assertions") - // dummy seed yaml + // sample seed yaml err := os.MkdirAll(s.seedAssertsDir, 0755) c.Assert(err, IsNil) seedYamlFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml") @@ -2295,7 +2307,7 @@ err := os.MkdirAll(s.seedAssertsDir, 0755) c.Assert(err, IsNil) - // write dummy modeenv + // write sample modeenv err = os.MkdirAll(filepath.Dir(dirs.SnapModeenvFile), 0755) c.Assert(err, IsNil) err = ioutil.WriteFile(dirs.SnapModeenvFile, mockModeenv, 0644) diff -Nru snapd-2.55.5+20.04/cmd/snap-seccomp/main.go snapd-2.57.5+20.04/cmd/snap-seccomp/main.go --- snapd-2.55.5+20.04/cmd/snap-seccomp/main.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-seccomp/main.go 2022-10-17 16:25:18.000000000 +0000 @@ -730,7 +730,7 @@ } // Because ActLog is functionally ActAllow with logging, if we don't - // support ActLog, fallback to ActLog. + // support ActLog, fallback to ActAllow. return seccomp.ActAllow } diff -Nru snapd-2.55.5+20.04/cmd/snap-seccomp-blacklist/.gitignore snapd-2.57.5+20.04/cmd/snap-seccomp-blacklist/.gitignore --- snapd-2.55.5+20.04/cmd/snap-seccomp-blacklist/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-seccomp-blacklist/.gitignore 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,4 @@ +*.bpf +*.o +*.pfc +snap-seccomp-blacklist diff -Nru snapd-2.55.5+20.04/cmd/snap-update-ns/change.go snapd-2.57.5+20.04/cmd/snap-update-ns/change.go --- snapd-2.55.5+20.04/cmd/snap-update-ns/change.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-update-ns/change.go 2022-10-17 16:25:18.000000000 +0000 @@ -465,6 +465,18 @@ logger.Debugf("cannot remove a mount point on read-only filesystem %q", path) return nil } + if err == syscall.EBUSY { + // It's still unclear how this can happen. For the time being + // let the operation succeed and log the event. + logger.Noticef("cannot remove mount point, got EBUSY: %q", path) + if isMount, err := osutil.IsMounted(path); isMount { + mounts, _ := osutil.LoadMountInfo() + logger.Noticef("%q is still a mount point:\n%s", path, mounts) + } else if err != nil { + logger.Noticef("cannot read mountinfo: %v", err) + } + return nil + } // If we were removing a directory but it was not empty then just // ignore the error. This is the equivalent of the non-empty file // check we do above. See rmdir(2) for explanation why we accept @@ -499,13 +511,20 @@ desired[i].Dir = filepath.Clean(desired[i].Dir) } + // Make yet another copy of the current entries, to retain their original + // order (the "current" variable is going to be sorted soon); just using + // currentProfile.Entries is not reliable because it didn't undergo the + // cleanup of the Dir paths. + unsortedCurrent := make([]osutil.MountEntry, len(current)) + copy(unsortedCurrent, current) + dumpMountEntries := func(entries []osutil.MountEntry, pfx string) { logger.Debugf(pfx) for _, en := range entries { logger.Debugf("- %v", en) } } - dumpMountEntries(desired, "desired mount entries") + dumpMountEntries(current, "current mount entries") // Sort only the desired lists by directory name with implicit trailing // slash and the mount kind. // Note that the current profile is a log of what was applied and should @@ -543,6 +562,13 @@ } skipDir = "" // reset skip prefix as it no longer applies + if current[i].XSnapdOrigin() == "rootfs" { + // This is the rootfs setup by snap-confine, we should not touch it + logger.Debugf("reusing rootfs") + reuse[dir] = true + continue + } + // Reuse synthetic entries if their needed-by entry is desired. // Synthetic entries cannot exist on their own and always couple to a // non-synthetic entry. @@ -582,7 +608,7 @@ var changes []*Change // Unmount entries not reused in reverse to handle children before their parent. - unmountOrder := currentProfile.Entries + unmountOrder := unsortedCurrent for i := len(unmountOrder) - 1; i >= 0; i-- { if reuse[unmountOrder[i].Dir] { changes = append(changes, &Change{Action: Keep, Entry: unmountOrder[i]}) diff -Nru snapd-2.55.5+20.04/cmd/snap-update-ns/change_test.go snapd-2.57.5+20.04/cmd/snap-update-ns/change_test.go --- snapd-2.55.5+20.04/cmd/snap-update-ns/change_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-update-ns/change_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -114,6 +114,33 @@ }) } +// When the rootfs was setup by snap-confine, don't touch it +func (s *changeSuite) TestNeededChangesKeepRootfs(c *C) { + current := &osutil.MountProfile{Entries: []osutil.MountEntry{ + {Dir: "/", Options: []string{"x-snapd.origin=rootfs"}}, + }} + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: current.Entries[0], Action: update.Keep}, + {Entry: desired.Entries[0], Action: update.Mount}, + }) +} + +// When the rootfs was *not* setup by snap-confine, it's umounted +func (s *changeSuite) TestNeededChangesUmountRootfs(c *C) { + current := &osutil.MountProfile{Entries: []osutil.MountEntry{ + // Like the test above, but without "x-snapd.origin=rootfs" + {Dir: "/"}, + }} + desired := &osutil.MountProfile{Entries: []osutil.MountEntry{{Dir: "/common/stuff"}}} + changes := update.NeededChanges(current, desired) + c.Assert(changes, DeepEquals, []*update.Change{ + {Entry: current.Entries[0], Action: update.Unmount}, + {Entry: desired.Entries[0], Action: update.Mount}, + }) +} + // When umounting we unmount children before parents. func (s *changeSuite) TestNeededChangesUnmountOrder(c *C) { current := &osutil.MountProfile{Entries: []osutil.MountEntry{ @@ -1970,12 +1997,14 @@ // Change.Perform wants to unmount a file bind mount made on empty tmpfs placeholder but it is busy!. func (s *changeSuite) TestPerformFileBindUnmountOnTmpfsEmptyButBusy(c *C) { + restore := osutil.MockMountInfo("") + defer restore() s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic}) s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{Size: 0}) s.sys.InsertFault(`remove "/target"`, syscall.EBUSY) chg := &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "/source", Dir: "/target", Options: []string{"bind", "x-snapd.kind=file"}}} synth, err := chg.Perform(s.as) - c.Assert(err, ErrorMatches, "device or resource busy") + c.Assert(err, IsNil) c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `unmount "/target" UMOUNT_NOFOLLOW`}, {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY|O_PATH 0`, R: 3}, diff -Nru snapd-2.55.5+20.04/cmd/snap-update-ns/system.go snapd-2.57.5+20.04/cmd/snap-update-ns/system.go --- snapd-2.55.5+20.04/cmd/snap-update-ns/system.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-update-ns/system.go 2022-10-17 16:25:18.000000000 +0000 @@ -69,7 +69,7 @@ // remapping for parallel installs only when the snap has an instance key as := &Assumptions{} instanceName := upCtx.InstanceName() - as.AddUnrestrictedPaths("/tmp", "/var/snap", "/snap/"+instanceName, "/dev/shm") + as.AddUnrestrictedPaths("/tmp", "/var/snap", "/snap/"+instanceName, "/dev/shm", "/run/systemd") if snapName := snap.InstanceSnap(instanceName); snapName != instanceName { as.AddUnrestrictedPaths("/snap/" + snapName) } diff -Nru snapd-2.55.5+20.04/cmd/snap-update-ns/system_test.go snapd-2.57.5+20.04/cmd/snap-update-ns/system_test.go --- snapd-2.55.5+20.04/cmd/snap-update-ns/system_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-update-ns/system_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -74,7 +74,7 @@ // Non-instances can access /tmp, /var/snap and /snap/$SNAP_NAME upCtx := update.NewSystemProfileUpdateContext("foo", false) as := upCtx.Assumptions() - c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo", "/dev/shm", "/var/lib/snapd/hostfs/tmp"}) + c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo", "/dev/shm", "/run/systemd", "/var/lib/snapd/hostfs/tmp"}) c.Check(as.ModeForPath("/stuff"), Equals, os.FileMode(0755)) c.Check(as.ModeForPath("/tmp"), Equals, os.FileMode(0755)) c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp"), Equals, os.FileMode(0755)) @@ -87,7 +87,7 @@ // Instances can, in addition, access /snap/$SNAP_INSTANCE_NAME upCtx = update.NewSystemProfileUpdateContext("foo_instance", false) as = upCtx.Assumptions() - c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo_instance", "/dev/shm", "/snap/foo", "/var/lib/snapd/hostfs/tmp"}) + c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo_instance", "/dev/shm", "/run/systemd", "/snap/foo", "/var/lib/snapd/hostfs/tmp"}) } func (s *systemSuite) TestLoadDesiredProfile(c *C) { diff -Nru snapd-2.55.5+20.04/cmd/snap-update-ns/trespassing_test.go snapd-2.57.5+20.04/cmd/snap-update-ns/trespassing_test.go --- snapd-2.55.5+20.04/cmd/snap-update-ns/trespassing_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-update-ns/trespassing_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -347,6 +347,35 @@ rs.Lift() } +func (s *trespassingSuite) TestRestrictionsForRunSystemd(c *C) { + a := &update.Assumptions{} + a.AddUnrestrictedPaths("/run/systemd") + + // There should be no restrictions under /run/systemd + rs := a.RestrictionsFor("/run/systemd/journal") + c.Assert(rs, IsNil) + rs = a.RestrictionsFor("/run/systemd/journal.namespace") + c.Assert(rs, IsNil) + + // however we should still disallow anything else under /run + rs = a.RestrictionsFor("/run/test.txt") + c.Assert(rs, NotNil) + + fd, err := s.sys.Open("/run", syscall.O_DIRECTORY, 0) + c.Assert(err, IsNil) + defer s.sys.Close(fd) + s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.Ext4Magic}) + s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{}) + + err = rs.Check(fd, "/run") + c.Assert(err, ErrorMatches, `cannot write to "/run/test.txt" because it would affect the host in "/run"`) + c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/run") + c.Assert(err.(*update.TrespassingError).DesiredPath, Equals, "/run/test.txt") + + rs.Lift() + c.Assert(rs.Check(fd, "/run"), IsNil) +} + func (s *trespassingSuite) TestRestrictionsForRootfsEntries(c *C) { a := &update.Assumptions{} diff -Nru snapd-2.55.5+20.04/cmd/snap-update-ns/utils_test.go snapd-2.57.5+20.04/cmd/snap-update-ns/utils_test.go --- snapd-2.55.5+20.04/cmd/snap-update-ns/utils_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/cmd/snap-update-ns/utils_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1047,7 +1047,7 @@ } func (s *utilsSuite) TestCleanTrailingSlash(c *C) { - // This is a sanity test for the use of filepath.Clean in secureMk{dir,file}All + // This is a validity test for the use of filepath.Clean in secureMk{dir,file}All c.Assert(filepath.Clean("/path/"), Equals, "/path") c.Assert(filepath.Clean("path/"), Equals, "path") c.Assert(filepath.Clean("path/."), Equals, "path") diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/config snapd-2.57.5+20.04/c-vendor/squashfuse/.git/config --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/config 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/config 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,11 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true +[remote "origin"] + url = https://github.com/vasi/squashfuse + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "master"] + remote = origin + merge = refs/heads/master diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/description snapd-2.57.5+20.04/c-vendor/squashfuse/.git/description --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/description 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/description 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/HEAD snapd-2.57.5+20.04/c-vendor/squashfuse/.git/HEAD --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/HEAD 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/HEAD 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1 @@ +74f4fe86ebd47a2fb7df5cb60d452354f977c72e diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/applypatch-msg.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/applypatch-msg.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/applypatch-msg.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/applypatch-msg.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/commit-msg.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/commit-msg.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/commit-msg.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/commit-msg.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/fsmonitor-watchman.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/fsmonitor-watchman.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/fsmonitor-watchman.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/fsmonitor-watchman.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,173 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + } + my $query = <<" END"; + ["query", "$git_work_tree", { + "since": $last_update_token, + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/post-update.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/post-update.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/post-update.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/post-update.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-applypatch.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-applypatch.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-applypatch.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-applypatch.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-commit.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-commit.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-commit.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-commit.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-merge-commit.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-merge-commit.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-merge-commit.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-merge-commit.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/prepare-commit-msg.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/prepare-commit-msg.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/prepare-commit-msg.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/prepare-commit-msg.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-push.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-push.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-push.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-push.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-rebase.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-rebase.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-rebase.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-rebase.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-receive.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-receive.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/pre-receive.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/pre-receive.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/push-to-checkout.sample snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/push-to-checkout.sample --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/hooks/push-to-checkout.sample 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/hooks/push-to-checkout.sample 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 Binary files /tmp/tmp5sb_mlum/Qh2o7mzbPh/snapd-2.55.5+20.04/c-vendor/squashfuse/.git/index and /tmp/tmp5sb_mlum/bBDMBpSel5/snapd-2.57.5+20.04/c-vendor/squashfuse/.git/index differ diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/info/exclude snapd-2.57.5+20.04/c-vendor/squashfuse/.git/info/exclude --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/info/exclude 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/info/exclude 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/logs/HEAD snapd-2.57.5+20.04/c-vendor/squashfuse/.git/logs/HEAD --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/logs/HEAD 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/logs/HEAD 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 d1d7ddafb765098b34239eacaf2f9abee1fbc27c Michael Vogt 1666033541 +0200 clone: from https://github.com/vasi/squashfuse +d1d7ddafb765098b34239eacaf2f9abee1fbc27c 74f4fe86ebd47a2fb7df5cb60d452354f977c72e Michael Vogt 1666033541 +0200 checkout: moving from master to 74f4fe86ebd47a2fb7df5cb60d452354f977c72e diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/logs/refs/heads/master snapd-2.57.5+20.04/c-vendor/squashfuse/.git/logs/refs/heads/master --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/logs/refs/heads/master 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/logs/refs/heads/master 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 d1d7ddafb765098b34239eacaf2f9abee1fbc27c Michael Vogt 1666033541 +0200 clone: from https://github.com/vasi/squashfuse diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/logs/refs/remotes/origin/HEAD snapd-2.57.5+20.04/c-vendor/squashfuse/.git/logs/refs/remotes/origin/HEAD --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/logs/refs/remotes/origin/HEAD 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/logs/refs/remotes/origin/HEAD 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1 @@ +0000000000000000000000000000000000000000 d1d7ddafb765098b34239eacaf2f9abee1fbc27c Michael Vogt 1666033541 +0200 clone: from https://github.com/vasi/squashfuse Binary files /tmp/tmp5sb_mlum/Qh2o7mzbPh/snapd-2.55.5+20.04/c-vendor/squashfuse/.git/objects/pack/pack-ced46e759c2861122b13827c836a0ca18eec38ac.idx and /tmp/tmp5sb_mlum/bBDMBpSel5/snapd-2.57.5+20.04/c-vendor/squashfuse/.git/objects/pack/pack-ced46e759c2861122b13827c836a0ca18eec38ac.idx differ Binary files /tmp/tmp5sb_mlum/Qh2o7mzbPh/snapd-2.55.5+20.04/c-vendor/squashfuse/.git/objects/pack/pack-ced46e759c2861122b13827c836a0ca18eec38ac.pack and /tmp/tmp5sb_mlum/bBDMBpSel5/snapd-2.57.5+20.04/c-vendor/squashfuse/.git/objects/pack/pack-ced46e759c2861122b13827c836a0ca18eec38ac.pack differ diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/packed-refs snapd-2.57.5+20.04/c-vendor/squashfuse/.git/packed-refs --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/packed-refs 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/packed-refs 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,25 @@ +# pack-refs with: peeled fully-peeled sorted +d834e4e09f81a7de3da2a88c5bb90081efe720fd refs/remotes/origin/api-redesign-old +3857c25a595aeec13ce6dc929f4dbc9a1614d1c1 refs/remotes/origin/branch_0.2 +8dd9125e7ef883f737a3e88aa339a258c424d034 refs/remotes/origin/cygwin-dokanx +c33c1122b3c96466fd0f278b2c609900e18a6271 refs/remotes/origin/dokan +8e67b09ff7fd3d5f93a0fef720c790960bf55ab2 refs/remotes/origin/fb2 +e5dddbfc6e402c82f5fbba115b0eb3476684f50d refs/remotes/origin/macos-cirrus +05d5014c44b7381d8c82811811cd8777e9c6f894 refs/remotes/origin/macos-fix +d1d7ddafb765098b34239eacaf2f9abee1fbc27c refs/remotes/origin/master +e9d66bba78bec764ff8a245ad74ee7d46fe21209 refs/remotes/origin/netbsd +5eb4055b302243b67839f65a5362cc26d9ba04aa refs/remotes/origin/vasi-ci-nbsd +ae6c13ebc2976c79d582106bdc5371ae7f162e27 refs/remotes/origin/vasi-fbsd +856452fe1c7d731bc56ae2865c03d76332eece09 refs/remotes/origin/vasi-fbsd-ci +740968d64fd94805d6b594a4124593cdb9192e3e refs/remotes/origin/vasi-osx-ci +2081febf8699a480851b53293ae2da04a2c8da4c refs/remotes/origin/vasi-osx-ci2 +fd3d41859233e30f35194fba64b515735dc05016 refs/remotes/origin/vasi-s390x +fa4aa65b02f9839d8482c2f05f44885c9f2db74f refs/remotes/origin/vasi-win-ci +11b9e78226ef93131fa6a08e94bb6497b70936df refs/remotes/origin/werror +78360b7af4911d46593ce50874ed2fc5f2a6848b refs/tags/0.1.100 +59706e53eedd1745d46025fe5ddaadcadb49c0e9 refs/tags/0.1.101 +3998713a3fc397d8d39dfcbda305d11d870935af refs/tags/0.1.102 +540204955134eee44201d50132a5f66a246bcfaf refs/tags/0.1.103 +67169fe009d596881d82859ae8ae0d0e425e6cc7 refs/tags/0.1.104 +d1d7ddafb765098b34239eacaf2f9abee1fbc27c refs/tags/0.1.105 +f8c4726e8a3bdb4b34bce1f94ec897cf71001229 refs/tags/v0.1 diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/refs/heads/master snapd-2.57.5+20.04/c-vendor/squashfuse/.git/refs/heads/master --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/refs/heads/master 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/refs/heads/master 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1 @@ +d1d7ddafb765098b34239eacaf2f9abee1fbc27c diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.git/refs/remotes/origin/HEAD snapd-2.57.5+20.04/c-vendor/squashfuse/.git/refs/remotes/origin/HEAD --- snapd-2.55.5+20.04/c-vendor/squashfuse/.git/refs/remotes/origin/HEAD 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.git/refs/remotes/origin/HEAD 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1 @@ +ref: refs/remotes/origin/master diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.gitattributes snapd-2.57.5+20.04/c-vendor/squashfuse/.gitattributes --- snapd-2.55.5+20.04/c-vendor/squashfuse/.gitattributes 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.gitattributes 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/.gitignore snapd-2.57.5+20.04/c-vendor/squashfuse/.gitignore --- snapd-2.55.5+20.04/c-vendor/squashfuse/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/.gitignore 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,31 @@ +/*.inc +squashfuse +squashfuse_ll +squashfuse_ls +squashfuse_extract +*.dSYM +*.o +*.lo +*.la + +*.tar.gz + +.libs +.deps +Makefile +Makefile.in +aclocal.m4 +autom4te.cache +build-aux +/config.* +configure +libtool +stamp-h1 +*.pc + +win/Debug +win/Release +*.sdf +*.opensdf +*.suo +*.vcxproj.user diff -Nru snapd-2.55.5+20.04/c-vendor/squashfuse/m4/.gitignore snapd-2.57.5+20.04/c-vendor/squashfuse/m4/.gitignore --- snapd-2.55.5+20.04/c-vendor/squashfuse/m4/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/c-vendor/squashfuse/m4/.gitignore 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,4 @@ +# Should these be included? +lt*.m4 +libtool.m4 +pkg.m4 diff -Nru snapd-2.55.5+20.04/daemon/api_aliases.go snapd-2.57.5+20.04/daemon/api_aliases.go --- snapd-2.55.5+20.04/daemon/api_aliases.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_aliases.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,6 +21,7 @@ import ( "encoding/json" + "errors" "fmt" "net/http" @@ -80,10 +81,10 @@ // or just an alias var snapst snapstate.SnapState err := snapstate.Get(st, a.Snap, &snapst) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return InternalError("%v", err) } - if err == state.ErrNoState { // not a snap + if errors.Is(err, state.ErrNoState) { // not a snap a.Snap = "" } } diff -Nru snapd-2.55.5+20.04/daemon/api_apps.go snapd-2.57.5+20.04/daemon/api_apps.go --- snapd-2.55.5+20.04/daemon/api_apps.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_apps.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,7 +34,6 @@ "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/strutil" - "github.com/snapcore/snapd/systemd" ) var ( @@ -209,13 +208,7 @@ return AppNotFound("no matching services") } - serviceNames := make([]string, len(appInfos)) - for i, appInfo := range appInfos { - serviceNames[i] = appInfo.ServiceName() - } - - sysd := systemd.New(systemd.SystemMode, progress.Null) - reader, err := sysd.LogReader(serviceNames, n, follow) + reader, err := servicestate.LogReader(appInfos, n, follow) if err != nil { return InternalError("cannot get logs: %v", err) } diff -Nru snapd-2.55.5+20.04/daemon/api_apps_test.go snapd-2.57.5+20.04/daemon/api_apps_test.go --- snapd-2.55.5+20.04/daemon/api_apps_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_apps_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -55,6 +55,7 @@ jctlSvcses [][]string jctlNs []int jctlFollows []bool + jctlNamespaces []bool jctlRCs []io.ReadCloser jctlErrs []error @@ -64,10 +65,11 @@ infoA, infoB, infoC, infoD, infoE *snap.Info } -func (s *appsSuite) journalctl(svcs []string, n int, follow bool) (rc io.ReadCloser, err error) { +func (s *appsSuite) journalctl(svcs []string, n int, follow, namespaces bool) (rc io.ReadCloser, err error) { s.jctlSvcses = append(s.jctlSvcses, svcs) s.jctlNs = append(s.jctlNs, n) s.jctlFollows = append(s.jctlFollows, follow) + s.jctlNamespaces = append(s.jctlNamespaces, namespaces) if len(s.jctlErrs) > 0 { err, s.jctlErrs = s.jctlErrs[0], s.jctlErrs[1:] @@ -99,7 +101,7 @@ serviceCommand.options = "reload" } // only one flag should ever be set (depending on Action), but appending - // them below acts as an extra sanity check. + // them below acts as an extra validity check. if inst.StartOptions.Enable { serviceCommand.options += "enable" } @@ -111,7 +113,7 @@ } s.serviceControlCalls = append(s.serviceControlCalls, serviceCommand) - t := st.NewTask("dummy", "") + t := st.NewTask("sample", "") ts := state.NewTaskSet(t) return []*state.TaskSet{ts}, nil } @@ -132,6 +134,7 @@ s.jctlSvcses = nil s.jctlNs = nil s.jctlFollows = nil + s.jctlNamespaces = nil s.jctlRCs = nil s.jctlErrs = nil @@ -154,6 +157,7 @@ d.Overlord().Loop() s.AddCleanup(func() { d.Overlord().Stop() }) + s.AddCleanup(systemd.MockSystemdVersion(237, nil)) } func (s *appsSuite) TestSplitAppName(c *check.C) { @@ -650,6 +654,7 @@ c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}}) c.Check(s.jctlNs, check.DeepEquals, []int{42}) c.Check(s.jctlFollows, check.DeepEquals, []bool{false}) + c.Check(s.jctlNamespaces, check.DeepEquals, []bool{false}) c.Check(rec.Code, check.Equals, 200) c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json-seq") @@ -662,6 +667,54 @@ `[1:]) } +func (s *appsSuite) TestLogsNoNamespaceOption(c *check.C) { + restore := systemd.MockSystemdVersion(237, nil) + defer restore() + + s.expectLogsAccess() + + s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(""))} + + req, err := http.NewRequest("GET", "/v2/logs?names=snap-a.svc2&n=42&follow=false", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + s.req(c, req, nil).ServeHTTP(rec, req) + + c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}}) + c.Check(s.jctlNs, check.DeepEquals, []int{42}) + c.Check(s.jctlFollows, check.DeepEquals, []bool{false}) + c.Check(s.jctlNamespaces, check.DeepEquals, []bool{false}) + + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.Header().Get("Content-Type"), check.Equals, "application/json-seq") + c.Check(rec.Body.String(), check.Equals, "") +} + +func (s *appsSuite) TestLogsWithNamespaceOption(c *check.C) { + restore := systemd.MockSystemdVersion(245, nil) + defer restore() + + s.expectLogsAccess() + + s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(""))} + + req, err := http.NewRequest("GET", "/v2/logs?names=snap-a.svc2&n=42&follow=false", nil) + c.Assert(err, check.IsNil) + + rec := httptest.NewRecorder() + s.req(c, req, nil).ServeHTTP(rec, req) + + c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}}) + c.Check(s.jctlNs, check.DeepEquals, []int{42}) + c.Check(s.jctlFollows, check.DeepEquals, []bool{false}) + c.Check(s.jctlNamespaces, check.DeepEquals, []bool{true}) + + c.Check(rec.Code, check.Equals, 200) + c.Check(rec.Header().Get("Content-Type"), check.Equals, "application/json-seq") + c.Check(rec.Body.String(), check.Equals, "") +} + func (s *appsSuite) TestLogsN(c *check.C) { s.expectLogsAccess() diff -Nru snapd-2.55.5+20.04/daemon/api_connections.go snapd-2.57.5+20.04/daemon/api_connections.go --- snapd-2.55.5+20.04/daemon/api_connections.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_connections.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package daemon import ( + "errors" "net/http" "sort" @@ -265,7 +266,7 @@ snapName = ifacestate.RemapSnapFromRequest(snapName) if snapName != "" { if err := checkSnapInstalled(c.d.overlord.State(), snapName); err != nil { - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return SnapNotFound(snapName, err) } return InternalError("cannot access snap state: %v", err) diff -Nru snapd-2.55.5+20.04/daemon/api_connections_test.go snapd-2.57.5+20.04/daemon/api_connections_test.go --- snapd-2.55.5+20.04/daemon/api_connections_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_connections_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -140,7 +140,7 @@ c.Check(err, check.IsNil) c.Check(body, check.DeepEquals, map[string]interface{}{ "result": map[string]interface{}{ - "message": "no state entry for key", + "message": `no state entry for key "snaps"`, "kind": "snap-not-found", "value": "not-found", }, diff -Nru snapd-2.55.5+20.04/daemon/api_debug.go snapd-2.57.5+20.04/daemon/api_debug.go --- snapd-2.55.5+20.04/daemon/api_debug.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_debug.go 2022-10-17 16:25:18.000000000 +0000 @@ -53,6 +53,7 @@ RecoverySystemLabel string `json:"recovery-system-label"` } `json:"params"` + Snaps []string `json:"snaps"` } type connectivityStatus struct { @@ -411,6 +412,8 @@ return getStacktraces() case "create-recovery-system": return createRecovery(st, a.Params.RecoverySystemLabel) + case "migrate-home": + return migrateHome(st, a.Snaps) default: return BadRequest("unknown debug action: %v", a.Action) } diff -Nru snapd-2.55.5+20.04/daemon/api_debug_migrate.go snapd-2.57.5+20.04/daemon/api_debug_migrate.go --- snapd-2.55.5+20.04/daemon/api_debug_migrate.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_debug_migrate.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 daemon + +import ( + "fmt" + + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" +) + +var snapstateMigrateHome = snapstate.MigrateHome + +func migrateHome(st *state.State, snaps []string) Response { + if len(snaps) == 0 { + return BadRequest("no snaps were provided") + } + + tss, err := snapstateMigrateHome(st, snaps) + if err != nil { + if terr, ok := err.(snap.NotInstalledError); ok { + return SnapNotFound(terr.Snap, err) + } + + return InternalError(err.Error()) + } + + chg := st.NewChange("migrate-home", fmt.Sprintf("Migrate snap homes to ~/Snap for snaps %s", strutil.Quoted(snaps))) + for _, ts := range tss { + chg.AddAll(ts) + } + chg.Set("api-data", map[string][]string{"snap-names": snaps}) + + ensureStateSoon(st) + return AsyncResponse(nil, chg.ID()) +} diff -Nru snapd-2.55.5+20.04/daemon/api_debug_seeding.go snapd-2.57.5+20.04/daemon/api_debug_seeding.go --- snapd-2.55.5+20.04/daemon/api_debug_seeding.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_debug_seeding.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package daemon import ( + "errors" "time" "github.com/snapcore/snapd/overlord/state" @@ -66,18 +67,18 @@ func getSeedingInfo(st *state.State) Response { var seeded, preseeded bool err := st.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return InternalError(err.Error()) } - if err = st.Get("preseeded", &preseeded); err != nil && err != state.ErrNoState { + if err = st.Get("preseeded", &preseeded); err != nil && !errors.Is(err, state.ErrNoState) { return InternalError(err.Error()) } var preseedSysKey, seedRestartSysKey interface{} - if err := st.Get("preseed-system-key", &preseedSysKey); err != nil && err != state.ErrNoState { + if err := st.Get("preseed-system-key", &preseedSysKey); err != nil && !errors.Is(err, state.ErrNoState) { return InternalError(err.Error()) } - if err := st.Get("seed-restart-system-key", &seedRestartSysKey); err != nil && err != state.ErrNoState { + if err := st.Get("seed-restart-system-key", &seedRestartSysKey); err != nil && !errors.Is(err, state.ErrNoState) { return InternalError(err.Error()) } @@ -116,7 +117,7 @@ {"seed-time", &data.SeedTime}, } { var tm time.Time - if err := st.Get(t.name, &tm); err != nil && err != state.ErrNoState { + if err := st.Get(t.name, &tm); err != nil && !errors.Is(err, state.ErrNoState) { return InternalError(err.Error()) } if !tm.IsZero() { @@ -124,7 +125,7 @@ } } - // XXX: consistency & sanity checks, e.g. if preseeded, then need to have + // XXX: consistency & validity checks, e.g. if preseeded, then need to have // preseed-start-time, preseeded-time, preseed-system-key etc? return SyncResponse(data) diff -Nru snapd-2.55.5+20.04/daemon/api_debug_test.go snapd-2.57.5+20.04/daemon/api_debug_test.go --- snapd-2.55.5+20.04/daemon/api_debug_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_debug_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,12 +22,16 @@ import ( "bytes" "encoding/json" + "errors" "net/http" + "strings" "gopkg.in/check.v1" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/daemon" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/timings" ) @@ -238,3 +242,97 @@ // validity c.Check(t.Lanes(), check.DeepEquals, []int{lane1, lane2}) } + +func (s *postDebugSuite) TestMigrateHome(c *check.C) { + d := s.daemonWithOverlordMock() + s.expectRootAccess() + + restore := daemon.MockSnapstateMigrate(func(*state.State, []string) ([]*state.TaskSet, error) { + st := state.New(nil) + st.Lock() + defer st.Unlock() + + var ts state.TaskSet + ts.AddTask(st.NewTask("bar", "")) + return []*state.TaskSet{&ts}, nil + }) + defer restore() + + body := strings.NewReader(`{"action": "migrate-home", "snaps": ["foo", "bar"]}`) + req, err := http.NewRequest("POST", "/v2/debug", body) + c.Assert(err, check.IsNil) + + rsp := s.req(c, req, nil) + c.Assert(rsp, check.FitsTypeOf, &daemon.RespJSON{}) + + rspJSON := rsp.(*daemon.RespJSON) + + st := d.Overlord().State() + st.Lock() + defer st.Unlock() + + chg := st.Change(rspJSON.Change) + var snaps map[string][]string + c.Assert(chg.Get("api-data", &snaps), check.IsNil) + c.Assert(snaps["snap-names"], check.DeepEquals, []string{"foo", "bar"}) +} + +func (s *postDebugSuite) TestMigrateHomeNoSnaps(c *check.C) { + s.daemonWithOverlordMock() + s.expectRootAccess() + + body := strings.NewReader(`{"action": "migrate-home"}`) + req, err := http.NewRequest("POST", "/v2/debug", body) + c.Assert(err, check.IsNil) + + rsp := s.req(c, req, nil) + c.Assert(rsp, check.FitsTypeOf, &daemon.APIError{}) + apiErr := rsp.(*daemon.APIError) + + c.Check(apiErr.Status, check.Equals, 400) + c.Check(apiErr.Message, check.Equals, "no snaps were provided") +} + +func (s *postDebugSuite) TestMigrateHomeNotInstalled(c *check.C) { + s.daemonWithOverlordMock() + s.expectRootAccess() + + restore := daemon.MockSnapstateMigrate(func(*state.State, []string) ([]*state.TaskSet, error) { + return nil, snap.NotInstalledError{Snap: "some-snap"} + }) + defer restore() + + body := strings.NewReader(`{"action": "migrate-home", "snaps": ["some-snap"]}`) + req, err := http.NewRequest("POST", "/v2/debug", body) + c.Assert(err, check.IsNil) + + rsp := s.req(c, req, nil) + c.Assert(rsp, check.FitsTypeOf, &daemon.APIError{}) + apiErr := rsp.(*daemon.APIError) + + c.Check(apiErr.Status, check.Equals, 404) + c.Check(apiErr.Message, check.Equals, `snap "some-snap" is not installed`) + c.Check(apiErr.Kind, check.Equals, client.ErrorKindSnapNotFound) + c.Check(apiErr.Value, check.Equals, "some-snap") +} + +func (s *postDebugSuite) TestMigrateHomeInternalError(c *check.C) { + s.daemonWithOverlordMock() + s.expectRootAccess() + + restore := daemon.MockSnapstateMigrate(func(*state.State, []string) ([]*state.TaskSet, error) { + return nil, errors.New("boom") + }) + defer restore() + + body := strings.NewReader(`{"action": "migrate-home", "snaps": ["some-snap"]}`) + req, err := http.NewRequest("POST", "/v2/debug", body) + c.Assert(err, check.IsNil) + + rsp := s.req(c, req, nil) + c.Assert(rsp, check.FitsTypeOf, &daemon.APIError{}) + apiErr := rsp.(*daemon.APIError) + + c.Check(apiErr.Status, check.Equals, 500) + c.Check(apiErr.Message, check.Equals, `boom`) +} diff -Nru snapd-2.55.5+20.04/daemon/api_download.go snapd-2.57.5+20.04/daemon/api_download.go --- snapd-2.55.5+20.04/daemon/api_download.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_download.go 2022-10-17 16:25:18.000000000 +0000 @@ -252,7 +252,7 @@ if err == nil { return secret, nil } - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } secret, err = randutil.CryptoTokenBytes(32) diff -Nru snapd-2.55.5+20.04/daemon/api_download_test.go snapd-2.57.5+20.04/daemon/api_download_test.go --- snapd-2.55.5+20.04/daemon/api_download_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_download_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019 Canonical Ltd + * Copyright (C) 2019-2022 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 @@ -65,9 +65,9 @@ Revision: snap.R(1), }, DownloadInfo: snap.DownloadInfo{ - Size: int64(len(snapContent)), - AnonDownloadURL: "http://localhost/bar", - Sha3_384: "sha3sha3sha3", + Size: int64(len(snapContent)), + DownloadURL: "http://localhost/bar", + Sha3_384: "sha3sha3sha3", }, }, "edge-bar": { @@ -78,9 +78,9 @@ Channel: "edge", }, DownloadInfo: snap.DownloadInfo{ - Size: int64(len(snapContent)), - AnonDownloadURL: "http://localhost/edge-bar", - Sha3_384: "sha3sha3sha3", + Size: int64(len(snapContent)), + DownloadURL: "http://localhost/edge-bar", + Sha3_384: "sha3sha3sha3", }, }, "rev7-bar": { @@ -90,16 +90,16 @@ Revision: snap.R(7), }, DownloadInfo: snap.DownloadInfo{ - Size: int64(len(snapContent)), - AnonDownloadURL: "http://localhost/rev7-bar", - Sha3_384: "sha3sha3sha3", + Size: int64(len(snapContent)), + DownloadURL: "http://localhost/rev7-bar", + Sha3_384: "sha3sha3sha3", }, }, "download-error-trigger-snap": { DownloadInfo: snap.DownloadInfo{ - Size: 100, - AnonDownloadURL: "http://localhost/foo", - Sha3_384: "sha3sha3sha3", + Size: 100, + DownloadURL: "http://localhost/foo", + Sha3_384: "sha3sha3sha3", }, }, "foo-resume-3": { @@ -108,9 +108,9 @@ Revision: snap.R(1), }, DownloadInfo: snap.DownloadInfo{ - Size: int64(len(snapContent)), - AnonDownloadURL: "http://localhost/foo-resume-3", - Sha3_384: "sha3sha3sha3", + Size: int64(len(snapContent)), + DownloadURL: "http://localhost/foo-resume-3", + Sha3_384: "sha3sha3sha3", }, }, } diff -Nru snapd-2.55.5+20.04/daemon/api_general.go snapd-2.57.5+20.04/daemon/api_general.go --- snapd-2.55.5+20.04/daemon/api_general.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_general.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,6 +22,7 @@ import ( "bytes" "encoding/json" + "errors" "net/http" "os/exec" "sort" @@ -113,7 +114,7 @@ return InternalError("cannot get refresh schedule: %s", err) } users, err := auth.Users(st) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return InternalError("cannot get user auth data: %s", err) } diff -Nru snapd-2.55.5+20.04/daemon/api.go snapd-2.57.5+20.04/daemon/api.go --- snapd-2.55.5+20.04/daemon/api.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api.go 2022-10-17 16:25:18.000000000 +0000 @@ -140,6 +140,7 @@ snapstateUpdateMany = snapstate.UpdateMany snapstateInstallMany = snapstate.InstallMany snapstateRemoveMany = snapstate.RemoveMany + snapstateEnforceSnaps = snapstate.EnforceSnaps snapstateRevert = snapstate.Revert snapstateRevertToRevision = snapstate.RevertToRevision snapstateSwitch = snapstate.Switch diff -Nru snapd-2.55.5+20.04/daemon/api_icons.go snapd-2.57.5+20.04/daemon/api_icons.go --- snapd-2.55.5+20.04/daemon/api_icons.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_icons.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package daemon import ( + "errors" "net/http" "github.com/snapcore/snapd/overlord/auth" @@ -50,7 +51,7 @@ var snapst snapstate.SnapState err := snapstate.Get(st, name, &snapst) if err != nil { - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return SnapNotFound(name, err) } return InternalError("cannot consult state: %v", err) diff -Nru snapd-2.55.5+20.04/daemon/api_interfaces.go snapd-2.57.5+20.04/daemon/api_interfaces.go --- snapd-2.55.5+20.04/daemon/api_interfaces.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_interfaces.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,6 +21,7 @@ import ( "encoding/json" + "errors" "fmt" "net/http" "sort" @@ -158,7 +159,7 @@ } var snapst snapstate.SnapState err := snapstate.Get(st, snapName, &snapst) - if (err == nil && !snapst.IsInstalled()) || err == state.ErrNoState { + if (err == nil && !snapst.IsInstalled()) || errors.Is(err, state.ErrNoState) { return fmt.Errorf("snap %q is not installed", snapName) } if err == nil { diff -Nru snapd-2.55.5+20.04/daemon/api_model.go snapd-2.57.5+20.04/daemon/api_model.go --- snapd-2.55.5+20.04/daemon/api_model.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_model.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,10 +21,12 @@ import ( "encoding/json" + "errors" "net/http" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/client/clientutil" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/state" @@ -53,11 +55,6 @@ NewModel string `json:"new-model"` } -type modelAssertJSON struct { - Headers map[string]interface{} `json:"headers,omitempty"` - Body string `json:"body,omitempty"` -} - func postModel(c *Command, r *http.Request, _ *auth.UserState) Response { defer r.Body.Close() var data postModelData @@ -102,7 +99,7 @@ devmgr := c.d.overlord.DeviceManager() model, err := devmgr.Model() - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return &apiError{ Status: 404, Message: "no model assertion yet", @@ -115,7 +112,7 @@ } if opts.jsonResult { - modelJSON := modelAssertJSON{} + modelJSON := clientutil.ModelAssertJSON{} modelJSON.Headers = model.Headers() if !opts.headersOnly { @@ -142,7 +139,7 @@ devmgr := c.d.overlord.DeviceManager() serial, err := devmgr.Serial() - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return &apiError{ Status: 404, Message: "no serial assertion yet", @@ -155,7 +152,7 @@ } if opts.jsonResult { - serialJSON := modelAssertJSON{} + serialJSON := clientutil.ModelAssertJSON{} serialJSON.Headers = serial.Headers() if !opts.headersOnly { diff -Nru snapd-2.55.5+20.04/daemon/api_model_test.go snapd-2.57.5+20.04/daemon/api_model_test.go --- snapd-2.55.5+20.04/daemon/api_model_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_model_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -33,6 +33,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/client/clientutil" "github.com/snapcore/snapd/daemon" "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" "github.com/snapcore/snapd/overlord/auth" @@ -217,9 +218,9 @@ c.Assert(err, check.IsNil) rsp := s.syncReq(c, req, nil) // get the body and try to unmarshal into modelAssertJSON - c.Assert(rsp.Result, check.FitsTypeOf, daemon.ModelAssertJSON{}) + c.Assert(rsp.Result, check.FitsTypeOf, clientutil.ModelAssertJSON{}) - jsonResponse := rsp.Result.(daemon.ModelAssertJSON) + jsonResponse := rsp.Result.(clientutil.ModelAssertJSON) // get the architecture key from the headers arch, ok := jsonResponse.Headers["architecture"] @@ -364,9 +365,9 @@ c.Assert(err, check.IsNil) rsp := s.syncReq(c, req, nil) // get the body and try to unmarshal into modelAssertJSON - c.Assert(rsp.Result, check.FitsTypeOf, daemon.ModelAssertJSON{}) + c.Assert(rsp.Result, check.FitsTypeOf, clientutil.ModelAssertJSON{}) - jsonResponse := rsp.Result.(daemon.ModelAssertJSON) + jsonResponse := rsp.Result.(clientutil.ModelAssertJSON) // get the architecture key from the headers devKey, ok := jsonResponse.Headers["device-key"] diff -Nru snapd-2.55.5+20.04/daemon/api_quotas.go snapd-2.57.5+20.04/daemon/api_quotas.go --- snapd-2.55.5+20.04/daemon/api_quotas.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_quotas.go 2022-10-17 16:25:18.000000000 +0000 @@ -95,7 +95,18 @@ Percentage: grp.CPULimit.Percentage, } constraints.CPUSet = &client.QuotaCPUSetValues{ - CPUs: grp.CPULimit.AllowedCPUs, + CPUs: grp.CPULimit.CPUSet, + } + } + if grp.JournalLimit != nil { + constraints.Journal = &client.QuotaJournalValues{ + Size: grp.JournalLimit.Size, + } + if grp.JournalLimit.RateEnabled { + constraints.Journal.QuotaJournalRate = &client.QuotaJournalRate{ + RateCount: grp.JournalLimit.RateCount, + RatePeriod: grp.JournalLimit.RatePeriod, + } } } return &constraints @@ -191,11 +202,20 @@ } } if values.CPUSet != nil && len(values.CPUSet.CPUs) != 0 { - resourcesBuilder.WithAllowedCPUs(values.CPUSet.CPUs) + resourcesBuilder.WithCPUSet(values.CPUSet.CPUs) } if values.Threads != 0 { resourcesBuilder.WithThreadLimit(values.Threads) } + if values.Journal != nil { + resourcesBuilder.WithJournalNamespace() + if values.Journal.Size != 0 { + resourcesBuilder.WithJournalSize(values.Journal.Size) + } + if values.Journal.QuotaJournalRate != nil { + resourcesBuilder.WithJournalRate(values.Journal.RateCount, values.Journal.RatePeriod) + } + } return resourcesBuilder.Build() } diff -Nru snapd-2.55.5+20.04/daemon/api_quotas_test.go snapd-2.57.5+20.04/daemon/api_quotas_test.go --- snapd-2.55.5+20.04/daemon/api_quotas_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_quotas_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,6 +25,7 @@ "fmt" "net/http" "net/http/httptest" + "time" "gopkg.in/check.v1" @@ -81,6 +82,48 @@ c.Assert(err, check.IsNil) } +func (s *apiQuotaSuite) TestCreateQuotaValues(c *check.C) { + st := s.d.Overlord().State() + st.Lock() + err := servicestatetest.MockQuotaInState(st, "ginger-ale", "", nil, + quota.NewResourcesBuilder(). + WithMemoryLimit(quantity.SizeMiB). + WithCPUCount(1). + WithCPUPercentage(100). + WithThreadLimit(256). + WithCPUSet([]int{0, 1}). + WithJournalRate(150, time.Second). + WithJournalSize(quantity.SizeMiB). + Build()) + allGroups, err2 := servicestate.AllQuotas(st) + st.Unlock() + c.Assert(err, check.IsNil) + c.Assert(err2, check.IsNil) + + c.Check(allGroups, check.HasLen, 1) + + grp := allGroups["ginger-ale"] + c.Check(grp, check.NotNil) + + quotaValues := daemon.CreateQuotaValues(grp) + c.Check(quotaValues.Memory, check.DeepEquals, quantity.SizeMiB) + c.Check(quotaValues.Threads, check.DeepEquals, 256) + c.Check(quotaValues.CPU, check.DeepEquals, &client.QuotaCPUValues{ + Count: 1, + Percentage: 100, + }) + c.Check(quotaValues.CPUSet, check.DeepEquals, &client.QuotaCPUSetValues{ + CPUs: []int{0, 1}, + }) + c.Check(quotaValues.Journal, check.DeepEquals, &client.QuotaJournalValues{ + Size: quantity.SizeMiB, + QuotaJournalRate: &client.QuotaJournalRate{ + RateCount: 150, + RatePeriod: time.Second, + }, + }) +} + func (s *apiQuotaSuite) TestPostQuotaUnknownAction(c *check.C) { data, err := json.Marshal(daemon.PostQuotaGroupData{Action: "foo", GroupName: "bar"}) c.Assert(err, check.IsNil) @@ -218,6 +261,43 @@ c.Assert(createCalled, check.Equals, 2) } +func (s *apiQuotaSuite) TestPostEnsureQuotaCreateJournalRateZeroHappy(c *check.C) { + var createCalled int + r := daemon.MockServicestateCreateQuota(func(st *state.State, name string, parentName string, snaps []string, resourceLimits quota.Resources) (*state.TaskSet, error) { + createCalled++ + c.Check(name, check.Equals, "booze") + c.Check(parentName, check.Equals, "foo") + c.Check(snaps, check.DeepEquals, []string{"some-snap"}) + c.Check(resourceLimits, check.DeepEquals, quota.NewResourcesBuilder().WithJournalRate(0, 0).Build()) + ts := state.NewTaskSet(st.NewTask("foo-quota", "...")) + return ts, nil + }) + defer r() + + data, err := json.Marshal(daemon.PostQuotaGroupData{ + Action: "ensure", + GroupName: "booze", + Parent: "foo", + Snaps: []string{"some-snap"}, + Constraints: client.QuotaValues{ + Journal: &client.QuotaJournalValues{ + QuotaJournalRate: &client.QuotaJournalRate{ + RateCount: 0, + RatePeriod: 0, + }, + }, + }, + }) + c.Assert(err, check.IsNil) + + req, err := http.NewRequest("POST", "/v2/quotas", bytes.NewBuffer(data)) + c.Assert(err, check.IsNil) + rsp := s.asyncReq(c, req, nil) + c.Assert(rsp.Status, check.Equals, 202) + c.Assert(createCalled, check.Equals, 1) + c.Assert(s.ensureSoonCalled, check.Equals, 1) +} + func (s *apiQuotaSuite) TestPostEnsureQuotaUpdateCpuHappy(c *check.C) { st := s.d.Overlord().State() st.Lock() @@ -297,7 +377,7 @@ c.Assert(name, check.Equals, "ginger-ale") c.Assert(opts, check.DeepEquals, servicestate.QuotaGroupUpdate{ AddSnaps: []string{"some-snap"}, - NewResourceLimits: quota.NewResourcesBuilder().WithCPUCount(1).WithCPUPercentage(100).WithAllowedCPUs([]int{0, 1}).Build(), + NewResourceLimits: quota.NewResourcesBuilder().WithCPUCount(1).WithCPUPercentage(100).WithCPUSet([]int{0, 1}).Build(), }) ts := state.NewTaskSet(st.NewTask("foo-quota", "...")) return ts, nil @@ -634,6 +714,68 @@ }, }) c.Check(s.ensureSoonCalled, check.Equals, 0) +} + +func (s *apiQuotaSuite) TestListJournalQuotas(c *check.C) { + st := s.d.Overlord().State() + st.Lock() + err := servicestatetest.MockQuotaInState(st, "foo", "", nil, quota.NewResourcesBuilder().WithJournalSize(64*quantity.SizeMiB).Build()) + c.Assert(err, check.IsNil) + err = servicestatetest.MockQuotaInState(st, "bar", "foo", nil, quota.NewResourcesBuilder().WithJournalRate(100, time.Hour).Build()) + c.Assert(err, check.IsNil) + err = servicestatetest.MockQuotaInState(st, "baz", "foo", nil, quota.NewResourcesBuilder().WithJournalRate(0, 0).Build()) + c.Assert(err, check.IsNil) + st.Unlock() + + calls := 0 + r := daemon.MockGetQuotaUsage(func(grp *quota.Group) (*client.QuotaValues, error) { + calls++ + return &client.QuotaValues{}, nil + }) + defer r() + defer func() { + c.Assert(calls, check.Equals, 3) + }() + + req, err := http.NewRequest("GET", "/v2/quotas", nil) + c.Assert(err, check.IsNil) + rsp := s.syncReq(c, req, nil) + c.Assert(rsp.Status, check.Equals, 200) + c.Assert(rsp.Result, check.FitsTypeOf, []client.QuotaGroupResult{}) + res := rsp.Result.([]client.QuotaGroupResult) + c.Check(res, check.DeepEquals, []client.QuotaGroupResult{ + { + GroupName: "bar", + Parent: "foo", + Constraints: &client.QuotaValues{Journal: &client.QuotaJournalValues{ + QuotaJournalRate: &client.QuotaJournalRate{ + RateCount: 100, + RatePeriod: time.Hour, + }, + }}, + Current: &client.QuotaValues{}, + }, + { + GroupName: "baz", + Parent: "foo", + Constraints: &client.QuotaValues{Journal: &client.QuotaJournalValues{ + QuotaJournalRate: &client.QuotaJournalRate{ + RateCount: 0, + RatePeriod: 0, + }, + }}, + Current: &client.QuotaValues{}, + }, + { + GroupName: "foo", + Subgroups: []string{"bar", "baz"}, + Constraints: &client.QuotaValues{Journal: &client.QuotaJournalValues{ + Size: 64 * quantity.SizeMiB, + }}, + Current: &client.QuotaValues{}, + }, + }) + c.Check(s.ensureSoonCalled, check.Equals, 0) } func (s *apiQuotaSuite) TestGetQuota(c *check.C) { diff -Nru snapd-2.55.5+20.04/daemon/api_sideload_n_try.go snapd-2.57.5+20.04/daemon/api_sideload_n_try.go --- snapd-2.55.5+20.04/daemon/api_sideload_n_try.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_sideload_n_try.go 2022-10-17 16:25:18.000000000 +0000 @@ -208,13 +208,18 @@ } func sideloadManySnaps(st *state.State, snapFiles []*uploadedSnap, flags sideloadFlags, user *auth.UserState) (*state.Change, *apiError) { + deviceCtx, err := snapstate.DevicePastSeeding(st, nil) + if err != nil { + return nil, InternalError(err.Error()) + } + sideInfos := make([]*snap.SideInfo, len(snapFiles)) names := make([]string, len(snapFiles)) tempPaths := make([]string, len(snapFiles)) origPaths := make([]string, len(snapFiles)) for i, snapFile := range snapFiles { - si, apiError := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags) + si, apiError := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags, deviceCtx.Model()) if apiError != nil { return nil, apiError } @@ -252,7 +257,12 @@ } } - sideInfo, apiErr := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags) + deviceCtx, err := snapstate.DevicePastSeeding(st, nil) + if err != nil { + return nil, InternalError(err.Error()) + } + + sideInfo, apiErr := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags, deviceCtx.Model()) if apiErr != nil { return nil, apiErr } @@ -278,11 +288,11 @@ return chg, nil } -func readSideInfo(st *state.State, tempPath string, origPath string, flags sideloadFlags) (*snap.SideInfo, *apiError) { +func readSideInfo(st *state.State, tempPath string, origPath string, flags sideloadFlags, model *asserts.Model) (*snap.SideInfo, *apiError) { var sideInfo *snap.SideInfo if !flags.dangerousOK { - si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st)) + si, err := snapasserts.DeriveSideInfo(tempPath, model, assertstate.DB(st)) switch { case err == nil: sideInfo = si diff -Nru snapd-2.55.5+20.04/daemon/api_sideload_n_try_test.go snapd-2.57.5+20.04/daemon/api_sideload_n_try_test.go --- snapd-2.55.5+20.04/daemon/api_sideload_n_try_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_sideload_n_try_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2020 Canonical Ltd + * Copyright (C) 2014-2022 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,7 +22,6 @@ import ( "bytes" "context" - "crypto" "crypto/rand" "errors" "fmt" @@ -31,7 +30,6 @@ "os" "path/filepath" "regexp" - "strconv" "time" "gopkg.in/check.v1" @@ -43,9 +41,11 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/sandbox" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/testutil" ) @@ -65,6 +65,19 @@ s.expectWriteAccess(daemon.AuthenticatedAccess{Polkit: "io.snapcraft.snapd.manage"}) } +func (s *sideloadSuite) markSeeded(d *daemon.Daemon) { + st := d.Overlord().State() + st.Lock() + defer st.Unlock() + st.Set("seeded", true) + model := s.Brands.Model("can0nical", "pc", map[string]interface{}{ + "architecture": "amd64", + "gadget": "gadget", + "kernel": "kernel", + }) + snapstatetest.MockDeviceModel(model) +} + var sideLoadBodyWithoutDevMode = "" + "----hello--\r\n" + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + @@ -143,6 +156,7 @@ func (s *sideloadSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedInstanceName string, expectedFlags snapstate.Flags) (summary string, systemRestartImmediate bool) { d := s.daemonWithFakeSnapManager(c) + s.markSeeded(d) soon := 0 var origEnsureStateSoon func(*state.State) @@ -222,7 +236,7 @@ summary = chg.Summary() err = chg.Get("system-restart-immediate", &systemRestartImmediate) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { c.Error(err) } return summary, systemRestartImmediate @@ -279,24 +293,32 @@ func (s *sideloadSuite) TestLocalInstallSnapDeriveSideInfo(c *check.C) { d := s.daemonWithOverlordMockAndStore() + s.markSeeded(d) // add the assertions first st := d.Overlord().State() + fooSnap := snaptest.MakeTestSnapWithFiles(c, `name: foo +version: 1`, nil) + digest, size, err := asserts.SnapFileSHA3_384(fooSnap) + c.Assert(err, check.IsNil) + fooSnapBytes, err := ioutil.ReadFile(fooSnap) + c.Assert(err, check.IsNil) + dev1Acct := assertstest.NewAccount(s.StoreSigning, "devel1", nil, "") snapDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ "series": "16", - "snap-id": "x-id", - "snap-name": "x", + "snap-id": "foo-id", + "snap-name": "foo", "publisher-id": dev1Acct.AccountID(), "timestamp": time.Now().Format(time.RFC3339), }, nil, "") c.Assert(err, check.IsNil) snapRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ - "snap-sha3-384": "YK0GWATaZf09g_fvspYPqm_qtaiqf-KjaNj5uMEQCjQpuXWPjqQbeBINL5H_A0Lo", - "snap-size": "5", - "snap-id": "x-id", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), + "snap-id": "foo-id", "snap-revision": "41", "developer-id": dev1Acct.AccountID(), "timestamp": time.Now().Format(time.RFC3339), @@ -309,25 +331,24 @@ assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""), dev1Acct, snapDecl, snapRev) }() - body := "" + - "----hello--\r\n" + - "Content-Disposition: form-data; name=\"snap\"; filename=\"x.snap\"\r\n" + - "\r\n" + - "xyzzy\r\n" + - "----hello--\r\n" - req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + bodyBuf := new(bytes.Buffer) + bodyBuf.WriteString("----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"foo.snap\"\r\n\r\n") + bodyBuf.Write(fooSnapBytes) + bodyBuf.WriteString("\r\n----hello--\r\n") + req, err := http.NewRequest("POST", "/v2/snaps", bodyBuf) c.Assert(err, check.IsNil) req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") defer daemon.MockSnapstateInstallPath(func(s *state.State, si *snap.SideInfo, path, name, channel string, flags snapstate.Flags) (*state.TaskSet, *snap.Info, error) { c.Check(flags, check.Equals, snapstate.Flags{RemoveSnapPath: true, Transaction: client.TransactionPerSnap}) c.Check(si, check.DeepEquals, &snap.SideInfo{ - RealName: "x", - SnapID: "x-id", + RealName: "foo", + SnapID: "foo-id", Revision: snap.R(41), }) - return state.NewTaskSet(), &snap.Info{SuggestedName: "x"}, nil + return state.NewTaskSet(), &snap.Info{SuggestedName: "foo"}, nil })() rsp := s.asyncReq(c, req, nil) @@ -336,16 +357,16 @@ defer st.Unlock() chg := st.Change(rsp.Change) c.Assert(chg, check.NotNil) - c.Check(chg.Summary(), check.Equals, `Install "x" snap from file "x.snap"`) + c.Check(chg.Summary(), check.Equals, `Install "foo" snap from file "foo.snap"`) var names []string err = chg.Get("snap-names", &names) c.Assert(err, check.IsNil) - c.Check(names, check.DeepEquals, []string{"x"}) + c.Check(names, check.DeepEquals, []string{"foo"}) var apiData map[string]interface{} err = chg.Get("api-data", &apiData) c.Assert(err, check.IsNil) c.Check(apiData, check.DeepEquals, map[string]interface{}{ - "snap-name": "x", + "snap-name": "foo", }) } @@ -356,7 +377,8 @@ "\r\n" + "xyzzy\r\n" + "----hello--\r\n" - s.daemonWithOverlordMockAndStore() + d := s.daemonWithOverlordMockAndStore() + s.markSeeded(d) req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) c.Assert(err, check.IsNil) @@ -405,7 +427,8 @@ "\r\n" + "true\r\n" + "----hello--\r\n" - s.daemonWithOverlordMockAndStore() + d := s.daemonWithOverlordMockAndStore() + s.markSeeded(d) defer daemon.MockUnsafeReadSnapInfo(func(path string) (*snap.Info, error) { return &snap.Info{SuggestedName: "foo"}, nil @@ -448,7 +471,8 @@ } func (s *sideloadSuite) TestSideloadSnapInstanceNameMismatch(c *check.C) { - s.daemonWithFakeSnapManager(c) + d := s.daemonWithFakeSnapManager(c) + s.markSeeded(d) defer daemon.MockUnsafeReadSnapInfo(func(path string) (*snap.Info, error) { return &snap.Info{SuggestedName: "bar"}, nil @@ -650,6 +674,7 @@ func (s *sideloadSuite) TestSideloadManySnaps(c *check.C) { d := s.daemonWithFakeSnapManager(c) + s.markSeeded(d) expectedFlags := &snapstate.Flags{RemoveSnapPath: true, DevMode: true, Transaction: client.TransactionAllSnaps} restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { @@ -818,33 +843,78 @@ func (s *sideloadSuite) TestSideloadManySnapsAsserted(c *check.C) { d := s.daemonWithOverlordMockAndStore() + s.markSeeded(d) st := d.Overlord().State() snaps := []string{"one", "two"} - s.mockAssertions(c, st, snaps) + snapData := s.mockAssertions(c, st, snaps) - body := "----hello--\r\n" expectedFlags := snapstate.Flags{RemoveSnapPath: true, Transaction: client.TransactionPerSnap} - s.testSideloadManySnaps(c, st, body, snaps, expectedFlags) + + restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { + c.Check(*flags, check.DeepEquals, expectedFlags) + + var tss []*state.TaskSet + for i, si := range infos { + c.Check(si, check.DeepEquals, &snap.SideInfo{ + RealName: snaps[i], + SnapID: snaps[i] + "-id", + Revision: snap.R(41), + }) + + ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName))) + tss = append(tss, ts) + } + + return tss, nil + }) + defer restore() + + bodyBuf := bytes.NewBufferString("----hello--\r\n") + fileSnaps := make([]string, len(snaps)) + for i, snap := range snaps { + fileSnaps[i] = "file-" + snap + bodyBuf.WriteString("Content-Disposition: form-data; name=\"snap\"; filename=\"" + fileSnaps[i] + "\"\r\n\r\n") + bodyBuf.Write(snapData[i]) + bodyBuf.WriteString("\r\n----hello--\r\n") + } + + req, err := http.NewRequest("POST", "/v2/snaps", bodyBuf) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + rsp := s.asyncReq(c, req, nil) + + c.Check(rsp.Status, check.Equals, 202) + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install snaps %s from files %s`, strutil.Quoted(snaps), strutil.Quoted(fileSnaps))) + } func (s *sideloadSuite) TestSideloadManySnapsOneNotAsserted(c *check.C) { d := s.daemonWithOverlordMockAndStore() + s.markSeeded(d) st := d.Overlord().State() snaps := []string{"one", "two"} - s.mockAssertions(c, st, []string{"one"}) - - body := "----hello--\r\n" + snapData := s.mockAssertions(c, st, []string{"one"}) + // unasserted snap + twoSnap := snaptest.MakeTestSnapWithFiles(c, `name: two +version: 1`, nil) + twoSnapData, err := ioutil.ReadFile(twoSnap) + c.Assert(err, check.IsNil) + snapData = append(snapData, twoSnapData) + bodyBuf := bytes.NewBufferString("----hello--\r\n") fileSnaps := make([]string, len(snaps)) for i, snap := range snaps { fileSnaps[i] = "file-" + snap - body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + fileSnaps[i] + "\"\r\n" + - "\r\n" + - snap + "\r\n" + - "----hello--\r\n" + bodyBuf.WriteString("Content-Disposition: form-data; name=\"snap\"; filename=\"" + fileSnaps[i] + "\"\r\n\r\n") + bodyBuf.Write(snapData[i]) + bodyBuf.WriteString("\r\n----hello--\r\n") } - req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + req, err := http.NewRequest("POST", "/v2/snaps", bodyBuf) c.Assert(err, check.IsNil) req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") rsp := s.errorReq(c, req, nil) @@ -853,15 +923,16 @@ c.Check(rsp.Message, check.Matches, "cannot find signatures with metadata for snap \"file-two\"") } -func (s *sideloadSuite) mockAssertions(c *check.C, st *state.State, snaps []string) { +func (s *sideloadSuite) mockAssertions(c *check.C, st *state.State, snaps []string) (snapData [][]byte) { for _, snap := range snaps { - hash := crypto.SHA3_384.New() - data := []byte(snap) - hash.Write(data) - digest := hash.Sum(nil) - - base64Digest, err := asserts.EncodeDigest(crypto.SHA3_384, digest) + thisSnap := snaptest.MakeTestSnapWithFiles(c, fmt.Sprintf(`name: %s +version: 1`, snap), nil) + digest, size, err := asserts.SnapFileSHA3_384(thisSnap) + c.Assert(err, check.IsNil) + thisSnapData, err := ioutil.ReadFile(thisSnap) c.Assert(err, check.IsNil) + snapData = append(snapData, thisSnapData) + dev1Acct := assertstest.NewAccount(s.StoreSigning, "devel1", nil, "") snapDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ "series": "16", @@ -872,8 +943,8 @@ }, nil, "") c.Assert(err, check.IsNil) snapRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ - "snap-sha3-384": base64Digest, - "snap-size": strconv.Itoa(len(data)), + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", size), "snap-id": snap + "-id", "snap-revision": "41", "developer-id": dev1Acct.AccountID(), @@ -885,48 +956,8 @@ assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""), dev1Acct, snapDecl, snapRev) st.Unlock() } -} - -func (s *sideloadSuite) testSideloadManySnaps(c *check.C, st *state.State, body string, snaps []string, expectedFlags snapstate.Flags) { - restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { - c.Check(*flags, check.DeepEquals, expectedFlags) - var tss []*state.TaskSet - for i, si := range infos { - c.Check(si, check.DeepEquals, &snap.SideInfo{ - RealName: snaps[i], - SnapID: snaps[i] + "-id", - Revision: snap.R(41), - }) - - ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName))) - tss = append(tss, ts) - } - - return tss, nil - }) - defer restore() - - fileSnaps := make([]string, len(snaps)) - for i, snap := range snaps { - fileSnaps[i] = "file-" + snap - body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + fileSnaps[i] + "\"\r\n" + - "\r\n" + - snap + "\r\n" + - "----hello--\r\n" - } - - req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) - c.Assert(err, check.IsNil) - req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") - rsp := s.asyncReq(c, req, nil) - - c.Check(rsp.Status, check.Equals, 202) - st.Lock() - defer st.Unlock() - chg := st.Change(rsp.Change) - c.Assert(chg, check.NotNil) - c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install snaps %s from files %s`, strutil.Quoted(snaps), strutil.Quoted(fileSnaps))) + return snapData } type trySuite struct { diff -Nru snapd-2.55.5+20.04/daemon/api_snap_file.go snapd-2.57.5+20.04/daemon/api_snap_file.go --- snapd-2.55.5+20.04/daemon/api_snap_file.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_snap_file.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package daemon import ( + "errors" "net/http" "github.com/snapcore/snapd/overlord/auth" @@ -48,14 +49,13 @@ if err == nil { info, err = snapst.CurrentInfo() } - switch err { - case nil: - // ok - case state.ErrNoState: - return SnapNotFound(name, err) - default: + if err != nil { + if errors.Is(err, state.ErrNoState) { + return SnapNotFound(name, err) + } return InternalError("cannot download file for snap %q: %v", name, err) } + if !snapst.Active { return BadRequest("cannot download file of inactive snap %q", name) } diff -Nru snapd-2.55.5+20.04/daemon/api_snaps.go snapd-2.57.5+20.04/daemon/api_snaps.go --- snapd-2.55.5+20.04/daemon/api_snaps.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_snaps.go 2022-10-17 16:25:18.000000000 +0000 @@ -28,6 +28,7 @@ "net/http" "strings" + "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" @@ -201,6 +202,7 @@ Transaction client.TransactionType `json:"transaction"` Snaps []string `json:"snaps"` Users []string `json:"users"` + ValidationSets []string `json:"validation-sets"` // The fields below should not be unmarshalled into. Do not export them. userID int @@ -387,9 +389,9 @@ } if inst.Revision.Unset() { - ts, err = snapstateRevert(st, inst.Snaps[0], flags) + ts, err = snapstateRevert(st, inst.Snaps[0], flags, "") } else { - ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags) + ts, err = snapstateRevertToRevision(st, inst.Snaps[0], inst.Revision, flags, "") } if err != nil { return "", nil, err @@ -560,7 +562,11 @@ func (inst *snapInstruction) dispatchForMany() (op snapManyActionFunc) { switch inst.Action { case "refresh": - op = snapUpdateMany + if len(inst.ValidationSets) > 0 { + op = snapEnforceValidationSets + } else { + op = snapUpdateMany + } case "install": op = snapInstallMany case "remove": @@ -654,6 +660,46 @@ }, nil } +func snapEnforceValidationSets(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { + if len(inst.ValidationSets) > 0 && len(inst.Snaps) != 0 { + return nil, fmt.Errorf("snap names cannot be specified with validation sets to enforce") + } + + snaps, ignoreValidationSnaps, err := snapstate.InstalledSnaps(st) + if err != nil { + return nil, err + } + + // we need refreshed snap-declarations, this ensures that snap-declarations + // and their prerequisite assertions are updated regularly; do not update all + // validation-set assertions (this is implied by passing nil opts) - only + // those requested via inst.ValidationSets will get updated by + // assertstateTryEnforceValidationSets below. + if err := assertstateRefreshSnapAssertions(st, inst.userID, nil); err != nil { + return nil, err + } + + var validationErr *snapasserts.ValidationSetsValidationError + err = assertstateTryEnforceValidationSets(st, inst.ValidationSets, inst.userID, snaps, ignoreValidationSnaps) + if err != nil { + var ok bool + validationErr, ok = err.(*snapasserts.ValidationSetsValidationError) + if !ok { + return nil, err + } + } + tss, affected, err := snapstateEnforceSnaps(context.TODO(), st, inst.ValidationSets, validationErr, inst.userID) + if err != nil { + return nil, err + } + + return &snapInstructionResult{ + Summary: fmt.Sprintf("Enforced validation sets: %s", strutil.Quoted(inst.ValidationSets)), + Affected: affected, + Tasksets: tss, + }, nil +} + func snapRemoveMany(inst *snapInstruction, st *state.State) (*snapInstructionResult, error) { removed, tasksets, err := snapstateRemoveMany(st, inst.Snaps) if err != nil { diff -Nru snapd-2.55.5+20.04/daemon/api_snaps_test.go snapd-2.57.5+20.04/daemon/api_snaps_test.go --- snapd-2.55.5+20.04/daemon/api_snaps_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_snaps_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -36,6 +36,7 @@ "gopkg.in/check.v1" "github.com/snapcore/snapd/arch" + "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/daemon" "github.com/snapcore/snapd/dirs" @@ -513,7 +514,7 @@ c.Check(chg.Get("api-data", &apiData), check.IsNil) c.Check(apiData["snap-names"], check.DeepEquals, []interface{}{"fake1", "fake2"}) err = chg.Get("system-restart-immediate", &systemRestartImmediate) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { c.Error(err) } return systemRestartImmediate @@ -1346,7 +1347,7 @@ summary = chg.Summary() err = chg.Get("system-restart-immediate", &systemRestartImmediate) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { c.Error(err) } return summary, systemRestartImmediate @@ -2073,12 +2074,12 @@ instFlags, err := inst.ModeFlags() c.Assert(err, check.IsNil) - defer daemon.MockSnapstateRevert(func(s *state.State, name string, flags snapstate.Flags) (*state.TaskSet, error) { + defer daemon.MockSnapstateRevert(func(s *state.State, name string, flags snapstate.Flags, fromChange string) (*state.TaskSet, error) { c.Check(flags, check.Equals, instFlags) queue = append(queue, name) return nil, nil })() - defer daemon.MockSnapstateRevertToRevision(func(s *state.State, name string, rev snap.Revision, flags snapstate.Flags) (*state.TaskSet, error) { + defer daemon.MockSnapstateRevertToRevision(func(s *state.State, name string, rev snap.Revision, flags snapstate.Flags, fromChange string) (*state.TaskSet, error) { c.Check(flags, check.Equals, instFlags) queue = append(queue, fmt.Sprintf("%s (%s)", name, rev)) return nil, nil @@ -2301,3 +2302,114 @@ c.Check(rspe.Message, check.Equals, expectedErr, check.Commentf("%q", action)) } } + +func (s *snapsSuite) TestRefreshEnforce(c *check.C) { + var refreshSnapAssertions bool + + defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { + refreshSnapAssertions = true + c.Check(opts, check.IsNil) + return nil + })() + + var tryEnforceValidationSets bool + defer daemon.MockAssertstateTryEnforceValidationSets(func(st *state.State, validationSets []string, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + tryEnforceValidationSets = true + return nil + })() + + defer daemon.MockSnapstateEnforceSnaps(func(ctx context.Context, st *state.State, validationSets []string, validErr *snapasserts.ValidationSetsValidationError, userID int) ([]*state.TaskSet, []string, error) { + c.Check(validationSets, check.DeepEquals, []string{"foo/bar=2", "foo/baz"}) + t := st.NewTask("fake-enforce-snaps", "...") + return []*state.TaskSet{state.NewTaskSet(t)}, []string{"some-snap", "other-snap"}, nil + })() + + d := s.daemon(c) + inst := &daemon.SnapInstruction{Action: "refresh", ValidationSets: []string{"foo/bar=2", "foo/baz"}} + + st := d.Overlord().State() + st.Lock() + defer st.Unlock() + + res, err := inst.DispatchForMany()(inst, st) + c.Assert(err, check.IsNil) + c.Check(res.Summary, check.Equals, `Enforced validation sets: "foo/bar=2", "foo/baz"`) + c.Check(res.Affected, check.DeepEquals, []string{"some-snap", "other-snap"}) + c.Check(refreshSnapAssertions, check.Equals, true) + c.Check(tryEnforceValidationSets, check.Equals, true) +} + +func (s *snapsSuite) TestRefreshEnforceTryEnforceValidationSetsError(c *check.C) { + var refreshSnapAssertions int + defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { + refreshSnapAssertions++ + c.Check(opts, check.IsNil) + return nil + })() + + tryEnforceErr := fmt.Errorf("boom") + defer daemon.MockAssertstateTryEnforceValidationSets(func(st *state.State, validationSets []string, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + return tryEnforceErr + })() + + var snapstateEnforceSnaps int + defer daemon.MockSnapstateEnforceSnaps(func(ctx context.Context, st *state.State, validationSets []string, validErr *snapasserts.ValidationSetsValidationError, userID int) ([]*state.TaskSet, []string, error) { + snapstateEnforceSnaps++ + c.Check(validErr, check.NotNil) + return nil, nil, nil + })() + + d := s.daemon(c) + inst := &daemon.SnapInstruction{Action: "refresh", ValidationSets: []string{"foo/baz"}} + + st := d.Overlord().State() + st.Lock() + defer st.Unlock() + + _, err := inst.DispatchForMany()(inst, st) + c.Assert(err, check.ErrorMatches, `boom`) + c.Check(refreshSnapAssertions, check.Equals, 1) + c.Check(snapstateEnforceSnaps, check.Equals, 0) + + // ValidationSetsValidationError is expected and fine + tryEnforceErr = &snapasserts.ValidationSetsValidationError{} + + _, err = inst.DispatchForMany()(inst, st) + c.Assert(err, check.IsNil) + c.Check(refreshSnapAssertions, check.Equals, 2) + c.Check(snapstateEnforceSnaps, check.Equals, 1) +} + +func (s *snapsSuite) TestRefreshEnforceWithSnapsIsAnError(c *check.C) { + var refreshSnapAssertions bool + defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { + refreshSnapAssertions = true + c.Check(opts, check.IsNil) + return fmt.Errorf("unexptected") + })() + + var tryEnforceValidationSets bool + defer daemon.MockAssertstateTryEnforceValidationSets(func(st *state.State, validationSets []string, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + tryEnforceValidationSets = true + return fmt.Errorf("unexpected") + })() + + var snapstateEnforceSnaps bool + defer daemon.MockSnapstateEnforceSnaps(func(ctx context.Context, st *state.State, validationSets []string, validErr *snapasserts.ValidationSetsValidationError, userID int) ([]*state.TaskSet, []string, error) { + snapstateEnforceSnaps = true + return nil, nil, fmt.Errorf("unexpected") + })() + + d := s.daemon(c) + inst := &daemon.SnapInstruction{Action: "refresh", Snaps: []string{"some-snap"}, ValidationSets: []string{"foo/baz"}} + + st := d.Overlord().State() + st.Lock() + defer st.Unlock() + + _, err := inst.DispatchForMany()(inst, st) + c.Assert(err, check.ErrorMatches, `snap names cannot be specified with validation sets to enforce`) + c.Check(refreshSnapAssertions, check.Equals, false) + c.Check(tryEnforceValidationSets, check.Equals, false) + c.Check(snapstateEnforceSnaps, check.Equals, false) +} diff -Nru snapd-2.55.5+20.04/daemon/api_system_recovery_keys.go snapd-2.57.5+20.04/daemon/api_system_recovery_keys.go --- snapd-2.55.5+20.04/daemon/api_system_recovery_keys.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_system_recovery_keys.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,35 +20,66 @@ package daemon import ( + "encoding/json" "net/http" - "path/filepath" - "github.com/snapcore/snapd/client" - "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/overlord/auth" - "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/overlord/devicestate" ) var systemRecoveryKeysCmd = &Command{ - Path: "/v2/system-recovery-keys", - GET: getSystemRecoveryKeys, - ReadAccess: rootAccess{}, + Path: "/v2/system-recovery-keys", + GET: getSystemRecoveryKeys, + POST: postSystemRecoveryKeys, + ReadAccess: rootAccess{}, + WriteAccess: rootAccess{}, } func getSystemRecoveryKeys(c *Command, r *http.Request, user *auth.UserState) Response { - var rsp client.SystemRecoveryKeysResponse + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() - rkey, err := secboot.RecoveryKeyFromFile(filepath.Join(dirs.SnapFDEDir, "recovery.key")) + keys, err := c.d.overlord.DeviceManager().EnsureRecoveryKeys() if err != nil { return InternalError(err.Error()) } - rsp.RecoveryKey = rkey.String() - reinstallKey, err := secboot.RecoveryKeyFromFile(filepath.Join(dirs.SnapFDEDir, "reinstall.key")) + return SyncResponse(keys) +} + +var deviceManagerRemoveRecoveryKeys = (*devicestate.DeviceManager).RemoveRecoveryKeys + +type postSystemRecoveryKeysData struct { + Action string `json:"action"` +} + +func postSystemRecoveryKeys(c *Command, r *http.Request, user *auth.UserState) Response { + + var postData postSystemRecoveryKeysData + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&postData); err != nil { + return BadRequest("cannot decode recovery keys action data from request body: %v", err) + } + if decoder.More() { + return BadRequest("spurious content after recovery keys action") + } + switch postData.Action { + case "": + return BadRequest("missing recovery keys action") + default: + return BadRequest("unsupported recovery keys action %q", postData.Action) + case "remove": + // only currently supported action + } + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + err := deviceManagerRemoveRecoveryKeys(c.d.overlord.DeviceManager()) if err != nil { return InternalError(err.Error()) } - rsp.ReinstallKey = reinstallKey.String() - - return SyncResponse(&rsp) + return SyncResponse(nil) } diff -Nru snapd-2.55.5+20.04/daemon/api_system_recovery_keys_test.go snapd-2.57.5+20.04/daemon/api_system_recovery_keys_test.go --- snapd-2.55.5+20.04/daemon/api_system_recovery_keys_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_system_recovery_keys_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2020 Canonical Ltd + * Copyright (C) 2022 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,7 +20,9 @@ package daemon_test import ( + "bytes" "encoding/hex" + "errors" "io/ioutil" "net/http" "net/http/httptest" @@ -30,8 +32,9 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/daemon" "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" ) var _ = Suite(&recoveryKeysSuite{}) @@ -63,8 +66,8 @@ c.Assert(err, IsNil) } -func (s *recoveryKeysSuite) TestSystemGetRecoveryKeysAsRootHappy(c *C) { - if (secboot.RecoveryKey{}).String() == "not-implemented" { +func (s *recoveryKeysSuite) TestGetSystemRecoveryKeysAsRootHappy(c *C) { + if (keys.RecoveryKey{}).String() == "not-implemented" { c.Skip("needs working secboot recovery key") } @@ -83,7 +86,7 @@ }) } -func (s *recoveryKeysSuite) TestSystemGetRecoveryAsUserErrors(c *C) { +func (s *recoveryKeysSuite) TestGetSystemRecoveryKeysAsUserErrors(c *C) { s.daemon(c) mockSystemRecoveryKeys(c) @@ -96,3 +99,70 @@ s.serveHTTP(c, rec, req) c.Assert(rec.Code, Equals, 403) } + +func (s *recoveryKeysSuite) TestPostSystemRecoveryKeysActionRemove(c *C) { + s.daemon(c) + + called := 0 + defer daemon.MockDeviceManagerRemoveRecoveryKeys(func() error { + called++ + return nil + })() + + buf := bytes.NewBufferString(`{"action":"remove"}`) + req, err := http.NewRequest("POST", "/v2/system-recovery-keys", buf) + c.Assert(err, IsNil) + rsp := s.syncReq(c, req, nil) + c.Check(rsp.Status, Equals, 200) + c.Check(called, Equals, 1) +} + +func (s *recoveryKeysSuite) TestPostSystemRecoveryKeysAsUserErrors(c *C) { + s.daemon(c) + mockSystemRecoveryKeys(c) + + req, err := http.NewRequest("POST", "/v2/system-recovery-keys", nil) + c.Assert(err, IsNil) + + // being properly authorized as user is not enough, needs root + s.asUserAuth(c, req) + rec := httptest.NewRecorder() + s.serveHTTP(c, rec, req) + c.Assert(rec.Code, Equals, 403) +} + +func (s *recoveryKeysSuite) TestPostSystemRecoveryKeysBadAction(c *C) { + s.daemon(c) + + called := 0 + defer daemon.MockDeviceManagerRemoveRecoveryKeys(func() error { + called++ + return nil + })() + + buf := bytes.NewBufferString(`{"action":"unknown"}`) + req, err := http.NewRequest("POST", "/v2/system-recovery-keys", buf) + c.Assert(err, IsNil) + + rspe := s.errorReq(c, req, nil) + c.Check(rspe, DeepEquals, daemon.BadRequest(`unsupported recovery keys action "unknown"`)) + c.Check(called, Equals, 0) +} + +func (s *recoveryKeysSuite) TestPostSystemRecoveryKeysActionRemoveError(c *C) { + s.daemon(c) + + called := 0 + defer daemon.MockDeviceManagerRemoveRecoveryKeys(func() error { + called++ + return errors.New("boom") + })() + + buf := bytes.NewBufferString(`{"action":"remove"}`) + req, err := http.NewRequest("POST", "/v2/system-recovery-keys", buf) + c.Assert(err, IsNil) + + rspe := s.errorReq(c, req, nil) + c.Check(rspe, DeepEquals, daemon.InternalError("boom")) + c.Check(called, Equals, 1) +} diff -Nru snapd-2.55.5+20.04/daemon/api_users.go snapd-2.57.5+20.04/daemon/api_users.go --- snapd-2.55.5+20.04/daemon/api_users.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_users.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,6 +21,7 @@ import ( "encoding/json" + "errors" "fmt" "net/http" "os/user" @@ -349,7 +350,7 @@ st.Lock() serial, err = c.d.overlord.DeviceManager().Serial() st.Unlock() - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return InternalError("cannot create user: cannot get serial: %v", err) } } diff -Nru snapd-2.55.5+20.04/daemon/api_validate.go snapd-2.57.5+20.04/daemon/api_validate.go --- snapd-2.55.5+20.04/daemon/api_validate.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_validate.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,6 +21,7 @@ import ( "encoding/json" + "errors" "fmt" "net/http" "sort" @@ -143,6 +144,38 @@ return vsets.CheckInstalledSnaps(snaps, ignoreValidation) } +func validationSetResultFromTracking(st *state.State, tr *assertstate.ValidationSetTracking) (*validationSetResult, error) { + var sequence int + if tr.PinnedAt > 0 { + sequence = tr.PinnedAt + } else { + sequence = tr.Current + } + modeStr, err := modeString(tr.Mode) + if err != nil { + return nil, err + } + + sets, err := validationSetForAssert(st, tr.AccountID, tr.Name, sequence) + if err != nil { + return nil, err + } + snaps, _, err := snapstate.InstalledSnaps(st) + if err != nil { + return nil, err + } + + validErr := checkInstalledSnaps(sets, snaps, nil) + return &validationSetResult{ + AccountID: tr.AccountID, + Name: tr.Name, + PinnedAt: tr.PinnedAt, + Mode: modeStr, + Sequence: tr.Current, + Valid: validErr == nil, + }, nil +} + func getValidationSet(c *Command, r *http.Request, user *auth.UserState) Response { vars := muxVars(r) accountID := vars["account"] @@ -177,7 +210,7 @@ var tr assertstate.ValidationSetTracking err := assertstate.GetValidationSet(st, accountID, name, &tr) - if err == state.ErrNoState || (err == nil && sequence != 0 && sequence != tr.PinnedAt) { + if errors.Is(err, state.ErrNoState) || (err == nil && sequence != 0 && sequence != tr.PinnedAt) { // not available locally, try to find in the store. return validateAgainstStore(st, accountID, name, sequence, user) } @@ -185,37 +218,12 @@ return InternalError("accessing validation sets failed: %v", err) } - modeStr, err := modeString(tr.Mode) - if err != nil { - return InternalError(err.Error()) - } - // evaluate against installed snaps - - if tr.PinnedAt > 0 { - sequence = tr.PinnedAt - } else { - sequence = tr.Current - } - sets, err := validationSetForAssert(st, tr.AccountID, tr.Name, sequence) + res, err := validationSetResultFromTracking(st, &tr) if err != nil { return InternalError(err.Error()) } - snaps, _, err := snapstate.InstalledSnaps(st) - if err != nil { - return InternalError(err.Error()) - } - - validErr := checkInstalledSnaps(sets, snaps, nil) - res := validationSetResult{ - AccountID: tr.AccountID, - Name: tr.Name, - PinnedAt: tr.PinnedAt, - Mode: modeStr, - Sequence: tr.Current, - Valid: validErr == nil, - } - return SyncResponse(res) + return SyncResponse(*res) } type validationSetApplyRequest struct { @@ -264,6 +272,7 @@ var assertstateMonitorValidationSet = assertstate.MonitorValidationSet var assertstateEnforceValidationSet = assertstate.EnforceValidationSet +var assertstateTryEnforceValidationSets = assertstate.TryEnforceValidationSets // updateValidationSet handles snap validate --monitor and --enforce accountId/name[=sequence]. func updateValidationSet(st *state.State, accountID, name string, reqMode string, sequence int, user *auth.UserState) Response { @@ -286,11 +295,16 @@ return enforceValidationSet(st, accountID, name, sequence, userID) } - err := assertstateMonitorValidationSet(st, accountID, name, sequence, userID) + tr, err := assertstateMonitorValidationSet(st, accountID, name, sequence, userID) if err != nil { return BadRequest("cannot get validation set assertion for %v: %v", assertstate.ValidationSetKey(accountID, name), err) } - return SyncResponse(nil) + + res, err := validationSetResultFromTracking(st, tr) + if err != nil { + return InternalError(err.Error()) + } + return SyncResponse(*res) } // forgetValidationSet forgets the validation set. @@ -299,7 +313,7 @@ // check if it exists first var tr assertstate.ValidationSetTracking err := assertstate.GetValidationSet(st, accountID, name, &tr) - if err == state.ErrNoState || (err == nil && sequence != 0 && sequence != tr.PinnedAt) { + if errors.Is(err, state.ErrNoState) || (err == nil && sequence != 0 && sequence != tr.PinnedAt) { return validationSetNotFound(accountID, name, sequence) } if err != nil { @@ -398,11 +412,16 @@ if err != nil { return InternalError(err.Error()) } - if err := assertstateEnforceValidationSet(st, accountID, name, sequence, userID, snaps, ignoreValidation); err != nil { + tr, err := assertstateEnforceValidationSet(st, accountID, name, sequence, userID, snaps, ignoreValidation) + if err != nil { // XXX: provide more specific error kinds? This would probably require // assertstate.ValidationSetAssertionForEnforce tuning too. return BadRequest("cannot enforce validation set: %v", err) } - return SyncResponse(nil) + res, err := validationSetResultFromTracking(st, tr) + if err != nil { + return InternalError(err.Error()) + } + return SyncResponse(*res) } diff -Nru snapd-2.55.5+20.04/daemon/api_validate_test.go snapd-2.57.5+20.04/daemon/api_validate_test.go --- snapd-2.55.5+20.04/daemon/api_validate_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/api_validate_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -360,7 +360,7 @@ c.Assert(sequence, check.Equals, 0) as, err := asserts.Decode(validationSetAssertion) c.Assert(err, check.IsNil) - // sanity + // validity c.Assert(as.Type().Name, check.Equals, "validation-set") return as, nil } @@ -599,12 +599,27 @@ } func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModePinnedLocalOnly(c *check.C) { + st := s.d.Overlord().State() + st.Lock() + s.mockValidationSetsTracking(st) + assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) + as := s.mockAssert(c, "bar", "99") + err := assertstate.Add(st, as) + st.Unlock() + c.Assert(err, check.IsNil) + var called int - restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) error { + restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) (*assertstate.ValidationSetTracking, error) { c.Assert(accountID, check.Equals, s.dev1acct.AccountID()) c.Assert(name, check.Equals, "bar") c.Assert(sequence, check.Equals, 99) called++ + return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, PinnedAt: 99, Current: 99999}, nil + }) + defer restore() + + restore = daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + // nil indicates successful validation return nil }) defer restore() @@ -615,12 +630,21 @@ rsp := s.syncReq(c, req, nil) c.Assert(rsp.Status, check.Equals, 200) + res := rsp.Result.(daemon.ValidationSetResult) + c.Check(res, check.DeepEquals, daemon.ValidationSetResult{ + AccountID: s.dev1acct.AccountID(), + Name: "bar", + Mode: "monitor", + PinnedAt: 99, + Sequence: 99999, + Valid: true, + }) c.Check(called, check.Equals, 1) } func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModeError(c *check.C) { - restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) error { - return fmt.Errorf("boom") + restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) (*assertstate.ValidationSetTracking, error) { + return nil, fmt.Errorf("boom") }) defer restore() @@ -651,7 +675,7 @@ var tr assertstate.ValidationSetTracking st.Lock() - // sanity, it exists before removing + // validity, it exists before removing err := assertstate.GetValidationSet(st, s.dev1acct.AccountID(), "foo", &tr) st.Unlock() c.Assert(err, check.IsNil) @@ -667,7 +691,7 @@ st.Lock() err = assertstate.GetValidationSet(st, s.dev1acct.AccountID(), "foo", &tr) st.Unlock() - c.Assert(err, check.Equals, state.ErrNoState) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) // and forget again fails req, err = http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/foo", s.dev1acct.AccountID()), strings.NewReader(body)) @@ -755,22 +779,28 @@ } func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceMode(c *check.C) { + st := s.d.Overlord().State() + st.Lock() + defer st.Unlock() + + s.mockValidationSetsTracking(st) + assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) + as := s.mockAssert(c, "bar", "99") + err := assertstate.Add(st, as) + c.Assert(err, check.IsNil) + var called int - restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) { c.Check(ignoreValidation, check.HasLen, 0) c.Assert(accountID, check.Equals, s.dev1acct.AccountID()) c.Assert(name, check.Equals, "bar") c.Assert(sequence, check.Equals, 0) c.Check(userID, check.Equals, 0) called++ - return nil + return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, Mode: assertstate.Enforce, Current: 99}, nil }) defer restore() - st := s.d.Overlord().State() - st.Lock() - defer st.Unlock() - snapstate.Set(st, "snap-b", &snapstate.SnapState{ Active: true, Sequence: []*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}, @@ -787,12 +817,30 @@ rsp := s.syncReq(c, req, nil) c.Assert(rsp.Status, check.Equals, 200) + res := rsp.Result.(daemon.ValidationSetResult) + c.Check(res, check.DeepEquals, daemon.ValidationSetResult{ + AccountID: s.dev1acct.AccountID(), + Name: "bar", + Mode: "enforce", + Sequence: 99, + Valid: true, + }) c.Check(called, check.Equals, 1) } func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeIgnoreValidationOK(c *check.C) { + st := s.d.Overlord().State() + st.Lock() + defer st.Unlock() + + s.mockValidationSetsTracking(st) + assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) + as := s.mockAssert(c, "bar", "99") + err := assertstate.Add(st, as) + c.Assert(err, check.IsNil) + var called int - restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) { c.Check(ignoreValidation, check.DeepEquals, map[string]bool{"snap-b": true}) c.Check(snaps, testutil.DeepUnsortedMatches, []*snapasserts.InstalledSnap{ snapasserts.NewInstalledSnap("snap-b", "yOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.R("1"))}) @@ -801,14 +849,10 @@ c.Assert(sequence, check.Equals, 0) c.Check(userID, check.Equals, 0) called++ - return nil + return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, Mode: assertstate.Enforce, Current: 99}, nil }) defer restore() - st := s.d.Overlord().State() - st.Lock() - defer st.Unlock() - snapstate.Set(st, "snap-b", &snapstate.SnapState{ Active: true, Sequence: []*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}, @@ -826,25 +870,39 @@ rsp := s.syncReq(c, req, nil) c.Assert(rsp.Status, check.Equals, 200) + res := rsp.Result.(daemon.ValidationSetResult) + c.Check(res, check.DeepEquals, daemon.ValidationSetResult{ + AccountID: s.dev1acct.AccountID(), + Name: "bar", + Mode: "enforce", + Sequence: 99, + Valid: true, + }) c.Check(called, check.Equals, 1) } func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeSpecificSequence(c *check.C) { + st := s.d.Overlord().State() + st.Lock() + defer st.Unlock() + + s.mockValidationSetsTracking(st) + assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) + as := s.mockAssert(c, "bar", "5") + err := assertstate.Add(st, as) + c.Assert(err, check.IsNil) + var called int - restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) { c.Assert(accountID, check.Equals, s.dev1acct.AccountID()) c.Assert(name, check.Equals, "bar") c.Assert(sequence, check.Equals, 5) c.Check(userID, check.Equals, 0) called++ - return nil + return &assertstate.ValidationSetTracking{AccountID: accountID, Name: name, Mode: assertstate.Enforce, PinnedAt: 5, Current: 5}, nil }) defer restore() - st := s.d.Overlord().State() - st.Lock() - defer st.Unlock() - snapstate.Set(st, "snap-b", &snapstate.SnapState{ Active: true, Sequence: []*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}, @@ -861,12 +919,21 @@ rsp := s.syncReq(c, req, nil) c.Assert(rsp.Status, check.Equals, 200) + res := rsp.Result.(daemon.ValidationSetResult) + c.Check(res, check.DeepEquals, daemon.ValidationSetResult{ + AccountID: s.dev1acct.AccountID(), + Name: "bar", + Mode: "enforce", + PinnedAt: 5, + Sequence: 5, + Valid: true, + }) c.Check(called, check.Equals, 1) } func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeError(c *check.C) { - restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { - return fmt.Errorf("boom") + restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error) { + return nil, fmt.Errorf("boom") }) defer restore() diff -Nru snapd-2.55.5+20.04/daemon/daemon.go snapd-2.57.5+20.04/daemon/daemon.go --- snapd-2.55.5+20.04/daemon/daemon.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/daemon.go 2022-10-17 16:25:18.000000000 +0000 @@ -254,8 +254,8 @@ // as readonlyOK. // // This is useful to report errors to the client when the daemon -// cannot work because e.g. a sanity check failed or the system is out -// of diskspace. +// cannot work because e.g. a snapd squashfs precondition check failed +// or the system is out of diskspace. // // When the system is fine again calling "DegradedMode(nil)" is enough // to put the daemon into full operation again. @@ -595,7 +595,7 @@ // see whether a reboot had already been scheduled var rebootAt time.Time err := d.state.Get("daemon-system-restart-at", &rebootAt) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return 0, err } rebootDelay := 1 * time.Minute @@ -673,7 +673,7 @@ func (d *Daemon) RebootDidNotHappen(st *state.State) error { var nTentative int err := st.Get("daemon-system-restart-tentative", &nTentative) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } nTentative++ diff -Nru snapd-2.55.5+20.04/daemon/daemon_test.go snapd-2.57.5+20.04/daemon/daemon_test.go --- snapd-2.55.5+20.04/daemon/daemon_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/daemon_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1297,8 +1297,8 @@ defer st.Unlock() var v interface{} // these were cleared - c.Check(st.Get("daemon-system-restart-at", &v), check.Equals, state.ErrNoState) - c.Check(st.Get("system-restart-from-boot-id", &v), check.Equals, state.ErrNoState) + c.Check(st.Get("daemon-system-restart-at", &v), testutil.ErrorIs, state.ErrNoState) + c.Check(st.Get("system-restart-from-boot-id", &v), testutil.ErrorIs, state.ErrNoState) } func (s *daemonSuite) TestRestartExpectedRebootGiveUp(c *check.C) { @@ -1321,9 +1321,9 @@ defer st.Unlock() var v interface{} // these were cleared - c.Check(st.Get("daemon-system-restart-at", &v), check.Equals, state.ErrNoState) - c.Check(st.Get("system-restart-from-boot-id", &v), check.Equals, state.ErrNoState) - c.Check(st.Get("daemon-system-restart-tentative", &v), check.Equals, state.ErrNoState) + c.Check(st.Get("daemon-system-restart-at", &v), testutil.ErrorIs, state.ErrNoState) + c.Check(st.Get("system-restart-from-boot-id", &v), testutil.ErrorIs, state.ErrNoState) + c.Check(st.Get("daemon-system-restart-tentative", &v), testutil.ErrorIs, state.ErrNoState) } func (s *daemonSuite) TestRestartIntoSocketModeNoNewChanges(c *check.C) { diff -Nru snapd-2.55.5+20.04/daemon/export_api_model_test.go snapd-2.57.5+20.04/daemon/export_api_model_test.go --- snapd-2.55.5+20.04/daemon/export_api_model_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/export_api_model_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -42,6 +42,5 @@ } type ( - PostModelData = postModelData - ModelAssertJSON = modelAssertJSON + PostModelData = postModelData ) diff -Nru snapd-2.55.5+20.04/daemon/export_api_system_recovery_keys_test.go snapd-2.57.5+20.04/daemon/export_api_system_recovery_keys_test.go --- snapd-2.55.5+20.04/daemon/export_api_system_recovery_keys_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/export_api_system_recovery_keys_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,33 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 daemon + +import ( + "github.com/snapcore/snapd/overlord/devicestate" + "github.com/snapcore/snapd/testutil" +) + +func MockDeviceManagerRemoveRecoveryKeys(f func() error) (restore func()) { + restore = testutil.Backup(&deviceManagerRemoveRecoveryKeys) + deviceManagerRemoveRecoveryKeys = func(*devicestate.DeviceManager) error { + return f() + } + return restore +} diff -Nru snapd-2.55.5+20.04/daemon/export_api_validate_test.go snapd-2.57.5+20.04/daemon/export_api_validate_test.go --- snapd-2.55.5+20.04/daemon/export_api_validate_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/export_api_validate_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,6 +21,7 @@ import ( "github.com/snapcore/snapd/asserts/snapasserts" + "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/state" ) @@ -36,7 +37,7 @@ } } -func MockAssertstateMonitorValidationSet(f func(st *state.State, accountID, name string, sequence int, userID int) error) func() { +func MockAssertstateMonitorValidationSet(f func(st *state.State, accountID, name string, sequence int, userID int) (*assertstate.ValidationSetTracking, error)) func() { old := assertstateMonitorValidationSet assertstateMonitorValidationSet = f return func() { @@ -44,7 +45,7 @@ } } -func MockAssertstateEnforceValidationSet(f func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error) func() { +func MockAssertstateEnforceValidationSet(f func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*assertstate.ValidationSetTracking, error)) func() { old := assertstateEnforceValidationSet assertstateEnforceValidationSet = f return func() { diff -Nru snapd-2.55.5+20.04/daemon/export_test.go snapd-2.57.5+20.04/daemon/export_test.go --- snapd-2.55.5+20.04/daemon/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -26,6 +26,7 @@ "github.com/gorilla/mux" + "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/assertstate" @@ -33,8 +34,11 @@ "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" ) +var CreateQuotaValues = createQuotaValues + func APICommands() []*Command { return api } @@ -116,6 +120,12 @@ } } +func MockAssertstateTryEnforceValidationSets(f func(st *state.State, validationSets []string, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error) (restore func()) { + r := testutil.Backup(&assertstateTryEnforceValidationSets) + assertstateTryEnforceValidationSets = f + return r +} + func MockSnapstateInstall(mock func(context.Context, *state.State, string, *snapstate.RevisionOptions, int, snapstate.Flags) (*state.TaskSet, error)) (restore func()) { oldSnapstateInstall := snapstateInstall snapstateInstall = mock @@ -156,7 +166,7 @@ } } -func MockSnapstateRevert(mock func(*state.State, string, snapstate.Flags) (*state.TaskSet, error)) (restore func()) { +func MockSnapstateRevert(mock func(*state.State, string, snapstate.Flags, string) (*state.TaskSet, error)) (restore func()) { oldSnapstateRevert := snapstateRevert snapstateRevert = mock return func() { @@ -164,7 +174,7 @@ } } -func MockSnapstateRevertToRevision(mock func(*state.State, string, snap.Revision, snapstate.Flags) (*state.TaskSet, error)) (restore func()) { +func MockSnapstateRevertToRevision(mock func(*state.State, string, snap.Revision, snapstate.Flags, string) (*state.TaskSet, error)) (restore func()) { oldSnapstateRevertToRevision := snapstateRevertToRevision snapstateRevertToRevision = mock return func() { @@ -204,6 +214,20 @@ } } +func MockSnapstateEnforceSnaps(f func(ctx context.Context, st *state.State, validationSets []string, validErr *snapasserts.ValidationSetsValidationError, userID int) ([]*state.TaskSet, []string, error)) func() { + r := testutil.Backup(&assertstateTryEnforceValidationSets) + snapstateEnforceSnaps = f + return r +} + +func MockSnapstateMigrate(mock func(*state.State, []string) ([]*state.TaskSet, error)) (restore func()) { + oldSnapstateMigrate := snapstateMigrateHome + snapstateMigrateHome = mock + return func() { + snapstateMigrateHome = oldSnapstateMigrate + } +} + func MockReboot(f func(boot.RebootAction, time.Duration, *boot.RebootInfo) error) func() { reboot = f return func() { reboot = boot.Reboot } diff -Nru snapd-2.55.5+20.04/daemon/snap.go snapd-2.57.5+20.04/daemon/snap.go --- snapd-2.55.5+20.04/daemon/snap.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/daemon/snap.go 2022-10-17 16:25:18.000000000 +0000 @@ -50,7 +50,7 @@ var snapst snapstate.SnapState err := snapstate.Get(st, name, &snapst) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return aboutSnap{}, fmt.Errorf("cannot consult state: %v", err) } @@ -62,7 +62,7 @@ return aboutSnap{}, fmt.Errorf("cannot read snap details: %v", err) } - info.Publisher, err = publisherAccount(st, info.SnapID) + info.Publisher, err = assertstate.PublisherStoreAccount(st, info.SnapID) if err != nil { return aboutSnap{}, err } @@ -118,7 +118,7 @@ // clear the error err = nil } - info.Publisher, err = publisherAccount(st, seq.SnapID) + info.Publisher, err = assertstate.PublisherStoreAccount(st, seq.SnapID) if err != nil && firstErr == nil { firstErr = err } @@ -127,7 +127,7 @@ } else { info, err = snapst.CurrentInfo() if err == nil { - info.Publisher, err = publisherAccount(st, info.SnapID) + info.Publisher, err = assertstate.PublisherStoreAccount(st, info.SnapID) aboutThis = append(aboutThis, aboutSnap{info, snapst, health}) } } @@ -145,23 +145,6 @@ return about, firstErr } -func publisherAccount(st *state.State, snapID string) (snap.StoreAccount, error) { - if snapID == "" { - return snap.StoreAccount{}, nil - } - - pubAcct, err := assertstate.Publisher(st, snapID) - if err != nil { - return snap.StoreAccount{}, fmt.Errorf("cannot find publisher details: %v", err) - } - return snap.StoreAccount{ - ID: pubAcct.AccountID(), - Username: pubAcct.Username(), - DisplayName: pubAcct.DisplayName(), - Validation: pubAcct.Validation(), - }, nil -} - func clientHealthFromHealthstate(h *healthstate.HealthState) *client.SnapHealth { if h == nil { return nil diff -Nru snapd-2.55.5+20.04/data/completion/bash/complete.sh snapd-2.57.5+20.04/data/completion/bash/complete.sh --- snapd-2.55.5+20.04/data/completion/bash/complete.sh 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/data/completion/bash/complete.sh 2022-10-17 16:25:18.000000000 +0000 @@ -102,10 +102,10 @@ } # this file can be sourced directly as e.g. /usr/lib/snapd/complete.sh, or via -# a symlink from /usr/share/bash-completion/completions/. In the first case we +# a symlink from /var/lib/snapd/desktop/bash-completion/completions/. In the first case we # want to load the default loader; in the second, the specific one. # -if [[ "${BASH_SOURCE[0]}" =~ ^/usr/share/bash-completion/completions/ ]]; then +if [[ "${BASH_SOURCE[0]}" =~ ^(/var/lib/snapd/desktop|/usr/share)/bash-completion/completions/ ]]; then complete -F _complete_from_snap "$1" else diff -Nru snapd-2.55.5+20.04/data/dbus/io.snapcraft.Prompt.service.in snapd-2.57.5+20.04/data/dbus/io.snapcraft.Prompt.service.in --- snapd-2.55.5+20.04/data/dbus/io.snapcraft.Prompt.service.in 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/data/dbus/io.snapcraft.Prompt.service.in 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=io.snapcraft.Prompt +Exec=@libexecdir@/snapd/snapd-aa-prompt-ui +AssumedAppArmorLabel=unconfined diff -Nru snapd-2.55.5+20.04/data/dbus/Makefile snapd-2.57.5+20.04/data/dbus/Makefile --- snapd-2.55.5+20.04/data/dbus/Makefile 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/data/dbus/Makefile 2022-10-17 16:25:18.000000000 +0000 @@ -14,6 +14,7 @@ # along with this program. If not, see . BINDIR := /usr/bin +LIBEXECDIR := /usr/lib DBUSDIR = /usr/share/dbus-1 DBUSSERVICESDIR := ${DBUSDIR}/services @@ -21,7 +22,10 @@ SERVICES := ${SERVICES_GENERATED} %.service: %.service.in - cat $< | sed 's:@bindir@:${BINDIR}:g' | cat > $@ + cat $< | \ + sed s:@libexecdir@:$(LIBEXECDIR):g | \ + sed 's:@bindir@:${BINDIR}:g' | \ + cat > $@ all: ${SERVICES} diff -Nru snapd-2.55.5+20.04/data/preseed.json snapd-2.57.5+20.04/data/preseed.json --- snapd-2.55.5+20.04/data/preseed.json 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/data/preseed.json 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,26 @@ +{ + "include": [ + "etc/udev/rules.d", + "etc/systemd", + "etc/dbus-1", + "snap", + "var/lib/extrausers", + "var/lib/snapd/state.json", + "var/lib/snapd/apparmor", + "var/lib/snapd/system-key", + "var/lib/snapd/assertions", + "var/lib/snapd/seccomp", + "var/lib/snapd/desktop", + "var/lib/snapd/sequence", + "var/lib/snapd/cookie", + "var/lib/snapd/dbus-1", + "var/lib/snapd/mount", + "var/snap", + "var/cache/snapd/aux", + "var/cache/apparmor" + ], + "exclude": [ + "var/lib/snapd/snaps/*.snap", + "var/lib/seed/*" + ] +} diff -Nru snapd-2.55.5+20.04/data/selinux/snappy.te snapd-2.57.5+20.04/data/selinux/snappy.te --- snapd-2.55.5+20.04/data/selinux/snappy.te 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/data/selinux/snappy.te 2022-10-17 16:25:18.000000000 +0000 @@ -818,6 +818,9 @@ # execute systemctl is-system-running when system-key mismatch is detected systemd_exec_systemctl(snappy_cli_t) +# allow snap to read SSL certs +miscfiles_read_all_certs(snappy_t) + ######################################## # # snappy (unconfined snap) local policy diff -Nru snapd-2.55.5+20.04/data/systemd/snapd.aa-prompt-listener.service.in snapd-2.57.5+20.04/data/systemd/snapd.aa-prompt-listener.service.in --- snapd-2.55.5+20.04/data/systemd/snapd.aa-prompt-listener.service.in 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/data/systemd/snapd.aa-prompt-listener.service.in 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,13 @@ +[Unit] +Description=Userspace listener for prompt events +After=snapd.socket +ConditionPathExists=/sys/kernel/security/apparmor/.notify + +[Service] +ExecStart=@libexecdir@/snapd/snapd-aa-prompt-listener +EnvironmentFile=-@SNAPD_ENVIRONMENT_FILE@ +Restart=on-failure +Type=simple + +[Install] +WantedBy=multi-user.target diff -Nru snapd-2.55.5+20.04/data/systemd/snapd.recovery-chooser-trigger.service.in snapd-2.57.5+20.04/data/systemd/snapd.recovery-chooser-trigger.service.in --- snapd-2.55.5+20.04/data/systemd/snapd.recovery-chooser-trigger.service.in 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/data/systemd/snapd.recovery-chooser-trigger.service.in 2022-10-17 16:25:18.000000000 +0000 @@ -1,6 +1,7 @@ [Unit] Description=Wait for the Ubuntu Core chooser trigger -Before=snapd.service +Wants=getty-pre.target +Before=getty-pre.target # don't run on classic or uc16/uc18 ConditionKernelCommandLine=snapd_recovery_mode # only run when there are input devices diff -Nru snapd-2.55.5+20.04/data/systemd/snapd.service.in snapd-2.57.5+20.04/data/systemd/snapd.service.in --- snapd-2.55.5+20.04/data/systemd/snapd.service.in 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/data/systemd/snapd.service.in 2022-10-17 16:25:18.000000000 +0000 @@ -1,6 +1,7 @@ [Unit] Description=Snap Daemon -After=snapd.socket +After=snapd.socket time-set.target +Wants=time-set.target Requires=snapd.socket OnFailure=snapd.failure.service # This is handled by snapd diff -Nru snapd-2.55.5+20.04/data/systemd-user/snapd.aa-prompt-ui.service.in snapd-2.57.5+20.04/data/systemd-user/snapd.aa-prompt-ui.service.in --- snapd-2.55.5+20.04/data/systemd-user/snapd.aa-prompt-ui.service.in 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/data/systemd-user/snapd.aa-prompt-ui.service.in 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,7 @@ +[Unit] +Description=snapd UI prompt + +[Service] +Type=dbus +BusName=io.snapcraft.Prompt +ExecStart=@bindir@/snapd-aa-prompt-ui diff -Nru snapd-2.55.5+20.04/debian/changelog snapd-2.57.5+20.04/debian/changelog --- snapd-2.55.5+20.04/debian/changelog 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/debian/changelog 2022-10-17 16:25:18.000000000 +0000 @@ -1,4 +1,660 @@ -snapd (2.55.5+20.04) focal; urgency=medium +snapd (2.57.5+20.04) focal; urgency=medium + + * New upstream release, LP: #1983035 + - image: clean snapd mount after preseeding + - wrappers,snap/quota: clear LogsDirectory= in the service unit + for journal namespaces + - cmd/snap,daemon: allow zero values from client to daemon for + journal rate-limit + - interfaces: steam-support allow pivot /run/media and /etc/nvidia + mount + - o/ifacestate: introduce DebugAutoConnectCheck hook + - release, snapd-apparmor, syscheck: distinguish WSL1 and WSL2 + - autopkgtests: fix running autopkgtest on kinetic + - interfaces: add microceph interface + - interfaces: steam-support allow additional mounts + - many: add stub services + - interfaces: add kconfig paths to system-observe + - i/b/system_observe: honour root dir when checking for + /boot/config-* + - interfaces: grant access to speech-dispatcher socket + - interfaces: rework logic of unclashMountEntries + + -- Michael Vogt Mon, 17 Oct 2022 18:25:18 +0200 + +snapd (2.57.4) xenial; urgency=medium + + * New upstream release, LP: #1983035 + - release, snapd-apparmor: fixed outdated WSL detection + - overlord/ifacestate: fix conflict detection of auto-connection + - overlord: run install-device hook during factory reset + - image/preseed/preseed_linux: add missing new line + - boot: add factory-reset cases for boot-flags. + - interfaces: added read/write access to /proc/self/coredump_filter + for process-control + - interfaces: add read access to /proc/cgroups and + /proc/sys/vm/swappiness to system-observe + - fde: run fde-reveal-key with `DefaultDependencies=no` + - snapdenv: added wsl to userAgent + - tests: fix restore section for persistent-journal-namespace + - i/b/mount-control: add optional `/` to umount rules + - cmd/snap-bootstrap: changes to be able to boot classic rootfs + - cmd/snap-bootstrap: add CVM mode + + -- Michael Vogt Thu, 29 Sep 2022 09:54:21 +0200 + +snapd (2.57.3) xenial; urgency=medium + + * New upstream release, LP: #1983035 + - wrappers: journal namespaces did not honor journal.persistent + - snap/quota,wrappers: allow using 0 values for the journal rate to + override the system default values + - multiple: clear up naming convention for cpu-set quota + - i/b/mount-control: allow custom filesystem types + - i/b/system-observe: allow reading processes security label + - sandbox/cgroup: don't check V1 cgroup if V2 is active + - asserts,boot,secboot: switch to a secboot version measuring + classic + + -- Michael Vogt Thu, 15 Sep 2022 12:37:30 +0200 + +snapd (2.57.2) xenial; urgency=medium + + * New upstream release, LP: #1983035 + - store/tooling,tests: support UBUNTU_STORE_URL override env var + - packaging/*/tests/integrationtests: reload ssh.service, not + sshd.service + - tests: check snap download with snapcraft v7+ export-login auth + data + - store/tooling: support using snapcraft v7+ base64-encoded auth + data + - many: progress bars should use the overridable stdouts + - many: refactor store code to be able to use simpler form of auth + creds + - snap,store: drop support/consideration for anonymous download urls + - data: include snapd/mounts in preseeded blob + - many: Set SNAPD_APPARMOR_REEXEC=1 + - overlord: track security profiles for non-active snaps + + -- Michael Vogt Fri, 02 Sep 2022 17:56:46 +0200 + +snapd (2.57.1) xenial; urgency=medium + + * New upstream release, LP: #1983035 + - cmd/snap-update-ns: handle mountpoint removal failures with EBUSY + - cmd/snap-update-ns: print current mount entries + - cmd/snap-update-ns: check the unused mounts with a cleaned path + - snap-confine: disable -Werror=array-bounds in __overflow tests to + fix build error on Ubuntu 22.10 + - systemd: add `WantedBy=default.target` to snap mount units + (LP: #1983528) + + -- Samuele Pedroni (Canonical Services Ltd.) Wed, 10 Aug 2022 09:30:50 +0300 + +snapd (2.57) xenial; urgency=medium + + * New upstream release, LP: #1983035 + - tests: Fix calls to systemctl is-system-running + - osutil/disks: handle GPT for 4k disk and too small tables + - packaging: import change from the 2.54.3-1.1 upload + - many: revert "features: disable refresh-app-awarness by default + again" + - tests: improve robustness of preparation for regression/lp-1803542 + - tests: get the ubuntu-image binary built with test keys + - tests: remove commented code from lxd test + - interfaces/builtin: add more permissions for steam-support + - tests: skip interfaces-network-control on i386 + - tests: tweak the "tests/nested/manual/connections" test + - interfaces: posix-mq: allow specifying message queue paths as an + array + - bootloader/assets: add ttyS0,115200n8 to grub.cfg + - i/b/desktop,unity7: remove name= specification on D-Bus signals + - tests: ensure that microk8s does not produce DENIED messages + - many: support non-default provenance snap-revisions in + DeriveSideInfo + - tests: fix `core20-new-snapd-does-not-break-old-initrd` test + - many: device and provenance revision authority cross checks + - tests: fix nested save-data test on 22.04 + - sandbox/cgroup: ignore container slices when tracking snaps + - tests: improve 'ignore-running' spread test + - tests: add `debug:` section to `tests/nested/manual/connections` + - tests: remove leaking `pc-kernel.snap` in `repack_kernel_snap` + - many: preparations for revision authority cross checks including + device scope + - daemon,overlord/servicestate: followup changes from PR #11960 to + snap logs + - cmd/snap: fix visual representation of 'AxB%' cpu quota modifier. + - many: expose and support provenance from snap.yaml metadata + - overlord,snap: add support for per-snap storage on ubuntu-save + - nested: fix core-early-config nested test + - tests: revert lxd change to support nested lxd launch + - tests: add invariant check for leftover cgroup scopes + - daemon,systemd: introduce support for namespaces in 'snap logs' + - cmd/snap: do not track apps that wish to stay outside of the life- + cycle system + - asserts: allow classic + snaps models and add distribution to + model + - cmd/snap: add snap debug connections/connection commands + - data: start snapd after time-set.target + - tests: remove ubuntu 21.10 from spread tests due to end of life + - tests: Update the whitebox word to avoid inclusive naming issues + - many: mount gadget in run folder + - interfaces/hardware-observe: clean up reading access to sysfs + - tests: use overlayfs for interfaces-opengl-nvidia test + - tests: update fake-netplan-apply test for 22.04 + - tests: add executions for ubuntu 22.04 + - tests: enable centos-9 + - tests: make more robust the files check in preseed-core20 test + - bootloader/assets: add fallback entry to grub.cfg + - interfaces/apparmor: add permissions for per-snap directory on + ubuntu-save partition + - devicestate: add more path to `fixupWritableDefaultDirs()` + - boot,secboot: reset DA lockout counter after successful boot + - many: Revert "overlord,snap: add support for per-snap storage on + ubuntu-save" + - overlord,snap: add support for per-snap storage on ubuntu-save + - tests: exclude centos-7 from kernel-module-load test + - dirs: remove unused SnapAppArmorAdditionalDir + - boot,device: extract SealedKey helpers from boot to device + - boot,gadget: add new `device.TpmLockoutAuthUnder()` and use it + - interfaces/display-control: allow changing brightness value + - asserts: add more context to key expiry error + - many: introduce IsUndo flag in LinkContext + - i/apparmor: allow calling which.debianutils + - tests: new profile id for apparmor in test preseed-core20 + - tests: detect 403 in apt-hooks and skip test in this case + - overlord/servicestate: restart the relevant journald service when + a journal quota group is modified + - client,cmd/snap: add journal quota frontend (5/n) + - gadget/device: introduce package which provides helpers for + locations of things + - features: disable refresh-app-awarness by default again + - many: install bash completion files in writable directory + - image: fix handling of var/lib/extrausers when preseeding + uc20 + - tests: force version 2.48.3 on xenial ESM + - tests: fix snap-network-erros on uc16 + - cmd/snap-confine: be compatible with a snap rootfs built as a + tmpfs + - o/snapstate: allow install of unasserted gadget/kernel on + dangerous models + - interfaces: dynamic loading of kernel modules + - many: add optional primary key provenance to snap-revision, allow + delegating via snap-declaration revision-authority + - tests: fix boringcripto errors in centos7 + - tests: fix snap-validate-enforce in opensuse-tumbleweed + - test: print User-Agent on failed checks + - interfaces: add memory stats to system_observe + - interfaces/pwm: Remove implicitOnCore/implicitOnClassic + - spread: add openSUSE Leap 15.4 + - tests: disable core20-to-core22 nested test + - tests: fix nested/manual/connections test + - tests: add spread test for migrate-home command + - overlord/servicestate: refresh security profiles when services are + affected by quotas + - interfaces/apparmor: add missing apparmor rules for journal + namespaces + - tests: add nested test variant that adds 4k sector size + - cmd/snap: fix test failing due to timezone differences + - build-aux/snap: build against the snappy-dev/image PPA + - daemon: implement api handler for refresh with enforced validation + sets + - preseed: suggest to install "qemu-user-static" + - many: add migrate-home debug command + - o/snapstate: support passing validation sets to storehelpers via + RevisionOptions + - cmd/snapd-apparmor: fix unit tests on distros which do not support + reexec + - o/devicestate: post factory reset ensure, spread test update + - tests/core/basic20: Enable on uc22 + - packaging/arch: install snapd-apparmor + - o/snapstate: support migrating snap home as change + - tests: enable snapd.apparmor service in all the opensuse systems + - snapd-apparmor: add more integration-ish tests + - asserts: store required revisions for missing snaps in + CheckInstalledSnaps + - overlord/ifacestate: fix path for journal redirect + - o/devicestate: factory reset with encryption + - cmd/snapd-apparmor: reimplement snapd-apparmor in Go + - squashfs: improve error reporting when `unsquashfs` fails + - o/assertstate: support multiple extra validation sets in + EnforcedValidationSets + - tests: enable mount-order-regression test for arm devices + - tests: fix interfaces network control + - interfaces: update AppArmor template to allow read the memory … + - cmd/snap-update-ns: add /run/systemd to unrestricted paths + - wrappers: fix LogNamespace being written to the wrong file + - boot: release the new PCR handles when sealing for factory reset + - tests: add support fof uc22 in test uboot-unpacked-assets + - boot: post factory reset cleanup + - tests: add support for uc22 in listing test + - spread.yaml: add ubuntu-22.04-06 to qemu-nested + - gadget: check also mbr type when testing for implicit data + partition + - interfaces/system-packages-doc: allow read-only access to + /usr/share/cups/doc-root/ and /usr/share/gimp/2.0/help/ + - tests/nested/manual/core20-early-config: revert changes that + disable netplan checks + - o/ifacestate: warn if the snapd.apparmor service is disabled + - tests: add spread execution for fedora 36 + - overlord/hookstate/ctlcmd: fix timestamp coming out of sync in + unit tests + - gadget/install: do not assume dm device has same block size as + disk + - interfaces: update network-control interface with permissions + required by resolvectl + - secboot: stage and transition encryption keys + - secboot, boot: support and use alternative PCR handles during + factory reset + - overlord/ifacestate: add journal bind-mount snap layout when snap + is in a journal quota group (4/n) + - secboot/keymgr, cmd/snap-fde-keymgr: two step encryption key + change + - cmd/snap: cleanup and make the code a bit easier to read/maintain + for quota options + - overlord/hookstate/ctlcmd: add 'snapctl model' command (3/3) + - cmd/snap-repair: fix snap-repair tests silently failing + - spread: drop openSUSE Leap 15.2 + - interfaces/builtin: remove the name=org.freedesktop.DBus + restriction in cups-control AppArmor rules + - wrappers: write journald config files for quota groups with + journal quotas (3/n) + - o/assertstate: auto aliases for apps that exist + - o/state: use more detailed NoStateError in state + - tests/main/interfaces-browser-support: verify jupyter notebooks + access + - o/snapstate: exclude services from refresh app awareness hard + running check + - tests/main/nfs-support: be robust against umount failures + - tests: update centos images and add new centos 9 image + - many: print valid/invalid status on snap validate --monitor + - secboot, boot: TPM provisioning mode enum, introduce + reprovisioning + - tests: allow to re-execute aborted tests + - cmd/snapd-apparmor: add explicit WSL detection to + is_container_with_internal_policy + - tests: avoid launching lxd inside lxd on cloud images + - interfaces: extra htop apparmor rules + - gadget/install: encrypted system factory reset support + - secboot: helpers for dealing with PCR handles and TPM resources + - systemd: improve error handling for systemd-sysctl command + - boot, secboot: separate the TPM provisioning and key sealing + - o/snapstate: fix validation sets restoring and snap revert on + failed refresh + - interfaces/builtin/system-observe: extend access for htop + - cmd/snap: support custom apparmor features dir with snap prepare- + image + - interfaces/mount-observe: Allow read access to /run/mount/utab + - cmd/snap: add help strings for set-quota options + - interfaces/builtin: add README file + - cmd/snap-confine: mount support cleanups + - overlord: execute snapshot cleanup in task + - i/b/accounts_service: fix path of introspectable objects + - interfaces/opengl: update allowed PCI accesses for RPi + - configcore: add core.system.ctrl-alt-del-action config option + - many: structured startup timings + - spread: switch back to building ubuntu-image from source + - many: optional recovery keys + - tests/lib/nested: fix unbound variable + - run-checks: fail on equality checks w/ ErrNoState + - snap-bootstrap: Mount as private + - tests: Test for gadget connections + - tests: set `br54.dhcp4=false` in the netplan-cfg test + - tests: core20 preseed/nested spread test + - systemd: remove the systemctl stop timeout handling + - interfaces/shared-memory: Update AppArmor permissions for + mmap+link + - many: replace ErrNoState equality checks w/ errors.Is() + - cmd/snap: exit w/ non-zero code on missing snap + - systemd: fix snapd systemd-unit stop progress notifications + - .github: Trigger daily riscv64 snapd edge builds + - interfaces/serial-port: add ttyGS to serial port allow list + - interfaces/modem-manager: Don't generate DBus plug policy + - tests: add spread test to test upgrade from release snapd to + current + - wrappers: refactor EnsureSnapServices + - testutil: add ErrorIs test checker + - tests: import spread shellcheck changes + - cmd/snap-fde-keymgr: best effort idempotency of add-recovery-key + - interfaces/udev: refactor handling of udevadm triggers for input + - secboot: support for changing encryption keys via keymgr + + -- Michael Vogt Thu, 28 Jul 2022 16:59:39 +0200 + +snapd (2.56.3) xenial; urgency=medium + + * New upstream release, LP: #1974147 + - devicestate: add more path to `fixupWritableDefaultDirs()` + - many: introduce IsUndo flag in LinkContext + - i/apparmor: allow calling which.debianutils + - interfaces: update AppArmor template to allow reading snap's + memory statistics + - interfaces: add memory stats to system_observe + - i/b/{mount,system}-observe: extend access for htop + - features: disable refresh-app-awarness by default again + - image: fix handling of var/lib/extrausers when preseeding + uc20 + - interfaces/modem-manager: Don't generate DBus policy for plugs + - interfaces/modem-manager: Only generate DBus plug policy on + Core + - interfaces/serial_port_test: fix static-checks errors + - interfaces/serial-port: add USB gadget serial devices (ttyGSX) to + allowed list + - interface/serial_port_test: adjust variable IDs + + -- Michael Vogt Wed, 13 Jul 2022 09:26:57 +0200 + +snapd (2.56.2) xenial; urgency=medium + + * New upstream release, LP: #1974147 + - o/snapstate: exclude services from refresh app awareness hard + running check + - cmd/snap: support custom apparmor features dir with snap + prepare-image + + -- Michael Vogt Wed, 15 Jun 2022 14:22:31 +0200 + +snapd (2.56.1) xenial; urgency=medium + + * New upstream release, LP: #1974147 + - gadget/install: do not assume dm device has same block size as + disk + - gadget: check also mbr type when testing for implicit data + partition + - interfaces: update network-control interface with permissions + required by resolvectl + - interfaces/builtin: remove the name=org.freedesktop.DBus + restriction in cups-control AppArmor rules + - many: print valid/invalid status on snap validate --monitor ... + - o/snapstate: fix validation sets restoring and snap revert on + failed refresh + - interfaces/opengl: update allowed PCI accesses for RPi + - interfaces/shared-memory: Update AppArmor permissions for + mmap+linkpaths + + -- Michael Vogt Wed, 15 Jun 2022 09:57:54 +0200 + +snapd (2.56) xenial; urgency=medium + + * New upstream release, LP: #1974147 + - portal-info: Add CommonID Field + - asserts/info,mkversion.sh: capture max assertion formats in + snapd/info + - tests: improve the unit testing workflow to run in parallel + - interfaces: allow map and execute permissions for files on + removable media + - tests: add spread test to verify that connections are preserved if + snap refresh fails + - tests: Apparmor sandbox profile mocking + - cmd/snap-fde-keymgr: support for multiple devices and + authorizations for add/remove recovery key + - cmd/snap-bootstrap: Listen to keyboard added after start and + handle switch root + - interfaces,overlord: add support for adding extra mount layouts + - cmd/snap: replace existing code for 'snap model' to use shared + code in clientutil (2/3) + - interfaces: fix opengl interface on RISC-V + - interfaces: allow access to the file locking for cryptosetup in + the dm-crypt interface + - interfaces: network-manager: add AppArmor rule for configuring + bridges + - i/b/hardware-observe.go: add access to the thermal sysfs + - interfaces: opengl: add rules for NXP i.MX GPU drivers + - i/b/mount_control: add an optional "/" to the mount target rule + - snap/quota: add values for journal quotas (journal quota 2/n) + - tests: spread test for uc20 preseeding covering snap prepare-image + - o/snapstate: remove deadcode breaking static checks + - secboot/keymgr: extend unit tests, add helper for identify keyslot + used error + - tests: use new snaps.name and snaps.cleanup tools + - interfaces: tweak getPath() slightly and add some more tests + - tests: update snapd testing tools + - client/clientutil: add shared code for printing model assertions + as yaml or json (1/3) + - debug-tools: list all snaps + - cmd/snap: join search terms passed in the command line + - osutil/disks: partition UUID lookup + - o/snapshotstate: refactor snapshot read/write logic + - interfaces: Allow locking in block-devices + - daemon: /v2/system-recovery-keys remove API + - snapstate: do not auto-migrate to ~/Snap for core22 just yet + - tests: run failed tests by default + - o/snapshotstate: check installed snaps before running 'save' tasks + - secboot/keymgr: remove recovery key, authorize with existing key + - deps: bump libseccomp to include build fixes, run unit tests using + CC=clang + - cmd/snap-seccomp: only compare the bottom 32-bits of the flags arg + of copy_file_range + - osutil/disks: helper for obtaining the UUID of a partition which + is a mount point source + - image/preseed: umount the base snap last after writable paths + - tests: new set of nested tests for uc22 + - tests: run failed tests on nested suite + - interfaces: posix-mq: add new interface + - tests/main/user-session-env: remove openSUSE-specific tweaks + - tests: skip external backend in mem-cgroup-disabled test + - snap/quota: change the journal quota period to be a time.Duration + - interfaces/apparmor: allow executing /usr/bin/numfmt in the base + template + - tests: add lz4 dependency for jammy to avoid issues repacking + kernel + - snap-bootstrap, o/devicestate: use seed parallelism + - cmd/snap-update-ns: correctly set sticky bit on created + directories where applicable + - tests: install snapd while restoring in snap-mgmt + - .github: skip misspell and ineffassign on go 1.13 + - many: use UC20+/pre-UC20 in user messages as needed + - o/devicestate: use snap handler for copying and checksuming + preseeded snaps + - image, cmd/snap-preseed: allow passing custom apparmor features + path + - o/assertstate: fix handling of validation set tracking update in + enforcing mode + - packaging: restart our units only after the upgrade + - interfaces: add a steam-support interface + - gadget/install, o/devicestate: do not create recovery and + reinstall keys during installation + - many: move recovery key responsibility to devicestate/secboot, + prepare for a future with just optional recovery key + - tests: do not run mem-cgroup-disabled on external backends + - snap: implement "star" developers + - o/devicestate: fix install tests on systems with + /var/lib/snapd/snap + - cmd/snap-fde-keymgr, secboot: followup cleanups + - seed: let SnapHandler provided a different final path for snaps + - o/devicestate: implement maybeApplyPreseededData function to apply + preseed artifact + - tests/lib/tools: add piboot to boot_path() + - interfaces/builtin: shared-memory drop plugs allow-installation: + true + - tests/main/user-session-env: for for opensuse + - cmd/snap-fde-keymgr, secboot: add a tiny FDE key manager + - tests: re-execute the failed tests when "Run failed" label is set + in the PR + - interfaces/builtin/custom-device: fix unit tests on hosts with + different libexecdir + - sandbox: move profile load/unload to sandbox/apparmor + - cmd/snap: handler call verifications for cmd_quota_tests + - secboot/keys: introduce a package for secboot key types, use the + package throughout the code base + - snap/quota: add journal quotas to resources.go + - many: let provide a SnapHandler to Seed.Load*Meta* + - osutil: allow setting desired mtime on the AtomicFile, preserve + mtime on copy + - systemd: add systemd.Run() wrapper for systemd-run + - tests: test fresh install of core22-based snap (#11696) + - tests: initial set of tests to uc22 nested execution + - o/snapstate: migration overwrites existing snap dir + - tests: fix interfaces-location-control tests leaking provider.py + process + - tests/nested: fix custom-device test + - tests: test migration w/ revert, refresh and XDG dir creation + - asserts,store: complete support for optional primary key headers + for assertions + - seed: support parallelism when loading/verifying snap metadata + - image/preseed, cmd/snap-preseed: create and sign preseed assertion + - tests: Initial changes to run nested tests on uc22 + - o/snapstate: fix TestSnapdRefreshTasks test after two r-a-a PRs + - interfaces: add ACRN hypervisor support + - o/snapstate: exclude TypeSnapd and TypeOS snaps from refresh-app- + awareness + - features: enable refresh-app-awareness by default + - libsnap-confine-private: show proper error when aa_change_onexec() + fails + - i/apparmor: remove leftover comment + - gadget: drop unused code in unit tests + - image, store: move ToolingStore to store/tooling package + - HACKING: update info for snapcraft remote build + - seed: return all essential snaps found if no types are given to + LoadEssentialMeta + - i/b/custom_device: fix generation of udev rules + - tests/nested/manual/core20-early-config: disable netplan checks + - bootloader/assets, tests: add factory-reset mode, test non- + encrypted factory-reset + - interfaces/modem-manager: add support for Cinterion modules + - gadget: fully support multi-volume gadget asset updates in + Update() on UC20+ + - i/b/content: use slot.Lookup() as suggested by TODO comment + - tests: install linux-tools-gcp on jammy to avoid bpftool + dependency error + - tests/main: add spread tests for new cpu and thread quotas + - snap-debug-info: print validation sets and validation set + assertions + - many: renaming related to inclusive language part 2 + - c/snap-seccomp: update syscalls to match libseccomp 2657109 + - github: cancel workflows when pushing to pull request branches + - .github: use reviewdog action from woke tool + - interfaces/system-packages-doc: allow read-only access to + /usr/share/gtk-doc + - interfaces: add max_map_count to system-observe + - o/snapstate: print pids of running processes on BusySnapError + - .github: run woke tool on PR's + - snapshots: follow-up on exclusions PR + - cmd/snap: add check switch for snap debug state + - tests: do not run mount-order-regression test on i386 + - interfaces/system-packages-doc: allow read-only access to + /usr/share/xubuntu-docs + - interfaces/hardware_observe: add read access for various devices + - packaging: use latest go to build spread + - tests: Enable more tests for UC22 + - interfaces/builtin/network-control: also allow for mstp and bchat + devices too + - interfaces/builtin: update apparmor profile to allow creating + mimic over /usr/share* + - data/selinux: allow snap-update-ns to mount on top of /var/snap + inside the mount ns + - interfaces/cpu-control: fix apparmor rules of paths with CPU ID + - tests: remove the file that configures nm as default + - tests: fix the change done for netplan-cfg test + - tests: disable netplan-cfg test + - cmd/snap-update-ns: apply content mounts before layouts + - overlord/state: add a helper to detect cyclic dependencies between + tasks in change + - packaging/ubuntu-16.04/control: recommend `fuse3 | fuse` + - many: change "transactional" flag to a "transaction" option + - b/piboot.go: check EEPROM version for RPi4 + - snap/quota,spread: raise lower memory quota limit to 640kb + - boot,bootloader: add missing grub.cfg assets mocks in some tests + - many: support --ignore-running with refresh many + - tests: skip the test interfaces-many-snap-provided in + trusty + - o/snapstate: rename XDG dirs during HOME migration + - cmd/snap,wrappers: fix wrong implementation of zero count cpu + quota + - i/b/kernel_module_load: expand $SNAP_COMMON in module options + - interfaces/u2f-devices: add Solo V2 + - overlord: add missing grub.cfg assets mocks in manager_tests.go + - asserts: extend optional primary keys support to the in-memory + backend + - tests: update the lxd-no-fuse test + - many: fix failing golangci checks + - seed,many: allow to limit LoadMeta to snaps of a precise mode + - tests: allow ubuntu-image to be built with a compatible snapd tree + - o/snapstate: account for repeat migration in ~/Snap undo + - asserts: start supporting optional primary keys in fs backend, + assemble and signing + - b/a: do not set console in kernel command line for arm64 + - tests/main/snap-quota-groups: fix spread test + - sandbox,quota: ensure cgroup is available when creating mem + quotas + - tests: add debug output what keeps `/home` busy + - sanity: rename "sanity.Check" to "syscheck.CheckSystem" + - interfaces: add pkcs11 interface + - o/snapstate: undo migration on 'snap revert' + - overlord: snapshot exclusions + - interfaces: add private /dev/shm support to shared-memory + interface + - gadget/install: implement factory reset for unencrypted system + - packaging: install Go snap from 1.17 channel in the integration + tests + - snap-exec: fix detection if `cups` interface is connected + - tests: extend gadget-config-defaults test with refresh.retain + - cmd/snap,strutil: move lineWrap to WordWrapPadded + - bootloader/piboot: add support for armhf + - snap,wrappers: add `sigint{,-all}` to supported stop-modes + - packaging/ubuntu-16.04/control: depend on fuse3 | fuse + - interfaces/system-packages-doc: allow read-only access to + /usr/share/libreoffice/help + - daemon: add a /v2/accessories/changes/{ID} endpoint + - interfaces/appstream-metadata: Re-create app-info links to + swcatalog + - debug-tools: add script to help debugging GCE instances which fail + to boot + - gadget/install, kernel: more ICE helpers/support + - asserts: exclude empty snap id from duplicates lookup with preseed + assert + - cmd/snap, signtool: move key-manager related helpers to signtool + package + - tests/main/snap-quota-groups: add 219 as possible exit code + - store: set validation-sets on actions when refreshing + - github/workflows: update golangci-lint version + - run-check: use go install instead of go get + - tests: set as manual the interfaces-cups-control test + - interfaces/appstream-metadata: Support new swcatalog directory + names + - image/preseed: migrate tests from cmd/snap-preseed + - tests/main/uc20-create-partitions: update the test for new Go + versions + - strutil: move wrapGeneric function to strutil as WordWrap + - many: small inconsequential tweaks + - quota: detect/error if cpu-set is used with cgroup v1 + - tests: moving ubuntu-image to candidate to fix uc16 tests + - image: integrate UC20 preseeding with image.Prepare + - cmd/snap,client: frontend for cpu/thread quotas + - quota: add test for `Resource.clone()` + - many: replace use of "sanity" with more inclusive naming (part 2) + - tests: switch to "test-snapd-swtpm" + - i/b/network-manager: split rule with more than one peers + - tests: fix restore of the BUILD_DIR in failover test on uc18 + - cmd/snap/debug: sort changes by their spawn times + - asserts,interfaces/policy: slot-snap-id allow-installation + constraints + - o/devicestate: factory reset mode, no encryption + - debug-tools/snap-debug-info.sh: print message if no gadget snap + found + - overlord/devicestate: install system cleanups + - cmd/snap-bootstrap: support booting into factory-reset mode + - o/snapstate, ifacestate: pass preseeding flag to + AddSnapdSnapServices + - o/devicestate: restore device key and serial when assertion is + found + - data: add static preseed.json file + - sandbox: improve error message from `ProbeCgroupVersion()` + - tests: fix the nested remodel tests + - quota: add some more unit tests around Resource.Change() + - debug-tools/snap-debug-info.sh: add debug script + - tests: workaround lxd issue lp:10079 (function not implemented) on + prep-snapd-in-lxd + - osutil/disks: blockdev need not be available in the PATH + - cmd/snap-preseed: address deadcode linter + - tests/lib/fakestore/store: return snap base in details + - tests/lib/nested.sh: rm core18 snap after download + - systemd: do not reload system when enabling/disabling services + - i/b/kubernetes_support: add access to Java certificates + + -- Michael Vogt Thu, 19 May 2022 09:57:33 +0200 + +snapd (2.55.5) xenial; urgency=medium * New upstream release, LP: #1965808 - snapstate: do not auto-migrate to ~/Snap for core22 just yet diff -Nru snapd-2.55.5+20.04/debian/control snapd-2.57.5+20.04/debian/control --- snapd-2.55.5+20.04/debian/control 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/debian/control 2022-10-17 16:25:18.000000000 +0000 @@ -74,7 +74,8 @@ ${dbussession:Depends} Replaces: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22), snapd-xdg-open (<= 0.0.0) Breaks: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22), snapd-xdg-open (<= 0.0.0), ${snapd:Breaks} -Recommends: gnupg +Recommends: gnupg, + fuse3 (>= 3.10.5-1) | fuse Suggests: zenity | kdialog Conflicts: snap (<< 2013-11-29-1ubuntu1) Built-Using: ${Built-Using} ${misc:Built-Using} diff -Nru snapd-2.55.5+20.04/debian/rules snapd-2.57.5+20.04/debian/rules --- snapd-2.55.5+20.04/debian/rules 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/debian/rules 2022-10-17 16:25:18.000000000 +0000 @@ -236,7 +236,7 @@ GO111MODULE=on \ dh_auto_test -- -mod=vendor $(BUILDFLAGS) $(TAGS) $(GCCGOFLAGS) $(DH_GOPKG)/... endif - + # a tested default (production) build should have no test keys ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) # check that only the main trusted account-keys are included @@ -274,6 +274,8 @@ rm -f ${CURDIR}/debian/tmp/usr/bin/chrorder # bootloader assets generator rm -f ${CURDIR}/debian/tmp/usr/bin/genasset + # asserts/info + rm -f ${CURDIR}/debian/tmp/usr/bin/info # docs generator rm -f ${CURDIR}/debian/tmp/usr/bin/docs diff -Nru snapd-2.55.5+20.04/debian/snapd.install snapd-2.57.5+20.04/debian/snapd.install --- snapd-2.55.5+20.04/debian/snapd.install 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/debian/snapd.install 2022-10-17 16:25:18.000000000 +0000 @@ -6,10 +6,14 @@ usr/bin/snap-failure /usr/lib/snapd/ usr/bin/snap-update-ns /usr/lib/snapd/ usr/bin/snapd /usr/lib/snapd/ +usr/bin/snapd-aa-prompt-listener /usr/lib/snapd/ +usr/bin/snapd-aa-prompt-ui /usr/lib/snapd/ usr/bin/snap-seccomp /usr/lib/snapd/ usr/bin/snap-bootstrap /usr/lib/snapd/ usr/bin/snap-preseed /usr/lib/snapd/ usr/bin/snap-recovery-chooser /usr/lib/snapd/ +usr/bin/snap-fde-keymgr /usr/lib/snapd/ +usr/bin/snapd-apparmor /usr/lib/snapd/ # bash completion data/completion/bash/snap /usr/share/bash-completion/completions @@ -48,6 +52,3 @@ usr/lib/systemd/system-environment-generators # but system generators end up in lib lib/systemd/system-generators - -# service for loading apparmor profiles for snap applications -usr/lib/snapd/snapd-apparmor diff -Nru snapd-2.55.5+20.04/debian/tests/integrationtests snapd-2.57.5+20.04/debian/tests/integrationtests --- snapd-2.55.5+20.04/debian/tests/integrationtests 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/debian/tests/integrationtests 2022-10-17 16:25:18.000000000 +0000 @@ -25,8 +25,14 @@ # ensure we can do a connect to localhost echo ubuntu:ubuntu|chpasswd -sed -i 's/\(PermitRootLogin\|PasswordAuthentication\)\>.*/\1 yes/' /etc/ssh/sshd_config -systemctl reload sshd.service +if [ -d /etc/ssh/sshd_config.d ]; then + printf 'PermitRootLogin=yes\nPasswordAuthentication=yes' > /etc/ssh/sshd_config.d/00-spread-settings.conf +else + sed -i 's/\(PermitRootLogin\|PasswordAuthentication\)\>.*/\1 yes/' /etc/ssh/sshd_config +fi +if systemctl is-active ssh.service; then + systemctl reload ssh.service +fi # 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 @@ -46,11 +52,11 @@ systemctl restart snapd # Spread will only buid with recent go -snap install --classic --channel 1.17/stable go +snap install --classic go # and now run spread against localhost export GOPATH=/tmp/go -/snap/bin/go get -u github.com/snapcore/spread/cmd/spread +/snap/bin/go install github.com/snapcore/spread/cmd/spread@latest # the tests need this: groupadd --gid 12345 test diff -Nru snapd-2.55.5+20.04/debug-tools/gce-serial-output-continuously-append.sh snapd-2.57.5+20.04/debug-tools/gce-serial-output-continuously-append.sh --- snapd-2.55.5+20.04/debug-tools/gce-serial-output-continuously-append.sh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/debug-tools/gce-serial-output-continuously-append.sh 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,70 @@ +#!/bin/bash -e + +INSTANCE="$1" + +if [ -z "$INSTANCE" ]; then + echo "first argument must be the GCE instance (for example, mar280632-365378)" + exit 1 +fi + +# wait for the instance to exist +until gcloud compute --project=snapd-spread instances describe "$INSTANCE" --zone=us-east1-b >/dev/null 2>&1; do + echo "waiting for instance to exist" + sleep 1 +done + +OUTPUT_FILE="console-output-$INSTANCE.txt" + +backgroundscript=$(mktemp --suffix=.gce-watcher) +cat >> "$backgroundscript" << 'EOF' +INSTANCE="$1" +OUTPUT_FILE="$2" +next=0 +truncate -s0 "$OUTPUT_FILE" +while true; do + # The get-serial-port-output command will print on the stdout the new lines + # that the machine emitted on the serial console since the last time it was + # queried. The bookmark is the number ("$next") that we pass with the + # --start parameter; this number is printed by gcloud to the stderr in this + # form: + # + # Specify --start=130061 in the next get-serial-port-output invocation to get only the new output starting from here. + # + # In the subshell below we compute the value of the "$next" variable: we + # store the original stdout into the console-output-bits.txt file, then + # (via a third file descriptor, not to mess with the original stdout) we + # redirect the stderr into the stdout and use "grep" to extract the + # suggested value for the --start parameter. + next=$( + gcloud compute \ + --project=snapd-spread \ + instances get-serial-port-output "$INSTANCE" \ + --start="$next" \ + --zone=us-east1-b 3>&1 1>"${OUTPUT_FILE}-bits.txt" 2>&3- | grep -Po -- '--start=\K[0-9]+') + trimmedConsoleSnippet="$(sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' < ${OUTPUT_FILE}-bits.txt)" + if [ -n "$trimmedConsoleSnippet" ]; then + echo "$trimmedConsoleSnippet" >> "$OUTPUT_FILE" + fi + sleep 1 +done +EOF + +# start collecting the console output +bash "$backgroundscript" "$INSTANCE" "$OUTPUT_FILE" & + +on_exit() { + # Restore the signal handlers to avoid recursion + trap - INT TERM QUIT EXIT + rm -f "$OUTPUT_FILE" "$OUTPUT_FILE-bits.txt" + # kill all processes in this group + kill 0 +} +trap "on_exit" INT TERM QUIT EXIT + +# wait for it to appear +until [ -f "$OUTPUT_FILE" ]; do + sleep 1 +done + +# watch it +tail -f "$OUTPUT_FILE" diff -Nru snapd-2.55.5+20.04/debug-tools/snap-debug-info.sh snapd-2.57.5+20.04/debug-tools/snap-debug-info.sh --- snapd-2.55.5+20.04/debug-tools/snap-debug-info.sh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/debug-tools/snap-debug-info.sh 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,68 @@ +#!/bin/bash + +# some commands need either root or sudo permissions, so check for that early +if [ "$(id -u)" != 0 ]; then + if ! sudo echo "authentication as root successful"; then + echo "this script needs to be run as root or use sudo permission" + exit 1 + fi +fi + +h1(){ echo -e "\n==================== $* ===================="; } +h2(){ echo -e "\n========== $* =========="; } +h3(){ echo -e "\n===== $* ====="; } + +h1 "SNAP VERSION"; snap version +h1 "SNAP WHOAMI"; snap whoami +h1 "SNAP MODEL"; snap model --verbose +h1 "SNAP MODEL SERIAL"; snap model --serial --verbose +h1 "SNAP LIST"; snap list --all +h1 "SNAP SERVICES"; snap services +h1 "SNAP CONNECTIONS"; snap connections + +h1 "PER-SNAP CONNECTIONS" +for sn in $(snap list | awk 'NR>1 {print $1}'); do + h2 "PER-SNAP $sn CONNECTIONS" + snap connections "$sn" +done +h1 "SNAP CHANGES" +snap changes --abs-time + +GADGET_SNAP="$(snap list | awk '($6 ~ /.*gadget.*$/) {print $1}')" +if [ -z "$GADGET_SNAP" ]; then + # could be a serious bug/problem or otherwise could be just on a classic + # device + h1 "NO GADGET SNAP DETECTED" +else + h1 "GADGET SNAP GADGET.YAML" + cat /snap/"$(snap list | awk '($6 ~ /.*gadget.*$/) {print $1}')"/current/meta/gadget.yaml +fi + +h1 "SNAP CHANGES (in Doing)" +# print off the output of snap tasks for every chg that is in Doing state +for chg in $(snap changes | tail -n +2 | grep -Po '(?:[0-9]+\s+Doing)' | awk '{print $1}'); do + h3 "tasks for $chg" + snap tasks "$chg" --abs-time +done + +h1 "SNAP CHANGES (in Error)" +# same as above, just for Error instead of Doing +for chg in $(snap changes | tail -n +2 | grep -Po '(?:[0-9]+\s+Error)' | awk '{print $1}'); do + h3 "tasks for $chg" + snap tasks "$chg" --abs-time +done + +h1 "VALIDATION SET ASSERTIONS" +snap known validation-set + +# sudo needed for these commands +h1 "VALIDATION SETS"; sudo snap validate +h1 "OFFLINE SNAP CHANGES"; sudo snap debug state --abs-time --changes /var/lib/snapd/state.json +h1 "SNAPD STACKTRACE"; sudo snap debug stacktraces +h1 "SNAP SYSTEM CONFIG"; sudo snap get system -d +h1 "SNAPD JOURNAL"; sudo journalctl --no-pager -u snapd +h1 "SNAPD.SERVICE STATUS"; sudo systemctl --no-pager status snapd +h1 "UPTIME"; uptime +h1 "DATE (IN UTC)"; date --utc +h1 "DISK SPACE"; df -h +h1 "DENIED MESSAGES"; sudo journalctl --no-pager | grep DENIED diff -Nru snapd-2.55.5+20.04/debug-tools/startup-timings snapd-2.57.5+20.04/debug-tools/startup-timings --- snapd-2.55.5+20.04/debug-tools/startup-timings 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/debug-tools/startup-timings 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import json +import re +import logging +import argparse + + +def parse_arguments(): + parser = argparse.ArgumentParser(description="startup timings parser") + parser.add_argument("log", help="snap startup log") + parser.add_argument( + "-v", "--verbose", help="verbose", action="store_true", default=False + ) + return parser.parse_args() + + +def main(opts): + if opts.verbose: + logging.basicConfig(level=logging.DEBUG) + + with open(opts.log, encoding="utf-8") as inf: + lines = inf.readlines() + + steps = [] + for line in lines: + match = re.match(r".*-- snap startup ({.*})", line) + if match: + logging.debug("got match: %s", match.group(1)) + rawdata = json.loads(match.group(1)) + steps.append(rawdata) + if not steps: + print("no logs found") + + total = 0.0 + for idx, current in enumerate(steps): + if idx == 0: + last = current + continue + diff = float(current["time"]) - float(last["time"]) + total += diff + print("{2:3f}s\t{0} -> {1}".format(last["stage"], current["stage"], diff)) + last = current + + print("approx. total: {0:3f}s".format(total)) + + +if __name__ == "__main__": + main(parse_arguments()) diff -Nru snapd-2.55.5+20.04/desktop/notification/gtk.go snapd-2.57.5+20.04/desktop/notification/gtk.go --- snapd-2.55.5+20.04/desktop/notification/gtk.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/desktop/notification/gtk.go 2022-10-17 16:25:18.000000000 +0000 @@ -44,7 +44,7 @@ // If the D-Bus service is not already running, assume it is // not available. // Use owner to verify that the return values of the method call have the - // types we expect, which is generally a good sanity check. + // types we expect, which is generally a good validity check. var owner string if err := conn.BusObject().Call("org.freedesktop.DBus.GetNameOwner", 0, gtkBusName).Store(&owner); err != nil { return nil, err diff -Nru snapd-2.55.5+20.04/dirs/dirs.go snapd-2.57.5+20.04/dirs/dirs.go --- snapd-2.55.5+20.04/dirs/dirs.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/dirs/dirs.go 2022-10-17 16:25:18.000000000 +0000 @@ -33,33 +33,34 @@ var ( GlobalRootDir string + RunDir string + SnapMountDir string DistroLibExecDir string HiddenSnapDataHomeGlob string - SnapBlobDir string - SnapDataDir string - SnapDataHomeGlob string - SnapDownloadCacheDir string - SnapAppArmorDir string - SnapAppArmorAdditionalDir string - SnapConfineAppArmorDir string - SnapSeccompBase string - SnapSeccompDir string - SnapMountPolicyDir string - SnapUdevRulesDir string - SnapKModModulesDir string - SnapKModModprobeDir string - LocaleDir string - SnapdSocket string - SnapSocket string - SnapRunDir string - SnapRunNsDir string - SnapRunLockDir string - SnapBootstrapRunDir string - SnapVoidDir string + SnapBlobDir string + SnapDataDir string + SnapDataHomeGlob string + SnapDownloadCacheDir string + SnapAppArmorDir string + SnapConfineAppArmorDir string + SnapSeccompBase string + SnapSeccompDir string + SnapMountPolicyDir string + SnapUdevRulesDir string + SnapKModModulesDir string + SnapKModModprobeDir string + LocaleDir string + SnapdSocket string + SnapSocket string + SnapRunDir string + SnapRunNsDir string + SnapRunLockDir string + SnapBootstrapRunDir string + SnapVoidDir string SnapdMaintenanceFile string @@ -100,6 +101,8 @@ SnapDesktopFilesDir string SnapDesktopIconsDir string SnapPolkitPolicyDir string + SnapSystemdDir string + SnapSystemdRunDir string SnapDBusSessionPolicyDir string SnapDBusSystemPolicyDir string @@ -111,6 +114,7 @@ SnapFDEDir string SnapSaveDir string SnapDeviceSaveDir string + SnapDataSaveDir string CloudMetaDataFile string CloudInstanceDataFile string @@ -121,6 +125,8 @@ XdgRuntimeDirGlob string CompletionHelperInCore string + BashCompletionScript string + LegacyCompletersDir string CompletersDir string SystemFontsDir string @@ -355,7 +361,6 @@ HiddenSnapDataHomeGlob = filepath.Join(rootdir, "/home/*/", HiddenSnapDataHomeDir) SnapAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "profiles") SnapConfineAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "snap-confine") - SnapAppArmorAdditionalDir = filepath.Join(rootdir, snappyDir, "apparmor", "additional") SnapDownloadCacheDir = filepath.Join(rootdir, snappyDir, "cache") SnapSeccompBase = filepath.Join(rootdir, snappyDir, "seccomp") SnapSeccompDir = filepath.Join(SnapSeccompBase, "bpf") @@ -368,6 +373,7 @@ // freedesktop.org specifications SnapDesktopFilesDir = filepath.Join(rootdir, snappyDir, "desktop", "applications") SnapDesktopIconsDir = filepath.Join(rootdir, snappyDir, "desktop", "icons") + RunDir = filepath.Join(rootdir, "/run") SnapRunDir = filepath.Join(rootdir, "/run/snapd") SnapRunNsDir = filepath.Join(SnapRunDir, "/ns") SnapRunLockDir = filepath.Join(SnapRunDir, "/lock") @@ -403,6 +409,7 @@ SnapFDEDir = SnapFDEDirUnder(rootdir) SnapSaveDir = SnapSaveDirUnder(rootdir) SnapDeviceSaveDir = filepath.Join(SnapSaveDir, "device") + SnapDataSaveDir = filepath.Join(SnapSaveDir, "snap") SnapRepairDir = filepath.Join(rootdir, snappyDir, "repair") SnapRepairStateFile = filepath.Join(SnapRepairDir, "repair.json") @@ -417,6 +424,8 @@ SnapRuntimeServicesDir = filepath.Join(rootdir, "/run/systemd/system") SnapUserServicesDir = filepath.Join(rootdir, "/etc/systemd/user") SnapSystemdConfDir = SnapSystemdConfDirUnder(rootdir) + SnapSystemdDir = filepath.Join(rootdir, "/etc/systemd") + SnapSystemdRunDir = filepath.Join(rootdir, "/run/systemd") SnapDBusSystemPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/system.d") SnapDBusSessionPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/session.d") @@ -468,7 +477,9 @@ XdgRuntimeDirGlob = filepath.Join(XdgRuntimeDirBase, "*/") CompletionHelperInCore = filepath.Join(CoreLibExecDir, "etelpmoc.sh") - CompletersDir = filepath.Join(rootdir, "/usr/share/bash-completion/completions/") + BashCompletionScript = filepath.Join(rootdir, "/usr/share/bash-completion/bash_completion") + LegacyCompletersDir = filepath.Join(rootdir, "/usr/share/bash-completion/completions/") + CompletersDir = filepath.Join(rootdir, snappyDir, "desktop/bash-completion/completions/") // These paths agree across all supported distros SystemFontsDir = filepath.Join(rootdir, "/usr/share/fonts") diff -Nru snapd-2.55.5+20.04/features/features.go snapd-2.57.5+20.04/features/features.go --- snapd-2.55.5+20.04/features/features.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/features/features.go 2022-10-17 16:25:18.000000000 +0000 @@ -53,6 +53,8 @@ DbusActivation // HiddenSnapDataHomeDir controls if the snaps' data dir is ~/.snap/data instead of ~/snap HiddenSnapDataHomeDir + // MoveSnapHomeDir controls whether snap user data under ~/snap (or ~/.snap/data) can be moved to ~/Snap. + MoveSnapHomeDir // CheckDiskSpaceRemove controls free disk space check on remove whenever automatic snapshot needs to be created. CheckDiskSpaceRemove // CheckDiskSpaceInstall controls free disk space check on snap install. @@ -95,6 +97,7 @@ DbusActivation: "dbus-activation", HiddenSnapDataHomeDir: "hidden-snap-folder", + MoveSnapHomeDir: "move-snap-home-dir", CheckDiskSpaceInstall: "check-disk-space-install", CheckDiskSpaceRefresh: "check-disk-space-refresh", @@ -123,6 +126,7 @@ ClassicPreservesXdgRuntimeDir: true, RobustMountNamespaceUpdates: true, HiddenSnapDataHomeDir: true, + MoveSnapHomeDir: true, } // String returns the name of a snapd feature. diff -Nru snapd-2.55.5+20.04/features/features_test.go snapd-2.57.5+20.04/features/features_test.go --- snapd-2.55.5+20.04/features/features_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/features/features_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -50,6 +50,7 @@ c.Check(features.UserDaemons.String(), Equals, "user-daemons") c.Check(features.DbusActivation.String(), Equals, "dbus-activation") c.Check(features.HiddenSnapDataHomeDir.String(), Equals, "hidden-snap-folder") + c.Check(features.MoveSnapHomeDir.String(), Equals, "move-snap-home-dir") c.Check(features.CheckDiskSpaceInstall.String(), Equals, "check-disk-space-install") c.Check(features.CheckDiskSpaceRefresh.String(), Equals, "check-disk-space-refresh") c.Check(features.CheckDiskSpaceRemove.String(), Equals, "check-disk-space-remove") @@ -79,6 +80,7 @@ c.Check(features.UserDaemons.IsExported(), Equals, false) c.Check(features.DbusActivation.IsExported(), Equals, false) c.Check(features.HiddenSnapDataHomeDir.IsExported(), Equals, true) + c.Check(features.MoveSnapHomeDir.IsExported(), Equals, true) c.Check(features.CheckDiskSpaceInstall.IsExported(), Equals, false) c.Check(features.CheckDiskSpaceRefresh.IsExported(), Equals, false) c.Check(features.CheckDiskSpaceRemove.IsExported(), Equals, false) @@ -116,6 +118,7 @@ c.Check(features.UserDaemons.IsEnabledWhenUnset(), Equals, false) c.Check(features.DbusActivation.IsEnabledWhenUnset(), Equals, true) c.Check(features.HiddenSnapDataHomeDir.IsEnabledWhenUnset(), Equals, false) + c.Check(features.MoveSnapHomeDir.IsEnabledWhenUnset(), Equals, false) c.Check(features.CheckDiskSpaceInstall.IsEnabledWhenUnset(), Equals, false) c.Check(features.CheckDiskSpaceRefresh.IsEnabledWhenUnset(), Equals, false) c.Check(features.CheckDiskSpaceRemove.IsEnabledWhenUnset(), Equals, false) @@ -128,6 +131,7 @@ c.Check(features.ParallelInstances.ControlFile(), Equals, "/var/lib/snapd/features/parallel-instances") c.Check(features.RobustMountNamespaceUpdates.ControlFile(), Equals, "/var/lib/snapd/features/robust-mount-namespace-updates") c.Check(features.HiddenSnapDataHomeDir.ControlFile(), Equals, "/var/lib/snapd/features/hidden-snap-folder") + c.Check(features.MoveSnapHomeDir.ControlFile(), Equals, "/var/lib/snapd/features/move-snap-home-dir") // Features that are not exported don't have a control file. c.Check(features.Layouts.ControlFile, PanicMatches, `cannot compute the control file of feature "layouts" because that feature is not exported`) } diff -Nru snapd-2.55.5+20.04/gadget/device/encrypt.go snapd-2.57.5+20.04/gadget/device/encrypt.go --- snapd-2.55.5+20.04/gadget/device/encrypt.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/device/encrypt.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,140 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 device + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +// encryptionMarkerUnder returns the path of the encrypted system marker under a +// given directory. +func encryptionMarkerUnder(deviceFDEDir string) string { + return filepath.Join(deviceFDEDir, "marker") +} + +// HasEncryptedMarkerUnder returns true when there is an encryption marker in a +// given directory. +func HasEncryptedMarkerUnder(deviceFDEDir string) bool { + return osutil.FileExists(encryptionMarkerUnder(deviceFDEDir)) +} + +// ReadEncryptionMarkers reads the encryption marker files at the appropriate +// locations. +func ReadEncryptionMarkers(dataFDEDir, saveFDEDir string) ([]byte, []byte, error) { + marker1, err := ioutil.ReadFile(encryptionMarkerUnder(dataFDEDir)) + if err != nil { + return nil, nil, err + } + marker2, err := ioutil.ReadFile(encryptionMarkerUnder(saveFDEDir)) + if err != nil { + return nil, nil, err + } + return marker1, marker2, nil +} + +// WriteEncryptionMarkers writes the encryption marker files at the appropriate +// locations. +func WriteEncryptionMarkers(dataFDEDir, saveFDEDir string, markerSecret []byte) error { + err := osutil.AtomicWriteFile(encryptionMarkerUnder(dataFDEDir), markerSecret, 0600, 0) + if err != nil { + return err + } + return osutil.AtomicWriteFile(encryptionMarkerUnder(saveFDEDir), markerSecret, 0600, 0) +} + +// DataSealedKeyUnder returns the path of the sealed key for ubuntu-data. +func DataSealedKeyUnder(deviceFDEDir string) string { + return filepath.Join(deviceFDEDir, "ubuntu-data.sealed-key") +} + +// SaveKeyUnder returns the path of a plain encryption key for ubuntu-save. +func SaveKeyUnder(deviceFDEDir string) string { + return filepath.Join(deviceFDEDir, "ubuntu-save.key") +} + +// RecoveryKeyUnder returns the path of the recovery key. +func RecoveryKeyUnder(deviceFDEDir string) string { + return filepath.Join(deviceFDEDir, "recovery.key") +} + +// FallbackDataSealedKeyUnder returns the path of a fallback ubuntu data key. +func FallbackDataSealedKeyUnder(seedDeviceFDEDir string) string { + return filepath.Join(seedDeviceFDEDir, "ubuntu-data.recovery.sealed-key") +} + +// FallbackSaveSealedKeyUnder returns the path of a fallback ubuntu save key. +func FallbackSaveSealedKeyUnder(seedDeviceFDEDir string) string { + return filepath.Join(seedDeviceFDEDir, "ubuntu-save.recovery.sealed-key") +} + +// FactoryResetFallbackSaveSealedKeyUnder returns the path of a fallback ubuntu +// save key object generated during factory reset. +func FactoryResetFallbackSaveSealedKeyUnder(seedDeviceFDEDir string) string { + return filepath.Join(seedDeviceFDEDir, "ubuntu-save.recovery.sealed-key.factory-reset") +} + +// TpmLockoutAuthUnder return the path of the tpm lockout authority key. +func TpmLockoutAuthUnder(saveDeviceFDEDir string) string { + return filepath.Join(saveDeviceFDEDir, "tpm-lockout-auth") +} + +/// ErrNoSealedKeys error if there are no sealed keys +var ErrNoSealedKeys = errors.New("no sealed keys") + +// SealingMethod represents the sealing method +type SealingMethod string + +const ( + SealingMethodLegacyTPM = SealingMethod("") + SealingMethodTPM = SealingMethod("tpm") + SealingMethodFDESetupHook = SealingMethod("fde-setup-hook") +) + +// StampSealedKeys writes what sealing method was used for key sealing +func StampSealedKeys(rootdir string, content SealingMethod) error { + stamp := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "sealed-keys") + if err := os.MkdirAll(filepath.Dir(stamp), 0755); err != nil { + return fmt.Errorf("cannot create device fde state directory: %v", err) + } + + if err := osutil.AtomicWriteFile(stamp, []byte(content), 0644, 0); err != nil { + return fmt.Errorf("cannot create fde sealed keys stamp file: %v", err) + } + return nil +} + +// SealedKeysMethod return whether any keys were sealed at all +func SealedKeysMethod(rootdir string) (sm SealingMethod, err error) { + // TODO:UC20: consider more than the marker for cases where we reseal + // outside of run mode + stamp := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "sealed-keys") + content, err := ioutil.ReadFile(stamp) + if os.IsNotExist(err) { + return sm, ErrNoSealedKeys + } + return SealingMethod(content), err +} diff -Nru snapd-2.55.5+20.04/gadget/device/encrypt_test.go snapd-2.57.5+20.04/gadget/device/encrypt_test.go --- snapd-2.55.5+20.04/gadget/device/encrypt_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/device/encrypt_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,153 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 device_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget/device" + "github.com/snapcore/snapd/testutil" +) + +func TestT(t *testing.T) { + TestingT(t) +} + +type deviceSuite struct{} + +var _ = Suite(&deviceSuite{}) + +func (s *deviceSuite) TestEncryptionMarkersRunThrough(c *C) { + d := c.MkDir() + c.Check(device.HasEncryptedMarkerUnder(d), Equals, false) + + c.Assert(os.MkdirAll(filepath.Join(d, boot.InstallHostFDEDataDir), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(d, boot.InstallHostFDESaveDir), 0755), IsNil) + + // nothing was written yet + c.Check(device.HasEncryptedMarkerUnder(filepath.Join(d, boot.InstallHostFDEDataDir)), Equals, false) + c.Check(device.HasEncryptedMarkerUnder(filepath.Join(d, boot.InstallHostFDESaveDir)), Equals, false) + + err := device.WriteEncryptionMarkers(filepath.Join(d, boot.InstallHostFDEDataDir), filepath.Join(d, boot.InstallHostFDESaveDir), []byte("foo")) + c.Assert(err, IsNil) + // both markers were written + c.Check(filepath.Join(d, boot.InstallHostFDEDataDir, "marker"), testutil.FileEquals, "foo") + c.Check(filepath.Join(d, boot.InstallHostFDESaveDir, "marker"), testutil.FileEquals, "foo") + // and can be read with device.ReadEncryptionMarkers + m1, m2, err := device.ReadEncryptionMarkers(filepath.Join(d, boot.InstallHostFDEDataDir), filepath.Join(d, boot.InstallHostFDESaveDir)) + c.Assert(err, IsNil) + c.Check(m1, DeepEquals, []byte("foo")) + c.Check(m2, DeepEquals, []byte("foo")) + // and are found via HasEncryptedMarkerUnder() + c.Check(device.HasEncryptedMarkerUnder(filepath.Join(d, boot.InstallHostFDEDataDir)), Equals, true) + c.Check(device.HasEncryptedMarkerUnder(filepath.Join(d, boot.InstallHostFDESaveDir)), Equals, true) +} + +func (s *deviceSuite) TestReadEncryptionMarkers(c *C) { + tmpdir := c.MkDir() + + // simulate two different markers in "ubuntu-data" and "ubuntu-save" + p1 := filepath.Join(tmpdir, boot.InstallHostFDEDataDir, "marker") + err := os.MkdirAll(filepath.Dir(p1), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(p1, []byte("marker-p1"), 0600) + c.Assert(err, IsNil) + + p2 := filepath.Join(tmpdir, boot.InstallHostFDESaveDir, "marker") + err = os.MkdirAll(filepath.Dir(p2), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(p2, []byte("marker-p2"), 0600) + c.Assert(err, IsNil) + + // reading them returns the two different values + m1, m2, err := device.ReadEncryptionMarkers(filepath.Join(tmpdir, boot.InstallHostFDEDataDir), filepath.Join(tmpdir, boot.InstallHostFDESaveDir)) + c.Assert(err, IsNil) + c.Check(m1, DeepEquals, []byte("marker-p1")) + c.Check(m2, DeepEquals, []byte("marker-p2")) +} + +func (s *deviceSuite) TestLocations(c *C) { + c.Check(device.DataSealedKeyUnder(boot.InitramfsBootEncryptionKeyDir), Equals, + "/run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key") + c.Check(device.SaveKeyUnder(dirs.SnapFDEDir), Equals, + "/var/lib/snapd/device/fde/ubuntu-save.key") + c.Check(device.FallbackDataSealedKeyUnder(boot.InitramfsSeedEncryptionKeyDir), Equals, + "/run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key") + c.Check(device.FallbackSaveSealedKeyUnder(boot.InitramfsSeedEncryptionKeyDir), Equals, + "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key") + c.Check(device.FactoryResetFallbackSaveSealedKeyUnder(boot.InitramfsSeedEncryptionKeyDir), Equals, + "/run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key.factory-reset") + + c.Check(device.TpmLockoutAuthUnder(dirs.SnapFDEDirUnderSave(dirs.SnapSaveDir)), Equals, + "/var/lib/snapd/save/device/fde/tpm-lockout-auth") +} + +func (s *deviceSuite) TestStampSealedKeysRunthrough(c *C) { + root := c.MkDir() + + for _, tc := range []struct { + mth device.SealingMethod + expected string + }{ + {device.SealingMethodLegacyTPM, ""}, + {device.SealingMethodTPM, "tpm"}, + {device.SealingMethodFDESetupHook, "fde-setup-hook"}, + } { + err := device.StampSealedKeys(root, tc.mth) + c.Assert(err, IsNil) + + mth, err := device.SealedKeysMethod(root) + c.Assert(err, IsNil) + c.Check(tc.mth, Equals, mth) + + content, err := ioutil.ReadFile(filepath.Join(root, "/var/lib/snapd/device/fde/sealed-keys")) + c.Assert(err, IsNil) + c.Check(string(content), Equals, tc.expected) + } +} + +func (s *deviceSuite) TestSealedKeysMethodWithMissingStamp(c *C) { + root := c.MkDir() + + _, err := device.SealedKeysMethod(root) + c.Check(err, Equals, device.ErrNoSealedKeys) +} + +func (s *deviceSuite) TestSealedKeysMethodWithWrongContentHappy(c *C) { + root := c.MkDir() + + mockSealedKeyPath := filepath.Join(root, "/var/lib/snapd/device/fde/sealed-keys") + err := os.MkdirAll(filepath.Dir(mockSealedKeyPath), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(mockSealedKeyPath, []byte("invalid-sealing-method"), 0600) + c.Assert(err, IsNil) + + // invalid/unknown sealing methods do not error + mth, err := device.SealedKeysMethod(root) + c.Check(err, IsNil) + c.Check(string(mth), Equals, "invalid-sealing-method") +} diff -Nru snapd-2.55.5+20.04/gadget/device_darwin.go snapd-2.57.5+20.04/gadget/device_darwin.go --- snapd-2.55.5+20.04/gadget/device_darwin.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/device_darwin.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,8 +21,6 @@ import ( "errors" - - "github.com/snapcore/snapd/gadget/quantity" ) var errNotImplemented = errors.New("not implemented") @@ -30,11 +28,3 @@ func FindDeviceForStructure(ps *LaidOutStructure) (string, error) { return "", errNotImplemented } - -func findDeviceForStructureWithFallback(ps *LaidOutStructure) (string, quantity.Offset, error) { - return "", 0, errNotImplemented -} - -func findMountPointForStructure(ps *LaidOutStructure) (string, error) { - return "", errNotImplemented -} diff -Nru snapd-2.55.5+20.04/gadget/device_linux.go snapd-2.57.5+20.04/gadget/device_linux.go --- snapd-2.55.5+20.04/gadget/device_linux.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/device_linux.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,12 +21,9 @@ import ( "fmt" - "os" "path/filepath" - "strings" "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" ) @@ -89,209 +86,3 @@ return found, nil } - -// findDeviceForStructureWithFallback attempts to find an existing block device -// partition containing given non-filesystem volume structure, by inspecting the -// structure's name. -// -// Should there be no match, attempts to find the block device corresponding to -// the volume enclosing the structure under the following conditions: -// - the structure has no filesystem -// - and the structure is of type: bare (no partition table entry) -// - or the structure has no name, but a partition table entry (hence no label -// by which we could find it) -// -// The fallback mechanism uses the fact that Core devices always have a mount at -// /writable. The system is booted from the parent of the device mounted at -// /writable. -// -// Returns the device name and an offset at which the structure content starts -// within the device or an error. -func findDeviceForStructureWithFallback(ps *LaidOutStructure) (dev string, offs quantity.Offset, err error) { - if ps.HasFilesystem() { - return "", 0, fmt.Errorf("internal error: cannot use with filesystem structures") - } - - dev, err = FindDeviceForStructure(ps) - if err == nil { - // found exact device representing this structure, thus the - // structure starts at 0 offset within the device - return dev, 0, nil - } - if err != ErrDeviceNotFound { - // error out on other errors - return "", 0, err - } - if err == ErrDeviceNotFound && ps.IsPartition() && ps.Name != "" { - // structures with partition table entry and a name must have - // been located already - return "", 0, err - } - - // we're left with structures that have no partition table entry, or - // have a partition but no name that could be used to find them - - dev, err = findParentDeviceWithWritableFallback() - if err != nil { - return "", 0, err - } - // start offset is calculated as an absolute position within the volume - return dev, ps.StartOffset, nil -} - -// findMountPointForStructure locates a mount point of a device that matches -// given structure. The structure must have a filesystem defined, otherwise an -// error is raised. -func findMountPointForStructure(ps *LaidOutStructure) (string, error) { - if !ps.HasFilesystem() { - return "", ErrNoFilesystemDefined - } - - devpath, err := FindDeviceForStructure(ps) - if err != nil { - return "", err - } - - var mountPoint string - mountInfo, err := osutil.LoadMountInfo() - if err != nil { - return "", fmt.Errorf("cannot read mount info: %v", err) - } - for _, entry := range mountInfo { - if entry.Root != "/" { - // only interested at the location where root of the - // structure filesystem is mounted - continue - } - if entry.MountSource == devpath && entry.FsType == ps.Filesystem { - mountPoint = entry.MountDir - break - } - } - - if mountPoint == "" { - return "", ErrMountNotFound - } - - return mountPoint, nil -} - -func isWritableMount(entry *osutil.MountInfoEntry) bool { - // example mountinfo entry: - // 26 27 8:3 / /writable rw,relatime shared:7 - ext4 /dev/sda3 rw,data=ordered - return entry.Root == "/" && entry.MountDir == "/writable" && entry.FsType == "ext4" -} - -func findDeviceForWritable() (device string, err error) { - mountInfo, err := osutil.LoadMountInfo() - if err != nil { - return "", fmt.Errorf("cannot read mount info: %v", err) - } - for _, entry := range mountInfo { - if isWritableMount(entry) { - return entry.MountSource, nil - } - } - return "", ErrDeviceNotFound -} - -func findParentDeviceWithWritableFallback() (string, error) { - partitionWritable, err := findDeviceForWritable() - if err != nil { - return "", err - } - return ParentDiskFromMountSource(partitionWritable) -} - -// ParentDiskFromMountSource will find the parent disk device for the given -// partition. E.g. /dev/nvmen0n1p5 -> /dev/nvme0n1. -// -// When the mount source is a symlink, it is resolved to the actual device that -// is mounted. Should the device be one created by device mapper, it is followed -// up to the actual underlying block device. As an example, this is how devices -// are followed with a /writable mounted from an encrypted volume: -// -// /dev/mapper/ubuntu-data- (a symlink) -// ⤷ /dev/dm-0 (set up by device mapper) -// ⤷ /dev/hda4 (actual partition with the content) -// ⤷ /dev/hda (returned by this function) -// -func ParentDiskFromMountSource(mountSource string) (string, error) { - // mount source can be a symlink - st, err := os.Lstat(mountSource) - if err != nil { - return "", err - } - if mode := st.Mode(); mode&os.ModeSymlink != 0 { - // resolve to actual device - target, err := filepath.EvalSymlinks(mountSource) - if err != nil { - return "", fmt.Errorf("cannot resolve mount source symlink %v: %v", mountSource, err) - } - mountSource = target - } - // /dev/sda3 -> sda3 - devname := filepath.Base(mountSource) - - if strings.HasPrefix(devname, "dm-") { - // looks like a device set up by device mapper - resolved, err := resolveParentOfDeviceMapperDevice(devname) - if err != nil { - return "", fmt.Errorf("cannot resolve device mapper device %v: %v", devname, err) - } - devname = resolved - } - - // do not bother with investigating major/minor devices (inconsistent - // across block device types) or mangling strings, but look at sys - // hierarchy for block devices instead: - // /sys/block/sda - main SCSI device - // /sys/block/sda/sda1 - partition 1 - // /sys/block/sda/sda - partition n - // /sys/block/nvme0n1 - main NVME device - // /sys/block/nvme0n1/nvme0n1p1 - partition 1 - matches, err := filepath.Glob(filepath.Join(dirs.GlobalRootDir, "/sys/block/*/", devname)) - if err != nil { - return "", fmt.Errorf("cannot glob /sys/block/ entries: %v", err) - } - if len(matches) != 1 { - return "", fmt.Errorf("unexpected number of matches (%v) for /sys/block/*/%s", len(matches), devname) - } - - // at this point we have /sys/block/sda/sda3 - // /sys/block/sda/sda3 -> /dev/sda - mainDev := filepath.Join(dirs.GlobalRootDir, "/dev/", filepath.Base(filepath.Dir(matches[0]))) - - if !osutil.FileExists(mainDev) { - return "", fmt.Errorf("device %v does not exist", mainDev) - } - return mainDev, nil -} - -func resolveParentOfDeviceMapperDevice(devname string) (string, error) { - // devices set up by device mapper have /dev/block/dm-*/slaves directory - // which lists the devices that are upper in the chain, follow that to - // find the first device that is non-dm one - dmSlavesLevel := 0 - const maxDmSlavesLevel = 5 - for strings.HasPrefix(devname, "dm-") { - // /sys/block/dm-*/slaves/ lists a device that this dm device is part of - slavesGlob := filepath.Join(dirs.GlobalRootDir, "/sys/block", devname, "slaves/*") - slaves, err := filepath.Glob(slavesGlob) - if err != nil { - return "", fmt.Errorf("cannot glob slaves of dm device %v: %v", devname, err) - } - if len(slaves) != 1 { - return "", fmt.Errorf("unexpected number of dm device %v slaves: %v", devname, len(slaves)) - } - devname = filepath.Base(slaves[0]) - - // if we're this deep in resolving dm devices, things are clearly getting out of hand - dmSlavesLevel++ - if dmSlavesLevel >= maxDmSlavesLevel { - return "", fmt.Errorf("too many levels") - } - - } - return devname, nil -} diff -Nru snapd-2.55.5+20.04/gadget/device_test.go snapd-2.57.5+20.04/gadget/device_test.go --- snapd-2.55.5+20.04/gadget/device_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/device_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,18 +21,14 @@ import ( "errors" - "fmt" "io/ioutil" "os" "path/filepath" - "strings" . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" - "github.com/snapcore/snapd/gadget/quantity" - "github.com/snapcore/snapd/osutil" ) type deviceSuite struct { @@ -59,52 +55,6 @@ dirs.SetRootDir("/") } -func (d *deviceSuite) setupMockSysfs(c *C) { - // setup everything for 'writable' - err := ioutil.WriteFile(filepath.Join(d.dir, "/dev/fakedevice0p1"), []byte(""), 0644) - c.Assert(err, IsNil) - err = os.Symlink("../../fakedevice0p1", filepath.Join(d.dir, "/dev/disk/by-label/writable")) - c.Assert(err, IsNil) - // make parent device - err = ioutil.WriteFile(filepath.Join(d.dir, "/dev/fakedevice0"), []byte(""), 0644) - c.Assert(err, IsNil) - // and fake /sys/block structure - err = os.MkdirAll(filepath.Join(d.dir, "/sys/block/fakedevice0/fakedevice0p1"), 0755) - c.Assert(err, IsNil) -} - -func (d *deviceSuite) setupMockSysfsForDevMapper(c *C) { - // setup a mock /dev/mapper environment (incomplete we have no "happy" - // test; use a complex setup that mimics LVM in LUKS: - // /dev/mapper/data_crypt (symlink) - // ⤷ /dev/dm-1 (LVM) - // ⤷ /dev/dm-0 (LUKS) - // ⤷ /dev/fakedevice0 (actual device) - err := ioutil.WriteFile(filepath.Join(d.dir, "/dev/dm-0"), nil, 0644) - c.Assert(err, IsNil) - err = ioutil.WriteFile(filepath.Join(d.dir, "/dev/dm-1"), nil, 0644) - c.Assert(err, IsNil) - err = ioutil.WriteFile(filepath.Join(d.dir, "/dev/fakedevice0"), []byte(""), 0644) - c.Assert(err, IsNil) - err = ioutil.WriteFile(filepath.Join(d.dir, "/dev/fakedevice"), []byte(""), 0644) - c.Assert(err, IsNil) - // symlinks added by dm/udev are relative - err = os.Symlink("../dm-1", filepath.Join(d.dir, "/dev/mapper/data_crypt")) - c.Assert(err, IsNil) - err = os.MkdirAll(filepath.Join(d.dir, "/sys/block/dm-1/slaves/"), 0755) - c.Assert(err, IsNil) - // sys symlinks are relative too - err = os.Symlink("../../dm-0", filepath.Join(d.dir, "/sys/block/dm-1/slaves/dm-0")) - c.Assert(err, IsNil) - err = os.MkdirAll(filepath.Join(d.dir, "/sys/block/dm-0/slaves/"), 0755) - c.Assert(err, IsNil) - // real symlink would point to ../../../..///block/fakedevice/fakedevice0 - err = os.Symlink("../../../../fakedevice/fakedevice0", filepath.Join(d.dir, "/sys/block/dm-0/slaves/fakedevice0")) - c.Assert(err, IsNil) - err = os.MkdirAll(filepath.Join(d.dir, "/sys/block/fakedevice/fakedevice0"), 0755) - c.Assert(err, IsNil) -} - func (d *deviceSuite) TestDeviceFindByStructureName(c *C) { names := []struct { escaped string @@ -315,445 +265,3 @@ c.Check(err, ErrorMatches, `cannot read device link: failed`) c.Check(found, Equals, "") } - -var writableMountInfoFmt = `26 27 8:3 / /writable rw,relatime shared:7 - ext4 %s/dev/fakedevice0p1 rw,data=ordered` - -func (d *deviceSuite) TestDeviceFindFallbackNotFoundNoWritable(c *C) { - badMountInfoFmt := `26 27 8:3 / /not-writable rw,relatime shared:7 - ext4 %s/dev/fakedevice0p1 rw,data=ordered` - restore := osutil.MockMountInfo(fmt.Sprintf(badMountInfoFmt, d.dir)) - defer restore() - - found, offs, err := gadget.FindDeviceForStructureWithFallback(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Type: "bare", - }, - StartOffset: 123, - }) - c.Check(err, ErrorMatches, `device not found`) - c.Check(found, Equals, "") - c.Check(offs, Equals, quantity.Offset(0)) -} - -func (d *deviceSuite) TestDeviceFindFallbackBadWritable(c *C) { - restore := osutil.MockMountInfo(fmt.Sprintf(writableMountInfoFmt, d.dir)) - defer restore() - - ps := &gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Type: "bare", - }, - StartOffset: 123, - } - - found, offs, err := gadget.FindDeviceForStructureWithFallback(ps) - c.Check(err, ErrorMatches, `lstat .*/dev/fakedevice0p1: no such file or directory`) - c.Check(found, Equals, "") - c.Check(offs, Equals, quantity.Offset(0)) - - c.Assert(ioutil.WriteFile(filepath.Join(d.dir, "dev/fakedevice0p1"), nil, 064), IsNil) - - found, offs, err = gadget.FindDeviceForStructureWithFallback(ps) - c.Check(err, ErrorMatches, `unexpected number of matches \(0\) for /sys/block/\*/fakedevice0p1`) - c.Check(found, Equals, "") - c.Check(offs, Equals, quantity.Offset(0)) - - err = os.MkdirAll(filepath.Join(d.dir, "/sys/block/fakedevice0/fakedevice0p1"), 0755) - c.Assert(err, IsNil) - - found, offs, err = gadget.FindDeviceForStructureWithFallback(ps) - c.Check(err, ErrorMatches, `device .*/dev/fakedevice0 does not exist`) - c.Check(found, Equals, "") - c.Check(offs, Equals, quantity.Offset(0)) -} - -func (d *deviceSuite) TestDeviceFindFallbackHappyWritable(c *C) { - d.setupMockSysfs(c) - restore := osutil.MockMountInfo(fmt.Sprintf(writableMountInfoFmt, d.dir)) - defer restore() - - psJustBare := &gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Type: "bare", - }, - StartOffset: 123, - } - psBareWithName := &gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Type: "bare", - Name: "foo", - }, - StartOffset: 123, - } - psMBR := &gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Type: "mbr", - Role: "mbr", - Name: "mbr", - }, - StartOffset: 0, - } - psNoName := &gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{}, - StartOffset: 123, - } - - for _, ps := range []*gadget.LaidOutStructure{psJustBare, psBareWithName, psNoName, psMBR} { - found, offs, err := gadget.FindDeviceForStructureWithFallback(ps) - c.Check(err, IsNil) - c.Check(found, Equals, filepath.Join(d.dir, "/dev/fakedevice0")) - if ps.Type != "mbr" { - c.Check(offs, Equals, quantity.Offset(123)) - } else { - c.Check(offs, Equals, quantity.Offset(0)) - } - } -} - -func (d *deviceSuite) TestDeviceFindFallbackNotForNamedWritable(c *C) { - d.setupMockSysfs(c) - restore := osutil.MockMountInfo(fmt.Sprintf(writableMountInfoFmt, d.dir)) - defer restore() - - // should not hit the fallback path - psNamed := &gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "foo", - }, - StartOffset: 123, - } - found, offs, err := gadget.FindDeviceForStructureWithFallback(psNamed) - c.Check(err, Equals, gadget.ErrDeviceNotFound) - c.Check(found, Equals, "") - c.Check(offs, Equals, quantity.Offset(0)) -} - -func (d *deviceSuite) TestDeviceFindFallbackNotForFilesystem(c *C) { - d.setupMockSysfs(c) - restore := osutil.MockMountInfo(writableMountInfoFmt) - defer restore() - - psFs := &gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Label: "foo", - Filesystem: "ext4", - }, - StartOffset: 123, - } - found, offs, err := gadget.FindDeviceForStructureWithFallback(psFs) - c.Check(err, ErrorMatches, "internal error: cannot use with filesystem structures") - c.Check(found, Equals, "") - c.Check(offs, Equals, quantity.Offset(0)) -} - -func (d *deviceSuite) TestDeviceFindFallbackBadMountInfo(c *C) { - d.setupMockSysfs(c) - restore := osutil.MockMountInfo("garbage") - defer restore() - psFs := &gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "foo", - Type: "bare", - }, - StartOffset: 123, - } - found, offs, err := gadget.FindDeviceForStructureWithFallback(psFs) - c.Check(err, ErrorMatches, "cannot read mount info: .*") - c.Check(found, Equals, "") - c.Check(offs, Equals, quantity.Offset(0)) -} - -func (d *deviceSuite) TestDeviceFindFallbackPassThrough(c *C) { - err := ioutil.WriteFile(filepath.Join(d.dir, "/dev/disk/by-partlabel/foo"), nil, 0644) - c.Assert(err, IsNil) - - ps := &gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "foo", - }, - } - found, offs, err := gadget.FindDeviceForStructureWithFallback(ps) - c.Check(err, ErrorMatches, `candidate .*/dev/disk/by-partlabel/foo is not a symlink`) - c.Check(found, Equals, "") - c.Check(offs, Equals, quantity.Offset(0)) - - // create a proper symlink - err = os.Remove(filepath.Join(d.dir, "/dev/disk/by-partlabel/foo")) - c.Assert(err, IsNil) - err = os.Symlink("../../fakedevice", filepath.Join(d.dir, "/dev/disk/by-partlabel/foo")) - c.Assert(err, IsNil) - - // this should be happy again - found, offs, err = gadget.FindDeviceForStructureWithFallback(ps) - c.Assert(err, IsNil) - c.Check(found, Equals, filepath.Join(d.dir, "/dev/fakedevice")) - c.Check(offs, Equals, quantity.Offset(0)) -} - -func (d *deviceSuite) TestDeviceFindMountPointErrorsWithBare(c *C) { - p, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - // no filesystem - Filesystem: "", - }, - }) - c.Assert(err, ErrorMatches, "no filesystem defined") - c.Check(p, Equals, "") - - p, err = gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - // also counts as bare structure - Filesystem: "none", - }, - }) - c.Assert(err, ErrorMatches, "no filesystem defined") - c.Check(p, Equals, "") -} - -func (d *deviceSuite) TestDeviceFindMountPointErrorsFromDevice(c *C) { - p, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Label: "bar", - Filesystem: "ext4", - }, - }) - c.Assert(err, ErrorMatches, "device not found") - c.Check(p, Equals, "") - - p, err = gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "bar", - Filesystem: "ext4", - }, - }) - c.Assert(err, ErrorMatches, "device not found") - c.Check(p, Equals, "") -} - -func (d *deviceSuite) TestDeviceFindMountPointErrorBadMountinfo(c *C) { - // taken from core18 system - - fakedevice := filepath.Join(d.dir, "/dev/sda2") - err := ioutil.WriteFile(fakedevice, []byte(""), 0644) - c.Assert(err, IsNil) - err = os.Symlink(fakedevice, filepath.Join(d.dir, "/dev/disk/by-label/system-boot")) - c.Assert(err, IsNil) - restore := osutil.MockMountInfo("garbage") - defer restore() - - found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "EFI System", - Label: "system-boot", - Filesystem: "vfat", - }, - }) - c.Check(err, ErrorMatches, "cannot read mount info: .*") - c.Check(found, Equals, "") -} - -func (d *deviceSuite) TestDeviceFindMountPointByLabeHappySimple(c *C) { - // taken from core18 system - - fakedevice := filepath.Join(d.dir, "/dev/sda2") - err := ioutil.WriteFile(fakedevice, []byte(""), 0644) - c.Assert(err, IsNil) - err = os.Symlink(fakedevice, filepath.Join(d.dir, "/dev/disk/by-label/system-boot")) - c.Assert(err, IsNil) - err = os.Symlink(fakedevice, filepath.Join(d.dir, `/dev/disk/by-partlabel/EFI\x20System`)) - c.Assert(err, IsNil) - - mountInfo := ` -170 27 8:2 / /boot/efi rw,relatime shared:58 - vfat ${rootDir}/dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro -172 27 8:2 /EFI/ubuntu /boot/grub rw,relatime shared:58 - vfat ${rootDir}/dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro -` - restore := osutil.MockMountInfo(strings.Replace(mountInfo[1:], "${rootDir}", d.dir, -1)) - defer restore() - - found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "EFI System", - Label: "system-boot", - Filesystem: "vfat", - }, - }) - c.Check(err, IsNil) - c.Check(found, Equals, "/boot/efi") -} - -func (d *deviceSuite) TestDeviceFindMountPointByLabeHappyReversed(c *C) { - // taken from core18 system - - fakedevice := filepath.Join(d.dir, "/dev/sda2") - err := ioutil.WriteFile(fakedevice, []byte(""), 0644) - c.Assert(err, IsNil) - // single property match - err = os.Symlink(fakedevice, filepath.Join(d.dir, "/dev/disk/by-label/system-boot")) - c.Assert(err, IsNil) - - // reverse the order of lines - mountInfoReversed := ` -172 27 8:2 /EFI/ubuntu /boot/grub rw,relatime shared:58 - vfat ${rootDir}/dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro -170 27 8:2 / /boot/efi rw,relatime shared:58 - vfat ${rootDir}/dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro -` - - restore := osutil.MockMountInfo(strings.Replace(mountInfoReversed[1:], "${rootDir}", d.dir, -1)) - defer restore() - - found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "EFI System", - Label: "system-boot", - Filesystem: "vfat", - }, - }) - c.Check(err, IsNil) - c.Check(found, Equals, "/boot/efi") -} - -func (d *deviceSuite) TestDeviceFindMountPointPicksFirstMatch(c *C) { - // taken from core18 system - - fakedevice := filepath.Join(d.dir, "/dev/sda2") - err := ioutil.WriteFile(fakedevice, []byte(""), 0644) - c.Assert(err, IsNil) - // single property match - err = os.Symlink(fakedevice, filepath.Join(d.dir, "/dev/disk/by-label/system-boot")) - c.Assert(err, IsNil) - - mountInfo := ` -852 134 8:2 / /mnt/foo rw,relatime shared:58 - vfat ${rootDir}/dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro -172 27 8:2 /EFI/ubuntu /boot/grub rw,relatime shared:58 - vfat ${rootDir}/dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro -170 27 8:2 / /boot/efi rw,relatime shared:58 - vfat ${rootDir}/dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro -` - - restore := osutil.MockMountInfo(strings.Replace(mountInfo[1:], "${rootDir}", d.dir, -1)) - defer restore() - - found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "EFI System", - Label: "system-boot", - Filesystem: "vfat", - }, - }) - c.Check(err, IsNil) - c.Check(found, Equals, "/mnt/foo") -} - -func (d *deviceSuite) TestDeviceFindMountPointByPartlabel(c *C) { - fakedevice := filepath.Join(d.dir, "/dev/fakedevice") - err := ioutil.WriteFile(fakedevice, []byte(""), 0644) - c.Assert(err, IsNil) - err = os.Symlink(fakedevice, filepath.Join(d.dir, `/dev/disk/by-partlabel/pinkié\x20pie`)) - c.Assert(err, IsNil) - - mountInfo := ` -170 27 8:2 / /mount-point rw,relatime shared:58 - ext4 ${rootDir}/dev/fakedevice rw -` - - restore := osutil.MockMountInfo(strings.Replace(mountInfo[1:], "${rootDir}", d.dir, -1)) - defer restore() - - found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "pinkié pie", - Filesystem: "ext4", - }, - }) - c.Check(err, IsNil) - c.Check(found, Equals, "/mount-point") -} - -func (d *deviceSuite) TestDeviceFindMountPointChecksFilesystem(c *C) { - fakedevice := filepath.Join(d.dir, "/dev/fakedevice") - err := ioutil.WriteFile(fakedevice, []byte(""), 0644) - c.Assert(err, IsNil) - err = os.Symlink(fakedevice, filepath.Join(d.dir, `/dev/disk/by-partlabel/label`)) - c.Assert(err, IsNil) - - mountInfo := ` -170 27 8:2 / /mount-point rw,relatime shared:58 - vfat ${rootDir}/dev/fakedevice rw -` - - restore := osutil.MockMountInfo(strings.Replace(mountInfo[1:], "${rootDir}", d.dir, -1)) - defer restore() - - found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "label", - // different fs than mount entry - Filesystem: "ext4", - }, - }) - c.Check(err, ErrorMatches, "mount point not found") - c.Check(found, Equals, "") -} - -func (d *deviceSuite) TestParentDiskFromMountSource(c *C) { - d.setupMockSysfs(c) - - disk, err := gadget.ParentDiskFromMountSource(filepath.Join(dirs.GlobalRootDir, "/dev/fakedevice0p1")) - c.Assert(err, IsNil) - c.Check(disk, Matches, ".*/dev/fakedevice0") -} - -func (d *deviceSuite) TestParentDiskFromMountSourceBadSymlinkErr(c *C) { - d.setupMockSysfs(c) - - err := os.Symlink("../bad-target", filepath.Join(d.dir, "/dev/mapper/bad-target-symlink")) - c.Assert(err, IsNil) - - _, err = gadget.ParentDiskFromMountSource(filepath.Join(dirs.GlobalRootDir, "/dev/mapper/bad-target-symlink")) - c.Assert(err, ErrorMatches, `cannot resolve mount source symlink .*/dev/mapper/bad-target-symlink: lstat .*/dev/bad-target: no such file or directory`) -} - -func (d *deviceSuite) TestParentDiskFromMountSourceDeviceMapperHappy(c *C) { - d.setupMockSysfsForDevMapper(c) - - disk, err := gadget.ParentDiskFromMountSource(filepath.Join(dirs.GlobalRootDir, "/dev/mapper/data_crypt")) - - c.Assert(err, IsNil) - c.Check(disk, Matches, ".*/dev/fakedevice") -} - -func (d *deviceSuite) TestParentDiskFromMountSourceDeviceMapperErrGlob(c *C) { - d.setupMockSysfsForDevMapper(c) - - // break the intermediate slaves directory - c.Assert(os.RemoveAll(filepath.Join(d.dir, "/sys/block/dm-0/slaves/fakedevice0")), IsNil) - - _, err := gadget.ParentDiskFromMountSource(filepath.Join(dirs.GlobalRootDir, "/dev/mapper/data_crypt")) - c.Assert(err, ErrorMatches, "cannot resolve device mapper device dm-1: unexpected number of dm device dm-0 slaves: 0") - - c.Assert(os.Chmod(filepath.Join(d.dir, "/sys/block/dm-0"), 0000), IsNil) - defer os.Chmod(filepath.Join(d.dir, "/sys/block/dm-0"), 0755) - - _, err = gadget.ParentDiskFromMountSource(filepath.Join(dirs.GlobalRootDir, "/dev/mapper/data_crypt")) - c.Assert(err, ErrorMatches, "cannot resolve device mapper device dm-1: unexpected number of dm device dm-0 slaves: 0") -} - -func (d *deviceSuite) TestParentDiskFromMountSourceDeviceMapperErrTargetDevice(c *C) { - d.setupMockSysfsForDevMapper(c) - - c.Assert(os.RemoveAll(filepath.Join(d.dir, "/sys/block/fakedevice")), IsNil) - - _, err := gadget.ParentDiskFromMountSource(filepath.Join(dirs.GlobalRootDir, "/dev/mapper/data_crypt")) - c.Assert(err, ErrorMatches, `unexpected number of matches \(0\) for /sys/block/\*/fakedevice0`) -} - -func (d *deviceSuite) TestParentDiskFromMountSourceDeviceMapperLevels(c *C) { - err := os.Symlink("../dm-6", filepath.Join(d.dir, "/dev/mapper/data_crypt")) - c.Assert(err, IsNil) - for i := 6; i > 0; i-- { - err := ioutil.WriteFile(filepath.Join(d.dir, fmt.Sprintf("/dev/dm-%v", i)), nil, 0644) - c.Assert(err, IsNil) - err = os.MkdirAll(filepath.Join(d.dir, fmt.Sprintf("/sys/block/dm-%v/slaves/", i)), 0755) - c.Assert(err, IsNil) - // sys symlinks are relative too - err = os.Symlink(fmt.Sprintf("../../dm-%v", i-1), filepath.Join(d.dir, fmt.Sprintf("/sys/block/dm-%v/slaves/dm-%v", i, i-1))) - c.Assert(err, IsNil) - } - - _, err = gadget.ParentDiskFromMountSource(filepath.Join(dirs.GlobalRootDir, "/dev/mapper/data_crypt")) - c.Assert(err, ErrorMatches, `cannot resolve device mapper device dm-6: too many levels`) -} diff -Nru snapd-2.55.5+20.04/gadget/export_test.go snapd-2.57.5+20.04/gadget/export_test.go --- snapd-2.55.5+20.04/gadget/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -47,9 +47,6 @@ NewRawStructureUpdater = newRawStructureUpdater NewMountedFilesystemUpdater = newMountedFilesystemUpdater - FindDeviceForStructureWithFallback = findDeviceForStructureWithFallback - FindMountPointForStructure = findMountPointForStructure - ParseRelativeOffset = parseRelativeOffset SplitKernelRef = splitKernelRef @@ -57,6 +54,7 @@ ResolveVolumeContent = resolveVolumeContent GadgetVolumeConsumesOneKernelUpdateAsset = gadgetVolumeConsumesOneKernelUpdateAsset + GadgetVolumeKernelUpdateAssetsConsumed = gadgetVolumeKernelUpdateAssetsConsumed BuildNewVolumeToDeviceMapping = buildNewVolumeToDeviceMapping ErrSkipUpdateProceedRefresh = errSkipUpdateProceedRefresh diff -Nru snapd-2.55.5+20.04/gadget/gadget.go snapd-2.57.5+20.04/gadget/gadget.go --- snapd-2.55.5+20.04/gadget/gadget.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/gadget.go 2022-10-17 16:25:18.000000000 +0000 @@ -1151,6 +1151,7 @@ // IsCompatible checks whether the current and an update are compatible. Returns // nil or an error describing the incompatibility. +// TODO: make this reasonably consistent with Update for multi-volume scenarios func IsCompatible(current, new *Info) error { // XXX: the only compatibility we have now is making sure that the new // layout can be used on an existing volume diff -Nru snapd-2.55.5+20.04/gadget/gadgettest/examples.go snapd-2.57.5+20.04/gadget/gadgettest/examples.go --- snapd-2.55.5+20.04/gadget/gadgettest/examples.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/gadgettest/examples.go 2022-10-17 16:25:18.000000000 +0000 @@ -60,6 +60,44 @@ type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 ` +// from a rpi without the kernel assets or content layout for simplicity's sake +// and without ubuntu-save +const RaspiSimplifiedNoSaveYaml = ` +volumes: + pi: + bootloader: u-boot + schema: mbr + structure: + - filesystem: vfat + name: ubuntu-seed + role: system-seed + size: 1200M + type: 0C + - filesystem: vfat + name: ubuntu-boot + role: system-boot + size: 750M + type: 0C + - filesystem: ext4 + name: ubuntu-data + role: system-data + size: 1500M + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 +` + +// from UC18 image, for testing the implicit system data partition case +const RaspiUC18SimplifiedYaml = ` +volumes: + pi: + schema: mbr + bootloader: u-boot + structure: + - type: 0C + filesystem: vfat + filesystem-label: system-boot + size: 256M +` + var expPiSeedStructureTraits = gadget.DiskStructureDeviceTraits{ OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", OriginalKernelPath: "/dev/mmcblk0p1", @@ -109,6 +147,19 @@ Size: (30528 - (1 + 1200 + 750 + 16)) * quantity.SizeMiB, } +var expPiDataNoSaveStructureTraits = gadget.DiskStructureDeviceTraits{ + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p3", + OriginalKernelPath: "/dev/mmcblk0p3", + PartitionUUID: "7c301cbd-03", + PartitionType: "83", + FilesystemUUID: "d7f39661-1da0-48de-8967-ce41343d4345", + FilesystemLabel: "ubuntu-data", + FilesystemType: "ext4", + Offset: (1 + 1200 + 750) * quantity.OffsetMiB, + // total size - offset of last structure + Size: (30528 - (1 + 1200 + 750)) * quantity.SizeMiB, +} + var ExpectedRaspiDiskVolumeDeviceTraits = gadget.DiskVolumeDeviceTraits{ OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", OriginalKernelPath: "/dev/mmcblk0", @@ -125,6 +176,21 @@ }, } +var ExpectedRaspiDiskVolumeDeviceNoSaveTraits = gadget.DiskVolumeDeviceTraits{ + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", + OriginalKernelPath: "/dev/mmcblk0", + DiskID: "7c301cbd", + Size: 30528 * quantity.SizeMiB, // ~ 32 GB SD card + SectorSize: 512, + Schema: "dos", + StructureEncryption: map[string]gadget.StructureEncryptionParameters{}, + Structure: []gadget.DiskStructureDeviceTraits{ + expPiSeedStructureTraits, + expPiBootStructureTraits, + expPiDataNoSaveStructureTraits, + }, +} + var expPiSaveEncStructureTraits = gadget.DiskStructureDeviceTraits{ OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p3", OriginalKernelPath: "/dev/mmcblk0p3", @@ -172,6 +238,33 @@ }, } +// ExpectedRaspiUC18DiskVolumeDeviceTraits, for testing the implicit system +// data partition case +var ExpectedRaspiUC18DiskVolumeDeviceTraits = gadget.DiskVolumeDeviceTraits{ + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", + OriginalKernelPath: "/dev/mmcblk0", + DiskID: "7c301cbd", + Size: 32010928128, + SectorSize: 512, + Schema: "dos", + Structure: []gadget.DiskStructureDeviceTraits{ + { + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", + OriginalKernelPath: "/dev/mmcblk0p1", + PartitionUUID: "7c301cbd-01", + PartitionType: "0C", + PartitionLabel: "", + FilesystemUUID: "23F9-881F", + FilesystemLabel: "system-boot", + FilesystemType: "vfat", + Offset: quantity.OffsetMiB, + Size: 256 * quantity.SizeMiB, + }, + // note no writable structure here - since it's not in the YAML, we + // don't save it in the traits either + }, +} + var mockSeedPartition = disks.Partition{ PartitionUUID: "7c301cbd-01", PartitionType: "0C", @@ -245,6 +338,35 @@ }, } +var ExpectedRaspiMockDiskMappingNoSave = &disks.MockDiskMapping{ + DevNode: "/dev/mmcblk0", + DevPath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", + DevNum: "179:0", + DiskUsableSectorEnd: 30528 * oneMeg / 512, + DiskSizeInBytes: 30528 * oneMeg, + SectorSizeBytes: 512, + DiskSchema: "dos", + ID: "7c301cbd", + Structure: []disks.Partition{ + mockSeedPartition, + mockBootPartition, + { + PartitionUUID: "7c301cbd-03", + PartitionType: "83", + FilesystemLabel: "ubuntu-data", + FilesystemUUID: "d7f39661-1da0-48de-8967-ce41343d4345", + FilesystemType: "ext4", + Major: 179, + Minor: 3, + KernelDeviceNode: "/dev/mmcblk0p3", + KernelDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p3", + DiskIndex: 3, + StartInBytes: (1 + 1200 + 750) * oneMeg, + SizeInBytes: (30528 - (1 + 1200 + 750)) * oneMeg, + }, + }, +} + // ExpectedLUKSEncryptedRaspiMockDiskMapping is like // ExpectedRaspiMockDiskMapping, but it uses the "-enc" suffix for the // filesystem labels and has crypto_LUKS as the filesystem types @@ -311,6 +433,48 @@ }, } +// ExpectedRaspiUC18MockDiskMapping, for testing the implicit system data partition case +var ExpectedRaspiUC18MockDiskMapping = &disks.MockDiskMapping{ + DevNode: "/dev/mmcblk0", + DevPath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", + DevNum: "179:0", + DiskUsableSectorEnd: 30528 * oneMeg / 512, + DiskSizeInBytes: 30528 * oneMeg, + SectorSizeBytes: 512, + DiskSchema: "dos", + ID: "7c301cbd", + Structure: []disks.Partition{ + { + PartitionUUID: "7c301cbd-01", + PartitionType: "0C", + FilesystemLabel: "system-boot", + FilesystemUUID: "23F9-881F", + FilesystemType: "vfat", + Major: 179, + Minor: 1, + KernelDeviceNode: "/dev/mmcblk0p1", + KernelDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", + DiskIndex: 1, + StartInBytes: oneMeg, + SizeInBytes: 256 * oneMeg, + }, + { + PartitionUUID: "7c301cbd-02", + PartitionType: "83", + FilesystemLabel: "writable", + FilesystemUUID: "cba2b8b3-c2e4-4e51-9a57-d35041b7bf9a", + FilesystemType: "ext4", + Major: 179, + Minor: 2, + KernelDeviceNode: "/dev/mmcblk0p2", + KernelDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p2", + DiskIndex: 2, + StartInBytes: (1 + 256) * oneMeg, + SizeInBytes: 32270 * oneMeg, + }, + }, +} + const ExpectedRaspiDiskVolumeDeviceTraitsJSON = ` { "pi": { @@ -372,6 +536,58 @@ } ] } +} +` + +const ExpectedRaspiDiskVolumeNoSaveDeviceTraitsJSON = ` +{ + "pi": { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", + "kernel-path": "/dev/mmcblk0", + "disk-id": "7c301cbd", + "size": 32010928128, + "sector-size": 512, + "schema": "dos", + "structure-encryption": {}, + "structure": [ + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", + "kernel-path": "/dev/mmcblk0p1", + "partition-uuid": "7c301cbd-01", + "partition-label": "", + "partition-type": "0C", + "filesystem-label": "ubuntu-seed", + "filesystem-uuid": "0E09-0822", + "filesystem-type": "vfat", + "offset": 1048576, + "size": 1258291200 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p2", + "kernel-path": "/dev/mmcblk0p2", + "partition-uuid": "7c301cbd-02", + "partition-label": "", + "partition-type": "0C", + "filesystem-label": "ubuntu-boot", + "filesystem-uuid": "23F9-881F", + "filesystem-type": "vfat", + "offset": 1259339776, + "size": 786432000 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p3", + "kernel-path": "/dev/mmcblk0p3", + "partition-uuid": "7c301cbd-03", + "partition-label": "", + "partition-type": "83", + "filesystem-label": "ubuntu-data", + "filesystem-uuid": "d7f39661-1da0-48de-8967-ce41343d4345", + "filesystem-type": "ext4", + "offset": 2045771776, + "size": 29965156352 + } + ] + } } ` diff -Nru snapd-2.55.5+20.04/gadget/gadgettest/gadgettest.go snapd-2.57.5+20.04/gadget/gadgettest/gadgettest.go --- snapd-2.55.5+20.04/gadget/gadgettest/gadgettest.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/gadgettest/gadgettest.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,13 +34,13 @@ // gadget.yaml string and works for either single or multiple volume // gadget.yaml's. An empty directory to use to create a gadget.yaml file should // be provided, such as c.MkDir() in tests. -func LayoutMultiVolumeFromYaml(newDir, gadgetYaml string, model gadget.Model) (map[string]*gadget.LaidOutVolume, error) { +func LayoutMultiVolumeFromYaml(newDir, kernelDir, gadgetYaml string, model gadget.Model) (map[string]*gadget.LaidOutVolume, error) { gadgetRoot, err := WriteGadgetYaml(newDir, gadgetYaml) if err != nil { return nil, err } - _, allVolumes, err := gadget.LaidOutVolumesFromGadget(gadgetRoot, "", model) + _, allVolumes, err := gadget.LaidOutVolumesFromGadget(gadgetRoot, kernelDir, model) if err != nil { return nil, fmt.Errorf("cannot layout volumes: %v", err) } diff -Nru snapd-2.55.5+20.04/gadget/gadget_test.go snapd-2.57.5+20.04/gadget/gadget_test.go --- snapd-2.55.5+20.04/gadget/gadget_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/gadget_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -2801,7 +2801,7 @@ smallDeviceLayout := mockDeviceLayout smallDeviceLayout.UsableSectorsEnd = uint64(100 * quantity.SizeMiB / 512) - // sanity check + // validity check c.Check(gadgetLayoutWithExtras.Size > quantity.Size(smallDeviceLayout.UsableSectorsEnd*uint64(smallDeviceLayout.SectorSize)), Equals, true) err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &smallDeviceLayout, nil) c.Assert(err, ErrorMatches, `device /dev/node \(last usable byte at 100 MiB\) is too small to fit the requested layout \(1\.17 GiB\)`) @@ -3528,6 +3528,7 @@ } vols, err := gadgettest.LayoutMultiVolumeFromYaml( c.MkDir(), + "", gadgettest.MultiVolumeUC20GadgetYaml, mod, ) diff -Nru snapd-2.55.5+20.04/gadget/install/content.go snapd-2.57.5+20.04/gadget/install/content.go --- snapd-2.55.5+20.04/gadget/install/content.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/content.go 2022-10-17 16:25:18.000000000 +0000 @@ -32,67 +32,53 @@ "github.com/snapcore/snapd/osutil/mkfs" ) -var contentMountpoint string - var mkfsImpl = mkfs.Make -func init() { - contentMountpoint = filepath.Join(dirs.SnapRunDir, "gadget-install") +type mkfsParams struct { + Type string + Device string + Label string + Size quantity.Size + SectorSize quantity.Size } // makeFilesystem creates a filesystem on the on-disk structure, according // to the filesystem type defined in the gadget. If sectorSize is specified, // that sector size is used when creating the filesystem, otherwise if it is // zero, automatic values are used instead. -func makeFilesystem(ds *gadget.OnDiskStructure, sectorSize quantity.Size) error { - if !ds.HasFilesystem() { - return fmt.Errorf("internal error: on disk structure for partition %s has no filesystem", ds.Node) - } - logger.Debugf("create %s filesystem on %s with label %q", ds.VolumeStructure.Filesystem, ds.Node, ds.VolumeStructure.Label) - if err := mkfsImpl(ds.VolumeStructure.Filesystem, ds.Node, ds.VolumeStructure.Label, ds.Size, sectorSize); err != nil { +func makeFilesystem(params mkfsParams) error { + logger.Debugf("create %s filesystem on %s with label %q", params.Type, params.Device, params.Label) + if err := mkfsImpl(params.Type, params.Device, params.Label, params.Size, params.SectorSize); err != nil { return err } - return udevTrigger(ds.Node) -} - -// writeContent populates the given on-disk structure, according to the contents -// defined in the gadget. -func writeContent(ds *gadget.OnDiskStructure, observer gadget.ContentObserver) error { - if ds.HasFilesystem() { - return writeFilesystemContent(ds, observer) - } - return fmt.Errorf("cannot write non-filesystem structures during install") + return udevTrigger(params.Device) } -// mountFilesystem mounts the on-disk structure filesystem under the given base -// directory, using the label defined in the gadget as the mount point name. -func mountFilesystem(ds *gadget.OnDiskStructure, baseMntPoint string) error { - if !ds.HasFilesystem() { - return fmt.Errorf("cannot mount a partition with no filesystem") - } - if ds.Label == "" { - return fmt.Errorf("cannot mount a filesystem with no label") - } - - mountpoint := filepath.Join(baseMntPoint, ds.Label) +// mountFilesystem mounts the filesystem on a given device under the given base +// directory, under the provided mount point name. +func mountFilesystem(fsDevice, fs, mntPointName, baseMntPoint string) error { + mountpoint := filepath.Join(baseMntPoint, mntPointName) if err := os.MkdirAll(mountpoint, 0755); err != nil { return fmt.Errorf("cannot create mountpoint: %v", err) } - if err := sysMount(ds.Node, mountpoint, ds.Filesystem, 0, ""); err != nil { - return fmt.Errorf("cannot mount filesystem %q at %q: %v", ds.Node, mountpoint, err) + if err := sysMount(fsDevice, mountpoint, fs, 0, ""); err != nil { + return fmt.Errorf("cannot mount filesystem %q at %q: %v", fsDevice, mountpoint, err) } return nil } -func writeFilesystemContent(ds *gadget.OnDiskStructure, observer gadget.ContentObserver) (err error) { - mountpoint := filepath.Join(contentMountpoint, strconv.Itoa(ds.DiskIndex)) +// writeContent populates the given on-disk filesystem structure with a +// corresponding filesystem device, according to the contents defined in the +// gadget. +func writeFilesystemContent(ds *gadget.OnDiskStructure, fsDevice string, observer gadget.ContentObserver) (err error) { + mountpoint := filepath.Join(dirs.SnapRunDir, "gadget-install", strconv.Itoa(ds.DiskIndex)) if err := os.MkdirAll(mountpoint, 0755); err != nil { return err } // temporarily mount the filesystem - if err := sysMount(ds.Node, mountpoint, ds.Filesystem, 0, ""); err != nil { + if err := sysMount(fsDevice, mountpoint, ds.Filesystem, 0, ""); err != nil { return fmt.Errorf("cannot mount filesystem %q at %q: %v", ds.Node, mountpoint, err) } defer func() { diff -Nru snapd-2.55.5+20.04/gadget/install/content_test.go snapd-2.57.5+20.04/gadget/install/content_test.go --- snapd-2.55.5+20.04/gadget/install/content_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/content_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -55,7 +55,9 @@ func (s *contentTestSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) + s.AddCleanup(func() { dirs.SetRootDir(dirs.GlobalRootDir) }) s.dir = c.MkDir() + dirs.SetRootDir(s.dir) s.mockMountErr = nil s.mockMountCalls = nil @@ -66,10 +68,8 @@ c.Assert(err, IsNil) s.mockMountPoint = c.MkDir() - restore := install.MockContentMountpoint(s.mockMountPoint) - s.AddCleanup(restore) - restore = install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error { + restore := install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error { s.mockMountCalls = append(s.mockMountCalls, struct{ source, target, fstype string }{source, target, fstype}) return s.mockMountErr }) @@ -82,24 +82,6 @@ s.AddCleanup(restore) } -var mockOnDiskStructureBiosBoot = gadget.OnDiskStructure{ - Node: "/dev/node1", - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "BIOS Boot", - Size: 1 * 1024 * 1024, - Type: "DA,21686148-6449-6E6F-744E-656564454649", - Content: []gadget.VolumeContent{ - { - Image: "pc-core.img", - }, - }, - }, - StartOffset: 0, - YamlIndex: 1, - }, -} - var mockOnDiskStructureSystemSeed = gadget.OnDiskStructure{ Node: "/dev/node2", LaidOutStructure: gadget.LaidOutStructure{ @@ -200,6 +182,8 @@ } func (s *contentTestSuite) TestWriteFilesystemContent(c *C) { + defer dirs.SetRootDir(dirs.GlobalRootDir) + for _, tc := range []struct { mountErr error unmountErr error @@ -223,12 +207,12 @@ err: "cannot create filesystem image: cannot write filesystem content of source:grubx64.efi: cannot observe file write: observe error", }, } { - mockMountpoint := c.MkDir() - - restore := install.MockContentMountpoint(mockMountpoint) - defer restore() + dirs.SetRootDir(c.MkDir()) - restore = install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error { + restore := install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error { + c.Check(source, Equals, "/dev/node2") + c.Check(fstype, Equals, "vfat") + c.Check(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/2")) return tc.mountErr }) defer restore() @@ -254,7 +238,7 @@ observeErr: tc.observeErr, expectedStruct: &m.LaidOutStructure, } - err := install.WriteContent(&m, obs) + err := install.WriteFilesystemContent(&m, "/dev/node2", obs) if tc.err == "" { c.Assert(err, IsNil) } else { @@ -263,11 +247,11 @@ if err == nil { // the target file system is mounted on a directory named after the structure index - content, err := ioutil.ReadFile(filepath.Join(mockMountpoint, "2", "EFI/boot/grubx64.efi")) + content, err := ioutil.ReadFile(filepath.Join(dirs.SnapRunDir, "gadget-install/2", "EFI/boot/grubx64.efi")) c.Assert(err, IsNil) c.Check(string(content), Equals, "grubx64.efi content") c.Assert(obs.content, DeepEquals, map[string][]*mockContentChange{ - filepath.Join(mockMountpoint, "2"): { + filepath.Join(dirs.SnapRunDir, "gadget-install/2"): { { path: "EFI/boot/grubx64.efi", change: &gadget.ContentChange{After: filepath.Join(s.gadgetRoot, "grubx64.efi")}, @@ -278,39 +262,6 @@ } } -func (s *contentTestSuite) TestWriteRawContentNotSupported(c *C) { - mockNode := filepath.Join(s.dir, "mock-node") - err := ioutil.WriteFile(mockNode, nil, 0644) - c.Assert(err, IsNil) - - // copy existing mock - m := mockOnDiskStructureBiosBoot - m.Node = mockNode - m.LaidOutContent = []gadget.LaidOutContent{ - { - VolumeContent: &gadget.VolumeContent{ - Image: "pc-core.img", - }, - StartOffset: 2, - Size: quantity.Size(len("pc-core.img content")), - }, - } - - err = install.WriteContent(&m, nil) - c.Assert(err, ErrorMatches, `cannot write non-filesystem structures during install`) -} - -func (s *contentTestSuite) TestMakeFilesystemStructureHasNoFilesystem(c *C) { - restore := install.MockMkfsMake(func(typ, img, label string, devSize, sectorSize quantity.Size) error { - c.Errorf("unexpected call to mkfs.Make()") - return fmt.Errorf("should not be called") - }) - defer restore() - - err := install.MakeFilesystem(&mockOnDiskStructureBiosBoot, quantity.Size(512)) - c.Assert(err, ErrorMatches, `internal error: on disk structure for partition /dev/node1 has no filesystem`) -} - func (s *contentTestSuite) TestMakeFilesystem(c *C) { mockUdevadm := testutil.MockCommand(c, "udevadm", "") defer mockUdevadm.Restore() @@ -325,7 +276,13 @@ }) defer restore() - err := install.MakeFilesystem(&mockOnDiskStructureWritable, quantity.Size(512)) + err := install.MakeFilesystem(install.MkfsParams{ + Type: mockOnDiskStructureWritable.Filesystem, + Device: mockOnDiskStructureWritable.Node, + Label: mockOnDiskStructureWritable.Label, + Size: mockOnDiskStructureWritable.Size, + SectorSize: quantity.Size(512), + }) c.Assert(err, IsNil) c.Assert(mockUdevadm.Calls(), DeepEquals, [][]string{ @@ -340,7 +297,13 @@ mockMkfsExt4 := testutil.MockCommand(c, "mkfs.ext4", "") defer mockMkfsExt4.Restore() - err := install.MakeFilesystem(&mockOnDiskStructureWritable, quantity.Size(512)) + err := install.MakeFilesystem(install.MkfsParams{ + Type: mockOnDiskStructureWritable.Filesystem, + Device: mockOnDiskStructureWritable.Node, + Label: mockOnDiskStructureWritable.Label, + Size: mockOnDiskStructureWritable.Size, + SectorSize: quantity.Size(512), + }) c.Assert(err, IsNil) c.Assert(mockUdevadm.Calls(), DeepEquals, [][]string{ @@ -356,15 +319,8 @@ dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("") - // mounting will only happen for devices with a label - mockOnDiskStructureBiosBoot.Label = "bios-boot" - defer func() { mockOnDiskStructureBiosBoot.Label = "" }() - - err := install.MountFilesystem(&mockOnDiskStructureBiosBoot, boot.InitramfsRunMntDir) - c.Assert(err, ErrorMatches, "cannot mount a partition with no filesystem") - // mount a filesystem... - err = install.MountFilesystem(&mockOnDiskStructureSystemSeed, boot.InitramfsRunMntDir) + err := install.MountFilesystem("/dev/node2", "vfat", "ubuntu-seed", boot.InitramfsRunMntDir) c.Assert(err, IsNil) // ...and check if it was mounted at the right mount point @@ -373,10 +329,8 @@ {"/dev/node2", boot.InitramfsUbuntuSeedDir, "vfat"}, }) - // now try to mount a filesystem with no label - mockOnDiskStructureSystemSeed.Label = "" - defer func() { mockOnDiskStructureSystemSeed.Label = "ubuntu-seed" }() - - err = install.MountFilesystem(&mockOnDiskStructureSystemSeed, boot.InitramfsRunMntDir) - c.Assert(err, ErrorMatches, "cannot mount a filesystem with no label") + // try again with mocked error + s.mockMountErr = fmt.Errorf("mock mount error") + err = install.MountFilesystem("/dev/node2", "vfat", "ubuntu-seed", boot.InitramfsRunMntDir) + c.Assert(err, ErrorMatches, `cannot mount filesystem "/dev/node2" at ".*/run/mnt/ubuntu-seed": mock mount error`) } diff -Nru snapd-2.55.5+20.04/gadget/install/encrypt.go snapd-2.57.5+20.04/gadget/install/encrypt.go --- snapd-2.55.5+20.04/gadget/install/encrypt.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/encrypt.go 2022-10-17 16:25:18.000000000 +0000 @@ -26,20 +26,22 @@ "fmt" "os/exec" + "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/kernel/fde" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" ) var ( secbootFormatEncryptedDevice = secboot.FormatEncryptedDevice - secbootAddRecoveryKey = secboot.AddRecoveryKey ) // encryptedDeviceCryptsetup represents a encrypted block device. type encryptedDevice interface { Node() string - AddRecoveryKey(key secboot.EncryptionKey, rkey secboot.RecoveryKey) error Close() error } @@ -55,7 +57,7 @@ // newEncryptedDeviceLUKS creates an encrypted device in the existing // partition using the specified key with the LUKS backend. -func newEncryptedDeviceLUKS(part *gadget.OnDiskStructure, key secboot.EncryptionKey, name string) (encryptedDevice, error) { +func newEncryptedDeviceLUKS(part *gadget.OnDiskStructure, key keys.EncryptionKey, name string) (encryptedDevice, error) { dev := &encryptedDeviceLUKS{ parent: part, name: name, @@ -76,10 +78,6 @@ return dev, nil } -func (dev *encryptedDeviceLUKS) AddRecoveryKey(key secboot.EncryptionKey, rkey secboot.RecoveryKey) error { - return secbootAddRecoveryKey(key, rkey, dev.parent.Node) -} - func (dev *encryptedDeviceLUKS) Node() string { return dev.node } @@ -88,7 +86,7 @@ return cryptsetupClose(dev.name) } -func cryptsetupOpen(key secboot.EncryptionKey, node, name string) error { +func cryptsetupOpen(key keys.EncryptionKey, node, name string) error { cmd := exec.Command("cryptsetup", "open", "--key-file", "-", node, name) cmd.Stdin = bytes.NewReader(key[:]) if output, err := cmd.CombinedOutput(); err != nil { @@ -103,3 +101,69 @@ } return nil } + +// encryptedDeviceWithSetupHook represents a block device that is setup using +// the "device-setup" hook. +type encryptedDeviceWithSetupHook struct { + parent *gadget.OnDiskStructure + name string + node string +} + +// expected interface is implemented +var _ = encryptedDevice(&encryptedDeviceWithSetupHook{}) + +// createEncryptedDeviceWithSetupHook creates an encrypted device in the +// existing partition using the specified key using the fde-setup hook +func createEncryptedDeviceWithSetupHook(part *gadget.OnDiskStructure, key keys.EncryptionKey, name string) (encryptedDevice, error) { + // for roles requiring encryption, the filesystem label is always set to + // either the implicit value or a value that has been validated + if part.Name != name || part.Label != name { + return nil, fmt.Errorf("cannot use partition name %q for an encrypted structure with %v role and filesystem with label %q", + name, part.Role, part.Label) + } + + // 1. create linear mapper device with 1Mb of reserved space + uuid := "" + offset := fde.DeviceSetupHookPartitionOffset + sizeMinusOffset := uint64(part.Size) - offset + mapperDevice, err := disks.CreateLinearMapperDevice(part.Node, name, uuid, offset, sizeMinusOffset) + if err != nil { + return nil, err + } + + // 2. run fde-setup "device-setup" on it + // TODO: We may need a different way to run the fde-setup hook + // here. The hook right now runs with a locked state. But + // when this runs the state will be unlocked but our hook + // mechanism needs a locked state. This means we either need + // something like "boot.RunFDE*Device*SetupHook" or we run + // the entire install with the state locked (which may not + // be as terrible as it sounds as this is a rare situation). + runHook := boot.RunFDESetupHook + params := &fde.DeviceSetupParams{ + Key: key, + Device: mapperDevice, + PartitionName: name, + } + if err := fde.DeviceSetup(runHook, params); err != nil { + return nil, err + } + + return &encryptedDeviceWithSetupHook{ + parent: part, + name: name, + node: mapperDevice, + }, nil +} + +func (dev *encryptedDeviceWithSetupHook) Close() error { + if output, err := exec.Command("dmsetup", "remove", dev.name).CombinedOutput(); err != nil { + return osutil.OutputErr(output, err) + } + return nil +} + +func (dev *encryptedDeviceWithSetupHook) Node() string { + return dev.node +} diff -Nru snapd-2.55.5+20.04/gadget/install/encrypt_test.go snapd-2.57.5+20.04/gadget/install/encrypt_test.go --- snapd-2.55.5+20.04/gadget/install/encrypt_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/encrypt_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -31,7 +31,9 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/install" - "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/kernel/fde" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/testutil" ) @@ -40,8 +42,8 @@ mockCryptsetup *testutil.MockCmd - mockedEncryptionKey secboot.EncryptionKey - mockedRecoveryKey secboot.RecoveryKey + mockedEncryptionKey keys.EncryptionKey + mockedRecoveryKey keys.RecoveryKey } var _ = Suite(&encryptSuite{}) @@ -49,12 +51,14 @@ var mockDeviceStructure = gadget.OnDiskStructure{ LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ - Name: "Test structure", - Size: 0x100000, + Role: gadget.SystemData, + Name: "Test structure", + Label: "some-label", }, StartOffset: 0, YamlIndex: 1, }, + Size: 3 * quantity.SizeMiB, Node: "/dev/node1", } @@ -63,11 +67,11 @@ c.Assert(os.MkdirAll(dirs.SnapRunDir, 0755), IsNil) // create empty key to prevent blocking on lack of system entropy - s.mockedEncryptionKey = secboot.EncryptionKey{} + s.mockedEncryptionKey = keys.EncryptionKey{} for i := range s.mockedEncryptionKey { s.mockedEncryptionKey[i] = byte(i) } - s.mockedRecoveryKey = secboot.RecoveryKey{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} + s.mockedRecoveryKey = keys.RecoveryKey{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} } func (s *encryptSuite) TestNewEncryptedDeviceLUKS(c *C) { @@ -101,7 +105,7 @@ s.AddCleanup(s.mockCryptsetup.Restore) calls := 0 - restore := install.MockSecbootFormatEncryptedDevice(func(key secboot.EncryptionKey, label, node string) error { + restore := install.MockSecbootFormatEncryptedDevice(func(key keys.EncryptionKey, label, node string) error { calls++ c.Assert(key, DeepEquals, s.mockedEncryptionKey) c.Assert(label, Equals, "some-label-enc") @@ -130,50 +134,118 @@ } } -func (s *encryptSuite) TestAddRecoveryKey(c *C) { +var mockDeviceStructureForDeviceSetupHook = gadget.OnDiskStructure{ + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Role: gadget.SystemData, + Name: "ubuntu-data", + Label: "ubuntu-data", + }, + StartOffset: 0, + YamlIndex: 1, + }, + Size: 3 * quantity.SizeMiB, + Node: "/dev/node1", +} + +func (s *encryptSuite) TestCreateEncryptedDeviceWithSetupHook(c *C) { + for _, tc := range []struct { - mockedAddErr error - expectedErr string + mockedOpenErr string + mockedRunFDESetupHookErr error + expectedErr string }{ - {mockedAddErr: nil, expectedErr: ""}, - {mockedAddErr: errors.New("add key error"), expectedErr: "add key error"}, + { + mockedOpenErr: "", + mockedRunFDESetupHookErr: nil, + expectedErr: "", + }, + { + mockedRunFDESetupHookErr: errors.New("fde-setup hook error"), + mockedOpenErr: "", + expectedErr: "device setup failed with: fde-setup hook error", + }, + + { + mockedOpenErr: "open error", + mockedRunFDESetupHookErr: nil, + expectedErr: `cannot create mapper "ubuntu-data" on /dev/node1: open error`, + }, } { - s.mockCryptsetup = testutil.MockCommand(c, "cryptsetup", "") - s.AddCleanup(s.mockCryptsetup.Restore) + script := "" + if tc.mockedOpenErr != "" { + script = fmt.Sprintf("echo '%s'>&2; exit 1", tc.mockedOpenErr) - restore := install.MockSecbootFormatEncryptedDevice(func(key secboot.EncryptionKey, label, node string) error { - return nil - }) - defer restore() + } - calls := 0 - restore = install.MockSecbootAddRecoveryKey(func(key secboot.EncryptionKey, rkey secboot.RecoveryKey, node string) error { - calls++ - c.Assert(key, DeepEquals, s.mockedEncryptionKey) - c.Assert(rkey, DeepEquals, s.mockedRecoveryKey) - c.Assert(node, Equals, "/dev/node1") - return tc.mockedAddErr + restore := install.MockBootRunFDESetupHook(func(req *fde.SetupRequest) ([]byte, error) { + return nil, tc.mockedRunFDESetupHookErr }) defer restore() - dev, err := install.NewEncryptedDeviceLUKS(&mockDeviceStructure, s.mockedEncryptionKey, "some-label") - c.Assert(err, IsNil) + mockDmsetup := testutil.MockCommand(c, "dmsetup", script) + s.AddCleanup(mockDmsetup.Restore) - err = dev.AddRecoveryKey(s.mockedEncryptionKey, s.mockedRecoveryKey) - c.Assert(calls, Equals, 1) + dev, err := install.CreateEncryptedDeviceWithSetupHook(&mockDeviceStructureForDeviceSetupHook, + s.mockedEncryptionKey, "ubuntu-data") if tc.expectedErr == "" { c.Assert(err, IsNil) } else { c.Assert(err, ErrorMatches, tc.expectedErr) continue } + c.Check(dev.Node(), Equals, "/dev/mapper/ubuntu-data") err = dev.Close() c.Assert(err, IsNil) - c.Assert(s.mockCryptsetup.Calls(), DeepEquals, [][]string{ - {"cryptsetup", "open", "--key-file", "-", "/dev/node1", "some-label"}, - {"cryptsetup", "close", "some-label"}, + c.Check(mockDmsetup.Calls(), DeepEquals, [][]string{ + // Caculation is in 512 byte blocks. The total + // size of the mock device is 3Mb: 2Mb + // (4096*512) length if left and the offset is + // 1Mb (2048*512) at the start + {"dmsetup", "create", "ubuntu-data", "--table", "0 4096 linear /dev/node1 2048"}, + {"dmsetup", "remove", "ubuntu-data"}, }) } } + +func (s *encryptSuite) TestCreateEncryptedDeviceWithSetupHookPartitionNameCheck(c *C) { + mockDeviceStructureBadName := gadget.OnDiskStructure{ + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Role: gadget.SystemData, + Name: "ubuntu-data", + Label: "ubuntu-data", + }, + StartOffset: 0, + YamlIndex: 1, + }, + Size: 3 * quantity.SizeMiB, + Node: "/dev/node1", + } + restore := install.MockBootRunFDESetupHook(func(req *fde.SetupRequest) ([]byte, error) { + c.Error("unexpected call") + return nil, fmt.Errorf("unexpected call") + }) + defer restore() + + mockDmsetup := testutil.MockCommand(c, "dmsetup", `echo "unexpected call" >&2; exit 1`) + s.AddCleanup(mockDmsetup.Restore) + + // pass a name that does not match partition name + dev, err := install.CreateEncryptedDeviceWithSetupHook(&mockDeviceStructureBadName, + s.mockedEncryptionKey, "some-name") + c.Assert(err, ErrorMatches, `cannot use partition name "some-name" for an encrypted structure with system-data role and filesystem with label "ubuntu-data"`) + c.Check(dev, IsNil) + c.Check(mockDmsetup.Calls(), HasLen, 0) + // make structure name different than the label, which is set to either + // the implicit value or has already been validated and matches what is + // expected for the particular role + mockDeviceStructureBadName.Name = "bad-name" + dev, err = install.CreateEncryptedDeviceWithSetupHook(&mockDeviceStructureBadName, + s.mockedEncryptionKey, "bad-name") + c.Assert(err, ErrorMatches, `cannot use partition name "bad-name" for an encrypted structure with system-data role and filesystem with label "ubuntu-data"`) + c.Check(dev, IsNil) + c.Check(mockDmsetup.Calls(), HasLen, 0) +} diff -Nru snapd-2.55.5+20.04/gadget/install/export_secboot_test.go snapd-2.57.5+20.04/gadget/install/export_secboot_test.go --- snapd-2.55.5+20.04/gadget/install/export_secboot_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/export_secboot_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,26 +22,27 @@ package install import ( - "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/kernel/fde" + "github.com/snapcore/snapd/secboot/keys" + "github.com/snapcore/snapd/testutil" ) var ( - DiskWithSystemSeed = diskWithSystemSeed - NewEncryptedDeviceLUKS = newEncryptedDeviceLUKS + DiskWithSystemSeed = diskWithSystemSeed + NewEncryptedDeviceLUKS = newEncryptedDeviceLUKS + CreateEncryptedDeviceWithSetupHook = createEncryptedDeviceWithSetupHook ) -func MockSecbootFormatEncryptedDevice(f func(key secboot.EncryptionKey, label, node string) error) (restore func()) { - old := secbootFormatEncryptedDevice +func MockSecbootFormatEncryptedDevice(f func(key keys.EncryptionKey, label, node string) error) (restore func()) { + r := testutil.Backup(&secbootFormatEncryptedDevice) secbootFormatEncryptedDevice = f - return func() { - secbootFormatEncryptedDevice = old - } + return r + } -func MockSecbootAddRecoveryKey(f func(key secboot.EncryptionKey, rkey secboot.RecoveryKey, node string) error) (restore func()) { - old := secbootAddRecoveryKey - secbootAddRecoveryKey = f - return func() { - secbootAddRecoveryKey = old - } +func MockBootRunFDESetupHook(f func(req *fde.SetupRequest) ([]byte, error)) (restore func()) { + r := testutil.Backup(&boot.RunFDESetupHook) + boot.RunFDESetupHook = f + return r } diff -Nru snapd-2.55.5+20.04/gadget/install/export_test.go snapd-2.57.5+20.04/gadget/install/export_test.go --- snapd-2.55.5+20.04/gadget/install/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -26,10 +26,12 @@ "github.com/snapcore/snapd/gadget/quantity" ) +type MkfsParams = mkfsParams + var ( - MakeFilesystem = makeFilesystem - WriteContent = writeContent - MountFilesystem = mountFilesystem + MakeFilesystem = makeFilesystem + WriteFilesystemContent = writeFilesystemContent + MountFilesystem = mountFilesystem CreateMissingPartitions = createMissingPartitions BuildPartitionList = buildPartitionList @@ -39,14 +41,6 @@ CreatedDuringInstall = createdDuringInstall ) -func MockContentMountpoint(new string) (restore func()) { - old := contentMountpoint - contentMountpoint = new - return func() { - contentMountpoint = old - } -} - func MockSysMount(f func(source, target, fstype string, flags uintptr, data string) error) (restore func()) { old := sysMount sysMount = f diff -Nru snapd-2.55.5+20.04/gadget/install/install_dummy.go snapd-2.57.5+20.04/gadget/install/install_dummy.go --- snapd-2.55.5+20.04/gadget/install/install_dummy.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/install_dummy.go 2022-10-17 16:25:18.000000000 +0000 @@ -31,3 +31,7 @@ func Run(model gadget.Model, gadgetRoot, kernelRoot, device string, options Options, _ gadget.ContentObserver, _ timings.Measurer) (*InstalledSystemSideData, error) { return nil, fmt.Errorf("build without secboot support") } + +func FactoryReset(model gadget.Model, gadgetRoot, kernelRoot, device string, options Options, _ gadget.ContentObserver, _ timings.Measurer) (*InstalledSystemSideData, error) { + return nil, fmt.Errorf("build without secboot support") +} diff -Nru snapd-2.55.5+20.04/gadget/install/install.go snapd-2.57.5+20.04/gadget/install/install.go --- snapd-2.55.5+20.04/gadget/install/install.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/install.go 2022-10-17 16:25:18.000000000 +0000 @@ -30,9 +30,11 @@ "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/timings" ) @@ -59,7 +61,7 @@ return "", fmt.Errorf("cannot find role system-seed in gadget") } -func roleOrLabelOrName(part gadget.OnDiskStructure) string { +func roleOrLabelOrName(part *gadget.OnDiskStructure) string { switch { case part.Role != "": return part.Role @@ -72,6 +74,109 @@ } } +func roleNeedsEncryption(role string) bool { + return role == gadget.SystemData || role == gadget.SystemSave +} + +func saveStorageTraits(allLaidOutVols map[string]*gadget.LaidOutVolume, optsPerVol map[string]*gadget.DiskVolumeValidationOptions, hasSavePartition bool) error { + allVolTraits, err := gadget.AllDiskVolumeDeviceTraits(allLaidOutVols, optsPerVol) + if err != nil { + return err + } + // save the traits to ubuntu-data host + if err := gadget.SaveDiskVolumesDeviceTraits(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir), allVolTraits); err != nil { + return fmt.Errorf("cannot save disk to volume device traits: %v", err) + } + // and also to ubuntu-save if it exists + if hasSavePartition { + if err := gadget.SaveDiskVolumesDeviceTraits(boot.InstallHostDeviceSaveDir, allVolTraits); err != nil { + return fmt.Errorf("cannot save disk to volume device traits: %v", err) + } + } + return nil +} + +func installOnePartition(part *gadget.OnDiskStructure, encryptionType secboot.EncryptionType, sectorSize quantity.Size, observer gadget.ContentObserver, perfTimings timings.Measurer) (fsDevice string, encryptionKey keys.EncryptionKey, err error) { + mustEncrypt := (encryptionType != secboot.EncryptionTypeNone) + partDisp := roleOrLabelOrName(part) + // a device that carries the filesystem, which is either the raw + // /dev/, or the mapped LUKS device if the structure is encrypted + fsDevice = part.Node + fsSectorSize := sectorSize + + if mustEncrypt && roleNeedsEncryption(part.Role) { + timings.Run(perfTimings, fmt.Sprintf("make-key-set[%s]", partDisp), + fmt.Sprintf("Create encryption key set for %s", partDisp), + func(timings.Measurer) { + encryptionKey, err = keys.NewEncryptionKey() + if err != nil { + err = fmt.Errorf("cannot create encryption key: %v", err) + } + }) + if err != nil { + return "", nil, err + } + logger.Noticef("encrypting partition device %v", part.Node) + var dataPart encryptedDevice + switch encryptionType { + case secboot.EncryptionTypeLUKS: + timings.Run(perfTimings, fmt.Sprintf("new-encrypted-device[%s]", partDisp), + fmt.Sprintf("Create encryption device for %s", partDisp), + func(timings.Measurer) { + dataPart, err = newEncryptedDeviceLUKS(part, encryptionKey, part.Label) + }) + if err != nil { + return "", nil, err + } + + case secboot.EncryptionTypeDeviceSetupHook: + timings.Run(perfTimings, fmt.Sprintf("new-encrypted-device-setup-hook[%s]", partDisp), + fmt.Sprintf("Create encryption device for %s using device-setup-hook", partDisp), + func(timings.Measurer) { + dataPart, err = createEncryptedDeviceWithSetupHook(part, encryptionKey, part.Name) + }) + if err != nil { + return "", nil, err + } + } + + // update the encrypted device node, such that subsequent steps + // operate on the right device + fsDevice = dataPart.Node() + logger.Noticef("encrypted filesystem device %v", fsDevice) + fsSectorSizeInt, err := disks.SectorSize(fsDevice) + if err != nil { + return "", nil, err + } + fsSectorSize = quantity.Size(fsSectorSizeInt) + } + + timings.Run(perfTimings, fmt.Sprintf("make-filesystem[%s]", partDisp), + fmt.Sprintf("Create filesystem for %s", fsDevice), + func(timings.Measurer) { + err = makeFilesystem(mkfsParams{ + Type: part.Filesystem, + Device: fsDevice, + Label: part.Label, + Size: part.Size, + SectorSize: fsSectorSize, + }) + }) + if err != nil { + return "", nil, fmt.Errorf("cannot make filesystem for partition %s: %v", partDisp, err) + } + + timings.Run(perfTimings, fmt.Sprintf("write-content[%s]", partDisp), + fmt.Sprintf("Write content for %s", partDisp), + func(timings.Measurer) { + err = writeFilesystemContent(part, fsDevice, observer) + }) + if err != nil { + return "", nil, err + } + return fsDevice, encryptionKey, nil +} + // Run bootstraps the partitions of a device, by either creating // missing ones or recreating installed ones. func Run(model gadget.Model, gadgetRoot, kernelRoot, bootDevice string, options Options, observer gadget.ContentObserver, perfTimings timings.Measurer) (*InstalledSystemSideData, error) { @@ -83,7 +188,7 @@ } if model.Grade() == asserts.ModelGradeUnset { - return nil, fmt.Errorf("cannot run install mode on non-UC20+ system") + return nil, fmt.Errorf("cannot run install mode on pre-UC20 system") } laidOutBootVol, allLaidOutVols, err := gadget.LaidOutVolumesFromGadget(gadgetRoot, kernelRoot, model) @@ -92,10 +197,8 @@ } // TODO: resolve content paths from gadget here - // XXX: the only situation where auto-detect is not desired is - // in (spread) testing - consider to remove forcing a device - // // auto-detect device if no device is forced + // device forcing is used for (spread) testing only if bootDevice == "" { bootDevice, err = diskWithSystemSeed(laidOutBootVol) if err != nil { @@ -136,25 +239,8 @@ return nil, fmt.Errorf("cannot create the partitions: %v", err) } - makeKeySet := func() (*EncryptionKeySet, error) { - key, err := secboot.NewEncryptionKey() - if err != nil { - return nil, fmt.Errorf("cannot create encryption key: %v", err) - } - - rkey, err := secboot.NewRecoveryKey() - if err != nil { - return nil, fmt.Errorf("cannot create recovery key: %v", err) - } - return &EncryptionKeySet{ - Key: key, - RecoveryKey: rkey, - }, nil - } - roleNeedsEncryption := func(role string) bool { - return role == gadget.SystemData || role == gadget.SystemSave - } - var keysForRoles map[string]*EncryptionKeySet + var keyForRole map[string]keys.EncryptionKey + devicesForRoles := map[string]string{} partsEncrypted := map[string]gadget.StructureEncryptionParameters{} @@ -167,49 +253,13 @@ } logger.Noticef("created new partition %v for structure %v (size %v) %s", part.Node, part, part.Size.IECString(), roleFmt) - encrypt := (options.EncryptionType != secboot.EncryptionTypeNone) - if part.Role == gadget.SystemSave { hasSavePartition = true } - - if encrypt && roleNeedsEncryption(part.Role) { - var keys *EncryptionKeySet - timings.Run(perfTimings, fmt.Sprintf("make-key-set[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create encryption key set for %s", roleOrLabelOrName(part)), func(timings.Measurer) { - keys, err = makeKeySet() - }) - if err != nil { - return nil, err - } - logger.Noticef("encrypting partition device %v", part.Node) - var dataPart encryptedDevice - timings.Run(perfTimings, fmt.Sprintf("new-encrypted-device[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create encryption device for %s", roleOrLabelOrName(part)), func(timings.Measurer) { - dataPart, err = newEncryptedDeviceLUKS(&part, keys.Key, part.Label) - }) - if err != nil { - return nil, err - } - - timings.Run(perfTimings, fmt.Sprintf("add-recovery-key[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Adding recovery key for %s", roleOrLabelOrName(part)), func(timings.Measurer) { - err = dataPart.AddRecoveryKey(keys.Key, keys.RecoveryKey) - }) - if err != nil { - return nil, err - } - - // update the encrypted device node - part.Node = dataPart.Node() - if keysForRoles == nil { - keysForRoles = map[string]*EncryptionKeySet{} - } - keysForRoles[part.Role] = keys - logger.Noticef("encrypted device %v", part.Node) - - // TODO: how to determine if this will be a LUKS or an ICE encrypted - // partition? - partsEncrypted[part.Name] = gadget.StructureEncryptionParameters{ - Method: gadget.EncryptionLUKS, - } + if part.Role != "" { + // keep track of the /dev/ (actual raw + // device) for each role + devicesForRoles[part.Role] = part.Node } // use the diskLayout.SectorSize here instead of lv.SectorSize, we check @@ -217,22 +267,33 @@ // matches what is on the disk, but sometimes there may not be a sector // size specified in the gadget.yaml, but we will always have the sector // size from the physical disk device - timings.Run(perfTimings, fmt.Sprintf("make-filesystem[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create filesystem for %s", part.Node), func(timings.Measurer) { - err = makeFilesystem(&part, diskLayout.SectorSize) - }) - if err != nil { - return nil, fmt.Errorf("cannot make filesystem for partition %s: %v", roleOrLabelOrName(part), err) - } - timings.Run(perfTimings, fmt.Sprintf("write-content[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Write content for %s", roleOrLabelOrName(part)), func(timings.Measurer) { - err = writeContent(&part, observer) - }) + // for encrypted device the filesystem device it will point to + // the mapper device otherwise it's the raw device node + fsDevice, encryptionKey, err := installOnePartition(&part, options.EncryptionType, + diskLayout.SectorSize, observer, perfTimings) if err != nil { return nil, err } + if encryptionKey != nil { + if keyForRole == nil { + keyForRole = map[string]keys.EncryptionKey{} + } + keyForRole[part.Role] = encryptionKey + switch options.EncryptionType { + case secboot.EncryptionTypeLUKS: + partsEncrypted[part.Name] = gadget.StructureEncryptionParameters{ + Method: gadget.EncryptionLUKS, + } + case secboot.EncryptionTypeDeviceSetupHook: + partsEncrypted[part.Name] = gadget.StructureEncryptionParameters{ + Method: gadget.EncryptionICE, + } + } + } if options.Mount && part.Label != "" && part.HasFilesystem() { - if err := mountFilesystem(&part, boot.InitramfsRunMntDir); err != nil { + if err := mountFilesystem(fsDevice, part.Filesystem, part.Label, boot.InitramfsRunMntDir); err != nil { return nil, err } } @@ -247,24 +308,129 @@ ExpectedStructureEncryption: partsEncrypted, }, } - allVolTraits, err := gadget.AllDiskVolumeDeviceTraits(allLaidOutVols, optsPerVol) - if err != nil { + // save the traits to ubuntu-data host and optionally to ubuntu-save if it exists + if err := saveStorageTraits(allLaidOutVols, optsPerVol, hasSavePartition); err != nil { return nil, err } - // save the traits to ubuntu-data host - if err := gadget.SaveDiskVolumesDeviceTraits(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir), allVolTraits); err != nil { - return nil, fmt.Errorf("cannot save disk to volume device traits: %v", err) + return &InstalledSystemSideData{ + KeyForRole: keyForRole, + DeviceForRole: devicesForRoles, + }, nil +} + +func FactoryReset(model gadget.Model, gadgetRoot, kernelRoot, bootDevice string, options Options, observer gadget.ContentObserver, perfTimings timings.Measurer) (*InstalledSystemSideData, error) { + logger.Noticef("performing factory reset on an installed system") + logger.Noticef(" gadget data from: %v", gadgetRoot) + logger.Noticef(" encryption: %v", options.EncryptionType) + if gadgetRoot == "" { + return nil, fmt.Errorf("cannot use empty gadget root directory") } - // and also to ubuntu-save if it exists + if model.Grade() == asserts.ModelGradeUnset { + return nil, fmt.Errorf("cannot run factory-reset mode on pre-UC20 system") + } + + laidOutBootVol, allLaidOutVols, err := gadget.LaidOutVolumesFromGadget(gadgetRoot, kernelRoot, model) + if err != nil { + return nil, fmt.Errorf("cannot layout volumes: %v", err) + } + // TODO: resolve content paths from gadget here + + // auto-detect device if no device is forced + // device forcing is used for (spread) testing only + if bootDevice == "" { + bootDevice, err = diskWithSystemSeed(laidOutBootVol) + if err != nil { + return nil, fmt.Errorf("cannot find device to create partitions on: %v", err) + } + } + + diskLayout, err := gadget.OnDiskVolumeFromDevice(bootDevice) + if err != nil { + return nil, fmt.Errorf("cannot read %v partitions: %v", bootDevice, err) + } + + layoutCompatOps := &gadget.EnsureLayoutCompatibilityOptions{ + AssumeCreatablePartitionsCreated: true, + ExpectedStructureEncryption: map[string]gadget.StructureEncryptionParameters{}, + } + if options.EncryptionType != secboot.EncryptionTypeNone { + var encryptionParam gadget.StructureEncryptionParameters + switch options.EncryptionType { + case secboot.EncryptionTypeLUKS: + encryptionParam = gadget.StructureEncryptionParameters{Method: gadget.EncryptionLUKS} + default: + // XXX what about ICE? + return nil, fmt.Errorf("unsupported encryption type %v", options.EncryptionType) + } + for _, volStruct := range laidOutBootVol.LaidOutStructure { + if !roleNeedsEncryption(volStruct.Role) { + continue + } + layoutCompatOps.ExpectedStructureEncryption[volStruct.Name] = encryptionParam + } + } + // factory reset is done on a system that was once installed, so this + // should be always successful unless the partition table has changed + if err := gadget.EnsureLayoutCompatibility(laidOutBootVol, diskLayout, layoutCompatOps); err != nil { + return nil, fmt.Errorf("gadget and system-boot device %v partition table not compatible: %v", bootDevice, err) + } + + var keyForRole map[string]keys.EncryptionKey + deviceForRole := map[string]string{} + + savePart := partitionsWithRolesAndContent(laidOutBootVol, diskLayout, []string{gadget.SystemSave}) + hasSavePartition := len(savePart) != 0 if hasSavePartition { - if err := gadget.SaveDiskVolumesDeviceTraits(boot.InstallHostDeviceSaveDir, allVolTraits); err != nil { - return nil, fmt.Errorf("cannot save disk to volume device traits: %v", err) + deviceForRole[gadget.SystemSave] = savePart[0].Node + } + rolesToReset := []string{gadget.SystemBoot, gadget.SystemData} + partsToReset := partitionsWithRolesAndContent(laidOutBootVol, diskLayout, rolesToReset) + for _, part := range partsToReset { + logger.Noticef("resetting %v structure %v (size %v) role %v", + part.Node, part, part.Size.IECString(), part.Role) + + if part.Role != "" { + // keep track of the /dev/ (actual raw + // device) for each role + deviceForRole[part.Role] = part.Node + } + + fsDevice, encryptionKey, err := installOnePartition(&part, options.EncryptionType, + diskLayout.SectorSize, observer, perfTimings) + if err != nil { + return nil, err } + if encryptionKey != nil { + if keyForRole == nil { + keyForRole = map[string]keys.EncryptionKey{} + } + keyForRole[part.Role] = encryptionKey + } + if options.Mount && part.Label != "" && part.HasFilesystem() { + if err := mountFilesystem(fsDevice, part.Filesystem, part.Label, boot.InitramfsRunMntDir); err != nil { + return nil, err + } + } + } + + // after we have created all partitions, build up the mapping of volumes + // to disk device traits and save it to disk for later usage + optsPerVol := map[string]*gadget.DiskVolumeValidationOptions{ + // this assumes that the encrypted partitions above are always only on the + // system-boot volume, this assumption may change + laidOutBootVol.Name: { + ExpectedStructureEncryption: layoutCompatOps.ExpectedStructureEncryption, + }, + } + // save the traits to ubuntu-data host and optionally to ubuntu-save if it exists + if err := saveStorageTraits(allLaidOutVols, optsPerVol, hasSavePartition); err != nil { + return nil, err } return &InstalledSystemSideData{ - KeysForRoles: keysForRoles, + KeyForRole: keyForRole, + DeviceForRole: deviceForRole, }, nil } diff -Nru snapd-2.55.5+20.04/gadget/install/install_test.go snapd-2.57.5+20.04/gadget/install/install_test.go --- snapd-2.55.5+20.04/gadget/install/install_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/install_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -3,7 +3,7 @@ // +build !nosecboot /* - * Copyright (C) 2019-2020 Canonical Ltd + * Copyright (C) 2019-2022 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 @@ -38,6 +38,7 @@ "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/timings" ) @@ -68,7 +69,7 @@ c.Check(sys, IsNil) sys, err = install.Run(&gadgettest.ModelCharacteristics{}, c.MkDir(), "", "", install.Options{}, nil, timings.New(nil)) - c.Assert(err, ErrorMatches, `cannot run install mode on non-UC20\+ system`) + c.Assert(err, ErrorMatches, `cannot run install mode on pre-UC20 system`) c.Check(sys, IsNil) } @@ -152,6 +153,11 @@ mockCryptsetup := testutil.MockCommand(c, "cryptsetup", "") defer mockCryptsetup.Restore() + if opts.encryption { + mockBlockdev := testutil.MockCommand(c, "blockdev", "case ${1} in --getss) echo 4096; exit 0;; esac; exit 1") + defer mockBlockdev.Restore() + } + restore = install.MockEnsureNodesExist(func(dss []gadget.OnDiskStructure, timeout time.Duration) error { c.Assert(timeout, Equals, 5*time.Second) c.Assert(dss, DeepEquals, []gadget.OnDiskStructure{ @@ -256,22 +262,24 @@ c.Assert(typ, Equals, "ext4") if opts.encryption { c.Assert(img, Equals, "/dev/mapper/ubuntu-save") + c.Assert(sectorSize, Equals, quantity.Size(4096)) } else { c.Assert(img, Equals, "/dev/mmcblk0p3") + c.Assert(sectorSize, Equals, quantity.Size(512)) } c.Assert(label, Equals, "ubuntu-save") c.Assert(devSize, Equals, 16*quantity.SizeMiB) - c.Assert(sectorSize, Equals, quantity.Size(512)) case 3: c.Assert(typ, Equals, "ext4") if opts.encryption { c.Assert(img, Equals, "/dev/mapper/ubuntu-data") + c.Assert(sectorSize, Equals, quantity.Size(4096)) } else { c.Assert(img, Equals, "/dev/mmcblk0p4") + c.Assert(sectorSize, Equals, quantity.Size(512)) } c.Assert(label, Equals, "ubuntu-data") c.Assert(devSize, Equals, (30528-(1+1200+750+16))*quantity.SizeMiB) - c.Assert(sectorSize, Equals, quantity.Size(512)) default: c.Errorf("unexpected call (%d) to mkfs.Make()", mkfsCall) return fmt.Errorf("test broken") @@ -280,17 +288,13 @@ }) defer restore() - mockMountpoint := c.MkDir() - restore = install.MockContentMountpoint(mockMountpoint) - defer restore() - mountCall := 0 restore = install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error { mountCall++ switch mountCall { case 1: c.Assert(source, Equals, "/dev/mmcblk0p2") - c.Assert(target, Equals, filepath.Join(mockMountpoint, "2")) + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/2")) c.Assert(fstype, Equals, "vfat") c.Assert(flags, Equals, uintptr(0)) c.Assert(data, Equals, "") @@ -300,7 +304,7 @@ } else { c.Assert(source, Equals, "/dev/mmcblk0p3") } - c.Assert(target, Equals, filepath.Join(mockMountpoint, "3")) + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/3")) c.Assert(fstype, Equals, "ext4") c.Assert(flags, Equals, uintptr(0)) c.Assert(data, Equals, "") @@ -310,7 +314,7 @@ } else { c.Assert(source, Equals, "/dev/mmcblk0p4") } - c.Assert(target, Equals, filepath.Join(mockMountpoint, "4")) + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/4")) c.Assert(fstype, Equals, "ext4") c.Assert(flags, Equals, uintptr(0)) c.Assert(data, Equals, "") @@ -327,13 +331,13 @@ umountCall++ switch umountCall { case 1: - c.Assert(target, Equals, filepath.Join(mockMountpoint, "2")) + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/2")) c.Assert(flags, Equals, 0) case 2: - c.Assert(target, Equals, filepath.Join(mockMountpoint, "3")) + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/3")) c.Assert(flags, Equals, 0) case 3: - c.Assert(target, Equals, filepath.Join(mockMountpoint, "4")) + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/4")) c.Assert(flags, Equals, 0) default: c.Errorf("unexpected umount call (%d)", umountCall) @@ -346,10 +350,10 @@ gadgetRoot, err := gadgettest.WriteGadgetYaml(c.MkDir(), gadgettest.RaspiSimplifiedYaml) c.Assert(err, IsNil) - var savePrimaryKey, dataPrimaryKey secboot.EncryptionKey + var saveEncryptionKey, dataEncryptionKey keys.EncryptionKey secbootFormatEncryptedDeviceCall := 0 - restore = install.MockSecbootFormatEncryptedDevice(func(key secboot.EncryptionKey, label, node string) error { + restore = install.MockSecbootFormatEncryptedDevice(func(key keys.EncryptionKey, label, node string) error { if !opts.encryption { c.Error("unexpected call to secboot.FormatEncryptedDevice when encryption is off") return fmt.Errorf("no encryption functions should be called") @@ -360,12 +364,12 @@ c.Assert(key, HasLen, 32) c.Assert(label, Equals, "ubuntu-save-enc") c.Assert(node, Equals, "/dev/mmcblk0p3") - savePrimaryKey = key + saveEncryptionKey = key case 2: c.Assert(key, HasLen, 32) c.Assert(label, Equals, "ubuntu-data-enc") c.Assert(node, Equals, "/dev/mmcblk0p4") - dataPrimaryKey = key + dataEncryptionKey = key default: c.Errorf("unexpected call to secboot.FormatEncryptedDevice (%d)", secbootFormatEncryptedDeviceCall) return fmt.Errorf("test broken") @@ -375,37 +379,6 @@ }) defer restore() - var saveRecoveryKey, dataRecoveryKey secboot.RecoveryKey - - secbootAddRecoveryKeyCall := 0 - restore = install.MockSecbootAddRecoveryKey(func(key secboot.EncryptionKey, rkey secboot.RecoveryKey, node string) error { - if !opts.encryption { - c.Error("unexpected call to secboot.AddRecoveryKey when encryption is off") - return fmt.Errorf("no encryption functions should be called") - } - secbootAddRecoveryKeyCall++ - switch secbootAddRecoveryKeyCall { - case 1: - c.Assert(key, HasLen, 32) - c.Assert(key, DeepEquals, savePrimaryKey) - c.Assert(rkey, HasLen, 16) - c.Assert(node, Equals, "/dev/mmcblk0p3") - saveRecoveryKey = rkey - case 2: - c.Assert(key, HasLen, 32) - c.Assert(key, DeepEquals, dataPrimaryKey) - c.Assert(rkey, HasLen, 16) - c.Assert(node, Equals, "/dev/mmcblk0p4") - dataRecoveryKey = rkey - default: - c.Errorf("unexpected call to secboot.AddRecoveryKey (%d)", secbootAddRecoveryKeyCall) - return fmt.Errorf("test broken") - } - - return nil - }) - defer restore() - // 10 million mocks later ... // finally actually run the install runOpts := install.Options{} @@ -417,19 +390,24 @@ if opts.encryption { c.Check(sys, Not(IsNil)) c.Assert(sys, DeepEquals, &install.InstalledSystemSideData{ - KeysForRoles: map[string]*install.EncryptionKeySet{ - gadget.SystemData: { - Key: dataPrimaryKey, - RecoveryKey: dataRecoveryKey, - }, - gadget.SystemSave: { - Key: savePrimaryKey, - RecoveryKey: saveRecoveryKey, - }, + KeyForRole: map[string]keys.EncryptionKey{ + gadget.SystemData: dataEncryptionKey, + gadget.SystemSave: saveEncryptionKey, + }, + DeviceForRole: map[string]string{ + "system-boot": "/dev/mmcblk0p2", + "system-save": "/dev/mmcblk0p3", + "system-data": "/dev/mmcblk0p4", }, }) } else { - c.Assert(sys, DeepEquals, &install.InstalledSystemSideData{}) + c.Assert(sys, DeepEquals, &install.InstalledSystemSideData{ + DeviceForRole: map[string]string{ + "system-boot": "/dev/mmcblk0p2", + "system-save": "/dev/mmcblk0p3", + "system-data": "/dev/mmcblk0p4", + }, + }) } expSfdiskCalls := [][]string{} @@ -476,10 +454,8 @@ c.Assert(umountCall, Equals, 3) if opts.encryption { c.Assert(secbootFormatEncryptedDeviceCall, Equals, 2) - c.Assert(secbootAddRecoveryKeyCall, Equals, 2) } else { c.Assert(secbootFormatEncryptedDeviceCall, Equals, 0) - c.Assert(secbootAddRecoveryKeyCall, Equals, 0) } // check the disk-mapping.json that was written as well @@ -609,3 +585,377 @@ _, err = install.DiskWithSystemSeed(lv) c.Assert(err, ErrorMatches, "cannot find role system-seed in gadget") } + +type factoryResetOpts struct { + encryption bool + err string + disk *disks.MockDiskMapping + noSave bool + gadgetYaml string + traitsJSON string + traits gadget.DiskVolumeDeviceTraits +} + +func (s *installSuite) testFactoryReset(c *C, opts factoryResetOpts) { + uc20Mod := &gadgettest.ModelCharacteristics{ + SystemSeed: true, + } + + if opts.noSave && opts.encryption { + c.Fatalf("unsupported test scenario, cannot use encryption without ubuntu-save") + } + + s.setupMockUdevSymlinks(c, "mmcblk0p1") + + // mock single partition mapping to a disk with only ubuntu-seed partition + c.Assert(opts.disk, NotNil, Commentf("mock disk must be provided")) + restore := disks.MockPartitionDeviceNodeToDiskMapping(map[string]*disks.MockDiskMapping{ + filepath.Join(s.dir, "/dev/mmcblk0p1"): opts.disk, + }) + defer restore() + + restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/mmcblk0": opts.disk, + }) + defer restore() + + mockSfdisk := testutil.MockCommand(c, "sfdisk", "") + defer mockSfdisk.Restore() + + mockPartx := testutil.MockCommand(c, "partx", "") + defer mockPartx.Restore() + + mockUdevadm := testutil.MockCommand(c, "udevadm", "") + defer mockUdevadm.Restore() + + mockCryptsetup := testutil.MockCommand(c, "cryptsetup", "") + defer mockCryptsetup.Restore() + + if opts.encryption { + mockBlockdev := testutil.MockCommand(c, "blockdev", "case ${1} in --getss) echo 4096; exit 0;; esac; exit 1") + defer mockBlockdev.Restore() + } + + dataDev := "/dev/mmcblk0p4" + if opts.noSave { + dataDev = "/dev/mmcblk0p3" + } + if opts.encryption { + dataDev = "/dev/mapper/ubuntu-data" + } + restore = install.MockEnsureNodesExist(func(dss []gadget.OnDiskStructure, timeout time.Duration) error { + c.Assert(timeout, Equals, 5*time.Second) + expectedDss := []gadget.OnDiskStructure{ + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + VolumeName: "pi", + Name: "ubuntu-boot", + Label: "ubuntu-boot", + Size: 750 * quantity.SizeMiB, + Type: "0C", + Role: gadget.SystemBoot, + Filesystem: "vfat", + }, + StartOffset: (1 + 1200) * quantity.OffsetMiB, + YamlIndex: 1, + }, + // note this is YamlIndex + 1, the YamlIndex starts at 0 + DiskIndex: 2, + Node: "/dev/mmcblk0p2", + Size: 750 * quantity.SizeMiB, + }, + } + if opts.noSave { + // just data + expectedDss = append(expectedDss, gadget.OnDiskStructure{ + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + VolumeName: "pi", + Name: "ubuntu-data", + Label: "ubuntu-data", + // TODO: this is set from the yaml, not from the actual + // calculated disk size, probably should be updated + // somewhere + Size: 1500 * quantity.SizeMiB, + Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", + Role: gadget.SystemData, + Filesystem: "ext4", + }, + StartOffset: (1 + 1200 + 750) * quantity.OffsetMiB, + YamlIndex: 2, + }, + // note this is YamlIndex + 1, the YamlIndex starts at 0 + DiskIndex: 3, + Node: dataDev, + Size: (30528 - (1 + 1200 + 750)) * quantity.SizeMiB, + }) + } else { + // data + save + expectedDss = append(expectedDss, []gadget.OnDiskStructure{{ + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + VolumeName: "pi", + Name: "ubuntu-save", + Label: "ubuntu-save", + Size: 16 * quantity.SizeMiB, + Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", + Role: gadget.SystemSave, + Filesystem: "ext4", + }, + StartOffset: (1 + 1200 + 750) * quantity.OffsetMiB, + YamlIndex: 2, + }, + // note this is YamlIndex + 1, the YamlIndex starts at 0 + DiskIndex: 3, + Node: "/dev/mmcblk0p3", + Size: 16 * quantity.SizeMiB, + }, { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + VolumeName: "pi", + Name: "ubuntu-data", + Label: "ubuntu-data", + // TODO: this is set from the yaml, not from the actual + // calculated disk size, probably should be updated + // somewhere + Size: 1500 * quantity.SizeMiB, + Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", + Role: gadget.SystemData, + Filesystem: "ext4", + }, + StartOffset: (1 + 1200 + 750 + 16) * quantity.OffsetMiB, + YamlIndex: 3, + }, + // note this is YamlIndex + 1, the YamlIndex starts at 0 + DiskIndex: 4, + Node: dataDev, + Size: (30528 - (1 + 1200 + 750 + 16)) * quantity.SizeMiB, + }}...) + } + c.Assert(dss, DeepEquals, expectedDss) + + return nil + }) + defer restore() + + mkfsCall := 0 + restore = install.MockMkfsMake(func(typ, img, label string, devSize, sectorSize quantity.Size) error { + mkfsCall++ + switch mkfsCall { + case 1: + c.Assert(typ, Equals, "vfat") + c.Assert(img, Equals, "/dev/mmcblk0p2") + c.Assert(label, Equals, "ubuntu-boot") + c.Assert(devSize, Equals, 750*quantity.SizeMiB) + c.Assert(sectorSize, Equals, quantity.Size(512)) + case 2: + c.Assert(typ, Equals, "ext4") + c.Assert(img, Equals, dataDev) + c.Assert(label, Equals, "ubuntu-data") + if opts.noSave { + c.Assert(devSize, Equals, (30528-(1+1200+750))*quantity.SizeMiB) + } else { + c.Assert(devSize, Equals, (30528-(1+1200+750+16))*quantity.SizeMiB) + } + if opts.encryption { + c.Assert(sectorSize, Equals, quantity.Size(4096)) + } else { + c.Assert(sectorSize, Equals, quantity.Size(512)) + } + default: + c.Errorf("unexpected call (%d) to mkfs.Make()", mkfsCall) + return fmt.Errorf("test broken") + } + return nil + }) + defer restore() + + mountCall := 0 + restore = install.MockSysMount(func(source, target, fstype string, flags uintptr, data string) error { + mountCall++ + switch mountCall { + case 1: + c.Assert(source, Equals, "/dev/mmcblk0p2") + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/2")) + c.Assert(fstype, Equals, "vfat") + c.Assert(flags, Equals, uintptr(0)) + c.Assert(data, Equals, "") + case 2: + c.Assert(source, Equals, dataDev) + if opts.noSave { + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/3")) + } else { + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/4")) + } + c.Assert(fstype, Equals, "ext4") + c.Assert(flags, Equals, uintptr(0)) + c.Assert(data, Equals, "") + default: + c.Errorf("unexpected mount call (%d)", mountCall) + return fmt.Errorf("test broken") + } + return nil + }) + defer restore() + + umountCall := 0 + restore = install.MockSysUnmount(func(target string, flags int) error { + umountCall++ + switch umountCall { + case 1: + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/2")) + c.Assert(flags, Equals, 0) + case 2: + if opts.noSave { + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/3")) + } else { + c.Assert(target, Equals, filepath.Join(dirs.SnapRunDir, "gadget-install/4")) + } + c.Assert(flags, Equals, 0) + default: + c.Errorf("unexpected umount call (%d)", umountCall) + return fmt.Errorf("test broken") + } + return nil + }) + defer restore() + + gadgetRoot, err := gadgettest.WriteGadgetYaml(c.MkDir(), opts.gadgetYaml) + c.Assert(err, IsNil) + + var dataPrimaryKey keys.EncryptionKey + secbootFormatEncryptedDeviceCall := 0 + restore = install.MockSecbootFormatEncryptedDevice(func(key keys.EncryptionKey, label, node string) error { + if !opts.encryption { + c.Error("unexpected call to secboot.FormatEncryptedDevice") + return fmt.Errorf("unexpected call") + } + secbootFormatEncryptedDeviceCall++ + switch secbootFormatEncryptedDeviceCall { + case 1: + c.Assert(key, HasLen, 32) + c.Assert(label, Equals, "ubuntu-data-enc") + c.Assert(node, Equals, "/dev/mmcblk0p4") + dataPrimaryKey = key + default: + c.Errorf("unexpected call to secboot.FormatEncryptedDevice (%d)", secbootFormatEncryptedDeviceCall) + return fmt.Errorf("test broken") + } + return nil + }) + defer restore() + + // 10 million mocks later ... + // finally actually run the factory reset + runOpts := install.Options{} + if opts.encryption { + runOpts.EncryptionType = secboot.EncryptionTypeLUKS + } + sys, err := install.FactoryReset(uc20Mod, gadgetRoot, "", "", runOpts, nil, timings.New(nil)) + if opts.err != "" { + c.Check(sys, IsNil) + c.Check(err, ErrorMatches, opts.err) + return + } + c.Assert(err, IsNil) + devsForRoles := map[string]string{ + "system-boot": "/dev/mmcblk0p2", + "system-save": "/dev/mmcblk0p3", + "system-data": "/dev/mmcblk0p4", + } + if opts.noSave { + devsForRoles = map[string]string{ + "system-boot": "/dev/mmcblk0p2", + "system-data": "/dev/mmcblk0p3", + } + } + if !opts.encryption { + c.Assert(sys, DeepEquals, &install.InstalledSystemSideData{ + DeviceForRole: devsForRoles, + }) + } else { + c.Assert(sys, DeepEquals, &install.InstalledSystemSideData{ + KeyForRole: map[string]keys.EncryptionKey{ + gadget.SystemData: dataPrimaryKey, + }, + DeviceForRole: devsForRoles, + }) + } + + c.Assert(mockSfdisk.Calls(), HasLen, 0) + c.Assert(mockPartx.Calls(), HasLen, 0) + + udevmadmCalls := [][]string{ + {"udevadm", "trigger", "--settle", "/dev/mmcblk0p2"}, + {"udevadm", "trigger", "--settle", dataDev}, + } + + c.Assert(mockUdevadm.Calls(), DeepEquals, udevmadmCalls) + c.Assert(mkfsCall, Equals, 2) + c.Assert(mountCall, Equals, 2) + c.Assert(umountCall, Equals, 2) + + // check the disk-mapping.json that was written as well + mappingOnData, err := gadget.LoadDiskVolumesDeviceTraits(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir)) + c.Assert(err, IsNil) + c.Assert(mappingOnData, DeepEquals, map[string]gadget.DiskVolumeDeviceTraits{ + "pi": opts.traits, + }) + + // we get the same thing on ubuntu-save + dataFile := filepath.Join(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir), "disk-mapping.json") + if !opts.noSave { + saveFile := filepath.Join(boot.InstallHostDeviceSaveDir, "disk-mapping.json") + c.Assert(dataFile, testutil.FileEquals, testutil.FileContentRef(saveFile)) + } + + // also for extra paranoia, compare the object we load with manually loading + // the static JSON to make sure they compare the same, this ensures that + // the JSON that is written always stays compatible + jsonBytes := []byte(opts.traitsJSON) + err = ioutil.WriteFile(dataFile, jsonBytes, 0644) + c.Assert(err, IsNil) + + mapping2, err := gadget.LoadDiskVolumesDeviceTraits(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir)) + c.Assert(err, IsNil) + + c.Assert(mapping2, DeepEquals, mappingOnData) +} + +func (s *installSuite) TestFactoryResetHappyWithExisting(c *C) { + s.testFactoryReset(c, factoryResetOpts{ + disk: gadgettest.ExpectedRaspiMockDiskMapping, + gadgetYaml: gadgettest.RaspiSimplifiedYaml, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeDeviceTraitsJSON, + traits: gadgettest.ExpectedRaspiDiskVolumeDeviceTraits, + }) +} + +func (s *installSuite) TestFactoryResetHappyWithoutDataAndBoot(c *C) { + s.testFactoryReset(c, factoryResetOpts{ + disk: gadgettest.ExpectedRaspiMockDiskInstallModeMapping, + gadgetYaml: gadgettest.RaspiSimplifiedYaml, + err: "gadget and system-boot device /dev/mmcblk0 partition table not compatible: cannot find .*ubuntu-boot.*", + }) +} + +func (s *installSuite) TestFactoryResetHappyWithoutSave(c *C) { + s.testFactoryReset(c, factoryResetOpts{ + disk: gadgettest.ExpectedRaspiMockDiskMappingNoSave, + gadgetYaml: gadgettest.RaspiSimplifiedNoSaveYaml, + noSave: true, + traitsJSON: gadgettest.ExpectedRaspiDiskVolumeNoSaveDeviceTraitsJSON, + traits: gadgettest.ExpectedRaspiDiskVolumeDeviceNoSaveTraits, + }) +} + +func (s *installSuite) TestFactoryResetHappyEncrypted(c *C) { + s.testFactoryReset(c, factoryResetOpts{ + encryption: true, + disk: gadgettest.ExpectedLUKSEncryptedRaspiMockDiskMapping, + gadgetYaml: gadgettest.RaspiSimplifiedYaml, + traitsJSON: gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraitsJSON, + traits: gadgettest.ExpectedLUKSEncryptedRaspiDiskVolumeDeviceTraits, + }) +} diff -Nru snapd-2.55.5+20.04/gadget/install/params.go snapd-2.57.5+20.04/gadget/install/params.go --- snapd-2.55.5+20.04/gadget/install/params.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/params.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,6 +21,7 @@ import ( "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" ) type Options struct { @@ -30,15 +31,13 @@ EncryptionType secboot.EncryptionType } -// EncryptionKeySet is a set of encryption keys. -type EncryptionKeySet struct { - Key secboot.EncryptionKey - RecoveryKey secboot.RecoveryKey -} - // InstalledSystemSideData carries side data of an installed system, eg. secrets // to access its partitions. type InstalledSystemSideData struct { // KeysForRoles contains key sets for the relevant structure roles. - KeysForRoles map[string]*EncryptionKeySet + KeyForRole map[string]keys.EncryptionKey + // DeviceForRole maps a roles to their corresponding device nodes. For + // structures with roles that require data to be encrypted, the device + // is the raw encrypted device node (eg. /dev/mmcblk0p1). + DeviceForRole map[string]string } diff -Nru snapd-2.55.5+20.04/gadget/install/partition.go snapd-2.57.5+20.04/gadget/install/partition.go --- snapd-2.55.5+20.04/gadget/install/partition.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/install/partition.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" + "github.com/snapcore/snapd/strutil" ) var ( @@ -264,6 +265,30 @@ return nil } +func partitionsWithRolesAndContent(lv *gadget.LaidOutVolume, dl *gadget.OnDiskVolume, roles []string) []gadget.OnDiskStructure { + roleForOffset := map[quantity.Offset]*gadget.LaidOutStructure{} + for idx, gs := range lv.LaidOutStructure { + if gs.Role != "" { + roleForOffset[gs.StartOffset] = &lv.LaidOutStructure[idx] + } + } + + var parts []gadget.OnDiskStructure + for _, part := range dl.Structure { + gs := roleForOffset[part.StartOffset] + if gs == nil || gs.Role == "" || !strutil.ListContains(roles, gs.Role) { + continue + } + // now that we have a match, override the laid out structure + // such that the fields reflect what was declared in the gadget, + // the on-disk-structure already has the right size as read from + // the partition table + part.LaidOutStructure = *gs + parts = append(parts, part) + } + return parts +} + // ensureNodeExists makes sure the device nodes for all device structures are // available and notified to udev, within a specified amount of time. func ensureNodesExistImpl(dss []gadget.OnDiskStructure, timeout time.Duration) error { diff -Nru snapd-2.55.5+20.04/gadget/layout_test.go snapd-2.57.5+20.04/gadget/layout_test.go --- snapd-2.55.5+20.04/gadget/layout_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/layout_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1081,7 +1081,7 @@ } func mockKernel(c *C, kernelYaml string, filesWithContent map[string]string) string { - // sanity + // precondition _, err := kernel.InfoFromKernelYaml([]byte(kernelYaml)) c.Assert(err, IsNil) diff -Nru snapd-2.55.5+20.04/gadget/quantity/size.go snapd-2.57.5+20.04/gadget/quantity/size.go --- snapd-2.55.5+20.04/gadget/quantity/size.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/quantity/size.go 2022-10-17 16:25:18.000000000 +0000 @@ -70,8 +70,8 @@ // IECString formats the size using multiples from IEC units (i.e. kibibytes, // mebibytes), that is as multiples of 1024. Printed values are truncated to 2 // decimal points. -func (s *Size) IECString() string { - return iecSizeString(int64(*s)) +func (s Size) IECString() string { + return iecSizeString(int64(s)) } func (s *Size) UnmarshalYAML(unmarshal func(interface{}) error) error { diff -Nru snapd-2.55.5+20.04/gadget/update.go snapd-2.57.5+20.04/gadget/update.go --- snapd-2.55.5+20.04/gadget/update.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/update.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,16 +22,17 @@ import ( "errors" "fmt" - "path/filepath" + "sort" "strings" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/kernel" "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" + "github.com/snapcore/snapd/strutil" ) var ( @@ -292,7 +293,7 @@ numPartsOnDisk := len(diskLayout.Structure) return s.Filesystem == "ext4" && - s.Type == "0FC63DAF-8483-4772-8E79-3D69D8477DE4" && // TODO: check hybrid and on MBR/DOS too + (s.Type == "0FC63DAF-8483-4772-8E79-3D69D8477DE4" || s.Type == "83") && s.Label == "writable" && // DiskIndex is 1-based s.DiskIndex == numPartsOnDisk && @@ -881,8 +882,7 @@ // check if there is a marker file written, that will indicate if // encryption was turned on - encryptionMarkerFile := filepath.Join(dirs.SnapFDEDir, "marker") - if osutil.FileExists(encryptionMarkerFile) { + if device.HasEncryptedMarkerUnder(dirs.SnapFDEDir) { // then we have the crypto marker file for encryption // cross-validation between ubuntu-data and ubuntu-save stored from // install mode, so mark ubuntu-save and data as expected to be @@ -946,9 +946,6 @@ } volumeStructureToLocation := make(map[string]map[int]StructureLocation, len(old.Info.Volumes)) - for volName := range old.Info.Volumes { - volumeStructureToLocation[volName] = make(map[int]StructureLocation) - } // now for each volume, iterate over the structures, putting the // necessary info into the map for that volume as we iterate @@ -958,6 +955,7 @@ // unsupported structure change is present in the new one, but we check that // situation after we have built the mapping for volName, diskDeviceTraits := range volToDeviceMapping { + volumeStructureToLocation[volName] = make(map[int]StructureLocation) vol, ok := old.Info.Volumes[volName] if !ok { return nil, fmt.Errorf("internal error: volume %s not present in gadget.yaml but present in traits mapping", volName) @@ -1071,7 +1069,15 @@ return volumeStructureToLocation, nil } -// exposed for mocking later on +func MockVolumeStructureToLocationMap(f func(_ GadgetData, _ Model, _ map[string]*LaidOutVolume) (map[string]map[int]StructureLocation, error)) (restore func()) { + old := volumeStructureToLocationMap + volumeStructureToLocationMap = f + return func() { + volumeStructureToLocationMap = old + } +} + +// use indirection to allow mocking var volumeStructureToLocationMap = volumeStructureToLocationMapImpl func volumeStructureToLocationMapImpl(old GadgetData, mod Model, laidOutVols map[string]*LaidOutVolume) (map[string]map[int]StructureLocation, error) { @@ -1174,74 +1180,185 @@ // d. After step (c) is completed the kernel refresh will now also // work (no more violation of rule 1) func Update(model Model, old, new GadgetData, rollbackDirPath string, updatePolicy UpdatePolicyFunc, observer ContentUpdateObserver) error { - // TODO: support multi-volume gadgets. But for now we simply - // do not do any gadget updates on those. We cannot error - // here because this would break refreshes of gadgets even - // when they don't require any updates. - if len(new.Info.Volumes) != 1 || len(old.Info.Volumes) != 1 { - logger.Noticef("WARNING: gadget assests cannot be updated yet when multiple volumes are used") - return nil + // if the volumes from the old and the new gadgets do not match, then fail - + // we don't support adding or removing volumes from the gadget.yaml + newVolumes := make([]string, 0, len(new.Info.Volumes)) + oldVolumes := make([]string, 0, len(old.Info.Volumes)) + for newVol := range new.Info.Volumes { + newVolumes = append(newVolumes, newVol) + } + for oldVol := range old.Info.Volumes { + oldVolumes = append(oldVolumes, oldVol) + } + common := strutil.Intersection(newVolumes, oldVolumes) + // check dissimilar cases between common, new and old + switch { + case len(common) != len(newVolumes) && len(common) != len(oldVolumes): + // there are both volumes removed from old and volumes added to new + return fmt.Errorf("cannot update gadget assets: volumes were both added and removed") + case len(common) != len(newVolumes): + // then there are volumes in old that are not in new, i.e. a volume + // was removed + return fmt.Errorf("cannot update gadget assets: volumes were removed") + case len(common) != len(oldVolumes): + // then there are volumes in new that are not in old, i.e. a volume + // was added + return fmt.Errorf("cannot update gadget assets: volumes were added") } - oldVol, newVol, err := resolveVolume(old.Info, new.Info) - if err != nil { - return err + if updatePolicy == nil { + updatePolicy = defaultPolicy } - if oldVol.Schema == "" || newVol.Schema == "" { - return fmt.Errorf("internal error: unset volume schemas: old: %q new: %q", oldVol.Schema, newVol.Schema) - } + // collect the updates and validate that they are doable from an abstract + // sense first - // layout old partially, without going deep into the layout of structure - // content - pOld, err := LayoutVolumePartially(oldVol, DefaultConstraints) - if err != nil { - return fmt.Errorf("cannot lay out the old volume: %v", err) - } + // note that this code is written such that before we perform any update, we + // validate that all updates are valid and that all volumes are compatible + // between the old and the new state, this is to prevent applying valid + // updates on one volume when another volume is invalid, if that's the case + // we treat the whole gadget as invalid and return an error blocking the + // refresh + + // TODO: should we handle the updates on multiple volumes in a + // deterministic order? iterating over maps is not deterministic, but we + // perform all updates at the end together in one call - // Layout new volume, delay resolving of filesystem content - constraints := DefaultConstraints - constraints.SkipResolveContent = true - pNew, err := LayoutVolume(new.RootDir, new.KernelRootDir, newVol, constraints) + // ensure all required kernel assets are found in the gadget + kernelInfo, err := kernel.ReadInfo(new.KernelRootDir) if err != nil { - return fmt.Errorf("cannot lay out the new volume: %v", err) + return err } - if err := canUpdateVolume(pOld, pNew); err != nil { - return fmt.Errorf("cannot apply update to volume: %v", err) + allKernelAssets := []string{} + for assetName, asset := range kernelInfo.Assets { + if !asset.Update { + continue + } + allKernelAssets = append(allKernelAssets, assetName) } - if updatePolicy == nil { - updatePolicy = defaultPolicy + atLeastOneKernelAssetConsumed := false + + allUpdates := []updatePair{} + laidOutVols := map[string]*LaidOutVolume{} + for volName, oldVol := range old.Info.Volumes { + newVol := new.Info.Volumes[volName] + + if oldVol.Schema == "" || newVol.Schema == "" { + return fmt.Errorf("internal error: unset volume schemas: old: %q new: %q", oldVol.Schema, newVol.Schema) + } + + // layout old partially, without going deep into the layout of structure + // content + pOld, err := LayoutVolumePartially(oldVol, DefaultConstraints) + if err != nil { + return fmt.Errorf("cannot lay out the old volume %s: %v", volName, err) + } + + // Layout new volume, delay resolving of filesystem content + constraints := DefaultConstraints + constraints.SkipResolveContent = true + pNew, err := LayoutVolume(new.RootDir, new.KernelRootDir, newVol, constraints) + if err != nil { + return fmt.Errorf("cannot lay out the new volume %s: %v", volName, err) + } + + laidOutVols[volName] = pNew + + if err := canUpdateVolume(pOld, pNew); err != nil { + return fmt.Errorf("cannot apply update to volume %s: %v", volName, err) + } + + // if we haven't consumed any kernel assets yet check if this volume + // consumes at least one - we require at least one asset to be consumed + // by some volume in the gadget + if !atLeastOneKernelAssetConsumed { + consumed, err := gadgetVolumeKernelUpdateAssetsConsumed(pNew.Volume, kernelInfo) + if err != nil { + return err + } + atLeastOneKernelAssetConsumed = consumed + } + + // now we know which structure is which, find which ones need an update + updates, err := resolveUpdate(pOld, pNew, updatePolicy, new.RootDir, new.KernelRootDir, kernelInfo) + if err != nil { + return err + } + + // can update old layout to new layout + for _, update := range updates { + if err := canUpdateStructure(update.from, update.to, pNew.Schema); err != nil { + return fmt.Errorf("cannot update volume structure %v for volume %s: %v", update.to, volName, err) + } + } + + // collect updates per volume into a single set of updates to perform + // at once + allUpdates = append(allUpdates, updates...) } - // ensure all required kernel assets are found in the gadget - kernelInfo, err := kernel.ReadInfo(new.KernelRootDir) - if err != nil { - return err + // check if there were kernel assets that at least one was consumed across + // any of the volumes + if len(allKernelAssets) != 0 && !atLeastOneKernelAssetConsumed { + sort.Strings(allKernelAssets) + return fmt.Errorf("gadget does not consume any of the kernel assets needing synced update %s", strutil.Quoted(allKernelAssets)) } - if err := gadgetVolumeConsumesOneKernelUpdateAsset(pNew.Volume, kernelInfo); err != nil { - return err + + if len(allUpdates) == 0 { + // nothing to update + return ErrNoUpdate } - // now we know which structure is which, find which ones need an update - updates, err := resolveUpdate(pOld, pNew, updatePolicy, new.RootDir, new.KernelRootDir, kernelInfo) + // build the map of volume structure locations where the first key is the + // volume name, and the second key is the structure's index in the list of + // structures on that volume, and the final value is the StructureLocation + // hat can actually be used to perform the lookup/update in applyUpdates + structureLocations, err := volumeStructureToLocationMap(old, model, laidOutVols) if err != nil { + if err == errSkipUpdateProceedRefresh { + // we couldn't successfully build a map for the structure locations, + // but for various reasons this isn't considered a fatal error for + // the gadget refresh, so just return nil instead, a message should + // already have been logged + return nil + } return err } - if len(updates) == 0 { - // nothing to update - return ErrNoUpdate - } - // can update old layout to new layout - for _, update := range updates { - if err := canUpdateStructure(update.from, update.to, pNew.Schema); err != nil { - return fmt.Errorf("cannot update volume structure %v: %v", update.to, err) + if len(new.Info.Volumes) != 1 { + logger.Debugf("gadget asset update routine for multiple volumes") + + // check if the structure location map has only one volume in it - this + // is the case in legacy update operations where we only support updates + // to the system-boot / main volume + if len(structureLocations) == 1 { + // log a message and drop all updates to structures not in the + // volume we have + supportedVolume := "" + for volName := range structureLocations { + supportedVolume = volName + } + keepUpdates := make([]updatePair, 0, len(allUpdates)) + for _, update := range allUpdates { + if update.volume.Name != supportedVolume { + // TODO: or should we error here instead? + logger.Noticef("skipping update on non-supported volume %s to structure %s", update.volume.Name, update.to.Name) + } else { + keepUpdates = append(keepUpdates, update) + } + } + allUpdates = keepUpdates } } - return applyUpdates(new, updates, rollbackDirPath, observer) + // apply all updates at once + if err := applyUpdates(structureLocations, new, allUpdates, rollbackDirPath, observer); err != nil { + return err + } + + return nil } func resolveVolume(old *Info, new *Info) (oldVol, newVol *Volume, err error) { @@ -1355,8 +1472,9 @@ } type updatePair struct { - from *LaidOutStructure - to *LaidOutStructure + from *LaidOutStructure + to *LaidOutStructure + volume *Volume } func defaultPolicy(from, to *LaidOutStructure) (bool, ResolvedContentFilterFunc) { @@ -1420,8 +1538,9 @@ // and add to updates updates = append(updates, updatePair{ - from: &oldVol.LaidOutStructure[j], - to: &newVol.LaidOutStructure[j], + from: &oldVol.LaidOutStructure[j], + to: &newVol.LaidOutStructure[j], + volume: newVol.Volume, }) } } @@ -1440,13 +1559,42 @@ Rollback() error } -func applyUpdates(new GadgetData, updates []updatePair, rollbackDir string, observer ContentUpdateObserver) error { +func updateLocationForStructure(structureLocations map[string]map[int]StructureLocation, ps *LaidOutStructure) (loc StructureLocation, err error) { + loc, ok := structureLocations[ps.VolumeName][ps.YamlIndex] + if !ok { + return loc, fmt.Errorf("structure with index %d on volume %s not found", ps.YamlIndex, ps.VolumeName) + } + if !ps.HasFilesystem() { + if loc.Device == "" { + return loc, fmt.Errorf("internal error: structure %d on volume %s should have had a device set but did not have one in an internal mapping", ps.YamlIndex, ps.VolumeName) + } + return loc, nil + } else { + if loc.RootMountPoint == "" { + // then we can't update this structure because it has a filesystem + // specified in the gadget.yaml, but that partition is not mounted + // anywhere writable for us to update the filesystem content + // there is a TODO in buildVolumeStructureToLocation above about + // possibly mounting it, we could also mount it here instead and + // then proceed with the update, but we should also have a way to + // unmount it when we are done updating it + return loc, fmt.Errorf("structure %d on volume %s does not have a writable mountpoint in order to update the filesystem content", ps.YamlIndex, ps.VolumeName) + } + return loc, nil + } +} + +func applyUpdates(structureLocations map[string]map[int]StructureLocation, new GadgetData, updates []updatePair, rollbackDir string, observer ContentUpdateObserver) error { updaters := make([]Updater, len(updates)) for i, one := range updates { - up, err := updaterForStructure(one.to, new.RootDir, rollbackDir, observer) + loc, err := updateLocationForStructure(structureLocations, one.to) if err != nil { - return fmt.Errorf("cannot prepare update for volume structure %v: %v", one.to, err) + return fmt.Errorf("cannot prepare update for volume structure %v on volume %s: %v", one.to, one.volume.Name, err) + } + up, err := updaterForStructure(loc, one.to, new.RootDir, rollbackDir, observer) + if err != nil { + return fmt.Errorf("cannot prepare update for volume structure %v on volume %s: %v", one.to, one.volume.Name, err) } updaters[i] = up } @@ -1454,7 +1602,7 @@ var backupErr error for i, one := range updaters { if err := one.Backup(); err != nil { - backupErr = fmt.Errorf("cannot backup volume structure %v: %v", updates[i].to, err) + backupErr = fmt.Errorf("cannot backup volume structure %v on volume %s: %v", updates[i].to, updates[i].volume.Name, err) break } } @@ -1482,7 +1630,7 @@ skipped++ continue } - updateErr = fmt.Errorf("cannot update volume structure %v: %v", updates[i].to, err) + updateErr = fmt.Errorf("cannot update volume structure %v on volume %s: %v", updates[i].to, updates[i].volume.Name, err) break } } @@ -1502,7 +1650,7 @@ one := updaters[i] if err := one.Rollback(); err != nil { // TODO: log errors to oplog - logger.Noticef("cannot rollback volume structure %v update: %v", updates[i].to, err) + logger.Noticef("cannot rollback volume structure %v update on volume %s: %v", updates[i].to, updates[i].volume.Name, err) } } @@ -1517,19 +1665,24 @@ var updaterForStructure = updaterForStructureImpl -func updaterForStructureImpl(ps *LaidOutStructure, newRootDir, rollbackDir string, observer ContentUpdateObserver) (Updater, error) { - var updater Updater - var err error +func updaterForStructureImpl(loc StructureLocation, ps *LaidOutStructure, newRootDir, rollbackDir string, observer ContentUpdateObserver) (Updater, error) { + // TODO: this is sort of clunky, we already did the lookup, but doing the + // lookup out of band from this function makes for easier mocking if !ps.HasFilesystem() { - updater, err = newRawStructureUpdater(newRootDir, ps, rollbackDir, findDeviceForStructureWithFallback) + lookup := func(ps *LaidOutStructure) (device string, offs quantity.Offset, err error) { + return loc.Device, loc.Offset, nil + } + return newRawStructureUpdater(newRootDir, ps, rollbackDir, lookup) } else { - updater, err = newMountedFilesystemUpdater(ps, rollbackDir, findMountPointForStructure, observer) + lookup := func(ps *LaidOutStructure) (string, error) { + return loc.RootMountPoint, nil + } + return newMountedFilesystemUpdater(ps, rollbackDir, lookup, observer) } - return updater, err } // MockUpdaterForStructure replace internal call with a mocked one, for use in tests only -func MockUpdaterForStructure(mock func(ps *LaidOutStructure, rootDir, rollbackDir string, observer ContentUpdateObserver) (Updater, error)) (restore func()) { +func MockUpdaterForStructure(mock func(loc StructureLocation, ps *LaidOutStructure, rootDir, rollbackDir string, observer ContentUpdateObserver) (Updater, error)) (restore func()) { old := updaterForStructure updaterForStructure = mock return func() { diff -Nru snapd-2.55.5+20.04/gadget/update_test.go snapd-2.57.5+20.04/gadget/update_test.go --- snapd-2.55.5+20.04/gadget/update_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/update_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,7 +25,7 @@ "io/ioutil" "os" "path/filepath" - "strings" + "sync" . "gopkg.in/check.v1" @@ -45,15 +45,28 @@ ) type updateTestSuite struct { + restoreVolumeStructureToLocationMap func() + testutil.BaseTest } +var _ = Suite(&updateTestSuite{}) + func (s *updateTestSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) s.AddCleanup(func() { dirs.SetRootDir("") }) -} -var _ = Suite(&updateTestSuite{}) + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.LaidOutVolume) (map[string]map[int]gadget.StructureLocation, error) { + return nil, fmt.Errorf("unmocked volume structure to loc map") + }) + restoreDoer := sync.Once{} + s.restoreVolumeStructureToLocationMap = func() { + restoreDoer.Do(r) + } + s.AddCleanup(func() { + s.restoreVolumeStructureToLocationMap() + }) +} func (u *updateTestSuite) TestResolveVolumeDifferentName(c *C) { oldInfo := &gadget.Info{ @@ -612,181 +625,1887 @@ return nil } -func (m *mockUpdater) Backup() error { - return callOrNil(m.backupCb) -} +func (m *mockUpdater) Backup() error { + return callOrNil(m.backupCb) +} + +func (m *mockUpdater) Rollback() error { + return callOrNil(m.rollbackCb) +} + +func (m *mockUpdater) Update() error { + return callOrNil(m.updateCb) +} + +func (u *updateTestSuite) updateDataSet(c *C) (oldData gadget.GadgetData, newData gadget.GadgetData, rollbackDir string) { + // prepare the stage + bareStruct := gadget.VolumeStructure{ + VolumeName: "foo", + Name: "first", + Size: 5 * quantity.SizeMiB, + Content: []gadget.VolumeContent{ + {Image: "first.img"}, + }, + } + fsStruct := gadget.VolumeStructure{ + VolumeName: "foo", + Name: "second", + Size: 10 * quantity.SizeMiB, + Filesystem: "ext4", + Content: []gadget.VolumeContent{ + {UnresolvedSource: "/second-content", Target: "/"}, + }, + } + lastStruct := gadget.VolumeStructure{ + VolumeName: "foo", + Name: "third", + Size: 5 * quantity.SizeMiB, + Filesystem: "vfat", + Content: []gadget.VolumeContent{ + {UnresolvedSource: "/third-content", Target: "/"}, + }, + } + // start with identical data for new and old infos, they get updated by + // the caller as needed + oldInfo := &gadget.Info{ + Volumes: map[string]*gadget.Volume{ + "foo": { + Name: "foo", + Bootloader: "grub", + Schema: "gpt", + Structure: []gadget.VolumeStructure{bareStruct, fsStruct, lastStruct}, + }, + }, + } + newInfo := &gadget.Info{ + Volumes: map[string]*gadget.Volume{ + "foo": { + Name: "foo", + Bootloader: "grub", + Schema: "gpt", + Structure: []gadget.VolumeStructure{bareStruct, fsStruct, lastStruct}, + }, + }, + } + + // reasonably default volume structure to location map - individual tests + // can override this + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.LaidOutVolume) (map[string]map[int]gadget.StructureLocation, error) { + return map[string]map[int]gadget.StructureLocation{ + "foo": { + 0: { + Device: "/dev/foo", + Offset: quantity.OffsetMiB, + }, + 1: { + RootMountPoint: "/foo", + }, + 2: { + RootMountPoint: "/foo", + }, + }, + }, nil + }) + u.AddCleanup(r) + + oldRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldRootDir, "/second-content/foo"), 0, nil) + makeSizedFile(c, filepath.Join(oldRootDir, "/third-content/bar"), 0, nil) + oldData = gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir} + + newRootDir := c.MkDir() + makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*quantity.SizeKiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "/second-content/foo"), quantity.SizeKiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "/third-content/bar"), quantity.SizeKiB, nil) + newData = gadget.GadgetData{Info: newInfo, RootDir: newRootDir} + + rollbackDir = c.MkDir() + return oldData, newData, rollbackDir +} + +type mockUpdateProcessObserver struct { + beforeWriteCalled int + canceledCalled int + beforeWriteErr error + canceledErr error +} + +func (m *mockUpdateProcessObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure, + targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { + return gadget.ChangeAbort, errors.New("unexpected call") +} + +func (m *mockUpdateProcessObserver) BeforeWrite() error { + m.beforeWriteCalled++ + return m.beforeWriteErr +} + +func (m *mockUpdateProcessObserver) Canceled() error { + m.canceledCalled++ + return m.canceledErr +} + +func (u *updateTestSuite) TestUpdateApplyHappy(c *C) { + oldData, newData, rollbackDir := u.updateDataSet(c) + // update two structs + newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 + newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1 + + muo := &mockUpdateProcessObserver{} + updaterForStructureCalls := 0 + updateCalls := make(map[string]bool) + backupCalls := make(map[string]bool) + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + c.Assert(psRootDir, Equals, newData.RootDir) + c.Assert(psRollbackDir, Equals, rollbackDir) + c.Assert(observer, Equals, muo) + // TODO:UC20 verify observer + + switch updaterForStructureCalls { + case 0: + c.Check(ps.Name, Equals, "first") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.Size, Equals, 5*quantity.SizeMiB) + c.Check(ps.IsPartition(), Equals, true) + // non MBR start offset defaults to 1MiB + c.Check(ps.StartOffset, Equals, 1*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 1) + c.Check(ps.LaidOutContent[0].Image, Equals, "first.img") + c.Check(ps.LaidOutContent[0].Size, Equals, 900*quantity.SizeKiB) + case 1: + c.Check(ps.Name, Equals, "second") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 10*quantity.SizeMiB) + // foo's start offset + foo's size + c.Check(ps.StartOffset, Equals, (1+5)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 1) + c.Check(ps.Content[0].UnresolvedSource, Equals, "/second-content") + c.Check(ps.Content[0].Target, Equals, "/") + default: + c.Fatalf("unexpected call") + } + updaterForStructureCalls++ + mu := &mockUpdater{ + backupCb: func() error { + backupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + updateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } + return mu, nil + }) + defer restore() + + // go go go + err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) + c.Assert(err, IsNil) + c.Assert(backupCalls, DeepEquals, map[string]bool{ + "first": true, + "second": true, + }) + c.Assert(updateCalls, DeepEquals, map[string]bool{ + "first": true, + "second": true, + }) + c.Assert(updaterForStructureCalls, Equals, 2) + c.Assert(muo.beforeWriteCalled, Equals, 1) + c.Assert(muo.canceledCalled, Equals, 0) +} + +func (u *updateTestSuite) TestUpdateApplyUC16FullLogic(c *C) { + u.restoreVolumeStructureToLocationMap() + oldData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + newData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + rollbackDir := c.MkDir() + + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.UC16YAMLImplicitSystemData, uc16Model) + c.Assert(err, IsNil) + + // put the same volumes into both the old and the new data so they are + // identical to start + for volName, laidOutVol := range allLaidOutVolumes { + // need to make separate copies of the volume since laidOUutVol.Volume + // is a pointer + numStructures := len(laidOutVol.Volume.Structure) + newData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(newData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + + oldData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(oldData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + } + + // setup symlink for the system-boot partition + err = os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel"), 0755) + c.Assert(err, IsNil) + fakedevicepart := filepath.Join(dirs.GlobalRootDir, "/dev/sda1") + err = os.Symlink(fakedevicepart, filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel", disks.BlkIDEncodeLabel("EFI System"))) + c.Assert(err, IsNil) + err = ioutil.WriteFile(fakedevicepart, nil, 0644) + c.Assert(err, IsNil) + + // mock the partition device node to mock disk + restore := disks.MockPartitionDeviceNodeToDiskMapping(map[string]*disks.MockDiskMapping{ + filepath.Join(dirs.GlobalRootDir, "/dev/sda1"): gadgettest.UC16ImplicitSystemDataMockDiskMapping, + }) + defer restore() + + // and the device name to the disk itself + restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/sda": gadgettest.UC16ImplicitSystemDataMockDiskMapping, + }) + defer restore() + + // add a writable mountpoint for the system-boot partition + restore = osutil.MockMountInfo( + fmt.Sprintf(`27 27 600:3 / %[1]s/boot/ubuntu rw,relatime shared:7 - vfat %[1]s/dev/sda2 rw`, dirs.GlobalRootDir), + ) + defer restore() + + // update all 3 structs + // mbr - raw structure + newData.Info.Volumes["pc"].Structure[0].Update.Edition = 1 + // bios - partition w/o filesystem + newData.Info.Volumes["pc"].Structure[1].Update.Edition = 1 + // system-boot - partition w/ filesystem struct + newData.Info.Volumes["pc"].Structure[2].Update.Edition = 1 + + muo := &mockUpdateProcessObserver{} + updaterForStructureCalls := 0 + updateCalls := make(map[string]bool) + backupCalls := make(map[string]bool) + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + c.Assert(psRootDir, Equals, newData.RootDir) + c.Assert(psRollbackDir, Equals, rollbackDir) + c.Assert(observer, Equals, muo) + // TODO:UC20 verify observer + + switch updaterForStructureCalls { + case 0: + // mbr raw structure + c.Check(ps.Name, Equals, "mbr") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.Size, Equals, quantity.Size(440)) + c.Check(ps.IsPartition(), Equals, false) + // no offset since we are updating the MBR itself + c.Check(ps.StartOffset, Equals, quantity.Offset(0)) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/sda", + Offset: quantity.Offset(0), + }) + case 1: + // bios boot + c.Check(ps.Name, Equals, "BIOS Boot") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/sda", + Offset: quantity.OffsetMiB, + }) + case 2: + // EFI system partition + c.Check(ps.Name, Equals, "EFI System") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "vfat") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 50*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/boot/ubuntu"), + }) + default: + c.Fatalf("unexpected call") + } + updaterForStructureCalls++ + mu := &mockUpdater{ + backupCb: func() error { + backupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + updateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } + return mu, nil + }) + defer restore() + + // go go go + err = gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) + c.Assert(err, IsNil) + c.Assert(updaterForStructureCalls, Equals, 3) + c.Assert(backupCalls, DeepEquals, map[string]bool{ + "mbr": true, + "BIOS Boot": true, + "EFI System": true, + }) + c.Assert(updateCalls, DeepEquals, map[string]bool{ + "mbr": true, + "BIOS Boot": true, + "EFI System": true, + }) + + c.Assert(muo.beforeWriteCalled, Equals, 1) + c.Assert(muo.canceledCalled, Equals, 0) +} + +func (u *updateTestSuite) TestUpdateApplyUC20MissingInitialMapFullLogicOnlySystemBoot(c *C) { + u.restoreVolumeStructureToLocationMap() + oldData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + newData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + rollbackDir := c.MkDir() + + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.MultiVolumeUC20GadgetYaml, uc20Model) + c.Assert(err, IsNil) + + // put the same volumes into both the old and the new data so they are + // identical to start + for volName, laidOutVol := range allLaidOutVolumes { + // need to make separate copies of the volume since laidOUutVol.Volume + // is a pointer + numStructures := len(laidOutVol.Volume.Structure) + newData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(newData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + + oldData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(oldData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + } + + // note don't need to mock anything for the second volume on disk, we don't + // consider it at all + + // setup symlink for the BIOS Boot partition + err = os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel"), 0755) + c.Assert(err, IsNil) + fakedevicepart := filepath.Join(dirs.GlobalRootDir, "/dev/vda1") + err = os.Symlink(fakedevicepart, filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel", disks.BlkIDEncodeLabel("BIOS Boot"))) + c.Assert(err, IsNil) + err = ioutil.WriteFile(fakedevicepart, nil, 0644) + c.Assert(err, IsNil) + + // mock the partition device node to mock disk + restore := disks.MockPartitionDeviceNodeToDiskMapping(map[string]*disks.MockDiskMapping{ + filepath.Join(dirs.GlobalRootDir, "/dev/vda1"): gadgettest.VMSystemVolumeDiskMapping, + }) + defer restore() + + // and the device name to the disk itself + restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/vda": gadgettest.VMSystemVolumeDiskMapping, + }) + defer restore() + + // setup mountinfo for root mount points of the partitions with filesystems + // note ubuntu-seed is mounted twice, but the impl always chooses the first + // mount point arbitrarily + restore = osutil.MockMountInfo( + fmt.Sprintf( + ` +27 27 600:3 / %[1]s/run/mnt/ubuntu-seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +27 27 600:3 / %[1]s/writable/system-data/var/lib/snapd/seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +28 27 600:4 / %[1]s/run/mnt/ubuntu-boot rw,relatime shared:7 - vfat %[1]s/dev/vda3 rw +29 27 600:5 / %[1]s/run/mnt/ubuntu-save rw,relatime shared:7 - vfat %[1]s/dev/vda4 rw +30 27 600:6 / %[1]s/run/mnt/data rw,relatime shared:7 - vfat %[1]s/dev/vda5 rw`[1:], + dirs.GlobalRootDir, + ), + ) + defer restore() + + // set all structs on system-boot volume to be updated - only structs on + // system-boot volume can be updated as per policy since the initial mapping + // was missing + + // mbr - bare structure + newData.Info.Volumes["pc"].Structure[0].Update.Edition = 1 + // bios - partition w/o filesystem + newData.Info.Volumes["pc"].Structure[1].Update.Edition = 1 + // ubuntu-seed + newData.Info.Volumes["pc"].Structure[2].Update.Edition = 1 + // ubuntu-boot + newData.Info.Volumes["pc"].Structure[3].Update.Edition = 1 + // ubuntu-save + newData.Info.Volumes["pc"].Structure[4].Update.Edition = 1 + // ubuntu-data + newData.Info.Volumes["pc"].Structure[5].Update.Edition = 1 + + muo := &mockUpdateProcessObserver{} + updaterForStructureCalls := 0 + updateCalls := make(map[string]bool) + backupCalls := make(map[string]bool) + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + c.Assert(psRootDir, Equals, newData.RootDir) + c.Assert(psRollbackDir, Equals, rollbackDir) + c.Assert(observer, Equals, muo) + // TODO:UC20 verify observer + + switch updaterForStructureCalls { + case 0: + // mbr raw structure + c.Check(ps.Name, Equals, "mbr") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.Size, Equals, quantity.Size(440)) + c.Check(ps.IsPartition(), Equals, false) + // no offset since we are updating the MBR itself + c.Check(ps.StartOffset, Equals, quantity.Offset(0)) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/vda", + Offset: quantity.Offset(0), + }) + case 1: + // bios boot + c.Check(ps.Name, Equals, "BIOS Boot") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/vda", + Offset: quantity.OffsetMiB, + }) + case 2: + // ubuntu-seed + c.Check(ps.Name, Equals, "ubuntu-seed") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "vfat") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 1200*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-seed"), + }) + case 3: + // ubuntu-boot + c.Check(ps.Name, Equals, "ubuntu-boot") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 750*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1+1200)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-boot"), + }) + case 4: + // ubuntu-save + c.Check(ps.Name, Equals, "ubuntu-save") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 16*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1+1200+750)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-save"), + }) + case 5: + // ubuntu-data + c.Check(ps.Name, Equals, "ubuntu-data") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + // NOTE: this is the laid out size, not the actual size (since data + // gets expanded), but the update op doesn't actually care about the + // size so it's okay + c.Check(ps.Size, Equals, quantity.SizeGiB) + c.Check(ps.StartOffset, Equals, (1+1+1200+750+16)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/data"), + }) + + default: + c.Fatalf("unexpected call") + } + updaterForStructureCalls++ + mu := &mockUpdater{ + backupCb: func() error { + backupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + updateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } + return mu, nil + }) + defer restore() + + // go go go + err = gadget.Update(uc20Model, oldData, newData, rollbackDir, nil, muo) + c.Assert(err, IsNil) + c.Assert(updaterForStructureCalls, Equals, 6) + c.Assert(backupCalls, DeepEquals, map[string]bool{ + "mbr": true, + "BIOS Boot": true, + "ubuntu-seed": true, + "ubuntu-boot": true, + "ubuntu-save": true, + "ubuntu-data": true, + }) + c.Assert(updateCalls, DeepEquals, map[string]bool{ + "mbr": true, + "BIOS Boot": true, + "ubuntu-seed": true, + "ubuntu-boot": true, + "ubuntu-save": true, + "ubuntu-data": true, + }) + + c.Assert(muo.beforeWriteCalled, Equals, 1) + c.Assert(muo.canceledCalled, Equals, 0) +} + +func (u *updateTestSuite) TestUpdateApplyUC20MissingInitialMapFullLogicOnlySystemBootEvenIfAllVolsHaveUpdates(c *C) { + u.restoreVolumeStructureToLocationMap() + mockLog, restore := logger.MockLogger() + defer restore() + + oldData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + newData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + rollbackDir := c.MkDir() + + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.MultiVolumeUC20GadgetYaml, uc20Model) + c.Assert(err, IsNil) + + // put the same volumes into both the old and the new data so they are + // identical to start + for volName, laidOutVol := range allLaidOutVolumes { + // need to make separate copies of the volume since laidOUutVol.Volume + // is a pointer + numStructures := len(laidOutVol.Volume.Structure) + newData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(newData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + + oldData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(oldData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + } + + // setup symlink for the system-boot partition + err = os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel"), 0755) + c.Assert(err, IsNil) + fakedevicepart := filepath.Join(dirs.GlobalRootDir, "/dev/vda1") + err = os.Symlink(fakedevicepart, filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel", disks.BlkIDEncodeLabel("BIOS Boot"))) + c.Assert(err, IsNil) + err = ioutil.WriteFile(fakedevicepart, nil, 0644) + c.Assert(err, IsNil) + + // mock the partition device node to mock disk + restore = disks.MockPartitionDeviceNodeToDiskMapping(map[string]*disks.MockDiskMapping{ + filepath.Join(dirs.GlobalRootDir, "/dev/vda1"): gadgettest.VMSystemVolumeDiskMapping, + }) + defer restore() + + // and the device name to the disk itself + restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/vda": gadgettest.VMSystemVolumeDiskMapping, + }) + defer restore() + + // setup mountinfo for root mount points of the partitions with filesystems + // note ubuntu-seed is mounted twice, but the impl always chooses the first + // mount point arbitrarily + restore = osutil.MockMountInfo( + fmt.Sprintf( + ` +27 27 600:3 / %[1]s/run/mnt/ubuntu-seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +27 27 600:3 / %[1]s/writable/system-data/var/lib/snapd/seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +28 27 600:4 / %[1]s/run/mnt/ubuntu-boot rw,relatime shared:7 - vfat %[1]s/dev/vda3 rw +29 27 600:5 / %[1]s/run/mnt/ubuntu-save rw,relatime shared:7 - vfat %[1]s/dev/vda4 rw +30 27 600:6 / %[1]s/run/mnt/data rw,relatime shared:7 - vfat %[1]s/dev/vda5 rw`[1:], + dirs.GlobalRootDir, + ), + ) + defer restore() + + // try to update all structures on both volumes, but only the structures on + // the system-boot volume will end up getting updated as per policy + + // mbr - bare structure + newData.Info.Volumes["pc"].Structure[0].Update.Edition = 1 + // bios - partition w/o filesystem + newData.Info.Volumes["pc"].Structure[1].Update.Edition = 1 + // ubuntu-seed + newData.Info.Volumes["pc"].Structure[2].Update.Edition = 1 + // ubuntu-boot + newData.Info.Volumes["pc"].Structure[3].Update.Edition = 1 + // ubuntu-save + newData.Info.Volumes["pc"].Structure[4].Update.Edition = 1 + // ubuntu-data + newData.Info.Volumes["pc"].Structure[5].Update.Edition = 1 + + // bare structure + newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 + // partition without a filesystem + newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1 + // some filesystem + newData.Info.Volumes["foo"].Structure[2].Update.Edition = 1 + + muo := &mockUpdateProcessObserver{} + updaterForStructureCalls := 0 + updateCalls := make(map[string]bool) + backupCalls := make(map[string]bool) + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + c.Assert(psRootDir, Equals, newData.RootDir) + c.Assert(psRollbackDir, Equals, rollbackDir) + c.Assert(observer, Equals, muo) + // TODO:UC20 verify observer + + switch updaterForStructureCalls { + case 0: + // mbr raw structure + c.Check(ps.Name, Equals, "mbr") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.Size, Equals, quantity.Size(440)) + c.Check(ps.IsPartition(), Equals, false) + // no offset since we are updating the MBR itself + c.Check(ps.StartOffset, Equals, quantity.Offset(0)) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/vda", + Offset: quantity.Offset(0), + }) + case 1: + // bios boot + c.Check(ps.Name, Equals, "BIOS Boot") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/vda", + Offset: quantity.OffsetMiB, + }) + case 2: + // ubuntu-seed + c.Check(ps.Name, Equals, "ubuntu-seed") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "vfat") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 1200*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-seed"), + }) + case 3: + // ubuntu-boot + c.Check(ps.Name, Equals, "ubuntu-boot") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 750*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1+1200)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-boot"), + }) + case 4: + // ubuntu-save + c.Check(ps.Name, Equals, "ubuntu-save") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 16*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1+1200+750)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-save"), + }) + case 5: + // ubuntu-data + c.Check(ps.Name, Equals, "ubuntu-data") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + // NOTE: this is the laid out size, not the actual size (since data + // gets expanded), but the update op doesn't actually care about the + // size so it's okay + c.Check(ps.Size, Equals, quantity.SizeGiB) + c.Check(ps.StartOffset, Equals, (1+1+1200+750+16)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/data"), + }) + + default: + c.Fatalf("unexpected call") + } + updaterForStructureCalls++ + mu := &mockUpdater{ + backupCb: func() error { + backupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + updateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } + return mu, nil + }) + defer restore() + + // go go go + err = gadget.Update(uc20Model, oldData, newData, rollbackDir, nil, muo) + c.Assert(err, IsNil) + c.Assert(updaterForStructureCalls, Equals, 6) + c.Assert(backupCalls, DeepEquals, map[string]bool{ + "mbr": true, + "BIOS Boot": true, + "ubuntu-seed": true, + "ubuntu-boot": true, + "ubuntu-save": true, + "ubuntu-data": true, + }) + c.Assert(updateCalls, DeepEquals, map[string]bool{ + "mbr": true, + "BIOS Boot": true, + "ubuntu-seed": true, + "ubuntu-boot": true, + "ubuntu-save": true, + "ubuntu-data": true, + }) + + c.Assert(muo.beforeWriteCalled, Equals, 1) + c.Assert(muo.canceledCalled, Equals, 0) + + c.Assert(mockLog.String(), testutil.Contains, "skipping update on non-supported volume foo to structure barething") + c.Assert(mockLog.String(), testutil.Contains, "skipping update on non-supported volume foo to structure nofspart") + c.Assert(mockLog.String(), testutil.Contains, "skipping update on non-supported volume foo to structure some-filesystem") +} + +func (u *updateTestSuite) TestUpdateApplyUC20WithInitialMapAllVolumesUpdatedFullLogic(c *C) { + u.restoreVolumeStructureToLocationMap() + + oldData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + newData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + rollbackDir := c.MkDir() + + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.MultiVolumeUC20GadgetYaml, uc20Model) + c.Assert(err, IsNil) + + err = os.MkdirAll(dirs.SnapDeviceDir, 0755) + c.Assert(err, IsNil) + // write out the provided traits JSON so we can at least load the traits for + // mocking via setupForVolumeStructureToLocation + err = ioutil.WriteFile( + filepath.Join(dirs.SnapDeviceDir, "disk-mapping.json"), + []byte(gadgettest.VMMultiVolumeUC20DiskTraitsJSON), + 0644, + ) + c.Assert(err, IsNil) + + // put the same volumes into both the old and the new data so they are + // identical to start + for volName, laidOutVol := range allLaidOutVolumes { + // need to make separate copies of the volume since laidOUutVol.Volume + // is a pointer + numStructures := len(laidOutVol.Volume.Structure) + newData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(newData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + + oldData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(oldData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + } + + // setup symlink for the system-boot partition + err = os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel"), 0755) + c.Assert(err, IsNil) + fakedevicepart := filepath.Join(dirs.GlobalRootDir, "/dev/vda1") + err = os.Symlink(fakedevicepart, filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel", disks.BlkIDEncodeLabel("BIOS Boot"))) + c.Assert(err, IsNil) + err = ioutil.WriteFile(fakedevicepart, nil, 0644) + c.Assert(err, IsNil) + + // mock the partition device node to mock disk + restore := disks.MockPartitionDeviceNodeToDiskMapping(map[string]*disks.MockDiskMapping{ + filepath.Join(dirs.GlobalRootDir, "/dev/vda1"): gadgettest.VMSystemVolumeDiskMapping, + filepath.Join(dirs.GlobalRootDir, "/dev/vdb1"): gadgettest.VMExtraVolumeDiskMapping, + }) + defer restore() + + // and the device name to the disk itself + restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/vda": gadgettest.VMSystemVolumeDiskMapping, + "/dev/vdb": gadgettest.VMExtraVolumeDiskMapping, + }) + defer restore() + + // setup mountinfo for root mount points of the partitions with filesystems + // note ubuntu-seed is mounted twice, but the impl always chooses the first + // mount point arbitrarily + + restore = osutil.MockMountInfo( + fmt.Sprintf( + ` +27 27 525:3 / %[1]s/foo/some-filesystem rw,relatime shared:7 - vfat %[1]s/dev/vdb2 rw +27 27 600:3 / %[1]s/run/mnt/ubuntu-seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +27 27 600:3 / %[1]s/writable/system-data/var/lib/snapd/seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +28 27 600:4 / %[1]s/run/mnt/ubuntu-boot rw,relatime shared:7 - vfat %[1]s/dev/vda3 rw +29 27 600:5 / %[1]s/run/mnt/ubuntu-save rw,relatime shared:7 - vfat %[1]s/dev/vda4 rw +30 27 600:6 / %[1]s/run/mnt/data rw,relatime shared:7 - vfat %[1]s/dev/vda5 rw`[1:], + dirs.GlobalRootDir, + ), + ) + defer restore() + + // update all structures + + // mbr - bare structure + newData.Info.Volumes["pc"].Structure[0].Update.Edition = 1 + // bios - partition w/o filesystem + newData.Info.Volumes["pc"].Structure[1].Update.Edition = 1 + // ubuntu-seed + newData.Info.Volumes["pc"].Structure[2].Update.Edition = 1 + // ubuntu-boot + newData.Info.Volumes["pc"].Structure[3].Update.Edition = 1 + // ubuntu-save + newData.Info.Volumes["pc"].Structure[4].Update.Edition = 1 + // ubuntu-data + newData.Info.Volumes["pc"].Structure[5].Update.Edition = 1 + + // bare structure + newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 + // partition without a filesystem + newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1 + // some filesystem + newData.Info.Volumes["foo"].Structure[2].Update.Edition = 1 + + muo := &mockUpdateProcessObserver{} + pcUpdaterForStructureCalls := 0 + fooUpdaterForStructureCalls := 0 + pcUpdateCalls := make(map[string]bool) + pcBackupCalls := make(map[string]bool) + fooUpdateCalls := make(map[string]bool) + fooBackupCalls := make(map[string]bool) + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + c.Assert(psRootDir, Equals, newData.RootDir) + c.Assert(psRollbackDir, Equals, rollbackDir) + c.Assert(observer, Equals, muo) + // TODO:UC20 verify observer + + var mu *mockUpdater + + switch ps.VolumeName { + case "pc": + switch pcUpdaterForStructureCalls { + case 0: + // mbr raw structure + c.Check(ps.Name, Equals, "mbr") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.Size, Equals, quantity.Size(440)) + c.Check(ps.IsPartition(), Equals, false) + // no offset since we are updating the MBR itself + c.Check(ps.StartOffset, Equals, quantity.Offset(0)) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/vda", + Offset: quantity.Offset(0), + }) + case 1: + // bios boot + c.Check(ps.Name, Equals, "BIOS Boot") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/vda", + Offset: quantity.OffsetMiB, + }) + case 2: + // ubuntu-seed + c.Check(ps.Name, Equals, "ubuntu-seed") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "vfat") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 1200*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-seed"), + }) + case 3: + // ubuntu-boot + c.Check(ps.Name, Equals, "ubuntu-boot") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 750*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1+1200)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-boot"), + }) + case 4: + // ubuntu-save + c.Check(ps.Name, Equals, "ubuntu-save") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 16*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1+1200+750)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-save"), + }) + case 5: + // ubuntu-data + c.Check(ps.Name, Equals, "ubuntu-data") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + // NOTE: this is the laid out size, not the actual size (since data + // gets expanded), but the update op doesn't actually care about the + // size so it's okay + c.Check(ps.Size, Equals, quantity.SizeGiB) + c.Check(ps.StartOffset, Equals, (1+1+1200+750+16)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/data"), + }) + } + pcUpdaterForStructureCalls++ + + mu = &mockUpdater{ + backupCb: func() error { + pcBackupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + pcUpdateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } + case "foo": + switch fooUpdaterForStructureCalls { + case 0: + c.Check(ps.Name, Equals, "barething") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.IsPartition(), Equals, false) + c.Check(ps.Size, Equals, quantity.Size(4096)) + c.Check(ps.StartOffset, Equals, quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/vdb", + Offset: quantity.OffsetMiB, + }) + case 1: + c.Check(ps.Name, Equals, "nofspart") + c.Check(ps.HasFilesystem(), Equals, false) + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, quantity.Size(4096)) + c.Check(ps.StartOffset, Equals, quantity.OffsetMiB+4096) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/vdb", + Offset: quantity.OffsetMiB + 4096, + }) + case 2: + c.Check(ps.Name, Equals, "some-filesystem") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, quantity.SizeGiB) + c.Check(ps.StartOffset, Equals, quantity.OffsetMiB+4096+4096) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/foo/some-filesystem"), + }) + default: + c.Fatalf("unexpected call") + } + fooUpdaterForStructureCalls++ + + mu = &mockUpdater{ + backupCb: func() error { + fooBackupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + fooUpdateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } + } + + return mu, nil + }) + defer restore() + + // go go go + err = gadget.Update(uc20Model, oldData, newData, rollbackDir, nil, muo) + c.Assert(err, IsNil) + c.Assert(pcUpdaterForStructureCalls, Equals, 6) + c.Assert(fooUpdaterForStructureCalls, Equals, 3) + c.Assert(pcBackupCalls, DeepEquals, map[string]bool{ + "mbr": true, + "BIOS Boot": true, + "ubuntu-seed": true, + "ubuntu-boot": true, + "ubuntu-save": true, + "ubuntu-data": true, + }) + c.Assert(pcUpdateCalls, DeepEquals, map[string]bool{ + "mbr": true, + "BIOS Boot": true, + "ubuntu-seed": true, + "ubuntu-boot": true, + "ubuntu-save": true, + "ubuntu-data": true, + }) + + c.Assert(fooBackupCalls, DeepEquals, map[string]bool{ + "barething": true, + "nofspart": true, + "some-filesystem": true, + }) + c.Assert(fooUpdateCalls, DeepEquals, map[string]bool{ + "barething": true, + "nofspart": true, + "some-filesystem": true, + }) + + c.Assert(muo.beforeWriteCalled, Equals, 1) + c.Assert(muo.canceledCalled, Equals, 0) +} + +func (u *updateTestSuite) TestUpdateApplyUC20WithInitialMapIncompatibleStructureChangesOnMultiVolumeUpdate(c *C) { + u.restoreVolumeStructureToLocationMap() + + oldData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + newData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + } + + rollbackDir := c.MkDir() + + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.MultiVolumeUC20GadgetYaml, uc20Model) + c.Assert(err, IsNil) + + err = os.MkdirAll(dirs.SnapDeviceDir, 0755) + c.Assert(err, IsNil) + // write out the provided traits JSON so we can at least load the traits for + // mocking via setupForVolumeStructureToLocation + err = ioutil.WriteFile( + filepath.Join(dirs.SnapDeviceDir, "disk-mapping.json"), + []byte(gadgettest.VMMultiVolumeUC20DiskTraitsJSON), + 0644, + ) + c.Assert(err, IsNil) + + // put the same volumes into both the old and the new data so they are + // identical to start + for volName, laidOutVol := range allLaidOutVolumes { + // need to make separate copies of the volume since laidOUutVol.Volume + // is a pointer + numStructures := len(laidOutVol.Volume.Structure) + newData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(newData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + + oldData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(oldData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + } + + // don't need to mock anything as we don't get that far + + // change the new nofspart structure size which is an incompatible change + + // ubuntu-save + newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2 + newData.Info.Volumes["foo"].Structure[1].Size = quantity.SizeMiB + + muo := &mockUpdateProcessObserver{} + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + c.Fatalf("unexpected call") + return nil, errors.New("not called") + }) + defer restore() + + // go go go + err = gadget.Update(uc20Model, oldData, newData, rollbackDir, nil, muo) + c.Assert(err, ErrorMatches, `cannot update volume structure #1 \("nofspart"\) for volume foo: cannot change structure size from 4096 to 1048576`) +} + +func (u *updateTestSuite) TestUpdateApplyUC20KernelAssetsOnAllVolumesWithInitialMapAllVolumesUpdatedFullLogic(c *C) { + u.restoreVolumeStructureToLocationMap() + + oldKernelDir := c.MkDir() + newKernelDir := c.MkDir() + + kernelYaml := []byte(`assets: + ref: + update: true + content: + - kernel-content + - kernel-content2`) + makeSizedFile(c, filepath.Join(newKernelDir, "meta/kernel.yaml"), 0, kernelYaml) + makeSizedFile(c, filepath.Join(oldKernelDir, "meta/kernel.yaml"), 0, kernelYaml) + + makeSizedFile(c, filepath.Join(newKernelDir, "kernel-content"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(newKernelDir, "kernel-content2"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldKernelDir, "kernel-content"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldKernelDir, "kernel-content2"), quantity.SizeMiB, nil) + + oldData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + KernelRootDir: oldKernelDir, + } + + newData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + KernelRootDir: newKernelDir, + } + + rollbackDir := c.MkDir() + + const multiVolWithKernel = ` +volumes: + pc: + schema: gpt + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 + - name: ubuntu-seed + role: system-seed + filesystem: vfat + # UEFI will boot the ESP partition by default first + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + size: 1200M + content: + - source: $kernel:ref/kernel-content + target: / + - name: ubuntu-boot + role: system-boot + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + # whats the appropriate size? + size: 750M + - name: ubuntu-save + role: system-save + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 16M + - name: ubuntu-data + role: system-data + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1G + foo: + schema: gpt + structure: + - name: barething + type: bare + size: 4096 + - name: nofspart + type: A11D2A7C-D82A-4C2F-8A01-1805240E6626 + size: 4096 + - name: some-filesystem + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1G + content: + - source: $kernel:ref/kernel-content2 + target: / +` + + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), oldKernelDir, multiVolWithKernel, uc20Model) + c.Assert(err, IsNil) + + err = os.MkdirAll(dirs.SnapDeviceDir, 0755) + c.Assert(err, IsNil) + // write out the provided traits JSON so we can at least load the traits for + // mocking via setupForVolumeStructureToLocation + err = ioutil.WriteFile( + filepath.Join(dirs.SnapDeviceDir, "disk-mapping.json"), + []byte(gadgettest.VMMultiVolumeUC20DiskTraitsJSON), + 0644, + ) + c.Assert(err, IsNil) + + // put the same volumes into both the old and the new data so they are + // identical to start + for volName, laidOutVol := range allLaidOutVolumes { + // need to make separate copies of the volume since laidOUutVol.Volume + // is a pointer + numStructures := len(laidOutVol.Volume.Structure) + newData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(newData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + + oldData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(oldData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) + } + + // setup symlink for the system-boot partition + err = os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel"), 0755) + c.Assert(err, IsNil) + fakedevicepart := filepath.Join(dirs.GlobalRootDir, "/dev/vda1") + err = os.Symlink(fakedevicepart, filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel", disks.BlkIDEncodeLabel("BIOS Boot"))) + c.Assert(err, IsNil) + err = ioutil.WriteFile(fakedevicepart, nil, 0644) + c.Assert(err, IsNil) + + // mock the partition device node to mock disk + restore := disks.MockPartitionDeviceNodeToDiskMapping(map[string]*disks.MockDiskMapping{ + filepath.Join(dirs.GlobalRootDir, "/dev/vda1"): gadgettest.VMSystemVolumeDiskMapping, + filepath.Join(dirs.GlobalRootDir, "/dev/vdb1"): gadgettest.VMExtraVolumeDiskMapping, + }) + defer restore() + + // and the device name to the disk itself + restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/vda": gadgettest.VMSystemVolumeDiskMapping, + "/dev/vdb": gadgettest.VMExtraVolumeDiskMapping, + }) + defer restore() + + // setup mountinfo for root mount points of the partitions with filesystems + // note ubuntu-seed is mounted twice, but the impl always chooses the first + // mount point arbitrarily + + restore = osutil.MockMountInfo( + fmt.Sprintf( + ` +27 27 525:3 / %[1]s/foo/some-filesystem rw,relatime shared:7 - vfat %[1]s/dev/vdb2 rw +27 27 600:3 / %[1]s/run/mnt/ubuntu-seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +27 27 600:3 / %[1]s/writable/system-data/var/lib/snapd/seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +28 27 600:4 / %[1]s/run/mnt/ubuntu-boot rw,relatime shared:7 - vfat %[1]s/dev/vda3 rw +29 27 600:5 / %[1]s/run/mnt/ubuntu-save rw,relatime shared:7 - vfat %[1]s/dev/vda4 rw +30 27 600:6 / %[1]s/run/mnt/data rw,relatime shared:7 - vfat %[1]s/dev/vda5 rw`[1:], + dirs.GlobalRootDir, + ), + ) + defer restore() + + // update the kernel asset referencing structures + + // ubuntu-seed + newData.Info.Volumes["pc"].Structure[2].Update.Edition = 1 + + // some filesystem + newData.Info.Volumes["foo"].Structure[2].Update.Edition = 1 + + muo := &mockUpdateProcessObserver{} + pcUpdaterForStructureCalls := 0 + fooUpdaterForStructureCalls := 0 + pcUpdateCalls := make(map[string]bool) + pcBackupCalls := make(map[string]bool) + fooUpdateCalls := make(map[string]bool) + fooBackupCalls := make(map[string]bool) + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + c.Assert(psRootDir, Equals, newData.RootDir) + c.Assert(psRollbackDir, Equals, rollbackDir) + c.Assert(observer, Equals, muo) + // TODO:UC20 verify observer + + var mu *mockUpdater + + switch ps.VolumeName { + case "pc": + switch pcUpdaterForStructureCalls { + case 0: + // ubuntu-seed + c.Check(ps.Name, Equals, "ubuntu-seed") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "vfat") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 1200*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, DeepEquals, []gadget.VolumeContent{ + { + UnresolvedSource: "$kernel:ref/kernel-content", + Target: "/", + }, + }) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-seed"), + }) + } + pcUpdaterForStructureCalls++ + + mu = &mockUpdater{ + backupCb: func() error { + pcBackupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + pcUpdateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } + case "foo": + switch fooUpdaterForStructureCalls { + case 0: + c.Check(ps.Name, Equals, "some-filesystem") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, quantity.SizeGiB) + c.Check(ps.StartOffset, Equals, quantity.OffsetMiB+4096+4096) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, DeepEquals, []gadget.VolumeContent{ + { + UnresolvedSource: "$kernel:ref/kernel-content2", + Target: "/", + }, + }) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/foo/some-filesystem"), + }) + default: + c.Fatalf("unexpected call") + } + fooUpdaterForStructureCalls++ + + mu = &mockUpdater{ + backupCb: func() error { + fooBackupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + fooUpdateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } + } + + return mu, nil + }) + defer restore() + + // go go go + err = gadget.Update(uc20Model, oldData, newData, rollbackDir, nil, muo) + c.Assert(err, IsNil) + c.Assert(pcUpdaterForStructureCalls, Equals, 1) + c.Assert(fooUpdaterForStructureCalls, Equals, 1) + c.Assert(pcBackupCalls, DeepEquals, map[string]bool{ + "ubuntu-seed": true, + }) + c.Assert(pcUpdateCalls, DeepEquals, map[string]bool{ + "ubuntu-seed": true, + }) + + c.Assert(fooBackupCalls, DeepEquals, map[string]bool{ + "some-filesystem": true, + }) + c.Assert(fooUpdateCalls, DeepEquals, map[string]bool{ + "some-filesystem": true, + }) + + c.Assert(muo.beforeWriteCalled, Equals, 1) + c.Assert(muo.canceledCalled, Equals, 0) +} + +func (u *updateTestSuite) TestUpdateApplyUC20KernelAssetsOnSingleVolumeWithInitialMapAllVolumesUpdatedFullLogic(c *C) { + u.restoreVolumeStructureToLocationMap() + + oldKernelDir := c.MkDir() + newKernelDir := c.MkDir() + + kernelYaml := []byte(`assets: + ref: + update: true + content: + - kernel-content + - kernel-content2`) + makeSizedFile(c, filepath.Join(newKernelDir, "meta/kernel.yaml"), 0, kernelYaml) + makeSizedFile(c, filepath.Join(oldKernelDir, "meta/kernel.yaml"), 0, kernelYaml) + + makeSizedFile(c, filepath.Join(newKernelDir, "kernel-content"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(newKernelDir, "kernel-content2"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldKernelDir, "kernel-content"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldKernelDir, "kernel-content2"), quantity.SizeMiB, nil) + + oldData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + KernelRootDir: oldKernelDir, + } + + newData := gadget.GadgetData{ + Info: &gadget.Info{ + Volumes: map[string]*gadget.Volume{}, + }, + RootDir: c.MkDir(), + KernelRootDir: newKernelDir, + } + + rollbackDir := c.MkDir() + + const multiVolWithKernel = ` +volumes: + pc: + schema: gpt + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 + - name: ubuntu-seed + role: system-seed + filesystem: vfat + # UEFI will boot the ESP partition by default first + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + size: 1200M + content: + - source: $kernel:ref/kernel-content + target: / + - name: ubuntu-boot + role: system-boot + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + # whats the appropriate size? + size: 750M + - name: ubuntu-save + role: system-save + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 16M + - name: ubuntu-data + role: system-data + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1G + foo: + schema: gpt + structure: + - name: barething + type: bare + size: 4096 + - name: nofspart + type: A11D2A7C-D82A-4C2F-8A01-1805240E6626 + size: 4096 + - name: some-filesystem + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1G +` + + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), oldKernelDir, multiVolWithKernel, uc20Model) + c.Assert(err, IsNil) -func (m *mockUpdater) Rollback() error { - return callOrNil(m.rollbackCb) -} + err = os.MkdirAll(dirs.SnapDeviceDir, 0755) + c.Assert(err, IsNil) + // write out the provided traits JSON so we can at least load the traits for + // mocking via setupForVolumeStructureToLocation + err = ioutil.WriteFile( + filepath.Join(dirs.SnapDeviceDir, "disk-mapping.json"), + []byte(gadgettest.VMMultiVolumeUC20DiskTraitsJSON), + 0644, + ) + c.Assert(err, IsNil) -func (m *mockUpdater) Update() error { - return callOrNil(m.updateCb) -} + // put the same volumes into both the old and the new data so they are + // identical to start + for volName, laidOutVol := range allLaidOutVolumes { + // need to make separate copies of the volume since laidOUutVol.Volume + // is a pointer + numStructures := len(laidOutVol.Volume.Structure) + newData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(newData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) -func updateDataSet(c *C) (oldData gadget.GadgetData, newData gadget.GadgetData, rollbackDir string) { - // prepare the stage - bareStruct := gadget.VolumeStructure{ - Name: "first", - Size: 5 * quantity.SizeMiB, - Content: []gadget.VolumeContent{ - {Image: "first.img"}, - }, - } - fsStruct := gadget.VolumeStructure{ - Name: "second", - Size: 10 * quantity.SizeMiB, - Filesystem: "ext4", - Content: []gadget.VolumeContent{ - {UnresolvedSource: "/second-content", Target: "/"}, - }, - } - lastStruct := gadget.VolumeStructure{ - Name: "third", - Size: 5 * quantity.SizeMiB, - Filesystem: "vfat", - Content: []gadget.VolumeContent{ - {UnresolvedSource: "/third-content", Target: "/"}, - }, - } - // start with identical data for new and old infos, they get updated by - // the caller as needed - oldInfo := &gadget.Info{ - Volumes: map[string]*gadget.Volume{ - "foo": { - Bootloader: "grub", - Schema: "gpt", - Structure: []gadget.VolumeStructure{bareStruct, fsStruct, lastStruct}, - }, - }, - } - newInfo := &gadget.Info{ - Volumes: map[string]*gadget.Volume{ - "foo": { - Bootloader: "grub", - Schema: "gpt", - Structure: []gadget.VolumeStructure{bareStruct, fsStruct, lastStruct}, - }, - }, + oldData.Info.Volumes[volName] = &gadget.Volume{ + Schema: laidOutVol.Volume.Schema, + Bootloader: laidOutVol.Volume.Bootloader, + ID: laidOutVol.Volume.ID, + Structure: make([]gadget.VolumeStructure, numStructures), + Name: laidOutVol.Volume.Name, + } + copy(oldData.Info.Volumes[volName].Structure, laidOutVol.Volume.Structure) } - oldRootDir := c.MkDir() - makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), quantity.SizeMiB, nil) - makeSizedFile(c, filepath.Join(oldRootDir, "/second-content/foo"), 0, nil) - makeSizedFile(c, filepath.Join(oldRootDir, "/third-content/bar"), 0, nil) - oldData = gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir} + // setup symlink for the system-boot partition + err = os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel"), 0755) + c.Assert(err, IsNil) + fakedevicepart := filepath.Join(dirs.GlobalRootDir, "/dev/vda1") + err = os.Symlink(fakedevicepart, filepath.Join(dirs.GlobalRootDir, "/dev/disk/by-partlabel", disks.BlkIDEncodeLabel("BIOS Boot"))) + c.Assert(err, IsNil) + err = ioutil.WriteFile(fakedevicepart, nil, 0644) + c.Assert(err, IsNil) - newRootDir := c.MkDir() - makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*quantity.SizeKiB, nil) - makeSizedFile(c, filepath.Join(newRootDir, "/second-content/foo"), quantity.SizeKiB, nil) - makeSizedFile(c, filepath.Join(newRootDir, "/third-content/bar"), quantity.SizeKiB, nil) - newData = gadget.GadgetData{Info: newInfo, RootDir: newRootDir} + // mock the partition device node to mock disk + restore := disks.MockPartitionDeviceNodeToDiskMapping(map[string]*disks.MockDiskMapping{ + filepath.Join(dirs.GlobalRootDir, "/dev/vda1"): gadgettest.VMSystemVolumeDiskMapping, + filepath.Join(dirs.GlobalRootDir, "/dev/vdb1"): gadgettest.VMExtraVolumeDiskMapping, + }) + defer restore() - rollbackDir = c.MkDir() - return oldData, newData, rollbackDir -} + // and the device name to the disk itself + restore = disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/vda": gadgettest.VMSystemVolumeDiskMapping, + "/dev/vdb": gadgettest.VMExtraVolumeDiskMapping, + }) + defer restore() -type mockUpdateProcessObserver struct { - beforeWriteCalled int - canceledCalled int - beforeWriteErr error - canceledErr error -} + // setup mountinfo for root mount points of the partitions with filesystems + // note ubuntu-seed is mounted twice, but the impl always chooses the first + // mount point arbitrarily -func (m *mockUpdateProcessObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure, - targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { - return gadget.ChangeAbort, errors.New("unexpected call") -} + restore = osutil.MockMountInfo( + fmt.Sprintf( + ` +27 27 525:3 / %[1]s/foo/some-filesystem rw,relatime shared:7 - vfat %[1]s/dev/vdb2 rw +27 27 600:3 / %[1]s/run/mnt/ubuntu-seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +27 27 600:3 / %[1]s/writable/system-data/var/lib/snapd/seed rw,relatime shared:7 - vfat %[1]s/dev/vda2 rw +28 27 600:4 / %[1]s/run/mnt/ubuntu-boot rw,relatime shared:7 - vfat %[1]s/dev/vda3 rw +29 27 600:5 / %[1]s/run/mnt/ubuntu-save rw,relatime shared:7 - vfat %[1]s/dev/vda4 rw +30 27 600:6 / %[1]s/run/mnt/data rw,relatime shared:7 - vfat %[1]s/dev/vda5 rw`[1:], + dirs.GlobalRootDir, + ), + ) + defer restore() -func (m *mockUpdateProcessObserver) BeforeWrite() error { - m.beforeWriteCalled++ - return m.beforeWriteErr -} + // update the kernel asset referencing structures -func (m *mockUpdateProcessObserver) Canceled() error { - m.canceledCalled++ - return m.canceledErr -} + // ubuntu-seed + newData.Info.Volumes["pc"].Structure[2].Update.Edition = 1 -func (u *updateTestSuite) TestUpdateApplyHappy(c *C) { - oldData, newData, rollbackDir := updateDataSet(c) - // update two structs - newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 - newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1 + // some filesystem + newData.Info.Volumes["foo"].Structure[2].Update.Edition = 1 muo := &mockUpdateProcessObserver{} - updaterForStructureCalls := 0 - updateCalls := make(map[string]bool) - backupCalls := make(map[string]bool) - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + pcUpdaterForStructureCalls := 0 + fooUpdaterForStructureCalls := 0 + pcUpdateCalls := make(map[string]bool) + pcBackupCalls := make(map[string]bool) + fooUpdateCalls := make(map[string]bool) + fooBackupCalls := make(map[string]bool) + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { c.Assert(psRootDir, Equals, newData.RootDir) c.Assert(psRollbackDir, Equals, rollbackDir) c.Assert(observer, Equals, muo) // TODO:UC20 verify observer - switch updaterForStructureCalls { - case 0: - c.Check(ps.Name, Equals, "first") - c.Check(ps.HasFilesystem(), Equals, false) - c.Check(ps.Size, Equals, 5*quantity.SizeMiB) - c.Check(ps.IsPartition(), Equals, true) - // non MBR start offset defaults to 1MiB - c.Check(ps.StartOffset, Equals, 1*quantity.OffsetMiB) - c.Assert(ps.LaidOutContent, HasLen, 1) - c.Check(ps.LaidOutContent[0].Image, Equals, "first.img") - c.Check(ps.LaidOutContent[0].Size, Equals, 900*quantity.SizeKiB) - case 1: - c.Check(ps.Name, Equals, "second") - c.Check(ps.HasFilesystem(), Equals, true) - c.Check(ps.Filesystem, Equals, "ext4") - c.Check(ps.IsPartition(), Equals, true) - c.Check(ps.Size, Equals, 10*quantity.SizeMiB) - // foo's start offset + foo's size - c.Check(ps.StartOffset, Equals, (1+5)*quantity.OffsetMiB) - c.Assert(ps.LaidOutContent, HasLen, 0) - c.Assert(ps.Content, HasLen, 1) - c.Check(ps.Content[0].UnresolvedSource, Equals, "/second-content") - c.Check(ps.Content[0].Target, Equals, "/") - default: - c.Fatalf("unexpected call") - } - updaterForStructureCalls++ - mu := &mockUpdater{ - backupCb: func() error { - backupCalls[ps.Name] = true - return nil - }, - updateCb: func() error { - updateCalls[ps.Name] = true - return nil - }, - rollbackCb: func() error { + var mu *mockUpdater + + switch ps.VolumeName { + case "pc": + switch pcUpdaterForStructureCalls { + case 0: + // ubuntu-seed + c.Check(ps.Name, Equals, "ubuntu-seed") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "vfat") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, 1200*quantity.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+1)*quantity.OffsetMiB) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, DeepEquals, []gadget.VolumeContent{ + { + UnresolvedSource: "$kernel:ref/kernel-content", + Target: "/", + }, + }) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-seed"), + }) + } + pcUpdaterForStructureCalls++ + + mu = &mockUpdater{ + backupCb: func() error { + pcBackupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + pcUpdateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } + case "foo": + switch fooUpdaterForStructureCalls { + case 0: + c.Check(ps.Name, Equals, "some-filesystem") + c.Check(ps.HasFilesystem(), Equals, true) + c.Check(ps.Filesystem, Equals, "ext4") + c.Check(ps.IsPartition(), Equals, true) + c.Check(ps.Size, Equals, quantity.SizeGiB) + c.Check(ps.StartOffset, Equals, quantity.OffsetMiB+4096+4096) + c.Assert(ps.LaidOutContent, HasLen, 0) + c.Assert(ps.Content, HasLen, 0) + c.Assert(loc, Equals, gadget.StructureLocation{ + RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/foo/some-filesystem"), + }) + default: c.Fatalf("unexpected call") - return errors.New("not called") - }, + } + fooUpdaterForStructureCalls++ + + mu = &mockUpdater{ + backupCb: func() error { + fooBackupCalls[ps.Name] = true + return nil + }, + updateCb: func() error { + fooUpdateCalls[ps.Name] = true + return nil + }, + rollbackCb: func() error { + c.Fatalf("unexpected call") + return errors.New("not called") + }, + } } + return mu, nil }) defer restore() // go go go - err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) + err = gadget.Update(uc20Model, oldData, newData, rollbackDir, nil, muo) c.Assert(err, IsNil) - c.Assert(backupCalls, DeepEquals, map[string]bool{ - "first": true, - "second": true, + c.Assert(pcUpdaterForStructureCalls, Equals, 1) + c.Assert(fooUpdaterForStructureCalls, Equals, 1) + c.Assert(pcBackupCalls, DeepEquals, map[string]bool{ + "ubuntu-seed": true, }) - c.Assert(updateCalls, DeepEquals, map[string]bool{ - "first": true, - "second": true, + c.Assert(pcUpdateCalls, DeepEquals, map[string]bool{ + "ubuntu-seed": true, }) - c.Assert(updaterForStructureCalls, Equals, 2) + + c.Assert(fooBackupCalls, DeepEquals, map[string]bool{ + "some-filesystem": true, + }) + c.Assert(fooUpdateCalls, DeepEquals, map[string]bool{ + "some-filesystem": true, + }) + c.Assert(muo.beforeWriteCalled, Equals, 1) c.Assert(muo.canceledCalled, Equals, 0) } func (u *updateTestSuite) TestUpdateApplyOnlyWhenNeeded(c *C) { - oldData, newData, rollbackDir := updateDataSet(c) + oldData, newData, rollbackDir := u.updateDataSet(c) // first structure is updated oldData.Info.Volumes["foo"].Structure[0].Update.Edition = 0 newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 @@ -799,7 +2518,7 @@ muo := &mockUpdateProcessObserver{} updaterForStructureCalls := 0 - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { c.Assert(psRootDir, Equals, newData.RootDir) c.Assert(psRollbackDir, Equals, rollbackDir) @@ -870,7 +2589,7 @@ // cannot lay out the new volume when bare struct data is missing err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, nil) - c.Assert(err, ErrorMatches, `cannot lay out the new volume: cannot lay out structure #0 \("foo"\): content "first.img": .* no such file or directory`) + c.Assert(err, ErrorMatches, `cannot lay out the new volume foo: cannot lay out structure #0 \("foo"\): content "first.img": .* no such file or directory`) makeSizedFile(c, filepath.Join(newRootDir, "first.img"), quantity.SizeMiB, nil) @@ -923,7 +2642,7 @@ makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*quantity.SizeKiB, nil) err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, nil) - c.Assert(err, ErrorMatches, `cannot apply update to volume: cannot change the number of structures within volume from 1 to 2`) + c.Assert(err, ErrorMatches, `cannot apply update to volume foo: cannot change the number of structures within volume from 1 to 2`) } func (u *updateTestSuite) TestUpdateApplyErrorIllegalStructureUpdate(c *C) { @@ -974,10 +2693,10 @@ makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), quantity.SizeMiB, nil) err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, nil) - c.Assert(err, ErrorMatches, `cannot update volume structure #0 \("foo"\): cannot change a bare structure to filesystem one`) + c.Assert(err, ErrorMatches, `cannot update volume structure #0 \("foo"\) for volume foo: cannot change a bare structure to filesystem one`) } -func (u *updateTestSuite) TestUpdateApplyErrorDifferentVolume(c *C) { +func (u *updateTestSuite) TestUpdateApplyErrorRenamedVolume(c *C) { // prepare the stage bareStruct := gadget.VolumeStructure{ Name: "foo", @@ -997,7 +2716,8 @@ } newInfo := &gadget.Info{ Volumes: map[string]*gadget.Volume{ - // same volume info but using a different name + // same volume info but using a different name which ends up being + // counted as having the old one removed and a new one added "foo-new": oldInfo.Volumes["foo"], }, } @@ -1006,14 +2726,14 @@ newData := gadget.GadgetData{Info: newInfo, RootDir: c.MkDir()} rollbackDir := c.MkDir() - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { c.Fatalf("unexpected call") return &mockUpdater{}, nil }) defer restore() err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, nil) - c.Assert(err, ErrorMatches, `cannot find entry for volume "foo" in updated gadget info`) + c.Assert(err, ErrorMatches, `cannot update gadget assets: volumes were both added and removed`) } func (u *updateTestSuite) TestUpdateApplyUpdatesAreOptInWithDefaultPolicy(c *C) { @@ -1052,7 +2772,7 @@ muo := &mockUpdateProcessObserver{} - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { c.Fatalf("unexpected call") return &mockUpdater{}, nil }) @@ -1065,23 +2785,51 @@ c.Assert(muo.beforeWriteCalled, Equals, 0) } -func policyDataSet(c *C) (oldData gadget.GadgetData, newData gadget.GadgetData, rollbackDir string) { - oldData, newData, rollbackDir = updateDataSet(c) +func (u *updateTestSuite) policyDataSet(c *C) (oldData gadget.GadgetData, newData gadget.GadgetData, rollbackDir string) { + oldData, newData, rollbackDir = u.updateDataSet(c) noPartitionStruct := gadget.VolumeStructure{ - Name: "no-partition", - Type: "bare", - Size: 5 * quantity.SizeMiB, + VolumeName: "foo", + Name: "no-partition", + Type: "bare", + Size: 5 * quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "first.img"}, }, } mbrStruct := gadget.VolumeStructure{ - Name: "mbr", - Role: "mbr", - Size: 446, - Offset: asOffsetPtr(0), + VolumeName: "foo", + Name: "mbr", + Role: "mbr", + Size: 446, + Offset: asOffsetPtr(0), } + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.LaidOutVolume) (map[string]map[int]gadget.StructureLocation, error) { + return map[string]map[int]gadget.StructureLocation{ + "foo": { + 0: { + Device: "/dev/foo", + Offset: quantity.OffsetMiB, + }, + 1: { + RootMountPoint: "/foo", + }, + 2: { + RootMountPoint: "/foo", + }, + 3: { + Device: "/dev/foo", + Offset: 10000, + }, + 4: { + Device: "/dev/foo", + Offset: 0, + }, + }, + }, nil + }) + u.AddCleanup(r) + oldVol := oldData.Info.Volumes["foo"] oldVol.Structure = append(oldVol.Structure, noPartitionStruct, mbrStruct) oldData.Info.Volumes["foo"] = oldVol @@ -1096,7 +2844,7 @@ } func (u *updateTestSuite) TestUpdateApplyUpdatesArePolicyControlled(c *C) { - oldData, newData, rollbackDir := policyDataSet(c) + oldData, newData, rollbackDir := u.policyDataSet(c) c.Assert(oldData.Info.Volumes["foo"].Structure, HasLen, 5) c.Assert(newData.Info.Volumes["foo"].Structure, HasLen, 5) // all structures have higher Edition, thus all would be updated under @@ -1108,7 +2856,7 @@ newData.Info.Volumes["foo"].Structure[4].Update.Edition = 5 toUpdate := map[string]int{} - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { toUpdate[ps.Name]++ return &mockUpdater{}, nil }) @@ -1149,7 +2897,7 @@ } func (u *updateTestSuite) TestUpdateApplyUpdatesRemodelPolicy(c *C) { - oldData, newData, rollbackDir := policyDataSet(c) + oldData, newData, rollbackDir := u.policyDataSet(c) // old structures have higher Edition, no update would occur under the default policy oldData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 @@ -1159,7 +2907,7 @@ oldData.Info.Volumes["foo"].Structure[4].Update.Edition = 5 toUpdate := map[string]int{} - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { toUpdate[ps.Name] = toUpdate[ps.Name] + 1 return &mockUpdater{}, nil }) @@ -1177,15 +2925,33 @@ } func (u *updateTestSuite) TestUpdateApplyBackupFails(c *C) { - oldData, newData, rollbackDir := updateDataSet(c) + oldData, newData, rollbackDir := u.updateDataSet(c) // update both structs newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1 newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3 + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.LaidOutVolume) (map[string]map[int]gadget.StructureLocation, error) { + return map[string]map[int]gadget.StructureLocation{ + "foo": { + 0: { + Device: "/dev/foo", + Offset: quantity.OffsetMiB, + }, + 1: { + RootMountPoint: "/foo", + }, + 2: { + RootMountPoint: "/foo", + }, + }, + }, nil + }) + defer r() + muo := &mockUpdateProcessObserver{} updaterForStructureCalls := 0 - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { updater := &mockUpdater{ updateCb: func() error { c.Fatalf("unexpected update call") @@ -1209,7 +2975,7 @@ // go go go err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) - c.Assert(err, ErrorMatches, `cannot backup volume structure #1 \("second"\): failed`) + c.Assert(err, ErrorMatches, `cannot backup volume structure #1 \("second"\) on volume foo: failed`) // update was canceled before backup pass completed c.Check(muo.canceledCalled, Equals, 1) @@ -1217,7 +2983,7 @@ } func (u *updateTestSuite) TestUpdateApplyUpdateFailsThenRollback(c *C) { - oldData, newData, rollbackDir := updateDataSet(c) + oldData, newData, rollbackDir := u.updateDataSet(c) // update all structs newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2 @@ -1228,7 +2994,7 @@ backupCalls := make(map[string]bool) rollbackCalls := make(map[string]bool) updaterForStructureCalls := 0 - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { updater := &mockUpdater{ backupCb: func() error { backupCalls[ps.Name] = true @@ -1258,7 +3024,7 @@ // go go go err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, muo) - c.Assert(err, ErrorMatches, `cannot update volume structure #1 \("second"\): failed`) + c.Assert(err, ErrorMatches, `cannot update volume structure #1 \("second"\) on volume foo: failed`) c.Assert(backupCalls, DeepEquals, map[string]bool{ // all were backed up "first": true, @@ -1285,7 +3051,7 @@ logbuf, restore := logger.MockLogger() defer restore() - oldData, newData, rollbackDir := updateDataSet(c) + oldData, newData, rollbackDir := u.updateDataSet(c) // update all structs newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2 @@ -1295,7 +3061,7 @@ backupCalls := make(map[string]bool) rollbackCalls := make(map[string]bool) updaterForStructureCalls := 0 - restore = gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { updater := &mockUpdater{ backupCb: func() error { backupCalls[ps.Name] = true @@ -1334,7 +3100,7 @@ // go go go err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, nil) // preserves update error - c.Assert(err, ErrorMatches, `cannot update volume structure #2 \("third"\): update error`) + c.Assert(err, ErrorMatches, `cannot update volume structure #2 \("third"\) on volume foo: update error`) c.Assert(backupCalls, DeepEquals, map[string]bool{ // all were backed up "first": true, @@ -1352,25 +3118,25 @@ "third": true, }) - c.Check(logbuf.String(), testutil.Contains, `cannot update gadget: cannot update volume structure #2 ("third"): update error`) - c.Check(logbuf.String(), testutil.Contains, `cannot rollback volume structure #1 ("second") update: rollback failed with different error`) + c.Check(logbuf.String(), testutil.Contains, `cannot update gadget: cannot update volume structure #2 ("third") on volume foo: update error`) + c.Check(logbuf.String(), testutil.Contains, `cannot rollback volume structure #1 ("second") update on volume foo: rollback failed with different error`) } func (u *updateTestSuite) TestUpdateApplyBadUpdater(c *C) { - oldData, newData, rollbackDir := updateDataSet(c) + oldData, newData, rollbackDir := u.updateDataSet(c) // update all structs newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2 newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3 - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { return nil, errors.New("bad updater for structure") }) defer restore() // go go go err := gadget.Update(uc16Model, oldData, newData, rollbackDir, nil, nil) - c.Assert(err, ErrorMatches, `cannot prepare update for volume structure #0 \("first"\): bad updater for structure`) + c.Assert(err, ErrorMatches, `cannot prepare update for volume structure #0 \("first"\) on volume foo: bad updater for structure`) } func (u *updateTestSuite) TestUpdaterForStructure(c *C) { @@ -1403,7 +3169,7 @@ }, StartOffset: 1 * quantity.OffsetMiB, } - updater, err := gadget.UpdaterForStructure(psBare, gadgetRootDir, rollbackDir, nil) + updater, err := gadget.UpdaterForStructure(gadget.StructureLocation{}, psBare, gadgetRootDir, rollbackDir, nil) c.Assert(err, IsNil) c.Assert(updater, FitsTypeOf, &gadget.RawStructureUpdater{}) @@ -1415,20 +3181,17 @@ }, StartOffset: 1 * quantity.OffsetMiB, } - updater, err = gadget.UpdaterForStructure(psFs, gadgetRootDir, rollbackDir, nil) + updater, err = gadget.UpdaterForStructure(gadget.StructureLocation{}, psFs, gadgetRootDir, rollbackDir, nil) c.Assert(err, IsNil) c.Assert(updater, FitsTypeOf, &gadget.MountedFilesystemUpdater{}) // trigger errors - updater, err = gadget.UpdaterForStructure(psBare, gadgetRootDir, "", nil) + updater, err = gadget.UpdaterForStructure(gadget.StructureLocation{}, psBare, gadgetRootDir, "", nil) c.Assert(err, ErrorMatches, "internal error: backup directory cannot be unset") c.Assert(updater, IsNil) } -func (u *updateTestSuite) TestUpdaterMultiVolumesDoesNotError(c *C) { - logbuf, restore := logger.MockLogger() - defer restore() - +func (u *updateTestSuite) TestUpdaterMultiVolumesAddedRemovedErrors(c *C) { multiVolume := gadget.GadgetData{ Info: &gadget.Info{ Volumes: map[string]*gadget.Volume{ @@ -1445,20 +3208,18 @@ }, } - // a new multi volume gadget update gives no error + // an update to a gadget with multiple volumes when we had just a single one + // before fails err := gadget.Update(uc16Model, singleVolume, multiVolume, "some-rollback-dir", nil, nil) - c.Assert(err, IsNil) - // but it warns that nothing happens either - c.Assert(logbuf.String(), testutil.Contains, "WARNING: gadget assests cannot be updated yet when multiple volumes are used") + c.Assert(err, ErrorMatches, "cannot update gadget assets: volumes were removed") - // same for old + // same for an update removing volumes err = gadget.Update(uc16Model, multiVolume, singleVolume, "some-rollback-dir", nil, nil) - c.Assert(err, IsNil) - c.Assert(strings.Count(logbuf.String(), "WARNING: gadget assests cannot be updated yet when multiple volumes are used"), Equals, 2) + c.Assert(err, ErrorMatches, "cannot update gadget assets: volumes were added") } func (u *updateTestSuite) TestUpdateApplyNoChangedContentInAll(c *C) { - oldData, newData, rollbackDir := updateDataSet(c) + oldData, newData, rollbackDir := u.updateDataSet(c) // first structure is updated oldData.Info.Volumes["foo"].Structure[0].Update.Edition = 0 newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 @@ -1469,7 +3230,7 @@ muo := &mockUpdateProcessObserver{} expectedStructs := []string{"first", "second"} updateCalls := 0 - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { mu := &mockUpdater{ updateCb: func() error { c.Assert(expectedStructs, testutil.Contains, ps.Name) @@ -1496,7 +3257,7 @@ } func (u *updateTestSuite) TestUpdateApplyNoChangedContentInSome(c *C) { - oldData, newData, rollbackDir := updateDataSet(c) + oldData, newData, rollbackDir := u.updateDataSet(c) // first structure is updated oldData.Info.Volumes["foo"].Structure[0].Update.Edition = 0 newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 @@ -1507,7 +3268,7 @@ muo := &mockUpdateProcessObserver{} expectedStructs := []string{"first", "second"} updateCalls := 0 - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { mu := &mockUpdater{ updateCb: func() error { c.Assert(expectedStructs, testutil.Contains, ps.Name) @@ -1537,10 +3298,10 @@ } func (u *updateTestSuite) TestUpdateApplyObserverBeforeWriteErrs(c *C) { - oldData, newData, rollbackDir := updateDataSet(c) + oldData, newData, rollbackDir := u.updateDataSet(c) newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { updater := &mockUpdater{ updateCb: func() error { c.Fatalf("unexpected call") @@ -1566,12 +3327,12 @@ logbuf, restore := logger.MockLogger() defer restore() - oldData, newData, rollbackDir := updateDataSet(c) + oldData, newData, rollbackDir := u.updateDataSet(c) newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1 backupErr := errors.New("backup fails") updateErr := errors.New("update fails") - restore = gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore = gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { updater := &mockUpdater{ backupCb: func() error { return backupErr }, updateCb: func() error { return updateErr }, @@ -1706,6 +3467,7 @@ func (u *updateTestSuite) TestUpdateApplyUpdatesWithKernelPolicy(c *C) { // prepare the stage fsStruct := gadget.VolumeStructure{ + VolumeName: "foo", Name: "foo", Size: 5 * quantity.SizeMiB, Filesystem: "ext4", @@ -1717,6 +3479,7 @@ oldInfo := &gadget.Info{ Volumes: map[string]*gadget.Volume{ "foo": { + Name: "foo", Bootloader: "grub", Schema: "gpt", Structure: []gadget.VolumeStructure{fsStruct}, @@ -1724,6 +3487,17 @@ }, } + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.LaidOutVolume) (map[string]map[int]gadget.StructureLocation, error) { + return map[string]map[int]gadget.StructureLocation{ + "foo": { + 0: { + RootMountPoint: "/foo", + }, + }, + }, nil + }) + defer r() + oldRootDir := c.MkDir() oldKernelDir := c.MkDir() oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir, KernelRootDir: oldKernelDir} @@ -1755,7 +3529,7 @@ // updater is only called with the kernel content, not with the // gadget content. mockUpdaterCalls := 0 - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { mockUpdaterCalls++ c.Check(ps.ResolvedContent, DeepEquals, []gadget.ResolvedContent{ { @@ -1824,7 +3598,7 @@ rollbackDir := c.MkDir() muo := &mockUpdateProcessObserver{} - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { panic("should not get called") }) defer restore() @@ -1955,6 +3729,7 @@ vols, err := gadgettest.LayoutMultiVolumeFromYaml( c.MkDir(), + "", gadgettest.MultiVolumeUC20GadgetYaml, uc20Model, ) @@ -2151,6 +3926,31 @@ c.Assert(traits, DeepEquals, gadgettest.UC16ImplicitSystemDataDeviceTraits) } +func (s *updateTestSuite) TestDiskTraitsFromDeviceAndValidateImplicitSystemDataRaspiHappy(c *C) { + // mock the device name + restore := disks.MockDeviceNameToDiskMapping(map[string]*disks.MockDiskMapping{ + "/dev/mmcblk0": gadgettest.ExpectedRaspiUC18MockDiskMapping, + }) + defer restore() + + lvol, err := gadgettest.LayoutFromYaml(c.MkDir(), gadgettest.RaspiUC18SimplifiedYaml, nil) + c.Assert(err, IsNil) + + // the volume cannot be found with no opts set + _, err = gadget.DiskTraitsFromDeviceAndValidate(lvol, "/dev/mmcblk0", nil) + c.Assert(err, ErrorMatches, `volume pi is not compatible with disk /dev/mmcblk0: cannot find disk partition /dev/mmcblk0p2 \(starting at 269484032\) in gadget: start offsets do not match \(disk: 269484032 \(257 MiB\) and gadget: 1048576 \(1 MiB\)\)`) + + // with opts for pc then it can be found + opts := &gadget.DiskVolumeValidationOptions{ + AllowImplicitSystemData: true, + } + + traits, err := gadget.DiskTraitsFromDeviceAndValidate(lvol, "/dev/mmcblk0", opts) + c.Assert(err, IsNil) + + c.Assert(traits, DeepEquals, gadgettest.ExpectedRaspiUC18DiskVolumeDeviceTraits) +} + func (s *updateTestSuite) TestSearchForVolumeWithTraitsImplicitSystemData(c *C) { allowImplicitDataOpts := &gadget.DiskVolumeValidationOptions{ AllowImplicitSystemData: true, @@ -2179,7 +3979,7 @@ }) defer r() - allVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), gadgettest.UC16YAMLImplicitSystemData, uc16Model) + allVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.UC16YAMLImplicitSystemData, uc16Model) c.Assert(err, IsNil) laidOutVol := allVolumes["pc"] @@ -2249,7 +4049,7 @@ }) defer r() - allVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), gadgetYaml, model) + allVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgetYaml, model) c.Assert(err, IsNil) laidOutVol := allVolumes[volName] @@ -2353,7 +4153,7 @@ } func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingImplicitSystemDataUC16(c *C) { - allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), gadgettest.UC16YAMLImplicitSystemData, uc16Model) + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.UC16YAMLImplicitSystemData, uc16Model) c.Assert(err, IsNil) old := gadget.GadgetData{ @@ -2523,7 +4323,7 @@ } func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingPreUC20NonFatalError(c *C) { - allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), gadgettest.UC16YAMLImplicitSystemData, uc16Model) + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.UC16YAMLImplicitSystemData, uc16Model) c.Assert(err, IsNil) old := gadget.GadgetData{ @@ -2548,7 +4348,7 @@ } func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingUC20MultiVolume(c *C) { - allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), gadgettest.MultiVolumeUC20GadgetYaml, uc20Model) + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.MultiVolumeUC20GadgetYaml, uc20Model) c.Assert(err, IsNil) old := gadget.GadgetData{ @@ -2591,7 +4391,7 @@ } func (u *updateTestSuite) TestBuildNewVolumeToDeviceMappingUC20Encryption(c *C) { - allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), gadgettest.RaspiSimplifiedYaml, uc20Model) + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", gadgettest.RaspiSimplifiedYaml, uc20Model) c.Assert(err, IsNil) old := gadget.GadgetData{ @@ -2940,7 +4740,7 @@ volMappings map[string]*disks.MockDiskMapping, expMapping map[string]map[int]gadget.StructureLocation, ) (gadget.GadgetData, map[string]*gadget.LaidOutVolume) { - allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), yaml, model) + allLaidOutVolumes, err := gadgettest.LayoutMultiVolumeFromYaml(c.MkDir(), "", yaml, model) c.Assert(err, IsNil) old := gadget.GadgetData{ @@ -3237,9 +5037,9 @@ 4: {RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-save")}, 5: {RootMountPoint: filepath.Join(dirs.GlobalRootDir, "/run/mnt/data")}, }, - // empty foo volume since because the disk-mapping.json was not written - // initially, we only handle updates to the pc / system-boot volume - "foo": {}, + // missing foo volume since because the disk-mapping.json was not + // written initially, we only handle updates to the pc / system-boot + // volume } // setup mountinfo for root mount points of the partitions with filesystems diff -Nru snapd-2.55.5+20.04/gadget/validate.go snapd-2.57.5+20.04/gadget/validate.go --- snapd-2.55.5+20.04/gadget/validate.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/validate.go 2022-10-17 16:25:18.000000000 +0000 @@ -375,8 +375,20 @@ // assets from the kernel.yaml has a reference in the given // LaidOutVolume. func gadgetVolumeConsumesOneKernelUpdateAsset(pNew *Volume, kernelInfo *kernel.Info) error { - notFoundAssets := make([]string, 0, len(kernelInfo.Assets)) - for assetName, asset := range kernelInfo.Assets { + notFoundAssets, _, err := searchConsumedAssets(pNew, kernelInfo.Assets) + if err != nil { + return err + } + if len(notFoundAssets) > 0 { + sort.Strings(notFoundAssets) + return fmt.Errorf("gadget does not consume any of the kernel assets needing synced update %s", strutil.Quoted(notFoundAssets)) + } + return nil +} + +func searchConsumedAssets(pNew *Volume, assets map[string]*kernel.Asset) (missingAssets []string, consumedAny bool, err error) { + notFoundAssets := make([]string, 0, len(assets)) + for assetName, asset := range assets { if !asset.Update { continue } @@ -389,20 +401,28 @@ } wantedAsset, _, err := splitKernelRef(pathOrRef) if err != nil { - return err + return nil, false, err } if assetName == wantedAsset { // found a valid kernel asset, // that is enough - return nil + return nil, true, nil } } } notFoundAssets = append(notFoundAssets, assetName) } - if len(notFoundAssets) > 0 { - sort.Strings(notFoundAssets) - return fmt.Errorf("gadget does not consume any of the kernel assets needing synced update %s", strutil.Quoted(notFoundAssets)) + + return notFoundAssets, false, nil +} + +// gadgetVolumeKernelUpdateAssetsConsumed ensures that at least one kernel +// assets from the kernel.yaml has a reference in the given +// LaidOutVolume. +func gadgetVolumeKernelUpdateAssetsConsumed(pNew *Volume, kernelInfo *kernel.Info) (bool, error) { + _, consumedAny, err := searchConsumedAssets(pNew, kernelInfo.Assets) + if err != nil { + return false, err } - return nil + return consumedAny, nil } diff -Nru snapd-2.55.5+20.04/gadget/validate_test.go snapd-2.57.5+20.04/gadget/validate_test.go --- snapd-2.55.5+20.04/gadget/validate_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/gadget/validate_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -905,34 +905,44 @@ } for _, tc := range []struct { - volumeContent []gadget.VolumeContent - kinfo *kernel.Info - expectedErr string + volumeContent []gadget.VolumeContent + kinfo *kernel.Info + consumed bool + consumedErr string + consumesOneErr string }{ // happy case: trivial - {contentNoKernelRef, kInfoNoRefs, ""}, + {contentNoKernelRef, kInfoNoRefs, false, "", ""}, // happy case: if kernel asset has "Update: false" - {contentNoKernelRef, kInfoOneRefButUpdateFlagFalse, ""}, + {contentNoKernelRef, kInfoOneRefButUpdateFlagFalse, false, "", ""}, // unhappy case: kernel has one or more unresolved references in gadget - {contentNoKernelRef, kInfoOneRef, `gadget does not consume any of the kernel assets needing synced update "ref"`}, - {contentNoKernelRef, kInfoTwoRefs, `gadget does not consume any of the kernel assets needing synced update "ref", "ref2"`}, + {contentNoKernelRef, kInfoOneRef, false, "", "gadget does not consume any of the kernel assets needing synced update \"ref\""}, + {contentNoKernelRef, kInfoTwoRefs, false, "", "gadget does not consume any of the kernel assets needing synced update \"ref\", \"ref2\""}, // unhappy case: gadget needs different asset than kernel provides - {contentOneKernelRef, kInfoOneRefDifferentName, `gadget does not consume any of the kernel assets needing synced update "ref-other"`}, + {contentOneKernelRef, kInfoOneRefDifferentName, false, "", "gadget does not consume any of the kernel assets needing synced update \"ref-other\""}, // happy case: exactly one matching kernel ref - {contentOneKernelRef, kInfoOneRef, ""}, + {contentOneKernelRef, kInfoOneRef, true, "", ""}, // happy case: one matching, one missing kernel ref, still considered fine - {contentTwoKernelRefs, kInfoTwoRefs, ""}, + {contentTwoKernelRefs, kInfoTwoRefs, true, "", ""}, } { lv.Structure[0].Content = tc.volumeContent - err := gadget.GadgetVolumeConsumesOneKernelUpdateAsset(lv.Volume, tc.kinfo) - if tc.expectedErr == "" { + consumed, err := gadget.GadgetVolumeKernelUpdateAssetsConsumed(lv.Volume, tc.kinfo) + if tc.consumedErr == "" { c.Check(err, IsNil, Commentf("should not fail %v", tc.volumeContent)) + c.Check(consumed, Equals, tc.consumed) } else { - c.Check(err, ErrorMatches, tc.expectedErr, Commentf("should fail %v", tc.volumeContent)) + c.Check(err, ErrorMatches, tc.consumedErr, Commentf("should fail %v", tc.volumeContent)) + } + + err = gadget.GadgetVolumeConsumesOneKernelUpdateAsset(lv.Volume, tc.kinfo) + if tc.consumesOneErr == "" { + c.Check(err, IsNil, Commentf("should not fail %v", tc.volumeContent)) + } else { + c.Check(err, ErrorMatches, tc.consumesOneErr, Commentf("should fail %v", tc.volumeContent)) } } } diff -Nru snapd-2.55.5+20.04/.github/workflows/naming.yml snapd-2.57.5+20.04/.github/workflows/naming.yml --- snapd-2.55.5+20.04/.github/workflows/naming.yml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/.github/workflows/naming.yml 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,21 @@ +name: Inclusive naming PR check +on: pull_request + +jobs: + inclusive-naming-check: + name: Inclusive-naming-check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - uses: tj-actions/changed-files@v18.7 + id: files + + - name: woke + uses: get-woke/woke-action-reviewdog@v0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-check + fail-on-error: true + woke-args: ${{ steps.files.outputs.all_changed_files }} diff -Nru snapd-2.55.5+20.04/.github/workflows/riscv64-builds.yml snapd-2.57.5+20.04/.github/workflows/riscv64-builds.yml --- snapd-2.55.5+20.04/.github/workflows/riscv64-builds.yml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/.github/workflows/riscv64-builds.yml 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,19 @@ +name: lp-snap-request-build-riscv64 + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + lp-snap-request-build: + runs-on: ubuntu-latest + steps: + - name: trigger-lp-snap-request-build + env: + SNAPD_RISCV64_BOT_BASE64: ${{ secrets.SNAPD_RISCV64_BOT_BASE64 }} + if: env.SNAPD_RISCV64_BOT_BASE64 != null + run: | + sudo apt install -y lptools + echo $SNAPD_RISCV64_BOT_BASE64 | base64 --decode > SNAPD_RISCV64_BOT.cred + lp-shell --credentials-file=SNAPD_RISCV64_BOT.cred -c 'snap=lp.load("~snappy-dev-riscv64/+snap/snapd-master-riscv64"); snap.requestBuilds(archive=snap.auto_build_archive_link, channels=snap.auto_build_channels, pocket=snap.auto_build_pocket)' diff -Nru snapd-2.55.5+20.04/.github/workflows/test.yaml snapd-2.57.5+20.04/.github/workflows/test.yaml --- snapd-2.55.5+20.04/.github/workflows/test.yaml 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/.github/workflows/test.yaml 2022-10-17 16:25:18.000000000 +0000 @@ -7,6 +7,10 @@ # branch, the master branch runs are just for unit tests + codecov.io branches: [ "master","release/**" ] +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: snap-builds: runs-on: ubuntu-20.04 @@ -16,37 +20,14 @@ steps: - name: Checkout code uses: actions/checkout@v2 - - name: Cache snapd snap build status - id: cache-snapd-build-status - uses: actions/cache@v1 - with: - path: "${{ github.workspace }}/.test-results" - key: "${{ github.run_id }}-${{ github.job }}-results" - - name: Check cached snap build - id: cached-results - run: | - CACHE_RESULT_STAMP="${{ github.workspace }}/.test-results/snap-build-success" - echo "CACHE_RESULT_STAMP=$CACHE_RESULT_STAMP" >> $GITHUB_ENV - if [ -e "$CACHE_RESULT_STAMP" ]; then - has_cached_snap=0 - while read name; do - has_cached_snap=1 - # bring back artifacts from the cache - cp -v "$name" "${{ github.workspace }}" - done < <(find "$(dirname $CACHE_RESULT_STAMP)" -name "*.snap") - if [ "$has_cached_snap" = "1" ]; then - # we have restored an artifact from the cache - echo "::set-output name=already-ran::true" - fi - fi + - name: Build snapd snap - if: steps.cached-results.outputs.already-ran != 'true' uses: snapcore/action-build@v1 with: snapcraft-channel: 4.x/candidate - - name: Cache and check built artifact + + - name: Check built artifact run: | - mkdir -p $(dirname "$CACHE_RESULT_STAMP") unsquashfs snapd*.snap meta/snap.yaml usr/lib/snapd/info if cat squashfs-root/meta/snap.yaml | grep -q "version:.*dirty.*"; then echo "PR produces dirty snapd snap version" @@ -57,19 +38,51 @@ cat squashfs-root/usr/lib/snapd/info exit 1 fi - cp -v *.snap "$(dirname $CACHE_RESULT_STAMP)/" + - name: Uploading snapd snap artifact uses: actions/upload-artifact@v2 with: name: snap-files path: "*.snap" - - name: Mark successful snap build + + cache-build-deps: + runs-on: ubuntu-20.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + # needed for git commit history + fetch-depth: 0 + # NOTE: checkout the code in a fixed location, even for forks, as this + # is relevant for go's import system. + path: ./src/github.com/snapcore/snapd + + # Fetch base ref, needed for golangci-lint + - name: Fetching base ref ${{ github.base_ref }} run: | - mkdir -p $(dirname "$CACHE_RESULT_STAMP") - touch "$CACHE_RESULT_STAMP" + cd ${{ github.workspace }}/src/github.com/snapcore/snapd + git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} + # golang latest ensures things work on the edge - unit-tests: + - name: Download Debian dependencies + run: | + sudo apt clean + sudo apt update + sudo apt build-dep -d -y ${{ github.workspace }}/src/github.com/snapcore/snapd + + - name: Copy dependencies + run: | + sudo tar cvf cached-apt.tar /var/cache/apt + + - name: upload Debian dependencies + uses: actions/upload-artifact@v2 + with: + name: debian-dependencies + path: ./cached-apt.tar + + static-checks: runs-on: ubuntu-20.04 + needs: [cache-build-deps] env: GOPATH: ${{ github.workspace }} # Set PATH to ignore the load of magic binaries from /usr/local/bin And @@ -79,6 +92,7 @@ PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin GOROOT: "" GITHUB_PULL_REQUEST: ${{ github.event.number }} + strategy: # we cache successful runs so it's fine to keep going fail-fast: false @@ -86,6 +100,7 @@ gochannel: - 1.13 - latest/stable + steps: - name: Checkout code uses: actions/checkout@v2 @@ -95,66 +110,50 @@ # NOTE: checkout the code in a fixed location, even for forks, as this # is relevant for go's import system. path: ./src/github.com/snapcore/snapd + # Fetch base ref, needed for golangci-lint - name: Fetching base ref ${{ github.base_ref }} run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} - - name: Cache Debian dependencies - id: cache-deb-downloads - uses: actions/cache@v1 - with: - path: /var/cache/apt - key: var-cache-apt-{{ hashFiles('**/debian/control') }} - - name: Run "apt update" - run: | - sudo apt update - - name: Download Debian dependencies - if: steps.cache-deb-downloads.outputs.cache-hit != 'true' - run: | - sudo apt clean - sudo apt build-dep -d -y ${{ github.workspace }}/src/github.com/snapcore/snapd - - name: Cache snapd test results - id: cache-snapd-test-results - uses: actions/cache@v1 + - name: Download Debian dependencies + uses: actions/download-artifact@v3 with: - path: "${{ github.workspace }}/.test-results" - # must include matrix or things get racy, i.e. when latest/edge - # finishes after 1.9 it overrides the results from 1.9 - key: "${{ github.run_id }}-${{ github.job }}-${{ matrix.gochannel }}-results" - - name: Check cached test results - id: cached-results + name: debian-dependencies + path: ./debian-deps/ + + - name: Copy dependencies run: | - CACHE_RESULT_STAMP="${{ github.workspace }}/.test-results/${{ matrix.gochannel }}-success" - echo "CACHE_RESULT_STAMP=$CACHE_RESULT_STAMP" >> $GITHUB_ENV - if [ -e "$CACHE_RESULT_STAMP" ]; then - echo "::set-output name=already-ran::true" - fi + test -f ./debian-deps/cached-apt.tar + sudo tar xvf ./debian-deps/cached-apt.tar -C / + - name: Install Debian dependencies - if: steps.cached-results.outputs.cached-results != 'true' run: | + sudo apt update sudo apt build-dep -y ${{ github.workspace }}/src/github.com/snapcore/snapd + # golang latest ensures things work on the edge - name: Install the go snap - if: steps.cached-results.outputs.already-ran != 'true' run: | sudo snap install --classic --channel=${{ matrix.gochannel }} go + - name: Install ShellCheck as a snap - if: steps.cached-results.outputs.already-ran != 'true' run: | sudo apt-get remove --purge shellcheck sudo snap install shellcheck + - name: Get C vendoring - run: cd ${{ github.workspace }}/src/github.com/snapcore/snapd/c-vendor && ./vendor.sh + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd/c-vendor && ./vendor.sh - name: golangci-lint - uses: golangci/golangci-lint-action@v2 if: ${{ matrix.gochannel == 'latest/stable' }} + uses: golangci/golangci-lint-action@v2 with: # version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` # to use the latest version - version: v1.40.0 + version: v1.45.2 working-directory: ./src/github.com/snapcore/snapd # show only new issues # use empty path prefix to make annotations work @@ -166,8 +165,19 @@ # XXX: does no work with working-directory # only-new-issues: true + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v19 + with: + path: ./src/github.com/snapcore/snapd + + - name: Save changes files + run: | + CHANGED_FILES="${{ steps.changed-files.outputs.all_changed_files }}" + echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV + echo "The changed files found are: $CHANGED_FILES" + - name: Run static checks - if: steps.cached-results.outputs.already-ran != 'true' run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 # run gofmt checks only with Go 1.13 @@ -182,41 +192,112 @@ fi sudo apt-get install -y python3-yamlordereddictloader ./run-checks --static + + unit-tests: + needs: [static-checks] + runs-on: ubuntu-20.04 + env: + GOPATH: ${{ github.workspace }} + # Set PATH to ignore the load of magic binaries from /usr/local/bin And + # to use the go snap automatically. Note that we install go from the + # snap in a step below. Without this we get the GitHub-controlled latest + # version of go. + PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin + GOROOT: "" + GITHUB_PULL_REQUEST: ${{ github.event.number }} + strategy: + # we cache successful runs so it's fine to keep going + fail-fast: false + matrix: + gochannel: + - 1.13 + - latest/stable + unit-scenario: + - normal + - snapd_debug + - withbootassetstesting + - nosecboot + - faultinject + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + # needed for git commit history + fetch-depth: 0 + # NOTE: checkout the code in a fixed location, even for forks, as this + # is relevant for go's import system. + path: ./src/github.com/snapcore/snapd + + # Fetch base ref, needed for golangci-lint + - name: Fetching base ref ${{ github.base_ref }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd + git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} + + - name: Download Debian dependencies + uses: actions/download-artifact@v3 + with: + name: debian-dependencies + path: ./debian-deps/ + + - name: Copy dependencies + run: | + test -f ./debian-deps/cached-apt.tar + sudo tar xvf ./debian-deps/cached-apt.tar -C / + + - name: Install Debian dependencies + run: | + sudo apt update + sudo apt build-dep -y ${{ github.workspace }}/src/github.com/snapcore/snapd + + # golang latest ensures things work on the edge + - name: Install the go snap + run: | + sudo snap install --classic --channel=${{ matrix.gochannel }} go + - name: Build C - if: steps.cached-results.outputs.already-ran != 'true' run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ ./autogen.sh make -j2 + + - name: Get deps + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd/ && ./get-deps.sh + - name: Build Go - if: steps.cached-results.outputs.already-ran != 'true' run: | go build github.com/snapcore/snapd/... + - name: Test C - if: steps.cached-results.outputs.already-ran != 'true' run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ && make check + - name: Reset code coverage data - if: steps.cached-results.outputs.already-ran != 'true' run: | rm -rf ${{ github.workspace }}/.coverage/ + - name: Test Go - if: steps.cached-results.outputs.already-ran != 'true' + if: ${{ matrix.unit-scenario == 'normal' }} run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 ./run-checks --unit + - name: Test Go (SNAPD_DEBUG=1) - if: steps.cached-results.outputs.already-ran != 'true' + if: ${{ matrix.unit-scenario == 'snapd_debug' }} run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 SKIP_DIRTY_CHECK=1 SNAPD_DEBUG=1 ./run-checks --unit + - name: Test Go (withbootassetstesting) - if: steps.cached-results.outputs.already-ran != 'true' + if: ${{ matrix.unit-scenario == 'withbootassetstesting' }} run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=withbootassetstesting ./run-checks --unit + - name: Test Go (nosecboot) - if: steps.cached-results.outputs.already-ran != 'true' + if: ${{ matrix.unit-scenario == 'nosecboot' }} run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 echo "Dropping github.com/snapcore/secboot" @@ -225,33 +306,59 @@ # ${{ github.workspace }}/bin/govendor remove github.com/snapcore/secboot # ${{ github.workspace }}/bin/govendor remove +unused SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=nosecboot ./run-checks --unit + - name: Test Go (faultinject) - if: steps.cached-results.outputs.already-ran != 'true' + if: ${{ matrix.unit-scenario == 'faultinject' }} run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=faultinject ./run-checks --unit + + - name: Upload the coverage results + if: ${{ matrix.gochannel != 'latest/stable' }} + uses: actions/upload-artifact@v2 + with: + name: coverage-files + path: "${{ github.workspace }}/src/github.com/snapcore/snapd/.coverage/coverage*.cov" + + code-coverage: + needs: [unit-tests] + runs-on: ubuntu-20.04 + env: + GOPATH: ${{ github.workspace }} + # Set PATH to ignore the load of magic binaries from /usr/local/bin And + # to use the go snap automatically. Note that we install go from the + # snap in a step below. Without this we get the GitHub-controlled latest + # version of go. + PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin + GOROOT: "" + GITHUB_PULL_REQUEST: ${{ github.event.number }} + steps: + - name: Download the coverage files + uses: actions/download-artifact@v3 + with: + name: coverage-files + path: .coverage/ + - name: Merge coverage results - if: ${{ steps.cached-results.outputs.already-ran != 'true' && matrix.gochannel != 'latest/stable' }} run: | - cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 - go install github.com/makholm/covertool + echo "List the coverage files downloaded" + ls -l .coverage/ + + echo "Install covertool and merge the results" + go install github.com/makholm/covertool@latest covertool merge -o .coverage/all.cov .coverage/coverage*.cov + - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 # uploading to codecov occasionally fails, so continue running the test # workflow regardless of the upload continue-on-error: true - if: ${{ steps.cached-results.outputs.already-ran != 'true' && matrix.gochannel != 'latest/stable' }} with: fail_ci_if_error: true flags: unittests name: codecov-umbrella files: .coverage/all.cov verbose: true - - name: Cache successful run - run: | - mkdir -p $(dirname "$CACHE_RESULT_STAMP") - touch "$CACHE_RESULT_STAMP" spread: needs: [unit-tests] @@ -272,23 +379,25 @@ - arch-linux-64 - centos-7-64 - centos-8-64 + - centos-9-64 - debian-10-64 - debian-11-64 - debian-sid-64 - - fedora-34-64 - fedora-35-64 + - fedora-36-64 - opensuse-15.3-64 + - opensuse-15.4-64 - opensuse-tumbleweed-64 - ubuntu-14.04-64 - ubuntu-16.04-64 - ubuntu-18.04-32 - ubuntu-18.04-64 - ubuntu-20.04-64 - - ubuntu-21.10-64 - ubuntu-22.04-64 - ubuntu-core-16-64 - ubuntu-core-18-64 - ubuntu-core-20-64 + - ubuntu-core-22-64 - ubuntu-secboot-20.04-64 steps: - name: Cleanup job workspace @@ -296,40 +405,63 @@ run: | rm -rf "${{ github.workspace }}" mkdir "${{ github.workspace }}" + - name: Checkout code uses: actions/checkout@v2 with: # spread uses tags as delta reference fetch-depth: 0 + - name: Cache snapd test results id: cache-snapd-test-results - uses: actions/cache@v1 + uses: pat-s/always-upload-cache@v2.1.5 with: path: "${{ github.workspace }}/.test-results" key: "${{ github.run_id }}-${{ github.job }}-${{ matrix.system }}-results" + - name: Check cached test results id: cached-results run: | - CACHE_RESULT_STAMP="${{ github.workspace }}/.test-results/${{ matrix.system }}-success" - echo "CACHE_RESULT_STAMP=$CACHE_RESULT_STAMP" >> $GITHUB_ENV - if [ -e "$CACHE_RESULT_STAMP" ]; then - echo "::set-output name=already-ran::true" + TEST_RESULTS_DIR="${{ github.workspace }}/.test-results" + mkdir -p "$TEST_RESULTS_DIR" + echo "TEST_RESULTS_DIR=$TEST_RESULTS_DIR" >> $GITHUB_ENV + + # Save the var with the failed tests file + echo "FAILED_TESTS_FILE=$TEST_RESULTS_DIR/${{ matrix.system }}-failed-tests" >> $GITHUB_ENV + + - name: Check failed tests to run + if: "!contains(github.event.pull_request.labels.*.name, 'Run all')" + run: | + # Save previous failed test results in FAILED_TESTS env var + FAILED_TESTS="" + if [ -f "$FAILED_TESTS_FILE" ]; then + echo "Failed tests file found" + FAILED_TESTS="$(cat $FAILED_TESTS_FILE)" + if [ -n "$FAILED_TESTS" ]; then + echo "Failed tests to run: $FAILED_TESTS" + echo "FAILED_TESTS=$FAILED_TESTS" >> $GITHUB_ENV + fi fi + - name: Run spread tests - if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread') && steps.cached-results.outputs.already-ran != 'true'" + if: "!contains(github.event.pull_request.labels.*.name, 'Skip spread')" env: SPREAD_GOOGLE_KEY: ${{ secrets.SPREAD_GOOGLE_KEY }} run: | # Register a problem matcher to highlight spread failures echo "::add-matcher::.github/spread-problem-matcher.json" + + # Save previous failed test results in FAILED_TESTS env var + RUN_TESTS="google:${{ matrix.system }}:tests/..." + if [ -n "$FAILED_TESTS" ]; then + RUN_TESTS="$FAILED_TESTS" + fi + # Run spread tests # "pipefail" ensures that a non-zero status from the spread is # propagated; and we use a subshell as this option could trigger # undesired changes elsewhere - (set -o pipefail; spread -abend google:${{ matrix.system }}:tests/... | tee spread.log) - - name: Cache successful run - run: | - mkdir -p $(dirname "$CACHE_RESULT_STAMP") - touch "$CACHE_RESULT_STAMP" + (set -o pipefail; spread -abend $RUN_TESTS | tee spread.log) + - name: Discard spread workers if: always() run: | @@ -337,6 +469,7 @@ for r in .spread-reuse.*.yaml; do spread -discard -reuse-pid="$(echo "$r" | grep -o -E '[0-9]+')"; done + - name: report spread errors if: always() run: | @@ -354,6 +487,26 @@ echo "No spread log found, skipping errors reporting" fi + - name: save spread results + if: always() + run: | + if [ -f spread.log ]; then + echo "Running spread log parser" + ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json + + echo "Determining which tests were executed" + RUN_TESTS="google:${{ matrix.system }}:tests/..." + if [ -n "$FAILED_TESTS" ]; then + RUN_TESTS="$FAILED_TESTS" + fi + + echo "Running spread log analyzer" + ./tests/lib/external/snapd-testing-tools/utils/log-analyzer list-reexecute-tasks "$RUN_TESTS" spread-results.json > "$FAILED_TESTS_FILE" + else + echo "No spread log found, saving empty list of failed tests" + touch "$FAILED_TESTS_FILE" + fi + spread-nested: needs: [unit-tests] # have spread jobs run on master on PRs only, but on both PRs and pushes to @@ -372,32 +525,52 @@ - ubuntu-16.04-64 - ubuntu-18.04-64 - ubuntu-20.04-64 + - ubuntu-22.04-64 steps: - name: Cleanup job workspace id: cleanup-job-workspace run: | rm -rf "${{ github.workspace }}" mkdir "${{ github.workspace }}" + - name: Checkout code uses: actions/checkout@v2 + - name: Cache snapd test results id: cache-snapd-test-results - uses: actions/cache@v1 + uses: pat-s/always-upload-cache@v2.1.5 with: path: "${{ github.workspace }}/.test-results" - key: "${{ github.run_id }}-${{ github.job }}-${{ matrix.system }}-nested-results" + key: "${{ github.run_id }}-${{ github.job }}-${{ matrix.system }}-results" + - name: Check cached test results id: cached-results run: | - CACHE_RESULT_STAMP="${{ github.workspace }}/.test-results/${{ matrix.system }}-nested-success" - echo "CACHE_RESULT_STAMP=$CACHE_RESULT_STAMP" >> $GITHUB_ENV - if [ -e "$CACHE_RESULT_STAMP" ]; then - echo "::set-output name=already-ran::true" + TEST_RESULTS_DIR="${{ github.workspace }}/.test-results" + mkdir -p "$TEST_RESULTS_DIR" + echo "TEST_RESULTS_DIR=$TEST_RESULTS_DIR" >> $GITHUB_ENV + + # Save the var with the failed tests file + echo "FAILED_TESTS_FILE=$TEST_RESULTS_DIR/${{ matrix.system }}-failed-tests" >> $GITHUB_ENV + + - name: Check failed tests to run + if: "!contains(github.event.pull_request.labels.*.name, 'Run all')" + run: | + # Save previous failed test results in FAILED_TESTS env var + FAILED_TESTS="" + if [ -f "$FAILED_TESTS_FILE" ]; then + echo "Failed tests file found" + FAILED_TESTS="$(cat $FAILED_TESTS_FILE)" + if [ -n "$FAILED_TESTS" ]; then + echo "Failed tests to run: $FAILED_TESTS" + echo "FAILED_TESTS=$FAILED_TESTS" >> $GITHUB_ENV + fi fi + - name: Run spread tests # run if the commit is pushed to the release/* branch or there is a 'Run # nested' label set on the PR - if: "(contains(github.event.pull_request.labels.*.name, 'Run nested') || contains(github.ref, 'refs/heads/release/')) && steps.cached-results.outputs.already-ran != 'true'" + if: "contains(github.event.pull_request.labels.*.name, 'Run nested') || contains(github.ref, 'refs/heads/release/')" env: SPREAD_GOOGLE_KEY: ${{ secrets.SPREAD_GOOGLE_KEY }} run: | @@ -405,14 +578,16 @@ echo "::add-matcher::.github/spread-problem-matcher.json" export NESTED_BUILD_SNAPD_FROM_CURRENT=true export NESTED_ENABLE_KVM=true + RUN_TESTS="google-nested:${{ matrix.system }}:tests/nested/..." + if [ -n "$FAILED_TESTS" ]; then + RUN_TESTS="$FAILED_TESTS" + fi + # Run spread tests # "pipefail" ensures that a non-zero status from the spread is # propagated; and we use a subshell as this option could trigger # undesired changes elsewhere - (set -o pipefail; spread -abend google-nested:${{ matrix.system }}:tests/nested/... | tee spread.log) - - name: Cache successful run - run: | - mkdir -p $(dirname "$CACHE_RESULT_STAMP") - touch "$CACHE_RESULT_STAMP" + (set -o pipefail; spread -abend $RUN_TESTS | tee spread.log) + - name: Discard spread workers if: always() run: | @@ -420,6 +595,7 @@ for r in .spread-reuse.*.yaml; do spread -discard -reuse-pid="$(echo "$r" | grep -o -E '[0-9]+')"; done + - name: report spread errors if: always() run: | @@ -436,3 +612,23 @@ else echo "No spread log found, skipping errors reporting" fi + + - name: save spread results + if: always() + run: | + if [ -f spread.log ]; then + echo "Running spread log parser" + ./tests/lib/external/snapd-testing-tools/utils/log-parser spread.log --output spread-results.json + + echo "Determining which tests were executed" + RUN_TESTS="google-nested:${{ matrix.system }}:tests/nested/..." + if [ -n "$FAILED_TESTS" ]; then + RUN_TESTS="$FAILED_TESTS" + fi + + echo "Running spread log analyzer" + ./tests/lib/external/snapd-testing-tools/utils/log-analyzer list-reexecute-tasks "$RUN_TESTS" spread-results.json > "$FAILED_TESTS_FILE" + else + echo "No spread log found, saving empty list of failed tests" + touch "$FAILED_TESTS_FILE" + fi diff -Nru snapd-2.55.5+20.04/.gitignore snapd-2.57.5+20.04/.gitignore --- snapd-2.55.5+20.04/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/.gitignore 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,94 @@ +./share +tags +.coverage +snapdtool/version_generated.go +cmd/version_generated.go +cmd/VERSION +*~ +*.swp +.vscode/ +.idea/ +vendor/*/ +vendor/modules.txt +c-vendor/*/ +.spread-reuse*.yaml +po/snappy.pot + +# snap-confine bits +*.a +*.o +*~ +.*.swp +.*.swp +.dirstamp +# Locally built binaries +cmd/decode-mount-opts/decode-mount-opts +cmd/libsnap-confine-private/unit-tests +cmd/snap-confine/snap-confine +cmd/snap-confine/snap-confine-debug +cmd/snap-confine/snap-confine.apparmor +cmd/snap-confine/unit-tests +cmd/snap-device-helper/snap-device-helper +cmd/snap-device-helper/unit-tests +cmd/snap-discard-ns/snap-discard-ns +cmd/snap-gdb-shim/snap-gdb-shim +cmd/snap-gdb-shim/snap-gdbserver-shim +cmd/snap-mgmt/snap-mgmt +cmd/snap-seccomp/snap-seccomp +cmd/snap-update-ns/snap-update-ns +cmd/snap-update-ns/unit-tests +cmd/snapd-apparmor/snapd-apparmor +cmd/snapd-env-generator/snapd-env-generator +cmd/snapd-generator/snapd-generator +cmd/system-shutdown/system-shutdown +cmd/system-shutdown/unit-tests +cmd/snap/snap +cmd/snapd/snapd + +# manual pages +cmd/*/*.[1-9] + +# auto-generated systemd units +data/systemd/*.service + +# auto-generated dbus services +data/dbus/*.service + +data/info +data/env/snapd.sh + +# test-driver +*.log +*.trs + +# Automake for the cmd/ parts +cmd/Makefile +cmd/Makefile.in +snap-confine-*.tar.gz +.deps + +# Autoconf +cmd/aclocal.m4 +cmd/autom4te.cache +cmd/compile +cmd/config.guess +cmd/config.h +cmd/config.h.in +cmd/config.status +cmd/config.sub +cmd/configure +cmd/depcomp +cmd/install-sh +cmd/missing +cmd/stamp-h1 +cmd/test-driver + +# Mypy +.mypy +.mypy_cache + +# Snap files +*.snap + +# Image files +*.img diff -Nru snapd-2.55.5+20.04/.golangci.yml snapd-2.57.5+20.04/.golangci.yml --- snapd-2.55.5+20.04/.golangci.yml 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/.golangci.yml 2022-10-17 16:25:18.000000000 +0000 @@ -183,7 +183,8 @@ # formatting is disabled until we move to Go 1.13 # - gofmt - ineffassign - - gci + # disabling until https://github.com/daixiang0/gci/issues/54 is fixed + # - gci - testpackage # disable everything else disable-all: true diff -Nru snapd-2.55.5+20.04/go.mod snapd-2.57.5+20.04/go.mod --- snapd-2.55.5+20.04/go.mod 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/go.mod 2022-10-17 16:25:18.000000000 +0000 @@ -17,20 +17,21 @@ github.com/mvo5/goconfigparser v0.0.0-20200803085309-72e476556adb // if below two libseccomp-golang lines are updated, one must also update packaging/ubuntu-14.04/rules github.com/mvo5/libseccomp-golang v0.9.1-0.20180308152521-f4de83b52afb // old trusty builds only - github.com/seccomp/libseccomp-golang v0.9.2-0.20210917151616-9da99da69b1b + github.com/seccomp/libseccomp-golang v0.9.2-0.20220502024300-f57e1d55ea18 github.com/snapcore/bolt v1.3.2-0.20210908134111-63c8bfcf7af8 github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 - github.com/snapcore/secboot v0.0.0-20211018143212-802bb19ca263 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365 - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 + github.com/snapcore/secboot v0.0.0-20220905094328-6a625ee231d3 + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 + golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect + golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 + golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/macaroon.v1 v1.0.0-20150121114231-ab3940c6c165 gopkg.in/mgo.v2 v2.0.0-20180704144907-a7e2c1d573e1 gopkg.in/retry.v1 v1.0.3 gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 gopkg.in/tylerb/graceful.v1 v1.2.15 - gopkg.in/yaml.v2 v2.3.0 + gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066 // indirect ) diff -Nru snapd-2.55.5+20.04/go.sum snapd-2.57.5+20.04/go.sum --- snapd-2.55.5+20.04/go.sum 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/go.sum 2022-10-17 16:25:18.000000000 +0000 @@ -40,32 +40,55 @@ github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= github.com/seccomp/libseccomp-golang v0.9.2-0.20210917151616-9da99da69b1b h1:x1CNwq9AHu5YhO+igGGZ4ov+eKU0U3kaiZCymcVUdSk= github.com/seccomp/libseccomp-golang v0.9.2-0.20210917151616-9da99da69b1b/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502024300-f57e1d55ea18 h1:A15Ffi2aT/BtygokOpAI0Diwrw8PTHuDwaAN5C48s74= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502024300-f57e1d55ea18/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/snapcore/bolt v1.3.2-0.20210908134111-63c8bfcf7af8 h1:WmyDfH38e3MaMWrMCO5YpW96BANq5Ti2iwbliM/xTW0= github.com/snapcore/bolt v1.3.2-0.20210908134111-63c8bfcf7af8/go.mod h1:Z6z3sf12AMDjT/4tbT/PmzzdACAxkWGhkuKWiVpTWLM= github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 h1:PaunR+BhraKSLxt2awQ42zofkP+NKh/VjQ0PjIMk/y4= github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785/go.mod h1:D3SsWAXK7wCCBZu+Vk5hc1EuKj/L3XN1puEMXTU4LrQ= github.com/snapcore/secboot v0.0.0-20211018143212-802bb19ca263 h1:cq2rG4JcNBCwHvo7iNdJL4nb8Ns7L/aOUd1EFs2toFs= github.com/snapcore/secboot v0.0.0-20211018143212-802bb19ca263/go.mod h1:72paVOkm4sJugXt+v9ItmnjXgO921D8xqsbH2OekouY= +github.com/snapcore/secboot v0.0.0-20220905094328-6a625ee231d3 h1:58BNTUsb16y89bcfYRU23V9ykMhOHHGy9zrVKgKFiqU= +github.com/snapcore/secboot v0.0.0-20220905094328-6a625ee231d3/go.mod h1:72paVOkm4sJugXt+v9ItmnjXgO921D8xqsbH2OekouY= github.com/snapcore/snapd v0.0.0-20201005140838-501d14ac146e/go.mod h1:3xrn7QDDKymcE5VO2rgWEQ5ZAUGb9htfwlXnoel6Io8= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw= golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365 h1:6wSTsvPddg9gc/mVEEyk9oOAoxn+bT4Z9q1zx+4RwA4= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -81,6 +104,8 @@ gopkg.in/tylerb/graceful.v1 v1.2.15/go.mod h1:yBhekWvR20ACXVObSSdD3u6S9DeSylanL2PAbAC/uJ8= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maze.io/x/crypto v0.0.0-20190131090603-9b94c9afe066 h1:UrD21H1Ue5Nl8f2x/NQJBRdc49YGmla3mRStinH8CCE= diff -Nru snapd-2.55.5+20.04/HACKING.md snapd-2.57.5+20.04/HACKING.md --- snapd-2.55.5+20.04/HACKING.md 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/HACKING.md 2022-10-17 16:25:18.000000000 +0000 @@ -103,17 +103,13 @@ #### Building for other architectures with snapcraft It is also sometimes useful to use snapcraft to build the snapd snap for -other architectures using the `remote-build` feature, however there is currently a -bug in snapcraft around using the `4.x` track and using `remote-build`, where the -Launchpad job created for the remote-build will attempt to use the `latest` track instead -of the `4.x` channel. This was recently fixed in snapcraft in https://github.com/snapcore/snapcraft/pull/3600 -which for now requires using the `latest/edge` channel of snapcraft instead of the -`4.x` track which is needed to build the snap locally. - -In order to build remotely with snapcraft, do the following: +other architectures using the `remote-build` feature. In order to build +remotely with snapcraft, make sure you have at least version `6.x` installed: +if the command `snap info snapcraft` shows that you are running an older +version, upgrade with: ``` -snap refresh snapcraft --channel=latest/edge +snap refresh snapcraft --channel=latest/stable ``` Now you can use remote-build with snapcraft on the snapd tree for any desired diff -Nru snapd-2.55.5+20.04/image/export_test.go snapd-2.57.5+20.04/image/export_test.go --- snapd-2.55.5+20.04/image/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,14 +22,10 @@ import ( "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/gadget" - "github.com/snapcore/snapd/overlord/auth" - "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/store/tooling" + "github.com/snapcore/snapd/testutil" ) -func MockToolingStore(sto Store) *ToolingStore { - return &ToolingStore{sto: sto} -} - var ( DecodeModelAssertion = decodeModelAssertion MakeLabel = makeLabel @@ -37,22 +33,7 @@ InstallCloudConfig = installCloudConfig ) -func (tsto *ToolingStore) User() *auth.UserState { - return tsto.user -} - -func ToolingStoreContext() store.DeviceAndAuthContext { - return toolingStoreContext{} -} - -func (opts *DownloadSnapOptions) Validate() error { - return opts.validate() -} - var ( - ErrRevisionAndCohort = errRevisionAndCohort - ErrPathInBase = errPathInBase - WriteResolvedContent = writeResolvedContent ) @@ -64,10 +45,22 @@ } } -func MockNewToolingStoreFromModel(f func(model *asserts.Model, fallbackArchitecture string) (*ToolingStore, error)) (restore func()) { +func MockNewToolingStoreFromModel(f func(model *asserts.Model, fallbackArchitecture string) (*tooling.ToolingStore, error)) (restore func()) { old := newToolingStoreFromModel newToolingStoreFromModel = f return func() { newToolingStoreFromModel = old } } + +func MockPreseedCore20(f func(dir string, key, aaDir string) error) (restore func()) { + r := testutil.Backup(&preseedCore20) + preseedCore20 = f + return r +} + +func MockSetupSeed(f func(tsto *tooling.ToolingStore, model *asserts.Model, opts *Options) error) (restore func()) { + r := testutil.Backup(&setupSeed) + setupSeed = f + return r +} diff -Nru snapd-2.55.5+20.04/image/helpers.go snapd-2.57.5+20.04/image/helpers.go --- snapd-2.55.5+20.04/image/helpers.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/helpers.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2021 Canonical Ltd + * Copyright (C) 2014-2022 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,464 +22,39 @@ // TODO: put these in appropriate package(s) once they are clarified a bit more import ( - "bytes" - "context" - "crypto" - "encoding/json" - "errors" "fmt" - "io/ioutil" - "net/url" "os" - "os/signal" "path/filepath" - "strings" - "syscall" - - "github.com/mvo5/goconfigparser" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/gadget" - "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/osutil" - "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/snap/naming" - "github.com/snapcore/snapd/snapdenv" - "github.com/snapcore/snapd/store" - "github.com/snapcore/snapd/strutil" ) -// A Store can find metadata on snaps, download snaps and fetch assertions. -type Store interface { - SnapAction(context.Context, []*store.CurrentSnap, []*store.SnapAction, store.AssertionQuery, *auth.UserState, *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) - Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error - - 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, toolingStoreContext{}) - 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) { - data, err := ioutil.ReadFile(authFn) - if err != nil { - return nil, fmt.Errorf("cannot read auth file %q: %v", authFn, err) - } - - creds, err := parseAuthFile(authFn, data) - if err != nil { - // try snapcraft login format instead - var err2 error - creds, err2 = parseSnapcraftLoginFile(authFn, data) - if err2 != nil { - trimmed := bytes.TrimSpace(data) - if len(trimmed) > 0 && trimmed[0] == '[' { - return nil, err2 - } - return nil, err - } - } - - return &auth.UserState{ - StoreMacaroon: creds.Macaroon, - StoreDischarges: creds.Discharges, - }, nil -} - -func parseAuthFile(authFn string, data []byte) (*authData, error) { - var creds authData - err := json.Unmarshal(data, &creds) - if 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 &creds, nil -} - -func snapcraftLoginSection() string { - if snapdenv.UseStagingStore() { - return "login.staging.ubuntu.com" - } - return "login.ubuntu.com" -} - -func parseSnapcraftLoginFile(authFn string, data []byte) (*authData, error) { - errPrefix := fmt.Sprintf("invalid snapcraft login file %q", authFn) - - cfg := goconfigparser.New() - if err := cfg.ReadString(string(data)); err != nil { - return nil, fmt.Errorf("%s: %v", errPrefix, err) - } - sec := snapcraftLoginSection() - macaroon, err := cfg.Get(sec, "macaroon") - if err != nil { - return nil, fmt.Errorf("%s: %s", errPrefix, err) - } - unboundDischarge, err := cfg.Get(sec, "unbound_discharge") - if err != nil { - return nil, fmt.Errorf("%s: %v", errPrefix, err) - } - if macaroon == "" || unboundDischarge == "" { - return nil, fmt.Errorf("invalid snapcraft login file %q: empty fields", authFn) - } - return &authData{ - Macaroon: macaroon, - Discharges: []string{unboundDischarge}, - }, nil -} - -// toolingStoreContext implements trivially store.DeviceAndAuthContext -// except implementing UpdateUserAuth properly to be used to refresh a -// soft-expired user macaroon. -type toolingStoreContext struct{} - -func (tac toolingStoreContext) CloudInfo() (*auth.CloudInfo, error) { - return nil, nil -} - -func (tac toolingStoreContext) Device() (*auth.DeviceState, error) { - return &auth.DeviceState{}, nil -} - -func (tac toolingStoreContext) DeviceSessionRequestParams(_ string) (*store.DeviceSessionRequestParams, error) { - return nil, store.ErrNoSerial -} - -func (tac toolingStoreContext) ProxyStoreParams(defaultURL *url.URL) (proxyStoreID string, proxySroreURL *url.URL, err error) { - return "", defaultURL, nil -} - -func (tac toolingStoreContext) StoreID(fallback string) (string, error) { - return fallback, nil -} - -func (tac toolingStoreContext) UpdateDeviceAuth(_ *auth.DeviceState, newSessionMacaroon string) (*auth.DeviceState, error) { - return nil, fmt.Errorf("internal error: no device state in tools") -} - -func (tac toolingStoreContext) UpdateUserAuth(user *auth.UserState, discharges []string) (*auth.UserState, error) { - user.StoreDischarges = discharges - return user, nil -} - -func NewToolingStoreFromModel(model *asserts.Model, fallbackArchitecture string) (*ToolingStore, error) { - architecture := model.Architecture() - // can happen on classic - if architecture == "" { - architecture = fallbackArchitecture - } - return newToolingStore(architecture, model.Store()) -} - -func NewToolingStore() (*ToolingStore, error) { - arch := os.Getenv("UBUNTU_STORE_ARCH") - storeID := os.Getenv("UBUNTU_STORE_ID") - return newToolingStore(arch, storeID) -} - -// DownloadSnapOptions carries options for downloading snaps plus assertions. -type DownloadSnapOptions struct { - TargetDir string - - Revision snap.Revision - Channel string - CohortKey string - Basename string - - LeavePartialOnError bool -} - -var ( - errRevisionAndCohort = errors.New("cannot specify both revision and cohort") - errPathInBase = errors.New("cannot specify a path in basename (use target dir for that)") -) - -func (opts *DownloadSnapOptions) validate() error { - if strings.ContainsRune(opts.Basename, filepath.Separator) { - return errPathInBase - } - if !(opts.Revision.Unset() || opts.CohortKey == "") { - return errRevisionAndCohort - } - return nil -} - -func (opts *DownloadSnapOptions) String() string { - spec := make([]string, 0, 5) - if !opts.Revision.Unset() { - spec = append(spec, fmt.Sprintf("(%s)", opts.Revision)) - } - if opts.Channel != "" { - spec = append(spec, fmt.Sprintf("from channel %q", opts.Channel)) - } - if opts.CohortKey != "" { - // cohort keys are really long, and the rightmost bit being the - // interesting bit, so ellipt the rest - spec = append(spec, fmt.Sprintf(`from cohort %q`, strutil.ElliptLeft(opts.CohortKey, 10))) - } - if opts.Basename != "" { - spec = append(spec, fmt.Sprintf("to %q", opts.Basename+".snap")) - } - if opts.TargetDir != "" { - spec = append(spec, fmt.Sprintf("in %q", opts.TargetDir)) - } - return strings.Join(spec, " ") -} - -type DownloadedSnap struct { - Path string - Info *snap.Info - RedirectChannel 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 and a snap.Info for it and optionally a -// channel the snap got redirected to. -func (tsto *ToolingStore) DownloadSnap(name string, opts DownloadSnapOptions) (downloadedSnap *DownloadedSnap, err error) { - if err := opts.validate(); err != nil { - return nil, err - } - sto := tsto.sto - - if opts.TargetDir == "" { - pwd, err := os.Getwd() - if err != nil { - return nil, err - } - opts.TargetDir = pwd - } - - if !opts.Revision.Unset() { - // XXX: is this really necessary (and, if it is, shoudn't we error out instead) - opts.Channel = "" - } - - logger.Debugf("Going to download snap %q %s.", name, &opts) - - actions := []*store.SnapAction{{ - Action: "download", - InstanceName: name, - Revision: opts.Revision, - CohortKey: opts.CohortKey, - Channel: opts.Channel, - }} - - sars, _, err := sto.SnapAction(context.TODO(), nil, actions, nil, tsto.user, nil) - if err != nil { - // err will be 'cannot download snap "foo": ' - return nil, err - } - sar := &sars[0] - - baseName := opts.Basename - if baseName == "" { - baseName = sar.Info.Filename() - } else { - baseName += ".snap" - } - targetFn := filepath.Join(opts.TargetDir, baseName) - - return tsto.snapDownload(targetFn, sar, opts) -} - -func (tsto *ToolingStore) snapDownload(targetFn string, sar *store.SnapActionResult, opts DownloadSnapOptions) (downloadedSnap *DownloadedSnap, err error) { - snap := sar.Info - redirectChannel := sar.RedirectChannel - - // check if we already have the right file - if osutil.FileExists(targetFn) { - sha3_384Dgst, size, err := osutil.FileDigest(targetFn, crypto.SHA3_384) - if err == nil && size == uint64(snap.DownloadInfo.Size) && fmt.Sprintf("%x", sha3_384Dgst) == snap.DownloadInfo.Sha3_384 { - logger.Debugf("not downloading, using existing file %s", targetFn) - return &DownloadedSnap{ - Path: targetFn, - Info: snap, - RedirectChannel: redirectChannel, - }, nil - } - logger.Debugf("File exists but has wrong hash, ignoring (here).") - } - - pb := progress.MakeProgressBar() - defer pb.Finished() - - // Intercept sigint - c := make(chan os.Signal, 3) - signal.Notify(c, syscall.SIGINT) - go func() { - <-c - pb.Finished() - os.Exit(1) - }() - - dlOpts := &store.DownloadOptions{LeavePartialOnError: opts.LeavePartialOnError} - if err = tsto.sto.Download(context.TODO(), snap.SnapName(), targetFn, &snap.DownloadInfo, pb, tsto.user, dlOpts); err != nil { - return nil, err - } - - signal.Reset(syscall.SIGINT) - - return &DownloadedSnap{ - Path: targetFn, - Info: snap, - RedirectChannel: redirectChannel, - }, nil -} - -type SnapToDownload struct { - Snap naming.SnapRef - Channel string - CohortKey string -} - -type CurrentSnap struct { - SnapName string - SnapID string - Revision snap.Revision - Channel string - Epoch snap.Epoch -} - -type DownloadManyOptions struct { - BeforeDownloadFunc func(*snap.Info) (targetPath string, err error) - EnforceValidation bool -} - -// DownloadMany downloads the specified snaps. -// curSnaps are meant to represent already downloaded snaps that will -// be installed in conjunction with the snaps to download, this is needed -// if enforcing validations (ops.EnforceValidation set to true) to -// have cross-gating work. -func (tsto *ToolingStore) DownloadMany(toDownload []SnapToDownload, curSnaps []*CurrentSnap, opts DownloadManyOptions) (downloadedSnaps map[string]*DownloadedSnap, err error) { - if len(toDownload) == 0 { - // nothing to do - return nil, nil - } - if opts.BeforeDownloadFunc == nil { - return nil, fmt.Errorf("internal error: DownloadManyOptions.BeforeDownloadFunc must be set") - } - - actionFlag := store.SnapActionIgnoreValidation - if opts.EnforceValidation { - actionFlag = store.SnapActionEnforceValidation - } - - downloadedSnaps = make(map[string]*DownloadedSnap, len(toDownload)) - current := make([]*store.CurrentSnap, 0, len(curSnaps)) - for _, csnap := range curSnaps { - ch := "stable" - if csnap.Channel != "" { - ch = csnap.Channel - } - current = append(current, &store.CurrentSnap{ - InstanceName: csnap.SnapName, - SnapID: csnap.SnapID, - Revision: csnap.Revision, - TrackingChannel: ch, - Epoch: csnap.Epoch, - IgnoreValidation: !opts.EnforceValidation, - }) - } - - actions := make([]*store.SnapAction, 0, len(toDownload)) - for _, sn := range toDownload { - actions = append(actions, &store.SnapAction{ - Action: "download", - InstanceName: sn.Snap.SnapName(), // XXX consider using snap-id first - Channel: sn.Channel, - CohortKey: sn.CohortKey, - Flags: actionFlag, - }) - } - - sars, _, err := tsto.sto.SnapAction(context.TODO(), current, actions, nil, tsto.user, nil) - if err != nil { - // err will be 'cannot download snap "foo": ' - return nil, err - } - - for _, sar := range sars { - targetPath, err := opts.BeforeDownloadFunc(sar.Info) - if err != nil { - return nil, err - } - dlSnap, err := tsto.snapDownload(targetPath, &sar, DownloadSnapOptions{}) - if err != nil { - return nil, err - } - downloadedSnaps[sar.SnapName()] = dlSnap - } - - return downloadedSnaps, nil -} - -// 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 tsto.sto.Assertion(ref.Type, ref.PrimaryKey, tsto.user) - } - save2 := func(a asserts.Assertion) error { - // for checking - err := db.Add(a) - if err != nil { - if _, ok := err.(*asserts.RevisionError); ok { - return nil - } - return fmt.Errorf("cannot add assertion %v: %v", a.Ref(), err) - } - return save(a) - } - return asserts.NewFetcher(db, retrieve, save2) -} - -// 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) (*asserts.SnapDeclaration, error) { +// FetchAndCheckSnapAssertions fetches and cross checks the snap assertions +// matching the given snap file using the provided asserts.Fetcher and +// assertion database. +// The optional model assertion must be passed for full cross checks. +func FetchAndCheckSnapAssertions(snapPath string, info *snap.Info, model *asserts.Model, f asserts.Fetcher, db asserts.RODatabase) (*asserts.SnapDeclaration, error) { sha3_384, size, err := asserts.SnapFileSHA3_384(snapPath) if err != nil { return nil, err } + expectedProv := info.Provenance() // this assumes series "16" - if err := snapasserts.FetchSnapAssertions(f, sha3_384); err != nil { + if err := snapasserts.FetchSnapAssertions(f, sha3_384, expectedProv); err != nil { return nil, fmt.Errorf("cannot fetch snap signatures/assertions: %v", err) } // cross checks - if err := snapasserts.CrossCheck(info.InstanceName(), sha3_384, size, &info.SideInfo, db); err != nil { + signedProv, err := snapasserts.CrossCheck(info.InstanceName(), sha3_384, expectedProv, size, &info.SideInfo, model, db) + if err != nil { + return nil, err + } + if err := snapasserts.CheckProvenance(snapPath, signedProv); err != nil { return nil, err } @@ -493,15 +68,6 @@ return a.(*asserts.SnapDeclaration), nil } -// Find provides the snapsserts.Finder interface for snapasserts.DerviceSideInfo -func (tsto *ToolingStore) Find(at *asserts.AssertionType, headers map[string]string) (asserts.Assertion, error) { - pk, err := asserts.PrimaryKeyFromHeaders(at, headers) - if err != nil { - return nil, err - } - return tsto.sto.Assertion(at, pk, tsto.user) -} - // var so that it can be mocked for tests var writeResolvedContent = writeResolvedContentImpl diff -Nru snapd-2.55.5+20.04/image/helpers_test.go snapd-2.57.5+20.04/image/helpers_test.go --- snapd-2.55.5+20.04/image/helpers_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/helpers_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -23,7 +23,6 @@ "os" "os/exec" "path/filepath" - "runtime" "sort" "strings" @@ -31,111 +30,9 @@ "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/image" - "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" ) -func (s *imageSuite) TestDownloadpOptionsString(c *check.C) { - tests := []struct { - opts image.DownloadSnapOptions - str string - }{ - {image.DownloadSnapOptions{LeavePartialOnError: true}, ""}, - {image.DownloadSnapOptions{}, ""}, - {image.DownloadSnapOptions{TargetDir: "/foo"}, `in "/foo"`}, - {image.DownloadSnapOptions{Basename: "foo"}, `to "foo.snap"`}, - {image.DownloadSnapOptions{Channel: "foo"}, `from channel "foo"`}, - {image.DownloadSnapOptions{Revision: snap.R(42)}, `(42)`}, - {image.DownloadSnapOptions{ - CohortKey: "AbCdEfGhIjKlMnOpQrStUvWxYz", - }, `from cohort "…rStUvWxYz"`}, - {image.DownloadSnapOptions{ - TargetDir: "/foo", - Basename: "bar", - Channel: "baz", - Revision: snap.R(13), - CohortKey: "MSBIc3dwOW9PemozYjRtdzhnY0MwMFh0eFduS0g5UWlDUSAxNTU1NDExNDE1IDBjYzJhNTc1ZjNjOTQ3ZDEwMWE1NTNjZWFkNmFmZDE3ZWJhYTYyNjM4ZWQ3ZGMzNjI5YmU4YjQ3NzAwMjdlMDk=", - }, `(13) from channel "baz" from cohort "…wMjdlMDk=" to "bar.snap" in "/foo"`}, // note this one is not 'valid' so it's ok if the string is a bit wonky - - } - - for _, t := range tests { - c.Check(t.opts.String(), check.Equals, t.str) - } -} - -func (s *imageSuite) TestDownloadSnapOptionsValid(c *check.C) { - tests := []struct { - opts image.DownloadSnapOptions - err error - }{ - {image.DownloadSnapOptions{}, nil}, // might want to error if no targetdir - {image.DownloadSnapOptions{TargetDir: "foo"}, nil}, - {image.DownloadSnapOptions{Channel: "foo"}, nil}, - {image.DownloadSnapOptions{Revision: snap.R(42)}, nil}, - {image.DownloadSnapOptions{ - CohortKey: "AbCdEfGhIjKlMnOpQrStUvWxYz", - }, nil}, - {image.DownloadSnapOptions{ - Channel: "foo", - Revision: snap.R(42), - }, nil}, - {image.DownloadSnapOptions{ - Channel: "foo", - CohortKey: "bar", - }, nil}, - {image.DownloadSnapOptions{ - Revision: snap.R(1), - CohortKey: "bar", - }, image.ErrRevisionAndCohort}, - {image.DownloadSnapOptions{ - Basename: "/foo", - }, image.ErrPathInBase}, - } - - for _, t := range tests { - t.opts.LeavePartialOnError = true - c.Check(t.opts.Validate(), check.Equals, t.err) - t.opts.LeavePartialOnError = false - c.Check(t.opts.Validate(), check.Equals, t.err) - } -} - -func (s *imageSuite) TestDownloadSnap(c *check.C) { - // TODO: maybe expand on this (test coverage of DownloadSnap is really bad) - - // env shenanigans - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - debug, hadDebug := os.LookupEnv("SNAPD_DEBUG") - os.Setenv("SNAPD_DEBUG", "1") - if hadDebug { - defer os.Setenv("SNAPD_DEBUG", debug) - } else { - defer os.Unsetenv("SNAPD_DEBUG") - } - logbuf, restore := logger.MockLogger() - defer restore() - - s.setupSnaps(c, map[string]string{ - "core": "canonical", - }, "") - - dlDir := c.MkDir() - opts := image.DownloadSnapOptions{ - TargetDir: dlDir, - } - dlSnap, err := s.tsto.DownloadSnap("core", opts) - c.Assert(err, check.IsNil) - c.Check(dlSnap.Path, check.Matches, filepath.Join(dlDir, `core_\d+.snap`)) - c.Check(dlSnap.Info.SnapName(), check.Equals, "core") - c.Check(dlSnap.RedirectChannel, check.Equals, "") - - c.Check(logbuf.String(), check.Matches, `.* DEBUG: Going to download snap "core" `+opts.String()+".\n") -} - var validGadgetYaml = ` volumes: vol1: diff -Nru snapd-2.55.5+20.04/image/image_linux.go snapd-2.57.5+20.04/image/image_linux.go --- snapd-2.55.5+20.04/image/image_linux.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/image_linux.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2021 Canonical Ltd + * Copyright (C) 2014-2022 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,9 +34,11 @@ "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" - "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/store/tooling" // to set sysconfig.ApplyFilesystemOnlyDefaults hook + "github.com/snapcore/snapd/image/preseed" + "github.com/snapcore/snapd/osutil" _ "github.com/snapcore/snapd/overlord/configstate/configcore" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/seed/seedwriter" @@ -50,6 +52,8 @@ var ( Stdout io.Writer = os.Stdout Stderr io.Writer = os.Stderr + + preseedCore20 = preseed.Core20 ) func (custo *Customizations) validate(model *asserts.Model) error { @@ -69,7 +73,7 @@ kind := "UC16/18" switch { case core20: - kind = "UC20" + kind = "UC20+" // TODO:UC20: consider supporting these with grade dangerous? unsupportedConsoleConfDisable() if custo.CloudInitUserData != "" { @@ -94,7 +98,7 @@ return model.Gadget() != "" || len(model.RequiredNoEssentialSnaps()) != 0 || len(opts.Snaps) != 0 } -var newToolingStoreFromModel = NewToolingStoreFromModel +var newToolingStoreFromModel = tooling.NewToolingStoreFromModel func Prepare(opts *Options) error { var model *asserts.Model @@ -134,6 +138,7 @@ if err != nil { return err } + tsto.Stdout = Stdout // FIXME: limitation until we can pass series parametrized much more if model.Series() != release.Series { @@ -144,7 +149,22 @@ return err } - return setupSeed(tsto, model, opts) + if err := setupSeed(tsto, model, opts); err != nil { + return err + } + + if opts.Preseed { + // TODO: support UC22 + if model.Classic() { + return fmt.Errorf("cannot preseed the image for a classic model") + } + if model.Base() != "core20" { + return fmt.Errorf("cannot preseed the image for a model other than core20") + } + return preseedCore20(opts.PrepareDir, opts.PreseedSignKey, opts.AppArmorKernelFeaturesDir) + } + + return nil } // these are postponed, not implemented or abandoned, not finalized, @@ -245,7 +265,7 @@ return now.UTC().Format("20060102") } -func setupSeed(tsto *ToolingStore, model *asserts.Model, opts *Options) error { +var setupSeed = func(tsto *tooling.ToolingStore, model *asserts.Model, opts *Options) error { if model.Classic() != opts.Classic { return fmt.Errorf("internal error: classic model but classic mode not set") } @@ -358,9 +378,9 @@ return err } - var curSnaps []*CurrentSnap + var curSnaps []*tooling.CurrentSnap for _, sn := range localSnaps { - si, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, f, db) + si, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, model, f, db) if err != nil && !asserts.IsNotFound(err) { return err } @@ -380,7 +400,7 @@ sn.ARefs = aRefs if info.ID() != "" { - curSnaps = append(curSnaps, &CurrentSnap{ + curSnaps = append(curSnaps, &tooling.CurrentSnap{ SnapName: info.SnapName(), SnapID: info.ID(), Revision: info.Revision, @@ -411,14 +431,14 @@ } return sn.Path, nil } - snapToDownloadOptions := make([]SnapToDownload, len(toDownload)) + snapToDownloadOptions := make([]tooling.SnapToDownload, len(toDownload)) for i, sn := range toDownload { byName[sn.SnapName()] = sn snapToDownloadOptions[i].Snap = sn snapToDownloadOptions[i].Channel = sn.Channel snapToDownloadOptions[i].CohortKey = opts.WideCohortKey } - downloadedSnaps, err := tsto.DownloadMany(snapToDownloadOptions, curSnaps, DownloadManyOptions{ + downloadedSnaps, err := tsto.DownloadMany(snapToDownloadOptions, curSnaps, tooling.DownloadManyOptions{ BeforeDownloadFunc: beforeDownload, EnforceValidation: opts.Customizations.Validation == "enforce", }) @@ -435,13 +455,13 @@ // fetch snap assertions prev := len(f.Refs()) - if _, err = FetchAndCheckSnapAssertions(dlsn.Path, dlsn.Info, f, db); err != nil { + if _, err = FetchAndCheckSnapAssertions(dlsn.Path, dlsn.Info, model, f, db); err != nil { return err } aRefs := f.Refs()[prev:] sn.ARefs = aRefs - curSnaps = append(curSnaps, &CurrentSnap{ + curSnaps = append(curSnaps, &tooling.CurrentSnap{ SnapName: sn.Info.SnapName(), SnapID: sn.Info.ID(), Revision: sn.Info.Revision, @@ -518,15 +538,14 @@ bootWith.RecoverySystemLabel = label } - // find the gadget file - // find the snap.Info/path for kernel/os/base so + // find the snap.Info/path for kernel/os/base/gadget so // that boot.MakeBootable can DTRT - gadgetFname := "" kernelFname := "" for _, sn := range bootSnaps { switch sn.Info.Type() { case snap.TypeGadget: - gadgetFname = sn.Path + bootWith.Gadget = sn.Info + bootWith.GadgetPath = sn.Path case snap.TypeOS, snap.TypeBase: bootWith.Base = sn.Info bootWith.BasePath = sn.Path @@ -538,7 +557,7 @@ } // unpacking the gadget for core models - if err := unpackSnap(gadgetFname, gadgetUnpackDir); err != nil { + if err := unpackSnap(bootWith.GadgetPath, gadgetUnpackDir); err != nil { return err } if err := unpackSnap(kernelFname, kernelUnpackDir); err != nil { diff -Nru snapd-2.55.5+20.04/image/image_test.go snapd-2.57.5+20.04/image/image_test.go --- snapd-2.55.5+20.04/image/image_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/image_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2021 Canonical Ltd + * Copyright (C) 2014-2022 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,7 +24,6 @@ "context" "fmt" "io/ioutil" - "net/url" "os" "path/filepath" "strings" @@ -51,6 +50,7 @@ "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/store/tooling" "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/timings" ) @@ -69,7 +69,7 @@ storeActions []*store.SnapAction curSnaps [][]*store.CurrentSnap - tsto *image.ToolingStore + tsto *tooling.ToolingStore // SeedSnaps helps creating and making available seed snaps // (it provides MakeAssertedSnap etc.) for the tests. @@ -96,7 +96,7 @@ image.Stdout = s.stdout s.stderr = &bytes.Buffer{} image.Stderr = s.stderr - s.tsto = image.MockToolingStore(s) + s.tsto = tooling.MockToolingStore(s) s.SeedSnaps = &seedtest.SeedSnaps{} s.SetupAssertSigning("canonical") @@ -596,7 +596,7 @@ label = filepath.Base(systems[0]) } - seed, err := seed.Open(seeddir, label) + sd, err := seed.Open(seeddir, label) c.Assert(err, IsNil) db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ @@ -609,14 +609,14 @@ return b.CommitTo(db, nil) } - err = seed.LoadAssertions(db, commitTo) + err = sd.LoadAssertions(db, commitTo) c.Assert(err, IsNil) - err = seed.LoadMeta(timings.New(nil)) + err = sd.LoadMeta(seed.AllModes, nil, timings.New(nil)) c.Assert(err, IsNil) - essSnaps = seed.EssentialSnaps() - runSnaps, err = seed.ModeSnaps("run") + essSnaps = sd.EssentialSnaps() + runSnaps, err = sd.ModeSnaps("run") c.Assert(err, IsNil) return essSnaps, runSnaps, db @@ -678,7 +678,7 @@ Channel: stableChannel, }) - // sanity + // precondition if name == "core" { c.Check(essSnaps[i].SideInfo.SnapID, Equals, s.AssertedSnapID("core")) } @@ -1226,7 +1226,7 @@ CloudInitUserData: "cloud-init-user-data", }, }) - c.Assert(err, ErrorMatches, `cannot support with UC20 model requested customizations: console-conf disable, cloud-init user-data`) + c.Assert(err, ErrorMatches, `cannot support with UC20\+ model requested customizations: console-conf disable, cloud-init user-data`) } func (s *imageSuite) TestPrepareClassicCustomizationsUnsupported(c *C) { @@ -1546,45 +1546,6 @@ c.Check(filepath.Join(targetDir, "etc/cloud/cloud.cfg"), testutil.FileEquals, 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"}) -} - -func (s *imageSuite) TestNewToolingStoreWithAuthFromSnapcraftLoginFile(c *C) { - tmpdir := c.MkDir() - authFn := filepath.Join(tmpdir, "auth.json") - err := ioutil.WriteFile(authFn, []byte(`[login.ubuntu.com] -macaroon = MACAROON -unbound_discharge = 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"}) -} - func (s *imageSuite) TestSetupSeedLocalSnapsWithStoreAsserts(c *C) { restore := image.MockTrusted(s.StoreSigning.Trusted) defer restore() @@ -2028,7 +1989,7 @@ defer restore() restore = sysdb.MockGenericClassicModel(s.StoreSigning.GenericClassicModel) defer restore() - restore = image.MockNewToolingStoreFromModel(func(model *asserts.Model, fallbackArchitecture string) (*image.ToolingStore, error) { + restore = image.MockNewToolingStoreFromModel(func(model *asserts.Model, fallbackArchitecture string) (*tooling.ToolingStore, error) { return s.tsto, nil }) defer restore() @@ -3201,7 +3162,7 @@ err := image.SetupSeed(s.tsto, model, opts) c.Assert(err, IsNil) - // sanity checks + // validity checks seeddir := filepath.Join(prepareDir, "system-seed") seedsnapsdir := filepath.Join(seeddir, "snaps") essSnaps, runSnaps, _ := s.loadSeed(c, seeddir) @@ -3302,56 +3263,85 @@ c.Assert(err, ErrorMatches, `no asset from the kernel.yaml needing synced update is consumed by the gadget at "/.*"`) } -type toolingStoreContextSuite struct { - sc store.DeviceAndAuthContext -} +func (s *imageSuite) TestPrepareWithUC20Preseed(c *C) { + restoreSetupSeed := image.MockSetupSeed(func(tsto *tooling.ToolingStore, model *asserts.Model, opts *image.Options) error { + return nil + }) + defer restoreSetupSeed() -var _ = Suite(&toolingStoreContextSuite{}) + var preseedCalled bool + restorePreseedCore20 := image.MockPreseedCore20(func(dir, key, aaDir string) error { + preseedCalled = true + c.Assert(dir, Equals, "/a/dir") + c.Assert(key, Equals, "foo") + c.Assert(aaDir, Equals, "/custom/aa/features") + return nil + }) + defer restorePreseedCore20() -func (s *toolingStoreContextSuite) SetUpTest(c *C) { - s.sc = image.ToolingStoreContext() -} + model := s.makeUC20Model(nil) + fn := filepath.Join(c.MkDir(), "model.assertion") + c.Assert(ioutil.WriteFile(fn, asserts.Encode(model), 0644), IsNil) -func (s *toolingStoreContextSuite) TestNopBits(c *C) { - info, err := s.sc.CloudInfo() - c.Assert(err, IsNil) - c.Check(info, IsNil) + err := image.Prepare(&image.Options{ + ModelFile: fn, + Preseed: true, + PrepareDir: "/a/dir", + PreseedSignKey: "foo", - device, err := s.sc.Device() + AppArmorKernelFeaturesDir: "/custom/aa/features", + }) c.Assert(err, IsNil) - c.Check(device, DeepEquals, &auth.DeviceState{}) + c.Check(preseedCalled, Equals, true) +} - p, err := s.sc.DeviceSessionRequestParams("") - c.Assert(err, Equals, store.ErrNoSerial) - c.Check(p, IsNil) +func (s *imageSuite) TestPrepareWithClassicPreseedError(c *C) { + restoreSetupSeed := image.MockSetupSeed(func(tsto *tooling.ToolingStore, model *asserts.Model, opts *image.Options) error { + return nil + }) + defer restoreSetupSeed() - defURL, err := url.Parse("http://store") - c.Assert(err, IsNil) - proxyStoreID, proxyStoreURL, err := s.sc.ProxyStoreParams(defURL) - c.Assert(err, IsNil) - c.Check(proxyStoreID, Equals, "") - c.Check(proxyStoreURL, Equals, defURL) + err := image.Prepare(&image.Options{ + Preseed: true, + Classic: true, + PrepareDir: "/a/dir", + }) + c.Assert(err, ErrorMatches, `cannot preseed the image for a classic model`) +} - storeID, err := s.sc.StoreID("") - c.Assert(err, IsNil) - c.Check(storeID, Equals, "") +func (s *imageSuite) TestSetupSeedCore20DelegatedSnap(c *C) { + bootloader.Force(nil) + restore := image.MockTrusted(s.StoreSigning.Trusted) + defer restore() - storeID, err = s.sc.StoreID("my-store") - c.Assert(err, IsNil) - c.Check(storeID, Equals, "my-store") + // a model that uses core20 + model := s.makeUC20Model(nil) - _, err = s.sc.UpdateDeviceAuth(nil, "") - c.Assert(err, NotNil) -} + prepareDir := c.MkDir() + + s.makeSnap(c, "snapd", nil, snap.R(1), "") + s.makeSnap(c, "core20", nil, snap.R(20), "") + s.makeSnap(c, "pc-kernel=20", nil, snap.R(1), "") + gadgetContent := [][]string{ + {"grub.conf", "# boot grub.cfg"}, + {"meta/gadget.yaml", pcUC20GadgetYaml}, + } + s.makeSnap(c, "pc=20", gadgetContent, snap.R(22), "") -func (s *toolingStoreContextSuite) TestUpdateUserAuth(c *C) { - u := &auth.UserState{ - StoreMacaroon: "macaroon", - StoreDischarges: []string{"discharge1"}, + ra := map[string]interface{}{ + "account-id": "my-brand", + "provenance": []interface{}{"delegated-prov"}, } + s.MakeAssertedDelegatedSnap(c, seedtest.SampleSnapYaml["required20"]+"\nprovenance: delegated-prov\n", nil, snap.R(1), "my-brand", "my-brand", "delegated-prov", ra, s.StoreSigning.Database) - u1, err := s.sc.UpdateUserAuth(u, []string{"discharge2"}) - c.Assert(err, IsNil) - c.Check(u1, Equals, u) - c.Check(u1.StoreDischarges, DeepEquals, []string{"discharge2"}) + opts := &image.Options{ + PrepareDir: prepareDir, + Customizations: image.Customizations{ + BootFlags: []string{"factory"}, + Validation: "ignore", + }, + } + + err := image.SetupSeed(s.tsto, model, opts) + c.Check(err, IsNil) } diff -Nru snapd-2.55.5+20.04/image/options.go snapd-2.57.5+20.04/image/options.go --- snapd-2.55.5+20.04/image/options.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/options.go 2022-10-17 16:25:18.000000000 +0000 @@ -23,6 +23,17 @@ ModelFile string Classic bool + // Preseed requests the image to be preseeded (only for UC20) + Preseed bool + // PreseedSignKey is the name of the key to use for signing preseed + // assertion (empty means the default key). + PreseedSignKey string + + // AppArmor kernel features directory to bind-mount when preseeding. + // If empty then the features from /sys/kernel/security/apparmor will be used. + // (only for UC20) + AppArmorKernelFeaturesDir string + Channel string // TODO: use OptionsSnap directly here? diff -Nru snapd-2.55.5+20.04/image/preseed/export_test.go snapd-2.57.5+20.04/image/preseed/export_test.go --- snapd-2.55.5+20.04/image/preseed/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/preseed/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,13 +20,18 @@ package preseed import ( + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/signtool" "github.com/snapcore/snapd/seed" + "github.com/snapcore/snapd/store/tooling" + "github.com/snapcore/snapd/testutil" ) var ( SystemSnapFromSeed = systemSnapFromSeed ChooseTargetSnapdVersion = chooseTargetSnapdVersion CreatePreseedArtifact = createPreseedArtifact + RunUC20PreseedMode = runUC20PreseedMode ) type PreseedOpts = preseedOpts @@ -42,3 +47,17 @@ func SnapdPathAndVersion(targetSnapd *targetSnapdInfo) (string, string) { return targetSnapd.path, targetSnapd.version } + +func MockGetKeypairManager(f func() (signtool.KeypairManager, error)) (restore func()) { + r := testutil.Backup(&getKeypairManager) + getKeypairManager = f + return r +} + +func MockNewToolingStoreFromModel(f func(model *asserts.Model, fallbackArchitecture string) (*tooling.ToolingStore, error)) (restore func()) { + old := newToolingStoreFromModel + newToolingStoreFromModel = f + return func() { + newToolingStoreFromModel = old + } +} diff -Nru snapd-2.55.5+20.04/image/preseed/preseed_classic_test.go snapd-2.57.5+20.04/image/preseed/preseed_classic_test.go --- snapd-2.55.5+20.04/image/preseed/preseed_classic_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/image/preseed/preseed_classic_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,362 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 preseed_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/image/preseed" + "github.com/snapcore/snapd/osutil" + apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" + "github.com/snapcore/snapd/testutil" +) + +func mockVersionFiles(c *C, rootDir1, version1, rootDir2, version2 string) { + versions := []string{version1, version2} + for i, root := range []string{rootDir1, rootDir2} { + c.Assert(os.MkdirAll(filepath.Join(root, dirs.CoreLibExecDir), 0755), IsNil) + infoFile := filepath.Join(root, dirs.CoreLibExecDir, "info") + c.Assert(ioutil.WriteFile(infoFile, []byte(fmt.Sprintf("VERSION=%s", versions[i])), 0644), IsNil) + } +} + +func (s *preseedSuite) TestChrootDoesntExist(c *C) { + c.Assert(preseed.Classic("/non-existing-dir"), ErrorMatches, `cannot verify "/non-existing-dir": is not a directory`) +} + +func (s *preseedSuite) TestChrootValidationUnhappy(c *C) { + tmpDir := c.MkDir() + defer osutil.MockMountInfo("")() + + c.Check(preseed.Classic(tmpDir), ErrorMatches, "cannot preseed without the following mountpoints:\n - .*/dev\n - .*/proc\n - .*/sys/kernel/security") +} + +func (s *preseedSuite) TestRunPreseedMountUnhappy(c *C) { + tmpDir := c.MkDir() + dirs.SetRootDir(tmpDir) + defer mockChrootDirs(c, tmpDir, true)() + + restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { return nil }) + defer restoreSyscallChroot() + + mockMountCmd := testutil.MockCommand(c, "mount", `echo "something went wrong" +exit 32 +`) + defer mockMountCmd.Restore() + + targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") + restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) + defer restoreMountPath() + + restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/core.snap", "", nil }) + defer restoreSystemSnapFromSeed() + + c.Check(preseed.Classic(tmpDir), ErrorMatches, `cannot mount .+ at .+ in preseed mode: exit status 32\n'mount -t squashfs -o ro,x-gdu.hide,x-gvfs-hide /a/core.snap .*/target-core-mounted-here' failed with: something went wrong\n`) +} + +func (s *preseedSuite) TestChrootValidationUnhappyNoApparmor(c *C) { + tmpDir := c.MkDir() + defer mockChrootDirs(c, tmpDir, false)() + + c.Check(preseed.Classic(tmpDir), ErrorMatches, `cannot preseed without access to ".*sys/kernel/security/apparmor"`) +} + +func (s *preseedSuite) TestChrootValidationAlreadyPreseeded(c *C) { + tmpDir := c.MkDir() + snapdDir := filepath.Dir(dirs.SnapStateFile) + c.Assert(os.MkdirAll(filepath.Join(tmpDir, snapdDir), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(tmpDir, dirs.SnapStateFile), nil, os.ModePerm), IsNil) + + c.Check(preseed.Classic(tmpDir), ErrorMatches, fmt.Sprintf("the system at %q appears to be preseeded, pass --reset flag to clean it up", tmpDir)) +} + +func (s *preseedSuite) TestChrootFailure(c *C) { + restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { + return fmt.Errorf("FAIL: %s", path) + }) + defer restoreSyscallChroot() + + tmpDir := c.MkDir() + defer mockChrootDirs(c, tmpDir, true)() + + c.Check(preseed.Classic(tmpDir), ErrorMatches, fmt.Sprintf("cannot chroot into %s: FAIL: %s", tmpDir, tmpDir)) +} + +func (s *preseedSuite) TestRunPreseedHappy(c *C) { + tmpDir := c.MkDir() + dirs.SetRootDir(tmpDir) + defer mockChrootDirs(c, tmpDir, true)() + + restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { return nil }) + defer restoreSyscallChroot() + + mockMountCmd := testutil.MockCommand(c, "mount", "") + defer mockMountCmd.Restore() + + mockUmountCmd := testutil.MockCommand(c, "umount", "") + defer mockUmountCmd.Restore() + + targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") + restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) + defer restoreMountPath() + + restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/core.snap", "", nil }) + defer restoreSystemSnapFromSeed() + + mockTargetSnapd := testutil.MockCommand(c, filepath.Join(targetSnapdRoot, "usr/lib/snapd/snapd"), `#!/bin/sh + if [ "$SNAPD_PRESEED" != "1" ]; then + exit 1 + fi +`) + defer mockTargetSnapd.Restore() + + mockSnapdFromDeb := testutil.MockCommand(c, filepath.Join(tmpDir, "usr/lib/snapd/snapd"), `#!/bin/sh + exit 1 +`) + defer mockSnapdFromDeb.Restore() + + // snapd from the snap is newer than deb + mockVersionFiles(c, targetSnapdRoot, "2.44.0", tmpDir, "2.41.0") + + c.Check(preseed.Classic(tmpDir), IsNil) + + c.Assert(mockMountCmd.Calls(), HasLen, 1) + // note, tmpDir, targetSnapdRoot are contactenated again cause we're not really chrooting in the test + // and mocking dirs.RootDir + c.Check(mockMountCmd.Calls()[0], DeepEquals, []string{"mount", "-t", "squashfs", "-o", "ro,x-gdu.hide,x-gvfs-hide", "/a/core.snap", filepath.Join(tmpDir, targetSnapdRoot)}) + + c.Assert(mockTargetSnapd.Calls(), HasLen, 1) + c.Check(mockTargetSnapd.Calls()[0], DeepEquals, []string{"snapd"}) + + c.Assert(mockSnapdFromDeb.Calls(), HasLen, 0) + + // relative chroot path works too + tmpDirPath, relativeChroot := filepath.Split(tmpDir) + pwd, err := os.Getwd() + c.Assert(err, IsNil) + defer func() { + os.Chdir(pwd) + }() + c.Assert(os.Chdir(tmpDirPath), IsNil) + c.Check(preseed.Classic(relativeChroot), IsNil) +} + +func (s *preseedSuite) TestRunPreseedHappyDebVersionIsNewer(c *C) { + tmpDir := c.MkDir() + dirs.SetRootDir(tmpDir) + defer mockChrootDirs(c, tmpDir, true)() + + restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { return nil }) + defer restoreSyscallChroot() + + mockMountCmd := testutil.MockCommand(c, "mount", "") + defer mockMountCmd.Restore() + + mockUmountCmd := testutil.MockCommand(c, "umount", "") + defer mockUmountCmd.Restore() + + targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") + restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) + defer restoreMountPath() + + restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/core.snap", "", nil }) + defer restoreSystemSnapFromSeed() + + c.Assert(os.MkdirAll(filepath.Join(targetSnapdRoot, "usr/lib/snapd/"), 0755), IsNil) + mockSnapdFromSnap := testutil.MockCommand(c, filepath.Join(targetSnapdRoot, "usr/lib/snapd/snapd"), `#!/bin/sh + exit 1 +`) + defer mockSnapdFromSnap.Restore() + + mockSnapdFromDeb := testutil.MockCommand(c, filepath.Join(tmpDir, "usr/lib/snapd/snapd"), `#!/bin/sh + if [ "$SNAPD_PRESEED" != "1" ]; then + exit 1 + fi +`) + defer mockSnapdFromDeb.Restore() + + // snapd from the deb is newer than snap + mockVersionFiles(c, targetSnapdRoot, "2.44.0", tmpDir, "2.45.0") + + c.Check(preseed.Classic(tmpDir), IsNil) + + c.Assert(mockMountCmd.Calls(), HasLen, 1) + // note, tmpDir, targetSnapdRoot are contactenated again cause we're not really chrooting in the test + // and mocking dirs.RootDir + c.Check(mockMountCmd.Calls()[0], DeepEquals, []string{"mount", "-t", "squashfs", "-o", "ro,x-gdu.hide,x-gvfs-hide", "/a/core.snap", filepath.Join(tmpDir, targetSnapdRoot)}) + + c.Assert(mockSnapdFromDeb.Calls(), HasLen, 1) + c.Check(mockSnapdFromDeb.Calls()[0], DeepEquals, []string{"snapd"}) + c.Assert(mockSnapdFromSnap.Calls(), HasLen, 0) +} + +func (s *preseedSuite) TestRunPreseedUnsupportedVersion(c *C) { + tmpDir := c.MkDir() + dirs.SetRootDir(tmpDir) + c.Assert(os.MkdirAll(filepath.Join(tmpDir, "usr/lib/snapd/"), 0755), IsNil) + defer mockChrootDirs(c, tmpDir, true)() + + restoreSyscallChroot := preseed.MockSyscallChroot(func(path string) error { return nil }) + defer restoreSyscallChroot() + + mockMountCmd := testutil.MockCommand(c, "mount", "") + defer mockMountCmd.Restore() + + targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") + restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) + defer restoreMountPath() + + restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/core.snap", "", nil }) + defer restoreSystemSnapFromSeed() + + c.Assert(os.MkdirAll(filepath.Join(targetSnapdRoot, "usr/lib/snapd/"), 0755), IsNil) + mockTargetSnapd := testutil.MockCommand(c, filepath.Join(targetSnapdRoot, "usr/lib/snapd/snapd"), "") + defer mockTargetSnapd.Restore() + + infoFile := filepath.Join(targetSnapdRoot, dirs.CoreLibExecDir, "info") + c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=2.43.0"), 0644), IsNil) + + // simulate snapd version from the deb + infoFile = filepath.Join(filepath.Join(tmpDir, dirs.CoreLibExecDir, "info")) + c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=2.41.0"), 0644), IsNil) + + c.Check(preseed.Classic(tmpDir), ErrorMatches, + `snapd 2.43.0 from the target system does not support preseeding, the minimum required version is 2.43.3\+`) +} + +func (s *preseedSuite) TestReset(c *C) { + startDir, err := os.Getwd() + c.Assert(err, IsNil) + defer func() { + os.Chdir(startDir) + }() + + for _, isRelative := range []bool{false, true} { + tmpDir := c.MkDir() + resetDirArg := tmpDir + if isRelative { + var parentDir string + parentDir, resetDirArg = filepath.Split(tmpDir) + os.Chdir(parentDir) + } + + // mock some preseeding artifacts + artifacts := []struct { + path string + // if symlinkTarget is not empty, then a path -> symlinkTarget symlink + // will be created instead of a regular file. + symlinkTarget string + }{ + {dirs.SnapStateFile, ""}, + {dirs.SnapSystemKeyFile, ""}, + {filepath.Join(dirs.SnapDesktopFilesDir, "foo.desktop"), ""}, + {filepath.Join(dirs.SnapDesktopIconsDir, "foo.png"), ""}, + {filepath.Join(dirs.SnapMountPolicyDir, "foo.fstab"), ""}, + {filepath.Join(dirs.SnapBlobDir, "foo.snap"), ""}, + {filepath.Join(dirs.SnapUdevRulesDir, "foo-snap.bar.rules"), ""}, + {filepath.Join(dirs.SnapDBusSystemPolicyDir, "snap.foo.bar.conf"), ""}, + {filepath.Join(dirs.SnapDBusSessionServicesDir, "org.example.Session.service"), ""}, + {filepath.Join(dirs.SnapDBusSystemServicesDir, "org.example.System.service"), ""}, + {filepath.Join(dirs.SnapServicesDir, "snap.foo.service"), ""}, + {filepath.Join(dirs.SnapServicesDir, "snap.foo.timer"), ""}, + {filepath.Join(dirs.SnapServicesDir, "snap.foo.socket"), ""}, + {filepath.Join(dirs.SnapServicesDir, "snap-foo.mount"), ""}, + {filepath.Join(dirs.SnapServicesDir, "multi-user.target.wants", "snap-foo.mount"), ""}, + {filepath.Join(dirs.SnapDataDir, "foo", "bar"), ""}, + {filepath.Join(dirs.SnapCacheDir, "foocache", "bar"), ""}, + {filepath.Join(apparmor_sandbox.CacheDir, "foo", "bar"), ""}, + {filepath.Join(dirs.SnapAppArmorDir, "foo"), ""}, + {filepath.Join(dirs.SnapAssertsDBDir, "foo"), ""}, + {filepath.Join(dirs.FeaturesDir, "foo"), ""}, + {filepath.Join(dirs.SnapDeviceDir, "foo-1", "bar"), ""}, + {filepath.Join(dirs.SnapCookieDir, "foo"), ""}, + {filepath.Join(dirs.SnapSeqDir, "foo.json"), ""}, + {filepath.Join(dirs.SnapMountDir, "foo", "bin"), ""}, + {filepath.Join(dirs.SnapSeccompDir, "foo.bin"), ""}, + {filepath.Join(runinhibit.InhibitDir, "foo.lock"), ""}, + // bash-completion symlinks + {filepath.Join(dirs.CompletersDir, "foo.bar"), "/a/snapd/complete.sh"}, + {filepath.Join(dirs.CompletersDir, "foo"), "foo.bar"}, + {filepath.Join(dirs.LegacyCompletersDir, "foo.bar"), "/a/snapd/complete.sh"}, + {filepath.Join(dirs.LegacyCompletersDir, "foo"), "foo.bar"}, + } + + for _, art := range artifacts { + fullPath := filepath.Join(tmpDir, art.path) + // create parent dir + c.Assert(os.MkdirAll(filepath.Dir(fullPath), 0755), IsNil) + if art.symlinkTarget != "" { + // note, symlinkTarget is not relative to tmpDir + c.Assert(os.Symlink(art.symlinkTarget, fullPath), IsNil) + } else { + c.Assert(ioutil.WriteFile(fullPath, nil, os.ModePerm), IsNil) + } + } + + checkArtifacts := func(exists bool) { + for _, art := range artifacts { + fullPath := filepath.Join(tmpDir, art.path) + if art.symlinkTarget != "" { + c.Check(osutil.IsSymlink(fullPath), Equals, exists, Commentf("offending symlink: %s", fullPath)) + } else { + c.Check(osutil.FileExists(fullPath), Equals, exists, Commentf("offending file: %s", fullPath)) + } + } + } + + // validity + checkArtifacts(true) + + snapdDir := filepath.Dir(dirs.SnapStateFile) + c.Assert(os.MkdirAll(filepath.Join(tmpDir, snapdDir), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(tmpDir, dirs.SnapStateFile), nil, os.ModePerm), IsNil) + + c.Assert(preseed.ResetPreseededChroot(resetDirArg), IsNil) + + checkArtifacts(false) + + // running reset again is ok + c.Assert(preseed.ResetPreseededChroot(resetDirArg), IsNil) + + // reset complains if target directory doesn't exist + c.Assert(preseed.ResetPreseededChroot("/non/existing/chrootpath"), ErrorMatches, `cannot reset non-existing directory "/non/existing/chrootpath"`) + + // reset complains if target is not a directory + fooFile := filepath.Join(resetDirArg, "foo") + c.Assert(ioutil.WriteFile(fooFile, nil, os.ModePerm), IsNil) + err = preseed.ResetPreseededChroot(fooFile) + // the error message is always with an absolute file, so make the path + // absolute if we are running the relative test to properly match + if isRelative { + var err2 error + fooFile, err2 = filepath.Abs(fooFile) + c.Assert(err2, IsNil) + } + c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot reset %q, it is not a directory`, fooFile)) + } + +} diff -Nru snapd-2.55.5+20.04/image/preseed/preseed.go snapd-2.57.5+20.04/image/preseed/preseed.go --- snapd-2.55.5+20.04/image/preseed/preseed.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/preseed/preseed.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,11 +25,26 @@ package preseed import ( + "crypto" + "fmt" "io" "os" + "path/filepath" + "time" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/signtool" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/seed" + "github.com/snapcore/snapd/seed/seedwriter" + "github.com/snapcore/snapd/store/tooling" + "github.com/snapcore/snapd/timings" ) var ( + seedOpen = seed.Open + Stdout io.Writer = os.Stdout Stderr io.Writer = os.Stderr ) @@ -39,9 +54,156 @@ PreseedChrootDir string SystemLabel string WritableDir string + PreseedSignKey string + // optional path to AppArmor kernel features directory + AppArmorKernelFeaturesDir string } type targetSnapdInfo struct { path string version string } + +var ( + getKeypairManager = signtool.GetKeypairManager + newToolingStoreFromModel = tooling.NewToolingStoreFromModel + trusted = sysdb.Trusted() +) + +func MockTrusted(mockTrusted []asserts.Assertion) (restore func()) { + prevTrusted := trusted + trusted = mockTrusted + return func() { + trusted = prevTrusted + } +} + +func writePreseedAssertion(artifactDigest []byte, opts *preseedOpts) error { + keypairMgr, err := getKeypairManager() + if err != nil { + return err + } + + key := opts.PreseedSignKey + if key == "" { + key = `default` + } + privKey, err := keypairMgr.GetByName(key) + if err != nil { + // TRANSLATORS: %q is the key name, %v the error message + return fmt.Errorf(i18n.G("cannot use %q key: %v"), key, err) + } + + sysDir := filepath.Join(opts.PrepareImageDir, "system-seed") + sd, err := seedOpen(sysDir, opts.SystemLabel) + if err != nil { + return err + } + + bs := asserts.NewMemoryBackstore() + adb, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Trusted: trusted, + KeypairManager: keypairMgr, + Backstore: bs, + }) + if err != nil { + return err + } + + commitTo := func(b *asserts.Batch) error { + return b.CommitTo(adb, nil) + } + + if err := sd.LoadAssertions(adb, commitTo); err != nil { + return err + } + model := sd.Model() + + tm := timings.New(nil) + if err := sd.LoadMeta("run", nil, tm); err != nil { + return err + } + + snaps := []interface{}{} + addSnap := func(sn *seed.Snap) { + preseedSnap := map[string]interface{}{} + preseedSnap["name"] = sn.SnapName() + if sn.ID() != "" { + preseedSnap["id"] = sn.ID() + preseedSnap["revision"] = sn.PlaceInfo().SnapRevision().String() + } + snaps = append(snaps, preseedSnap) + } + + modeSnaps, err := sd.ModeSnaps("run") + if err != nil { + return err + } + essSnaps := sd.EssentialSnaps() + if err != nil { + return err + } + for _, ess := range essSnaps { + addSnap(ess) + } + for _, msnap := range modeSnaps { + addSnap(msnap) + } + + base64Digest, err := asserts.EncodeDigest(crypto.SHA3_384, artifactDigest) + if err != nil { + return err + } + headers := map[string]interface{}{ + "type": "preseed", + "authority-id": model.AuthorityID(), + "series": "16", + "brand-id": model.BrandID(), + "model": model.Model(), + "system-label": opts.SystemLabel, + "artifact-sha3-384": base64Digest, + "timestamp": time.Now().UTC().Format(time.RFC3339), + "snaps": snaps, + } + + signedAssert, err := adb.Sign(asserts.PreseedType, headers, nil, privKey.PublicKey().ID()) + if err != nil { + return fmt.Errorf("cannot sign preseed assertion: %v", err) + } + + tsto, err := newToolingStoreFromModel(model, "") + if err != nil { + return err + } + tsto.Stdout = Stdout + + newFetcher := func(save func(asserts.Assertion) error) asserts.Fetcher { + return tsto.AssertionFetcher(adb, save) + } + + f := seedwriter.MakeRefAssertsFetcher(newFetcher) + if err := f.Save(signedAssert); err != nil { + return fmt.Errorf("cannot fetch assertion: %v", err) + } + + serialized, err := os.OpenFile(filepath.Join(sysDir, "systems", opts.SystemLabel, "preseed"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + if err != nil { + return err + } + defer serialized.Close() + + enc := asserts.NewEncoder(serialized) + for _, aref := range f.Refs() { + if aref.Type == asserts.PreseedType || aref.Type == asserts.AccountKeyType { + as, err := aref.Resolve(adb.Find) + if err != nil { + return fmt.Errorf("internal error: %v", err) + } + if err := enc.Encode(as); err != nil { + return fmt.Errorf("cannot write assertion %s: %v", aref, err) + } + } + } + + return nil +} diff -Nru snapd-2.55.5+20.04/image/preseed/preseed_linux.go snapd-2.57.5+20.04/image/preseed/preseed_linux.go --- snapd-2.55.5+20.04/image/preseed/preseed_linux.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/preseed/preseed_linux.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,7 +20,9 @@ package preseed import ( + "crypto" "encoding/json" + "errors" "fmt" "io/ioutil" "os" @@ -33,7 +35,6 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/squashfs" - "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snapdtool" "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/timings" @@ -97,8 +98,6 @@ return nil } -var seedOpen = seed.Open - var systemSnapFromSeed = func(seedDir, sysLabel string) (systemSnap string, baseSnap string, err error) { seed, err := seedOpen(seedDir, sysLabel) if err != nil { @@ -112,15 +111,16 @@ model := seed.Model() tm := timings.New(nil) - if err := seed.LoadMeta(tm); err != nil { + + if err := seed.LoadEssentialMeta(nil, tm); err != nil { return "", "", err } if model.Classic() { - fmt.Fprintf(Stdout, "ubuntu classic preseeding") + fmt.Fprintf(Stdout, "ubuntu classic preseeding\n") } else { if model.Base() == "core20" { - fmt.Fprintf(Stdout, "UC20 preseeding") + fmt.Fprintf(Stdout, "UC20+ preseeding\n") } else { // TODO: support uc20+ return "", "", fmt.Errorf("preseeding of ubuntu core with base %s is not supported", model.Base()) @@ -203,7 +203,7 @@ return &targetSnapdInfo{path: snapdPath, version: whichVer}, nil } -func prepareCore20Mountpoints(prepareImageDir, tmpPreseedChrootDir, snapdSnapBlob, baseSnapBlob, writable string) (cleanupMounts func(), err error) { +func prepareCore20Mountpoints(prepareImageDir, tmpPreseedChrootDir, snapdSnapBlob, baseSnapBlob, aaFeaturesDir, writable string) (cleanupMounts func(), err error) { underPreseed := func(path string) string { return filepath.Join(tmpPreseedChrootDir, path) } @@ -218,13 +218,19 @@ var mounted []string + doUnmount := func(mnt string) { + cmd := exec.Command("umount", mnt) + if out, err := cmd.CombinedOutput(); err != nil { + fmt.Fprintf(Stdout, "cannot unmount: %v\n'umount %s' failed with: %s", err, mnt, out) + } + } + cleanupMounts = func() { - for i := len(mounted) - 1; i >= 0; i-- { + // unmount all the mounts but the first one, which is the base + // and it is cleaned up last + for i := len(mounted) - 1; i > 0; i-- { mnt := mounted[i] - cmd := exec.Command("umount", mnt) - if out, err := cmd.CombinedOutput(); err != nil { - fmt.Fprintf(Stdout, "cannot unmount: %v\n'umount %s' failed with: %s", err, mnt, out) - } + doUnmount(mnt) } entries, err := osutil.LoadMountInfo() @@ -234,13 +240,15 @@ } // cleanup after handle-writable-paths for _, ent := range entries { - if strings.HasPrefix(ent.MountDir, tmpPreseedChrootDir) { - cmd := exec.Command("umount", ent.MountDir) - if out, err := cmd.CombinedOutput(); err != nil { - fmt.Fprintf(Stdout, "cannot unmount: %v\n'umount %s' failed with: %s", err, ent.MountDir, out) - } + if ent.MountDir != tmpPreseedChrootDir && strings.HasPrefix(ent.MountDir, tmpPreseedChrootDir) { + doUnmount(ent.MountDir) } } + + // finally, umount the base snap + if len(mounted) > 0 { + doUnmount(mounted[0]) + } } cleanupOnError := func() { @@ -284,7 +292,8 @@ for _, dir := range []string{ "etc/udev/rules.d", "etc/systemd/system", "etc/dbus-1/session.d", "var/lib/snapd/seed", "var/cache/snapd", "var/cache/apparmor", - "var/snap", "snap"} { + "var/snap", "snap", "var/lib/extrausers", + } { if err = os.MkdirAll(filepath.Join(writable, dir), 0755); err != nil { return nil, err } @@ -302,10 +311,15 @@ {"--bind", underWritable("system-data/etc/systemd"), underPreseed("etc/systemd")}, {"--bind", underWritable("system-data/etc/dbus-1"), underPreseed("etc/dbus-1")}, {"--bind", underWritable("system-data/etc/udev/rules.d"), underPreseed("etc/udev/rules.d")}, + {"--bind", underWritable("system-data/var/lib/extrausers"), underPreseed("var/lib/extrausers")}, {"--bind", filepath.Join(snapdMountPath, "/usr/lib/snapd"), underPreseed("/usr/lib/snapd")}, {"--bind", filepath.Join(prepareImageDir, "system-seed"), underPreseed("var/lib/snapd/seed")}, } + if aaFeaturesDir != "" { + mounts = append(mounts, []string{"--bind", aaFeaturesDir, underPreseed("sys/kernel/security/apparmor/features")}) + } + for _, mountArgs := range mounts { cmd := exec.Command("mount", mountArgs...) if out, err = cmd.CombinedOutput(); err != nil { @@ -336,7 +350,7 @@ return ioutil.TempDir("", "writable-") } -func prepareCore20Chroot(prepareImageDir string) (preseed *preseedOpts, cleanup func(), err error) { +func prepareCore20Chroot(prepareImageDir, aaFeaturesDir string) (preseed *preseedOpts, cleanup func(), err error) { sysDir := filepath.Join(prepareImageDir, "system-seed") sysLabel, err := systemForPreseeding(sysDir) if err != nil { @@ -363,7 +377,7 @@ return nil, nil, fmt.Errorf("cannot prepare uc20 chroot: %v", err) } - cleanupMounts, err := prepareCore20Mountpoints(prepareImageDir, tmpPreseedChrootDir, snapdSnapPath, baseSnapPath, writableTmpDir) + cleanupMounts, err := prepareCore20Mountpoints(prepareImageDir, tmpPreseedChrootDir, snapdSnapPath, baseSnapPath, aaFeaturesDir, writableTmpDir) if err != nil { return nil, nil, fmt.Errorf("cannot prepare uc20 mountpoints: %v", err) } @@ -376,6 +390,9 @@ if err := os.RemoveAll(writableTmpDir); err != nil { fmt.Fprintf(Stdout, "%v", err) } + if err := os.RemoveAll(snapdMountPath); err != nil { + fmt.Fprintf(Stdout, "%v", err) + } } opts := &preseedOpts{ @@ -454,20 +471,20 @@ Include []string `json:"include"` } -func createPreseedArtifact(opts *preseedOpts) error { +func createPreseedArtifact(opts *preseedOpts) (digest []byte, err error) { artifactPath := filepath.Join(opts.PrepareImageDir, "system-seed", "systems", opts.SystemLabel, "preseed.tgz") systemData := filepath.Join(opts.WritableDir, "system-data") - patternsFile := filepath.Join(systemData, "var/lib/snapd/preseed-export.json") + patternsFile := filepath.Join(opts.PreseedChrootDir, "usr/lib/snapd/preseed.json") pf, err := os.Open(patternsFile) if err != nil { - return err + return nil, err } var patterns preseedFilePatterns dec := json.NewDecoder(pf) if err := dec.Decode(&patterns); err != nil { - return err + return nil, err } args := []string{"-czf", artifactPath, "-p", "-C", systemData} @@ -479,12 +496,12 @@ // handle globs explicitly. matches, err := filepath.Glob(filepath.Join(systemData, incl)) if err != nil { - return err + return nil, err } for _, m := range matches { relPath, err := filepath.Rel(systemData, m) if err != nil { - return err + return nil, err } args = append(args, relPath) } @@ -492,9 +509,11 @@ cmd := exec.Command("tar", args...) if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("%v (%s)", err, out) + return nil, fmt.Errorf("%v (%s)", err, out) } - return nil + + sha3_384, _, err := osutil.FileDigest(artifactPath, crypto.SHA3_384) + return sha3_384, err } // runPreseedMode runs snapd in a preseed mode. It assumes running in a chroot. @@ -523,33 +542,57 @@ cmd.Env = append(cmd.Env, "SNAPD_PRESEED=1") cmd.Stderr = Stderr cmd.Stdout = Stdout - fmt.Fprintf(Stdout, "starting to preseed UC20 system: %s", opts.PreseedChrootDir) + fmt.Fprintf(Stdout, "starting to preseed UC20+ system: %s\n", opts.PreseedChrootDir) if err := cmd.Run(); err != nil { + var errno syscall.Errno + if errors.As(err, &errno) && errno == syscall.ENOEXEC { + return fmt.Errorf(`error running snapd, please try installing the "qemu-user-static" package: %v`, err) + } + return fmt.Errorf("error running snapd in preseed mode: %v\n", err) } - if err := createPreseedArtifact(opts); err != nil { + digest, err := createPreseedArtifact(opts) + if err != nil { return fmt.Errorf("cannot create preseed.tgz: %v", err) } + if err := writePreseedAssertion(digest, opts); err != nil { + return fmt.Errorf("cannot create preseed assertion: %v", err) + } + return nil } // Core20 runs preseeding of UC20 system prepared by prepare-image in prepareImageDir // and stores the resulting preseed preseed.tgz file in system-seed/systems//preseed.tgz. // Expects single systemlabel under systems directory. -func Core20(prepareImageDir string) error { - popts, cleanup, err := prepareCore20Chroot(prepareImageDir) +func Core20(prepareImageDir, preseedSignKey, aaFeaturesDir string) error { + var err error + prepareImageDir, err = filepath.Abs(prepareImageDir) + if err != nil { + return err + } + + popts, cleanup, err := prepareCore20Chroot(prepareImageDir, aaFeaturesDir) if err != nil { return err } defer cleanup() + + popts.PreseedSignKey = preseedSignKey return runUC20PreseedMode(popts) } // Classic runs preseeding of a classic ubuntu system pointed by chrootDir. func Classic(chrootDir string) error { + var err error + chrootDir, err = filepath.Abs(chrootDir) + if err != nil { + return err + } + if err := checkChroot(chrootDir); err != nil { return err } diff -Nru snapd-2.55.5+20.04/image/preseed/preseed_other.go snapd-2.57.5+20.04/image/preseed/preseed_other.go --- snapd-2.55.5+20.04/image/preseed/preseed_other.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/preseed/preseed_other.go 2022-10-17 16:25:18.000000000 +0000 @@ -31,6 +31,6 @@ return preseedNotAvailableError } -func Core20(chrootDir string) error { +func Core20(chrootDir, key, aaFaaFeaturesDir string) error { return preseedNotAvailableError } diff -Nru snapd-2.55.5+20.04/image/preseed/preseed_test.go snapd-2.57.5+20.04/image/preseed/preseed_test.go --- snapd-2.55.5+20.04/image/preseed/preseed_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/preseed/preseed_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -24,6 +24,7 @@ "io/ioutil" "os" "path/filepath" + "strings" "testing" . "gopkg.in/check.v1" @@ -32,6 +33,7 @@ "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/image/preseed" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/squashfs" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" @@ -58,12 +60,25 @@ dirs.SetRootDir("") } -type Fake16Seed struct { +type FakeSeed struct { AssertsModel *asserts.Model Essential []*seed.Snap + SnapsForMode map[string][]*seed.Snap LoadMetaErr error LoadAssertionsErr error UsesSnapd bool + loadAssertions func(db asserts.RODatabase, commitTo func(*asserts.Batch) error) error +} + +func mockChrootDirs(c *C, rootDir string, apparmorDir bool) func() { + if apparmorDir { + c.Assert(os.MkdirAll(filepath.Join(rootDir, "/sys/kernel/security/apparmor"), 0755), IsNil) + } + mockMountInfo := `912 920 0:57 / ${rootDir}/proc rw,nosuid,nodev,noexec,relatime - proc proc rw +914 913 0:7 / ${rootDir}/sys/kernel/security rw,nosuid,nodev,noexec,relatime master:8 - securityfs securityfs rw +915 920 0:58 / ${rootDir}/dev rw,relatime - tmpfs none rw,size=492k,mode=755,uid=100000,gid=100000 +` + return osutil.MockMountInfo(strings.Replace(mockMountInfo, "${rootDir}", rootDir, -1)) } // Fake implementation of seed.Seed interface @@ -81,15 +96,18 @@ return assertstest.FakeAssertion(headers, nil).(*asserts.Model) } -func (fs *Fake16Seed) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Batch) error) error { +func (fs *FakeSeed) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Batch) error) error { + if fs.loadAssertions != nil { + return fs.loadAssertions(db, commitTo) + } return fs.LoadAssertionsErr } -func (fs *Fake16Seed) Model() *asserts.Model { +func (fs *FakeSeed) Model() *asserts.Model { return fs.AssertsModel } -func (fs *Fake16Seed) Brand() (*asserts.Account, error) { +func (fs *FakeSeed) Brand() (*asserts.Account, error) { headers := map[string]interface{}{ "type": "account", "account-id": "brand", @@ -100,31 +118,37 @@ return assertstest.FakeAssertion(headers, nil).(*asserts.Account), nil } -func (fs *Fake16Seed) LoadEssentialMeta(essentialTypes []snap.Type, tm timings.Measurer) error { - panic("unexpected") +func (fs *FakeSeed) SetParallelism(int) {} + +func (fs *FakeSeed) LoadEssentialMeta(essentialTypes []snap.Type, tm timings.Measurer) error { + return fs.LoadMetaErr +} + +func (fs *FakeSeed) LoadEssentialMetaWithSnapHandler(essentialTypes []snap.Type, handler seed.SnapHandler, tm timings.Measurer) error { + return fs.LoadMetaErr } -func (fs *Fake16Seed) LoadMeta(tm timings.Measurer) error { +func (fs *FakeSeed) LoadMeta(mode string, handler seed.SnapHandler, tm timings.Measurer) error { return fs.LoadMetaErr } -func (fs *Fake16Seed) UsesSnapdSnap() bool { +func (fs *FakeSeed) UsesSnapdSnap() bool { return fs.UsesSnapd } -func (fs *Fake16Seed) EssentialSnaps() []*seed.Snap { +func (fs *FakeSeed) EssentialSnaps() []*seed.Snap { return fs.Essential } -func (fs *Fake16Seed) ModeSnaps(mode string) ([]*seed.Snap, error) { - return nil, nil +func (fs *FakeSeed) ModeSnaps(mode string) ([]*seed.Snap, error) { + return fs.SnapsForMode[mode], nil } -func (fs *Fake16Seed) NumSnaps() int { +func (fs *FakeSeed) NumSnaps() int { return 0 } -func (fs *Fake16Seed) Iter(f func(sn *seed.Snap) error) error { +func (fs *FakeSeed) Iter(f func(sn *seed.Snap) error) error { return nil } @@ -132,7 +156,7 @@ tmpDir := c.MkDir() restore := preseed.MockSeedOpen(func(rootDir, label string) (seed.Seed, error) { - return &Fake16Seed{ + return &FakeSeed{ AssertsModel: mockClassicModel(), Essential: []*seed.Snap{{Path: "/some/path/core", SideInfo: &snap.SideInfo{RealName: "core"}}}, }, nil @@ -148,7 +172,7 @@ tmpDir := c.MkDir() restore := preseed.MockSeedOpen(func(rootDir, label string) (seed.Seed, error) { - return &Fake16Seed{ + return &FakeSeed{ AssertsModel: mockClassicModel(), Essential: []*seed.Snap{{Path: "/some/path/snapd.snap", SideInfo: &snap.SideInfo{RealName: "snapd"}}}, UsesSnapd: true, @@ -174,7 +198,7 @@ func (s *preseedSuite) TestSystemSnapFromSeedErrors(c *C) { tmpDir := c.MkDir() - fakeSeed := &Fake16Seed{} + fakeSeed := &FakeSeed{} fakeSeed.AssertsModel = mockClassicModel() restore := preseed.MockSeedOpen(func(rootDir, label string) (seed.Seed, error) { return fakeSeed, nil }) @@ -301,15 +325,18 @@ "exclude": ["/etc/bar/x*"], "include": ["/etc/bar/a", "/baz/*"] }` - c.Assert(os.MkdirAll(filepath.Join(writableDir, "system-data/var/lib/snapd"), 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(writableDir, "system-data/var/lib/snapd/preseed-export.json"), []byte(exportFileContents), 0644), IsNil) + c.Assert(os.MkdirAll(filepath.Join(tmpDir, "/usr/lib/snapd"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(tmpDir, "/usr/lib/snapd/preseed.json"), []byte(exportFileContents), 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(prepareDir, "system-seed/systems/20220203/preseed.tgz"), nil, 0644), IsNil) opts := &preseed.PreseedOpts{ - PrepareImageDir: prepareDir, - WritableDir: writableDir, - SystemLabel: "20220203", + PreseedChrootDir: tmpDir, + PrepareImageDir: prepareDir, + WritableDir: writableDir, + SystemLabel: "20220203", } - c.Assert(preseed.CreatePreseedArtifact(opts), Equals, nil) + _, err := preseed.CreatePreseedArtifact(opts) + c.Assert(err, IsNil) c.Check(mockTar.Calls(), DeepEquals, [][]string{ {"tar", "-czf", filepath.Join(tmpDir, "prepare-dir/system-seed/systems/20220203/preseed.tgz"), "-p", "-C", filepath.Join(writableDir, "system-data"), "--exclude", "/etc/bar/x*", "etc/bar/a", "baz/b"}, diff -Nru snapd-2.55.5+20.04/image/preseed/preseed_uc20_test.go snapd-2.57.5+20.04/image/preseed/preseed_uc20_test.go --- snapd-2.55.5+20.04/image/preseed/preseed_uc20_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/image/preseed/preseed_uc20_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,360 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 preseed_test + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/signtool" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/image/preseed" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/progress" + "github.com/snapcore/snapd/seed" + "github.com/snapcore/snapd/seed/seedtest" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/store" + "github.com/snapcore/snapd/store/tooling" + "github.com/snapcore/snapd/testutil" +) + +type fakeKeyMgr struct { + key asserts.PrivateKey +} + +func (f *fakeKeyMgr) Put(privKey asserts.PrivateKey) error { return nil } +func (f *fakeKeyMgr) Get(keyID string) (asserts.PrivateKey, error) { return f.key, nil } +func (f *fakeKeyMgr) Delete(keyID string) error { return nil } +func (f *fakeKeyMgr) GetByName(keyNname string) (asserts.PrivateKey, error) { return f.key, nil } +func (f *fakeKeyMgr) Export(keyName string) ([]byte, error) { return nil, nil } +func (f *fakeKeyMgr) List() ([]asserts.ExternalKeyInfo, error) { return nil, nil } +func (f *fakeKeyMgr) DeleteByName(keyName string) error { return nil } + +type toolingStore struct { + *seedtest.SeedSnaps +} + +func (t *toolingStore) SnapAction(_ context.Context, curSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, _ *auth.UserState, _ *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { + panic("not expected") +} + +func (s *toolingStore) Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error { + panic("not expected") +} + +func (s *toolingStore) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) { + ref := &asserts.Ref{Type: assertType, PrimaryKey: primaryKey} + as, err := ref.Resolve(s.StoreSigning.Find) + if err != nil { + return nil, err + } + return as, nil +} + +func (s *preseedSuite) testRunPreseedUC20Happy(c *C, customAppArmorFeaturesDir string) { + + testKey, _ := assertstest.GenerateKey(752) + + ts := &toolingStore{&seedtest.SeedSnaps{}} + ts.SeedSnaps.SetupAssertSigning("canonical") + ts.Brands.Register("my-brand", testKey, map[string]interface{}{ + "verification": "verified", + }) + + assertstest.AddMany(ts.StoreSigning, ts.Brands.AccountsAndKeys("my-brand")...) + + tsto := tooling.MockToolingStore(ts) + restoreToolingStore := preseed.MockNewToolingStoreFromModel(func(model *asserts.Model, fallbackArchitecture string) (*tooling.ToolingStore, error) { + return tsto, nil + }) + defer restoreToolingStore() + + restoreTrusted := preseed.MockTrusted(ts.StoreSigning.Trusted) + defer restoreTrusted() + + model := ts.Brands.Model("my-brand", "my-model-uc20", map[string]interface{}{ + "display-name": "My Model", + "architecture": "amd64", + "base": "core20", + "grade": "dangerous", + "timestamp": "2019-11-01T08:00:00+00:00", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": "pckernelidididididididididididid", + "type": "kernel", + }, + map[string]interface{}{ + "name": "pc", + "id": "pcididididididididididididididid", + "type": "gadget", + }, + }, + }) + + restoreSeedOpen := preseed.MockSeedOpen(func(rootDir, label string) (seed.Seed, error) { + return &FakeSeed{ + AssertsModel: model, + UsesSnapd: true, + Essential: []*seed.Snap{{ + Path: "/some/path/snapd.snap", + SideInfo: &snap.SideInfo{ + RealName: "snapd", + SnapID: "snapdidididididididididididididd", + Revision: snap.R("1")}}, + }, + SnapsForMode: map[string][]*seed.Snap{ + "run": {{ + Path: "/some/path/foo.snap", + SideInfo: &snap.SideInfo{ + RealName: "foo"}, + }}}, + loadAssertions: func(db asserts.RODatabase, commitTo func(*asserts.Batch) error) error { + batch := asserts.NewBatch(nil) + c.Assert(batch.Add(ts.StoreSigning.StoreAccountKey("")), IsNil) + c.Assert(commitTo(batch), IsNil) + return nil + }, + }, nil + }) + defer restoreSeedOpen() + + keyMgr := &fakeKeyMgr{testKey} + restoreGetKeypairMgr := preseed.MockGetKeypairManager(func() (signtool.KeypairManager, error) { + return keyMgr, nil + }) + defer restoreGetKeypairMgr() + + tmpDir := c.MkDir() + dirs.SetRootDir(tmpDir) + defer mockChrootDirs(c, tmpDir, true)() + + mockChootCmd := testutil.MockCommand(c, "chroot", "") + defer mockChootCmd.Restore() + + mockMountCmd := testutil.MockCommand(c, "mount", "") + defer mockMountCmd.Restore() + + mockUmountCmd := testutil.MockCommand(c, "umount", "") + defer mockUmountCmd.Restore() + + preseedTmpDir := filepath.Join(tmpDir, "preseed-tmp") + restoreMakePreseedTmpDir := preseed.MockMakePreseedTempDir(func() (string, error) { + return preseedTmpDir, nil + }) + defer restoreMakePreseedTmpDir() + + writableTmpDir := filepath.Join(tmpDir, "writable-tmp") + restoreMakeWritableTempDir := preseed.MockMakeWritableTempDir(func() (string, error) { + return writableTmpDir, nil + }) + defer restoreMakeWritableTempDir() + + c.Assert(os.MkdirAll(filepath.Join(writableTmpDir, "system-data/etc/bar"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(writableTmpDir, "system-data/etc/bar/a"), nil, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(writableTmpDir, "system-data/etc/bar/b"), nil, 0644), IsNil) + + mockTar := testutil.MockCommand(c, "tar", "") + defer mockTar.Restore() + + const exportFileContents = `{ +"exclude": ["foo"], +"include": ["/etc/bar/a", "/etc/bar/b"] +}` + + c.Assert(os.MkdirAll(filepath.Join(preseedTmpDir, "usr/lib/snapd"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(preseedTmpDir, "usr/lib/snapd/preseed.json"), []byte(exportFileContents), 0644), IsNil) + + mockWritablePaths := testutil.MockCommand(c, filepath.Join(preseedTmpDir, "/usr/lib/core/handle-writable-paths"), "") + defer mockWritablePaths.Restore() + + restore := osutil.MockMountInfo(fmt.Sprintf(`130 30 42:1 / %s/somepath rw,relatime shared:54 - ext4 /some/path rw +`, preseedTmpDir)) + defer restore() + + targetSnapdRoot := filepath.Join(tmpDir, "target-core-mounted-here") + restoreMountPath := preseed.MockSnapdMountPath(targetSnapdRoot) + defer restoreMountPath() + + restoreSystemSnapFromSeed := preseed.MockSystemSnapFromSeed(func(string, string) (string, string, error) { return "/a/snapd.snap", "/a/base.snap", nil }) + defer restoreSystemSnapFromSeed() + + c.Assert(os.MkdirAll(filepath.Join(tmpDir, "system-seed/systems/20220203"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(tmpDir, "system-seed/systems/20220203/preseed.tgz"), []byte(`hello world`), 0644), IsNil) + + c.Assert(preseed.Core20(tmpDir, "", customAppArmorFeaturesDir), IsNil) + + c.Check(mockChootCmd.Calls()[0], DeepEquals, []string{"chroot", preseedTmpDir, "/usr/lib/snapd/snapd"}) + + expectedMountCalls := [][]string{ + {"mount", "-o", "loop", "/a/base.snap", preseedTmpDir}, + {"mount", "-o", "loop", "/a/snapd.snap", targetSnapdRoot}, + {"mount", "-t", "tmpfs", "tmpfs", filepath.Join(preseedTmpDir, "run")}, + {"mount", "-t", "tmpfs", "tmpfs", filepath.Join(preseedTmpDir, "var/tmp")}, + {"mount", "--bind", filepath.Join(preseedTmpDir, "/var/tmp"), filepath.Join(preseedTmpDir, "tmp")}, + {"mount", "-t", "proc", "proc", filepath.Join(preseedTmpDir, "proc")}, + {"mount", "-t", "sysfs", "sysfs", filepath.Join(preseedTmpDir, "sys")}, + {"mount", "-t", "devtmpfs", "udev", filepath.Join(preseedTmpDir, "dev")}, + {"mount", "-t", "securityfs", "securityfs", filepath.Join(preseedTmpDir, "sys/kernel/security")}, + {"mount", "--bind", writableTmpDir, filepath.Join(preseedTmpDir, "writable")}, + {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/var/lib/snapd"), filepath.Join(preseedTmpDir, "var/lib/snapd")}, + {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/var/cache/snapd"), filepath.Join(preseedTmpDir, "var/cache/snapd")}, + {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/var/cache/apparmor"), filepath.Join(preseedTmpDir, "var/cache/apparmor")}, + {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/var/snap"), filepath.Join(preseedTmpDir, "var/snap")}, + {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/snap"), filepath.Join(preseedTmpDir, "snap")}, + {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/etc/systemd"), filepath.Join(preseedTmpDir, "etc/systemd")}, + {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/etc/dbus-1"), filepath.Join(preseedTmpDir, "etc/dbus-1")}, + {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/etc/udev/rules.d"), filepath.Join(preseedTmpDir, "etc/udev/rules.d")}, + {"mount", "--bind", filepath.Join(writableTmpDir, "system-data/var/lib/extrausers"), filepath.Join(preseedTmpDir, "var/lib/extrausers")}, + {"mount", "--bind", filepath.Join(targetSnapdRoot, "/usr/lib/snapd"), filepath.Join(preseedTmpDir, "usr/lib/snapd")}, + {"mount", "--bind", filepath.Join(tmpDir, "system-seed"), filepath.Join(preseedTmpDir, "var/lib/snapd/seed")}, + } + + expectedUmountCalls := [][]string{ + {"umount", filepath.Join(preseedTmpDir, "var/lib/snapd/seed")}, + {"umount", filepath.Join(preseedTmpDir, "usr/lib/snapd")}, + {"umount", filepath.Join(preseedTmpDir, "var/lib/extrausers")}, + {"umount", filepath.Join(preseedTmpDir, "etc/udev/rules.d")}, + {"umount", filepath.Join(preseedTmpDir, "etc/dbus-1")}, + {"umount", filepath.Join(preseedTmpDir, "etc/systemd")}, + {"umount", filepath.Join(preseedTmpDir, "snap")}, + {"umount", filepath.Join(preseedTmpDir, "var/snap")}, + {"umount", filepath.Join(preseedTmpDir, "var/cache/apparmor")}, + {"umount", filepath.Join(preseedTmpDir, "var/cache/snapd")}, + {"umount", filepath.Join(preseedTmpDir, "var/lib/snapd")}, + {"umount", filepath.Join(preseedTmpDir, "writable")}, + {"umount", filepath.Join(preseedTmpDir, "sys/kernel/security")}, + {"umount", filepath.Join(preseedTmpDir, "dev")}, + {"umount", filepath.Join(preseedTmpDir, "sys")}, + {"umount", filepath.Join(preseedTmpDir, "proc")}, + {"umount", filepath.Join(preseedTmpDir, "tmp")}, + {"umount", filepath.Join(preseedTmpDir, "var/tmp")}, + {"umount", filepath.Join(preseedTmpDir, "run")}, + {"umount", filepath.Join(tmpDir, "target-core-mounted-here")}, + // from handle-writable-paths + {"umount", filepath.Join(preseedTmpDir, "somepath")}, + {"umount", preseedTmpDir}, + } + + if customAppArmorFeaturesDir != "" { + expectedMountCalls = append(expectedMountCalls, []string{"mount", "--bind", "/custom-aa-features", filepath.Join(preseedTmpDir, "sys/kernel/security/apparmor/features")}) + // order of umounts is reversed, prepend + expectedUmountCalls = append([][]string{{"umount", filepath.Join(preseedTmpDir, "/sys/kernel/security/apparmor/features")}}, expectedUmountCalls...) + } + c.Check(mockMountCmd.Calls(), DeepEquals, expectedMountCalls) + + c.Check(mockTar.Calls(), DeepEquals, [][]string{ + {"tar", "-czf", filepath.Join(tmpDir, "system-seed/systems/20220203/preseed.tgz"), "-p", "-C", + filepath.Join(writableTmpDir, "system-data"), "--exclude", "foo", "etc/bar/a", "etc/bar/b"}, + }) + + c.Check(mockUmountCmd.Calls(), DeepEquals, expectedUmountCalls) + + // validity check; -1 to account for handle-writable-paths mock which doesn’t trigger mount in the test + c.Check(len(mockMountCmd.Calls()), Equals, len(mockUmountCmd.Calls())-1) + + preseedAssertionPath := filepath.Join(tmpDir, "system-seed/systems/20220203/preseed") + r, err := os.Open(preseedAssertionPath) + c.Assert(err, IsNil) + defer r.Close() + + // check directory targetSnapdRoot was deleted + _, err = os.Stat(targetSnapdRoot) + c.Assert(err, NotNil) + c.Check(os.IsNotExist(err), Equals, true) + + seen := make(map[string]bool) + dec := asserts.NewDecoder(r) + for { + as, err := dec.Decode() + if err == io.EOF { + break + } + c.Assert(err, IsNil) + + tpe := as.Type().Name + + switch as.Type() { + case asserts.AccountKeyType: + acckeyAs := as.(*asserts.AccountKey) + tpe = fmt.Sprintf("%s:%s", as.Type().Name, acckeyAs.AccountID()) + case asserts.PreseedType: + preseedAs := as.(*asserts.Preseed) + c.Check(preseedAs.Revision(), Equals, 0) + c.Check(preseedAs.Series(), Equals, "16") + c.Check(preseedAs.AuthorityID(), Equals, "my-brand") + c.Check(preseedAs.BrandID(), Equals, "my-brand") + c.Check(preseedAs.Model(), Equals, "my-model-uc20") + c.Check(preseedAs.SystemLabel(), Equals, "20220203") + c.Check(preseedAs.ArtifactSHA3_384(), Equals, "g7_yjd4bG_WBAHHGZDwI5bBb24Nu_9cLQD6o6gpjTcSZfrEFOqNZP1kPnGNjDdkL") + c.Check(preseedAs.Snaps(), DeepEquals, []*asserts.PreseedSnap{{ + Name: "snapd", + SnapID: "snapdidididididididididididididd", + Revision: 1, + }, { + Name: "foo", + }}) + default: + c.Fatalf("unexpected assertion: %s", as.Type().Name) + } + seen[tpe] = true + } + + c.Check(seen, DeepEquals, map[string]bool{ + "account-key:my-brand": true, + "preseed": true, + }) +} + +func (s *preseedSuite) TestRunPreseedUC20Happy(c *C) { + s.testRunPreseedUC20Happy(c, "") +} + +func (s *preseedSuite) TestRunPreseedUC20HappyCustomApparmorFeaturesDir(c *C) { + s.testRunPreseedUC20Happy(c, "/custom-aa-features") +} + +func (s *preseedSuite) TestRunPreseedUC20ExecFormatError(c *C) { + tmpdir := c.MkDir() + + // Mock an exec-format error - the first thing that runUC20PreseedMode + // does is start snapd in a chroot. So we can override the "chroot" + // call with a simulated exec format error to simulate the error a + // user would get when running preseeding on a architecture that is + // not the image target architecture. + mockChrootCmd := testutil.MockCommand(c, "chroot", "") + defer mockChrootCmd.Restore() + err := ioutil.WriteFile(mockChrootCmd.Exe(), []byte("invalid-exe"), 0755) + c.Check(err, IsNil) + + opts := &preseed.PreseedOpts{PreseedChrootDir: tmpdir} + err = preseed.RunUC20PreseedMode(opts) + c.Check(err, ErrorMatches, `error running snapd, please try installing the "qemu-user-static" package: fork/exec .* exec format error`) +} diff -Nru snapd-2.55.5+20.04/image/preseed/reset.go snapd-2.57.5+20.04/image/preseed/reset.go --- snapd-2.55.5+20.04/image/preseed/reset.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/image/preseed/reset.go 2022-10-17 16:25:18.000000000 +0000 @@ -31,9 +31,59 @@ apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" ) +// bash-completion symlinks; note there are symlinks that point at +// completer, and symlinks that point at the completer symlinks. +// e.g. +// lxd.lxc -> /snap/core/current/usr/lib/snapd/complete.sh +// lxc -> lxd.lxc +func resetCompletionSymlinks(completersPath string) error { + files, err := ioutil.ReadDir(completersPath) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("error reading %s: %v", completersPath, err) + } + completeShSymlinks := make(map[string]string) + var otherSymlinks []string + + // pass 1: find all symlinks pointing at complete.sh + for _, fileInfo := range files { + if fileInfo.Mode()&os.ModeSymlink == 0 { + continue + } + fullPath := filepath.Join(completersPath, fileInfo.Name()) + if dirs.IsCompleteShSymlink(fullPath) { + if err := os.Remove(fullPath); err != nil { + return fmt.Errorf("error removing symlink %s: %v", fullPath, err) + } + completeShSymlinks[fileInfo.Name()] = fullPath + } else { + otherSymlinks = append(otherSymlinks, fullPath) + } + } + // pass 2: find all symlinks that point at the symlinks found in pass 1. + for _, other := range otherSymlinks { + target, err := os.Readlink(other) + if err != nil { + return fmt.Errorf("error reading symlink target of %s: %v", other, err) + } + if _, ok := completeShSymlinks[target]; ok { + if err := os.Remove(other); err != nil { + return fmt.Errorf("error removing symlink %s: %v", other, err) + } + } + } + + return nil +} + // ResetPreseededChroot removes all preseeding artifacts from preseedChroot // (classic Ubuntu only). func ResetPreseededChroot(preseedChroot string) error { + var err error + preseedChroot, err = filepath.Abs(preseedChroot) + if err != nil { + return err + } + exists, isDir, err := osutil.DirExists(preseedChroot) if err != nil { return fmt.Errorf("cannot reset %q: %v", preseedChroot, err) @@ -124,43 +174,9 @@ } } - // bash-completion symlinks; note there are symlinks that point at - // completer, and symlinks that point at the completer symlinks. - // e.g. - // lxd.lxc -> /snap/core/current/usr/lib/snapd/complete.sh - // lxc -> lxd.lxc - files, err := ioutil.ReadDir(filepath.Join(preseedChroot, dirs.CompletersDir)) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("error reading %s: %v", dirs.CompletersDir, err) - } - completeShSymlinks := make(map[string]string) - var otherSymlinks []string - - // pass 1: find all symlinks pointing at complete.sh - for _, fileInfo := range files { - if fileInfo.Mode()&os.ModeSymlink == 0 { - continue - } - fullPath := filepath.Join(preseedChroot, dirs.CompletersDir, fileInfo.Name()) - if dirs.IsCompleteShSymlink(fullPath) { - if err := os.Remove(fullPath); err != nil { - return fmt.Errorf("error removing symlink %s: %v", fullPath, err) - } - completeShSymlinks[fileInfo.Name()] = fullPath - } else { - otherSymlinks = append(otherSymlinks, fullPath) - } - } - // pass 2: find all symlinks that point at the symlinks found in pass 1. - for _, other := range otherSymlinks { - target, err := os.Readlink(other) - if err != nil { - return fmt.Errorf("error reading symlink target of %s: %v", other, err) - } - if _, ok := completeShSymlinks[target]; ok { - if err := os.Remove(other); err != nil { - return fmt.Errorf("error removing symlink %s: %v", other, err) - } + for _, completersPath := range []string{dirs.CompletersDir, dirs.LegacyCompletersDir} { + if err := resetCompletionSymlinks(filepath.Join(preseedChroot, completersPath)); err != nil { + return err } } diff -Nru snapd-2.55.5+20.04/interfaces/apparmor/apparmor.go snapd-2.57.5+20.04/interfaces/apparmor/apparmor.go --- snapd-2.55.5+20.04/interfaces/apparmor/apparmor.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/apparmor/apparmor.go 2022-10-17 16:25:18.000000000 +0000 @@ -27,15 +27,7 @@ import ( "fmt" - "io" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" "strings" - - "github.com/snapcore/snapd/osutil" ) // ValidateNoAppArmorRegexp will check that the given string does not @@ -50,163 +42,3 @@ } return nil } - -type aaParserFlags int - -const ( - // skipReadCache causes apparmor_parser to be invoked with --skip-read-cache. - // This allows us to essentially overwrite a cache that we know is stale regardless - // of the time and date settings (apparmor_parser caching is based on mtime). - // Note that writing of the cache relies on --write-cache but we pass that - // command-line option unconditionally. - skipReadCache aaParserFlags = 1 << iota - - // conserveCPU tells apparmor_parser to spare up to two CPUs on multi-core systems to - // reduce load when processing many profiles at once. - conserveCPU aaParserFlags = 1 << iota - - // skipKernelLoad tells apparmor_parser not to load profiles into the kernel. The use - // case of this is when in pre-seeding mode. - skipKernelLoad aaParserFlags = 1 << iota -) - -var runtimeNumCPU = runtime.NumCPU - -func maybeSetNumberOfJobs() string { - cpus := runtimeNumCPU() - // Do not use all CPUs as this may have negative impact when booting. - if cpus > 2 { - // otherwise spare 2 - cpus = cpus - 2 - } else { - // Systems with only two CPUs, spare 1. - // - // When there is a a single CPU, pass -j1 to allow a single - // compilation job only. Note, we could pass -j0 in such case - // for further improvement, but that has incompatible meaning - // between apparmor 2.x (automatic job count, equivalent to - // -jauto) and 3.x (compile everything in the main process). - cpus = 1 - } - - return fmt.Sprintf("-j%d", cpus) -} - -// loadProfiles loads apparmor profiles from the given files. -// -// If no such profiles were previously loaded then they are simply added to the kernel. -// If there were some profiles with the same name before, those profiles are replaced. -func loadProfiles(fnames []string, cacheDir string, flags aaParserFlags) error { - if len(fnames) == 0 { - return nil - } - - // Use no-expr-simplify since expr-simplify is actually slower on armhf (LP: #1383858) - args := []string{"--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s", cacheDir)} - if flags&conserveCPU != 0 { - if jobArg := maybeSetNumberOfJobs(); jobArg != "" { - args = append(args, jobArg) - } - } - - if flags&skipKernelLoad != 0 { - args = append(args, "--skip-kernel-load") - } - - if flags&skipReadCache != 0 { - args = append(args, "--skip-read-cache") - } - if !osutil.GetenvBool("SNAPD_DEBUG") { - args = append(args, "--quiet") - } - args = append(args, fnames...) - - output, err := exec.Command("apparmor_parser", args...).CombinedOutput() - if err != nil { - return fmt.Errorf("cannot load apparmor profiles: %s\napparmor_parser output:\n%s", err, string(output)) - } - return nil -} - -// unloadProfiles is meant to remove the named profiles from the running -// kernel and then remove any cache files. Importantly, we can only unload -// profiles when we are sure there are no lingering processes from the snap -// (ie, forcibly stop all running processes from the snap). Otherwise, any -// running processes will become unconfined. Since we don't have this guarantee -// yet, leave the profiles loaded in the kernel but remove the cache files from -// the system so the policy is gone on the next reboot. LP: #1818241 -func unloadProfiles(names []string, cacheDir string) error { - if len(names) == 0 { - return nil - } - - /* TODO: uncomment when no lingering snap processes is guaranteed - // By the time this function is called, all the profiles (names) have - // been removed from dirs.SnapAppArmorDir, so to unload the profiles - // from the running kernel we must instead use sysfs and write the - // profile names one at a time to - // /sys/kernel/security/apparmor/.remove (with no trailing \n). - apparmorSysFsRemove := "/sys/kernel/security/apparmor/.remove" - if !osutil.IsWritable(appArmorSysFsRemove) { - return fmt.Errorf("cannot unload apparmor profile: %s does not exist\n", appArmorSysFsRemove) - } - for _, n := range names { - // ignore errors since it is ok if the profile isn't removed - // from the kernel - ioutil.WriteFile(appArmorSysFsRemove, []byte(n), 0666) - } - */ - - // AppArmor 2.13 and higher has a cache forest while 2.12 and lower has - // a flat directory (on 2.12 and earlier, .features and the snap - // profiles are in the top-level directory instead of a subdirectory). - // With 2.13+, snap profiles are not expected to be in every - // subdirectory, so don't error on ENOENT but otherwise if we get an - // error, something weird happened so stop processing. - if li, err := filepath.Glob(filepath.Join(cacheDir, "*/.features")); err == nil && len(li) > 0 { // 2.13+ - for _, p := range li { - dir := path.Dir(p) - if err := osutil.UnlinkMany(dir, names); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("cannot remove apparmor profile cache in %s: %s", dir, err) - } - } - } else if err := osutil.UnlinkMany(cacheDir, names); err != nil && !os.IsNotExist(err) { // 2.12- - return fmt.Errorf("cannot remove apparmor profile cache: %s", err) - } - return nil -} - -// profilesPath contains information about the currently loaded apparmor profiles. -const realProfilesPath = "/sys/kernel/security/apparmor/profiles" - -var profilesPath = realProfilesPath - -// LoadedProfiles interrogates the kernel and returns a list of loaded apparmor profiles. -// -// Snappy manages apparmor profiles named "snap.*". Other profiles might exist on -// the system (via snappy dimension) and those are filtered-out. -func LoadedProfiles() ([]string, error) { - file, err := os.Open(profilesPath) - if err != nil { - return nil, err - } - defer file.Close() - var profiles []string - for { - var name, mode string - n, err := fmt.Fscanf(file, "%s %s\n", &name, &mode) - if n > 0 && n != 2 { - return nil, fmt.Errorf("syntax error, expected: name (mode)") - } - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - if strings.HasPrefix(name, "snap.") { - profiles = append(profiles, name) - } - } - return profiles, nil -} diff -Nru snapd-2.55.5+20.04/interfaces/apparmor/apparmor_test.go snapd-2.57.5+20.04/interfaces/apparmor/apparmor_test.go --- snapd-2.55.5+20.04/interfaces/apparmor/apparmor_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/apparmor/apparmor_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,18 +20,11 @@ package apparmor_test import ( - "io/ioutil" - "os" - "path" - "path/filepath" "testing" . "gopkg.in/check.v1" - "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces/apparmor" - "github.com/snapcore/snapd/osutil" - apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" "github.com/snapcore/snapd/testutil" ) @@ -41,195 +34,34 @@ type appArmorSuite struct { testutil.BaseTest - profilesFilename string } var _ = Suite(&appArmorSuite{}) func (s *appArmorSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) - // Mock the list of profiles in the running kernel - s.profilesFilename = path.Join(c.MkDir(), "profiles") - apparmor.MockProfilesPath(&s.BaseTest, s.profilesFilename) } -// Tests for LoadProfiles() - -func (s *appArmorSuite) TestLoadProfilesRunsAppArmorParserReplace(c *C) { - cmd := testutil.MockCommand(c, "apparmor_parser", "") - defer cmd.Restore() - err := apparmor.LoadProfiles([]string{"/path/to/snap.samba.smbd"}, apparmor_sandbox.CacheDir, 0) - c.Assert(err, IsNil) - c.Assert(cmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", "--cache-loc=/var/cache/apparmor", "--quiet", "/path/to/snap.samba.smbd"}, - }) -} - -func (s *appArmorSuite) TestLoadProfilesMany(c *C) { - cmd := testutil.MockCommand(c, "apparmor_parser", "") - defer cmd.Restore() - err := apparmor.LoadProfiles([]string{"/path/to/snap.samba.smbd", "/path/to/another.profile"}, apparmor_sandbox.CacheDir, 0) - c.Assert(err, IsNil) - c.Assert(cmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", "--cache-loc=/var/cache/apparmor", "--quiet", "/path/to/snap.samba.smbd", "/path/to/another.profile"}, - }) -} - -func (s *appArmorSuite) TestLoadProfilesNone(c *C) { - cmd := testutil.MockCommand(c, "apparmor_parser", "") - defer cmd.Restore() - err := apparmor.LoadProfiles([]string{}, apparmor_sandbox.CacheDir, 0) - c.Assert(err, IsNil) - c.Check(cmd.Calls(), HasLen, 0) -} - -func (s *appArmorSuite) TestLoadProfilesReportsErrors(c *C) { - cmd := testutil.MockCommand(c, "apparmor_parser", "exit 42") - defer cmd.Restore() - err := apparmor.LoadProfiles([]string{"/path/to/snap.samba.smbd"}, apparmor_sandbox.CacheDir, 0) - c.Assert(err.Error(), Equals, `cannot load apparmor profiles: exit status 42 -apparmor_parser output: -`) - c.Assert(cmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", "--cache-loc=/var/cache/apparmor", "--quiet", "/path/to/snap.samba.smbd"}, - }) -} - -func (s *appArmorSuite) TestLoadProfilesRunsAppArmorParserReplaceWithSnapdDebug(c *C) { - os.Setenv("SNAPD_DEBUG", "1") - defer os.Unsetenv("SNAPD_DEBUG") - cmd := testutil.MockCommand(c, "apparmor_parser", "") - defer cmd.Restore() - err := apparmor.LoadProfiles([]string{"/path/to/snap.samba.smbd"}, apparmor_sandbox.CacheDir, 0) - c.Assert(err, IsNil) - c.Assert(cmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", "--cache-loc=/var/cache/apparmor", "/path/to/snap.samba.smbd"}, - }) -} - -// Tests for Profile.Unload() - -func (s *appArmorSuite) TestUnloadProfilesMany(c *C) { - err := apparmor.UnloadProfiles([]string{"/path/to/snap.samba.smbd", "/path/to/another.profile"}, apparmor_sandbox.CacheDir) - c.Assert(err, IsNil) -} - -func (s *appArmorSuite) TestUnloadProfilesNone(c *C) { - err := apparmor.UnloadProfiles([]string{}, apparmor_sandbox.CacheDir) - c.Assert(err, IsNil) -} - -func (s *appArmorSuite) TestUnloadRemovesCachedProfile(c *C) { - cmd := testutil.MockCommand(c, "apparmor_parser", "") - defer cmd.Restore() - - dirs.SetRootDir(c.MkDir()) - defer dirs.SetRootDir("") - err := os.MkdirAll(apparmor_sandbox.CacheDir, 0755) - c.Assert(err, IsNil) - - fname := filepath.Join(apparmor_sandbox.CacheDir, "profile") - ioutil.WriteFile(fname, []byte("blob"), 0600) - err = apparmor.UnloadProfiles([]string{"profile"}, apparmor_sandbox.CacheDir) - c.Assert(err, IsNil) - _, err = os.Stat(fname) - c.Check(os.IsNotExist(err), Equals, true) -} - -func (s *appArmorSuite) TestUnloadRemovesCachedProfileInForest(c *C) { - cmd := testutil.MockCommand(c, "apparmor_parser", "") - defer cmd.Restore() - - dirs.SetRootDir(c.MkDir()) - defer dirs.SetRootDir("") - err := os.MkdirAll(apparmor_sandbox.CacheDir, 0755) - c.Assert(err, IsNil) - // mock the forest subdir and features file - subdir := filepath.Join(apparmor_sandbox.CacheDir, "deadbeef.0") - err = os.MkdirAll(subdir, 0700) - c.Assert(err, IsNil) - features := filepath.Join(subdir, ".features") - ioutil.WriteFile(features, []byte("blob"), 0644) - - fname := filepath.Join(subdir, "profile") - ioutil.WriteFile(fname, []byte("blob"), 0600) - err = apparmor.UnloadProfiles([]string{"profile"}, apparmor_sandbox.CacheDir) - c.Assert(err, IsNil) - _, err = os.Stat(fname) - c.Check(os.IsNotExist(err), Equals, true) - c.Check(osutil.FileExists(features), Equals, true) -} - -// Tests for LoadedProfiles() - -func (s *appArmorSuite) TestLoadedApparmorProfilesReturnsErrorOnMissingFile(c *C) { - profiles, err := apparmor.LoadedProfiles() - c.Assert(err, ErrorMatches, "open .*: no such file or directory") - c.Check(profiles, IsNil) -} - -func (s *appArmorSuite) TestLoadedApparmorProfilesCanParseEmptyFile(c *C) { - ioutil.WriteFile(s.profilesFilename, []byte(""), 0600) - profiles, err := apparmor.LoadedProfiles() - c.Assert(err, IsNil) - c.Check(profiles, HasLen, 0) -} - -func (s *appArmorSuite) TestLoadedApparmorProfilesParsesAndFiltersData(c *C) { - ioutil.WriteFile(s.profilesFilename, []byte( - // The output contains some of the snappy-specific elements - // and some non-snappy elements pulled from Ubuntu 16.04 desktop - // - // The pi2-piglow.{background,foreground}.snap entries are the only - // ones that should be reported by the function. - `/sbin/dhclient (enforce) -/usr/bin/ubuntu-core-launcher (enforce) -/usr/bin/ubuntu-core-launcher (enforce) -/usr/lib/NetworkManager/nm-dhcp-client.action (enforce) -/usr/lib/NetworkManager/nm-dhcp-helper (enforce) -/usr/lib/connman/scripts/dhclient-script (enforce) -/usr/lib/lightdm/lightdm-guest-session (enforce) -/usr/lib/lightdm/lightdm-guest-session//chromium (enforce) -/usr/lib/telepathy/telepathy-* (enforce) -/usr/lib/telepathy/telepathy-*//pxgsettings (enforce) -/usr/lib/telepathy/telepathy-*//sanitized_helper (enforce) -snap.pi2-piglow.background (enforce) -snap.pi2-piglow.foreground (enforce) -webbrowser-app (enforce) -webbrowser-app//oxide_helper (enforce) -`), 0600) - profiles, err := apparmor.LoadedProfiles() - c.Assert(err, IsNil) - c.Check(profiles, DeepEquals, []string{ - "snap.pi2-piglow.background", - "snap.pi2-piglow.foreground", - }) -} - -func (s *appArmorSuite) TestLoadedApparmorProfilesHandlesParsingErrors(c *C) { - ioutil.WriteFile(s.profilesFilename, []byte("broken stuff here\n"), 0600) - profiles, err := apparmor.LoadedProfiles() - c.Assert(err, ErrorMatches, "newline in format does not match input") - c.Check(profiles, IsNil) - ioutil.WriteFile(s.profilesFilename, []byte("truncated"), 0600) - profiles, err = apparmor.LoadedProfiles() - c.Assert(err, ErrorMatches, `syntax error, expected: name \(mode\)`) - c.Check(profiles, IsNil) -} - -func (s *appArmorSuite) TestMaybeSetNumberOfJobs(c *C) { - var cpus int - restore := apparmor.MockRuntimeNumCPU(func() int { - return cpus - }) - defer restore() - - cpus = 10 - c.Check(apparmor.MaybeSetNumberOfJobs(), Equals, "-j8") - - cpus = 2 - c.Check(apparmor.MaybeSetNumberOfJobs(), Equals, "-j1") - - cpus = 1 - c.Check(apparmor.MaybeSetNumberOfJobs(), Equals, "-j1") +func (s *appArmorSuite) TestValidateNoAppArmorRegexp(c *C) { + for _, testData := range []struct { + inputString string + expectedError string + }{ + {"", ""}, + {"This is f1ne!", ""}, + {"No questions?", `"No questions\?" contains a reserved apparmor char.*`}, + {"Brackets[]", `"Brackets\[\]" contains a reserved apparmor char.*`}, + {"Braces{}", `"Braces{}" contains a reserved apparmor char.*`}, + {"Star*", `"Star\*" contains a reserved apparmor char.*`}, + {"hat^", `"hat\^" contains a reserved apparmor char.*`}, + {`double"quotes`, `"double\\"quotes" contains a reserved apparmor char.*`}, + } { + testLabel := Commentf("input: %s", testData.inputString) + err := apparmor.ValidateNoAppArmorRegexp(testData.inputString) + if testData.expectedError != "" { + c.Check(err, ErrorMatches, testData.expectedError, testLabel) + } else { + c.Check(err, IsNil, testLabel) + } + } } diff -Nru snapd-2.55.5+20.04/interfaces/apparmor/backend.go snapd-2.57.5+20.04/interfaces/apparmor/backend.go --- snapd-2.55.5+20.04/interfaces/apparmor/backend.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/apparmor/backend.go 2022-10-17 16:25:18.000000000 +0000 @@ -65,6 +65,8 @@ isRootWritableOverlay = osutil.IsRootWritableOverlay kernelFeatures = apparmor_sandbox.KernelFeatures parserFeatures = apparmor_sandbox.ParserFeatures + loadProfiles = apparmor_sandbox.LoadProfiles + unloadProfiles = apparmor_sandbox.UnloadProfiles // make sure that apparmor profile fulfills the late discarding backend // interface @@ -207,12 +209,11 @@ return nil } - aaFlags := skipReadCache + aaFlags := apparmor_sandbox.SkipReadCache if b.preseed { - aaFlags |= skipKernelLoad + aaFlags |= apparmor_sandbox.SkipKernelLoad } - // We are not using apparmor.LoadProfiles() because it uses other cache. if err := loadProfiles([]string{profilePath}, apparmor_sandbox.SystemCacheDir, aaFlags); err != nil { // When we cannot reload the profile then let's remove the generated // policy. Maybe we have caused the problem so it's better to let other @@ -334,9 +335,9 @@ pathnames[i] = filepath.Join(dir, profile) } - var aaFlags aaParserFlags + var aaFlags apparmor_sandbox.AaParserFlags if b.preseed { - aaFlags = skipKernelLoad + aaFlags = apparmor_sandbox.SkipKernelLoad } errReload := loadProfiles(pathnames, cache, aaFlags) errUnload := unloadProfiles(removed, cache) @@ -411,6 +412,9 @@ // Add snippets derived from the layout definition. spec.(*Specification).AddLayout(snapInfo) + // Add additional mount layouts rules for the snap. + spec.(*Specification).AddExtraLayouts(snapInfo, opts.ExtraLayouts) + // core on classic is special if snapName == "core" && release.OnClassic && apparmor_sandbox.ProbedLevel() != apparmor_sandbox.Unsupported { if err := b.setupSnapConfineReexec(snapInfo); err != nil { @@ -500,9 +504,9 @@ // work despite time being wrong (e.g. in the past). For more details see // https://forum.snapcraft.io/t/apparmor-profile-caching/1268/18 var errReloadChanged error - aaFlags := skipReadCache + aaFlags := apparmor_sandbox.SkipReadCache if b.preseed { - aaFlags |= skipKernelLoad + aaFlags |= apparmor_sandbox.SkipKernelLoad } timings.Run(tm, "load-profiles[changed]", fmt.Sprintf("load changed security profiles of snap %q", snapInfo.InstanceName()), func(nesttm timings.Measurer) { errReloadChanged = loadProfiles(prof.changed, apparmor_sandbox.CacheDir, aaFlags) @@ -514,7 +518,7 @@ var errReloadOther error aaFlags = 0 if b.preseed { - aaFlags |= skipKernelLoad + aaFlags |= apparmor_sandbox.SkipKernelLoad } timings.Run(tm, "load-profiles[unchanged]", fmt.Sprintf("load unchanged security profiles of snap %q", snapInfo.InstanceName()), func(nesttm timings.Measurer) { errReloadOther = loadProfiles(prof.unchanged, apparmor_sandbox.CacheDir, aaFlags) @@ -552,18 +556,18 @@ } if !fallback { - aaFlags := skipReadCache | conserveCPU + aaFlags := apparmor_sandbox.SkipReadCache | apparmor_sandbox.ConserveCPU if b.preseed { - aaFlags |= skipKernelLoad + aaFlags |= apparmor_sandbox.SkipKernelLoad } var errReloadChanged error timings.Run(tm, "load-profiles[changed-many]", fmt.Sprintf("load changed security profiles of %d snaps", len(snaps)), func(nesttm timings.Measurer) { errReloadChanged = loadProfiles(allChangedPaths, apparmor_sandbox.CacheDir, aaFlags) }) - aaFlags = conserveCPU + aaFlags = apparmor_sandbox.ConserveCPU if b.preseed { - aaFlags |= skipKernelLoad + aaFlags |= apparmor_sandbox.SkipKernelLoad } var errReloadOther error timings.Run(tm, "load-profiles[unchanged-many]", fmt.Sprintf("load unchanged security profiles %d snaps", len(snaps)), func(nesttm timings.Measurer) { @@ -901,6 +905,11 @@ snippet := strings.Replace(overlayRootSnippet, "###UPPERDIR###", overlayRoot, -1) tagSnippets += snippet } + + // Add core specific snippets when not on classic + if !release.OnClassic { + tagSnippets += coreSnippet + } } if !ignoreSnippets { diff -Nru snapd-2.55.5+20.04/interfaces/apparmor/backend_test.go snapd-2.57.5+20.04/interfaces/apparmor/backend_test.go --- snapd-2.55.5+20.04/interfaces/apparmor/backend_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/apparmor/backend_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package apparmor_test import ( + "errors" "fmt" "io/ioutil" "os" @@ -45,13 +46,27 @@ "github.com/snapcore/snapd/timings" ) +type loadProfilesParams struct { + fnames []string + cacheDir string + flags apparmor_sandbox.AaParserFlags +} + +type unloadProfilesParams struct { + fnames []string + cacheDir string +} + type backendSuite struct { ifacetest.BackendSuite - parserCmd *testutil.MockCmd - perf *timings.Timings meas *timings.Span + + loadProfilesCalls []loadProfilesParams + loadProfilesReturn error + unloadProfilesCalls []unloadProfilesParams + unloadProfilesReturn error } var _ = Suite(&backendSuite{}) @@ -63,98 +78,6 @@ {Classic: true}, } -// fakeAppAprmorParser contains shell program that creates fake binary cache entries -// in accordance with what real apparmor_parser would do. -const fakeAppArmorParser = ` -cache_dir="" -profile="" -write="" -while [ -n "$1" ]; do - case "$1" in - --cache-loc=*) - cache_dir="$(echo "$1" | cut -d = -f 2)" || exit 1 - ;; - --write-cache) - write=yes - ;; - --quiet|--replace|--remove) - # Ignore - ;; - -O) - # Ignore, discard argument - shift - ;; - *) - profile=$(basename "$1") - ;; - esac - shift -done -if [ "$write" = yes ]; then - echo fake > "$cache_dir/$profile" -fi -` - -// permanently broken apparmor parser -const fakeBrokenAppArmorParser = ` -echo "permanent failure" -exit 1 -` - -// apparmor parser that fails when processing more than 3 profiles, i.e. -// when reloading profiles in a batch, but succeeds when run for individual -// runs for snaps with less profiles. -const fakeFailingAppArmorParser = ` -profiles="0" -while [ -n "$1" ]; do - case "$1" in - --cache-loc=*) - ;; - --write-cache|--quiet|--replace|--remove|-j*) - ;; - -O) - # Ignore, discard argument - shift - ;; - *) - profiles=$(( profiles + 1 )) - ;; - esac - shift -done -if [ "$profiles" -gt 3 ]; then - echo "batch failure ($profiles profiles)" - exit 1 -fi -` - -// apparmor parser that fails on snap.samba.smbd profile -const fakeFailingAppArmorParserOneProfile = ` -profile="" -while [ -n "$1" ]; do - case "$1" in - --cache-loc=*) - # Ignore - ;; - --quiet|--replace|--remove|--write-cache|-j*) - # Ignore - ;; - -O) - # Ignore, discard argument - shift - ;; - *) - profile=$(basename "$1") - if [ "$profile" = "snap.samba.smbd" ]; then - echo "failure: $profile" - exit 1 - fi - ;; - esac - shift -done -` - func (s *backendSuite) SetUpTest(c *C) { s.Backend = &apparmor.Backend{} s.BackendSuite.SetUpTest(c) @@ -165,23 +88,38 @@ err := os.MkdirAll(apparmor_sandbox.CacheDir, 0700) c.Assert(err, IsNil) - // Mock away any real apparmor interaction - s.parserCmd = testutil.MockCommand(c, "apparmor_parser", fakeAppArmorParser) - apparmor.MockRuntimeNumCPU(func() int { return 99 }) restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"}) s.AddCleanup(restore) restore = apparmor_sandbox.MockFeatures(nil, nil, nil, nil) s.AddCleanup(restore) + s.loadProfilesCalls = nil + s.loadProfilesReturn = nil + s.unloadProfilesCalls = nil + s.unloadProfilesReturn = nil + restore = apparmor.MockLoadProfiles(func(fnames []string, cacheDir string, flags apparmor_sandbox.AaParserFlags) error { + // To simplify testing, ignore invocations with no profiles (as a + // matter of fact, the real implementation is doing the same) + if len(fnames) == 0 { + return nil + } + s.loadProfilesCalls = append(s.loadProfilesCalls, loadProfilesParams{fnames, cacheDir, flags}) + return s.loadProfilesReturn + }) + s.AddCleanup(restore) + restore = apparmor.MockUnloadProfiles(func(fnames []string, cacheDir string) error { + s.unloadProfilesCalls = append(s.unloadProfilesCalls, unloadProfilesParams{fnames, cacheDir}) + return s.unloadProfilesReturn + }) + s.AddCleanup(restore) + err = s.Backend.Initialize(ifacetest.DefaultInitializeOpts) c.Assert(err, IsNil) } func (s *backendSuite) TearDownTest(c *C) { - s.parserCmd.Restore() - s.BackendSuite.TearDownTest(c) } @@ -366,8 +304,8 @@ _, err := os.Stat(profile) c.Check(err, IsNil) // apparmor_parser was used to load that file - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--skip-read-cache", "--quiet", updateNSProfile, profile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{updateNSProfile, profile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.SkipReadCache}, }) } @@ -380,8 +318,8 @@ _, err := os.Stat(profile) c.Check(err, IsNil) // apparmor_parser was used to load that file - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--skip-read-cache", "--quiet", updateNSProfile, profile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{updateNSProfile, profile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.SkipReadCache}, }) } @@ -406,8 +344,8 @@ c.Check(err, IsNil) // TODO: check for layout snippets inside the generated file once we have some snippets to check for. // apparmor_parser was used to load them - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--skip-read-cache", "--quiet", updateNSProfile, appProfile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{updateNSProfile, appProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.SkipReadCache}, }) } @@ -421,7 +359,7 @@ // any apparmor profiles because there is no executable content that would need // an execution environment and the corresponding mount namespace. s.InstallSnap(c, interfaces.ConfinementOptions{}, "", gadgetYaml, 1) - c.Check(s.parserCmd.Calls(), HasLen, 0) + c.Check(s.loadProfilesCalls, HasLen, 0) } func (s *backendSuite) TestTimings(c *C) { @@ -463,13 +401,13 @@ func (s *backendSuite) TestProfilesAreAlwaysLoaded(c *C) { for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil err := s.Backend.Setup(snapInfo, opts, s.Repo, s.meas) c.Assert(err, IsNil) updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.samba") profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--quiet", updateNSProfile, profile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{updateNSProfile, profile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0}, }) s.RemoveSnap(c, snapInfo) } @@ -478,47 +416,37 @@ func (s *backendSuite) TestRemovingSnapRemovesAndUnloadsProfiles(c *C) { for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) - s.parserCmd.ForgetCalls() + s.unloadProfilesCalls = nil s.RemoveSnap(c, snapInfo) - profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") - // file called "snap.sambda.smbd" was removed - _, err := os.Stat(profile) - c.Check(os.IsNotExist(err), Equals, true) - // apparmor cache file was removed - cache := filepath.Join(apparmor_sandbox.CacheDir, "snap.samba.smbd") - _, err = os.Stat(cache) - c.Check(os.IsNotExist(err), Equals, true) + c.Check(s.unloadProfilesCalls, DeepEquals, []unloadProfilesParams{ + {[]string{"snap-update-ns.samba", "snap.samba.smbd"}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir)}, + }) } } func (s *backendSuite) TestRemovingSnapWithHookRemovesAndUnloadsProfiles(c *C) { for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.HookYaml, 1) - s.parserCmd.ForgetCalls() + s.unloadProfilesCalls = nil s.RemoveSnap(c, snapInfo) - profile := filepath.Join(dirs.SnapAppArmorDir, "snap.foo.hook.configure") - // file called "snap.foo.hook.configure" was removed - _, err := os.Stat(profile) - c.Check(os.IsNotExist(err), Equals, true) - // apparmor cache file was removed - cache := filepath.Join(apparmor_sandbox.CacheDir, "snap.foo.hook.configure") - _, err = os.Stat(cache) - c.Check(os.IsNotExist(err), Equals, true) + c.Check(s.unloadProfilesCalls, DeepEquals, []unloadProfilesParams{ + {[]string{"snap-update-ns.foo", "snap.foo.hook.configure"}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir)}, + }) } } func (s *backendSuite) TestUpdatingSnapMakesNeccesaryChanges(c *C) { for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 2) updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.samba") profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") // apparmor_parser was used to reload the profile because snap revision // is inside the generated policy. - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--skip-read-cache", "--quiet", profile}, - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--quiet", updateNSProfile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{profile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.SkipReadCache}, + {[]string{updateNSProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0}, }) s.RemoveSnap(c, snapInfo) } @@ -527,7 +455,7 @@ func (s *backendSuite) TestUpdatingSnapToOneWithMoreApps(c *C) { for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil // NOTE: the revision is kept the same to just test on the new application being added snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbd, 1) updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.samba") @@ -537,9 +465,9 @@ _, err := os.Stat(nmbdProfile) c.Check(err, IsNil) // apparmor_parser was used to load all the profiles, the nmbd profile is new so we force invalidate its cache (if any). - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--skip-read-cache", "--quiet", nmbdProfile}, - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--quiet", updateNSProfile, smbdProfile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{nmbdProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.SkipReadCache}, + {[]string{updateNSProfile, smbdProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0}, }) s.RemoveSnap(c, snapInfo) } @@ -548,7 +476,7 @@ func (s *backendSuite) TestUpdatingSnapToOneWithMoreHooks(c *C) { for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1WithNmbd, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil // NOTE: the revision is kept the same to just test on the new application being added snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlWithHook, 1) updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.samba") @@ -560,9 +488,9 @@ _, err := os.Stat(hookProfile) c.Check(err, IsNil) // apparmor_parser was used to load all the profiles, the hook profile has changed so we force invalidate its cache. - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--skip-read-cache", "--quiet", hookProfile}, - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--quiet", updateNSProfile, nmbdProfile, smbdProfile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{hookProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.SkipReadCache}, + {[]string{updateNSProfile, nmbdProfile, smbdProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0}, }) s.RemoveSnap(c, snapInfo) } @@ -571,7 +499,7 @@ func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) { for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1WithNmbd, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil // NOTE: the revision is kept the same to just test on the application being removed snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1, 1) updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.samba") @@ -581,8 +509,8 @@ _, err := os.Stat(nmbdProfile) c.Check(os.IsNotExist(err), Equals, true) // apparmor_parser was used to remove the unused profile - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--quiet", updateNSProfile, smbdProfile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{updateNSProfile, smbdProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0}, }) s.RemoveSnap(c, snapInfo) } @@ -591,7 +519,7 @@ func (s *backendSuite) TestUpdatingSnapToOneWithFewerHooks(c *C) { for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlWithHook, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil // NOTE: the revision is kept the same to just test on the application being removed snapInfo = s.UpdateSnap(c, snapInfo, opts, ifacetest.SambaYamlV1WithNmbd, 1) updateNSProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.samba") @@ -603,8 +531,8 @@ _, err := os.Stat(hookProfile) c.Check(os.IsNotExist(err), Equals, true) // apparmor_parser was used to remove the unused profile - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--quiet", updateNSProfile, nmbdProfile, smbdProfile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{updateNSProfile, nmbdProfile, smbdProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0}, }) s.RemoveSnap(c, snapInfo) } @@ -616,7 +544,7 @@ for _, opts := range testedConfinementOpts { snapInfo1 := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) snapInfo2 := s.InstallSnap(c, opts, "", ifacetest.SomeSnapYamlV1, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil setupManyInterface, ok := s.Backend.(interfaces.SecurityBackendSetupMany) c.Assert(ok, Equals, true) err := setupManyInterface.SetupMany([]*snap.Info{snapInfo1, snapInfo2}, func(snapName string) interfaces.ConfinementOptions { return opts }, s.Repo, s.meas) @@ -625,8 +553,8 @@ snap1AAprofile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") snap2nsProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.some-snap") snap2AAprofile := filepath.Join(dirs.SnapAppArmorDir, "snap.some-snap.someapp") - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "-j97", "--quiet", snap1nsProfile, snap1AAprofile, snap2nsProfile, snap2AAprofile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{snap1nsProfile, snap1AAprofile, snap2nsProfile, snap2AAprofile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.ConserveCPU}, }) s.RemoveSnap(c, snapInfo1) s.RemoveSnap(c, snapInfo2) @@ -637,7 +565,7 @@ for _, opts := range testedConfinementOpts { snapInfo1 := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) snapInfo2 := s.InstallSnap(c, opts, "", ifacetest.SomeSnapYamlV1, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil snap1nsProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.samba") snap1AAprofile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") @@ -654,9 +582,9 @@ c.Assert(err, IsNil) // expect two batch executions - one for changed profiles, second for unchanged profiles. - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "-j97", "--skip-read-cache", "--quiet", snap1AAprofile, snap2AAprofile}, - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "-j97", "--quiet", snap1nsProfile, snap2nsProfile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{snap1AAprofile, snap2AAprofile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.SkipReadCache | apparmor_sandbox.ConserveCPU}, + {[]string{snap1nsProfile, snap2nsProfile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.ConserveCPU}, }) s.RemoveSnap(c, snapInfo1) s.RemoveSnap(c, snapInfo2) @@ -664,17 +592,17 @@ } // helper for checking for apparmor parser calls where batch run is expected to fail and is followed by two separate runs for individual snaps. -func (s *backendSuite) checkSetupManyCallsWithFallback(c *C, cmd *testutil.MockCmd) { +func (s *backendSuite) checkSetupManyCallsWithFallback(c *C, invocations []loadProfilesParams) { snap1nsProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.samba") snap1AAprofile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") snap2nsProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.some-snap") snap2AAprofile := filepath.Join(dirs.SnapAppArmorDir, "snap.some-snap.someapp") // We expect three calls to apparmor_parser due to the failure of batch run. First is the failed batch run, followed by succesfull fallback runs. - c.Check(cmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "-j97", "--quiet", snap1nsProfile, snap1AAprofile, snap2nsProfile, snap2AAprofile}, - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--quiet", snap1nsProfile, snap1AAprofile}, - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--quiet", snap2nsProfile, snap2AAprofile}, + c.Check(invocations, DeepEquals, []loadProfilesParams{ + {[]string{snap1nsProfile, snap1AAprofile, snap2nsProfile, snap2AAprofile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), apparmor_sandbox.ConserveCPU}, + {[]string{snap1nsProfile, snap1AAprofile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0}, + {[]string{snap2nsProfile, snap2AAprofile}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0}, }) } @@ -688,23 +616,23 @@ // note, InstallSnap here uses s.parserCmd which mocks happy apparmor_parser snapInfo1 := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) snapInfo2 := s.InstallSnap(c, opts, "", ifacetest.SomeSnapYamlV1, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil setupManyInterface, ok := s.Backend.(interfaces.SecurityBackendSetupMany) c.Assert(ok, Equals, true) // mock apparmor_parser again with a failing one (and restore immediately for the next iteration of the test) - failingParserCmd := testutil.MockCommand(c, "apparmor_parser", fakeBrokenAppArmorParser) + s.loadProfilesReturn = errors.New("apparmor_parser crash") errs := setupManyInterface.SetupMany([]*snap.Info{snapInfo1, snapInfo2}, func(snapName string) interfaces.ConfinementOptions { return opts }, s.Repo, s.meas) - failingParserCmd.Restore() + s.loadProfilesReturn = nil - s.checkSetupManyCallsWithFallback(c, failingParserCmd) + s.checkSetupManyCallsWithFallback(c, s.loadProfilesCalls) // two errors expected: SetupMany failure on multiple snaps falls back to one-by-one apparmor invocations. Both fail on apparmor_parser again and we only see // individual failures. Error from batch run is only logged. c.Assert(errs, HasLen, 2) - c.Check(errs[0], ErrorMatches, ".*cannot setup profiles for snap \"samba\".*\napparmor_parser output:\npermanent failure\n") - c.Check(errs[1], ErrorMatches, ".*cannot setup profiles for snap \"some-snap\".*\napparmor_parser output:\npermanent failure\n") - c.Check(log.String(), Matches, ".*failed to batch-reload unchanged profiles: cannot load apparmor profiles: exit status 1\n.*\n.*\n") + c.Check(errs[0], ErrorMatches, ".*cannot setup profiles for snap \"samba\": apparmor_parser crash") + c.Check(errs[1], ErrorMatches, ".*cannot setup profiles for snap \"some-snap\": apparmor_parser crash") + c.Check(log.String(), Matches, ".*failed to batch-reload unchanged profiles: apparmor_parser crash\n") s.RemoveSnap(c, snapInfo1) s.RemoveSnap(c, snapInfo2) @@ -721,22 +649,31 @@ // note, InstallSnap here uses s.parserCmd which mocks happy apparmor_parser snapInfo1 := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) snapInfo2 := s.InstallSnap(c, opts, "", ifacetest.SomeSnapYamlV1, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil setupManyInterface, ok := s.Backend.(interfaces.SecurityBackendSetupMany) c.Assert(ok, Equals, true) // mock apparmor_parser again with a failing one (and restore immediately for the next iteration of the test) - failingParserCmd := testutil.MockCommand(c, "apparmor_parser", fakeFailingAppArmorParser) + r := apparmor.MockLoadProfiles(func(fnames []string, cacheDir string, flags apparmor_sandbox.AaParserFlags) error { + if len(fnames) == 0 { + return nil + } + s.loadProfilesCalls = append(s.loadProfilesCalls, loadProfilesParams{fnames, cacheDir, flags}) + if len(fnames) > 3 { + return errors.New("some error") + } + return nil + }) errs := setupManyInterface.SetupMany([]*snap.Info{snapInfo1, snapInfo2}, func(snapName string) interfaces.ConfinementOptions { return opts }, s.Repo, s.meas) - failingParserCmd.Restore() + r() - s.checkSetupManyCallsWithFallback(c, failingParserCmd) + s.checkSetupManyCallsWithFallback(c, s.loadProfilesCalls) // no errors expected: error from batch run is only logged, but individual apparmor parser execution as part of the fallback are successful. // note, tnis scenario is unlikely to happen in real life, because if a profile failed in a batch, it would fail when parsed alone too. It is // tested here just to exercise various execution paths. c.Assert(errs, HasLen, 0) - c.Check(log.String(), Matches, ".*failed to batch-reload unchanged profiles: cannot load apparmor profiles: exit status 1\napparmor_parser output:\nbatch failure \\(4 profiles\\)\n") + c.Check(log.String(), Matches, ".*failed to batch-reload unchanged profiles: some error\n") s.RemoveSnap(c, snapInfo1) s.RemoveSnap(c, snapInfo2) @@ -753,22 +690,35 @@ // note, InstallSnap here uses s.parserCmd which mocks happy apparmor_parser snapInfo1 := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) snapInfo2 := s.InstallSnap(c, opts, "", ifacetest.SomeSnapYamlV1, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil setupManyInterface, ok := s.Backend.(interfaces.SecurityBackendSetupMany) c.Assert(ok, Equals, true) // mock apparmor_parser with a failing one - failingParserCmd := testutil.MockCommand(c, "apparmor_parser", fakeFailingAppArmorParserOneProfile) + r := apparmor.MockLoadProfiles(func(fnames []string, cacheDir string, flags apparmor_sandbox.AaParserFlags) error { + if len(fnames) == 0 { + return nil + } + s.loadProfilesCalls = append(s.loadProfilesCalls, loadProfilesParams{fnames, cacheDir, flags}) + // If the profile list contains SAMBA, we fail + for _, profilePath := range fnames { + name := filepath.Base(profilePath) + if name == "snap.samba.smbd" { + return errors.New("fail on samba") + } + } + return nil + }) errs := setupManyInterface.SetupMany([]*snap.Info{snapInfo1, snapInfo2}, func(snapName string) interfaces.ConfinementOptions { return opts }, s.Repo, s.meas) - failingParserCmd.Restore() + r() - s.checkSetupManyCallsWithFallback(c, failingParserCmd) + s.checkSetupManyCallsWithFallback(c, s.loadProfilesCalls) // the batch reload fails because of snap.samba.smbd profile failing - c.Check(log.String(), Matches, ".* failed to batch-reload unchanged profiles: cannot load apparmor profiles: exit status 1\napparmor_parser output:\nfailure: snap.samba.smbd\n") + c.Check(log.String(), Matches, ".* failed to batch-reload unchanged profiles: fail on samba\n") // and we also fail when running that profile in fallback mode c.Assert(errs, HasLen, 1) - c.Assert(errs[0], ErrorMatches, "cannot setup profiles for snap \"samba\": cannot load apparmor profiles: exit status 1\n.*apparmor_parser output:\nfailure: snap.samba.smbd\n") + c.Assert(errs[0], ErrorMatches, "cannot setup profiles for snap \"samba\": fail on samba") s.RemoveSnap(c, snapInfo1) s.RemoveSnap(c, snapInfo2) @@ -1403,8 +1353,8 @@ } `, dirs.SnapMountDir)) - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s", apparmor_sandbox.CacheDir), "--quiet", newAA[0]}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + {[]string{newAA[0]}, fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), 0}, }) // snap-confine directory was created @@ -1422,7 +1372,7 @@ s.writeVanillaSnapConfineProfile(c, snapdInfo) err := s.Backend.Setup(snapdInfo, interfaces.ConfinementOptions{}, s.Repo, s.perf) c.Assert(err, IsNil) - // sanity + // precondition c.Assert(filepath.Join(dirs.SnapAppArmorDir, "snap-confine.snapd.222"), testutil.FilePresent) // place a canary c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapAppArmorDir, "snap-confine.snapd.111"), nil, 0644), IsNil) @@ -1578,10 +1528,6 @@ restore = apparmor.MockIsRootWritableOverlay(func() (string, error) { return "", nil }) defer restore() - // Intercept interaction with apparmor_parser - cmd := testutil.MockCommand(c, "apparmor_parser", "") - defer cmd.Restore() - // Intercept the /proc/self/exe symlink and point it to the distribution // executable (the path doesn't matter as long as it is not from the // mounted core snap). This indicates that snapd is not re-executing @@ -1617,15 +1563,10 @@ c.Assert(fn, testutil.FileContains, "network inet6,") // The system apparmor profile of snap-confine was reloaded. - c.Assert(cmd.Calls(), HasLen, 1) - c.Assert(cmd.Calls(), DeepEquals, [][]string{{ - "apparmor_parser", "--replace", - "--write-cache", - "-O", "no-expr-simplify", - "--cache-loc=" + apparmor_sandbox.SystemCacheDir, - "--skip-read-cache", - "--quiet", - profilePath, + c.Assert(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{{ + []string{profilePath}, + apparmor_sandbox.SystemCacheDir, + apparmor_sandbox.SkipReadCache, }}) } @@ -1758,8 +1699,7 @@ defer restore() // Intercept interaction with apparmor_parser and make it fail. - cmd := testutil.MockCommand(c, "apparmor_parser", "echo testing; exit 1") - defer cmd.Restore() + s.loadProfilesReturn = errors.New("bad luck") // Intercept the /proc/self/exe symlink. fakeExe := filepath.Join(s.RootDir, "fake-proc-self-exe") @@ -1775,7 +1715,7 @@ // Setup generated policy for snap-confine. err = (&apparmor.Backend{}).Initialize(ifacetest.DefaultInitializeOpts) - c.Assert(err, ErrorMatches, "cannot reload snap-confine apparmor profile: .*\n.*\ntesting\n") + c.Assert(err, ErrorMatches, "cannot reload snap-confine apparmor profile: bad luck") // While created the policy file initially we also removed it so that // no side-effects remain. @@ -1784,7 +1724,7 @@ c.Assert(files, HasLen, 0) // We tried to reload the policy. - c.Assert(cmd.Calls(), HasLen, 1) + c.Assert(s.loadProfilesCalls, HasLen, 1) } // Test behavior when MkdirAll fails @@ -1924,9 +1864,6 @@ defer restore() restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil }) defer restore() - // Intercept interaction with apparmor_parser - cmd := testutil.MockCommand(c, "apparmor_parser", "") - defer cmd.Restore() // Intercept the /proc/self/exe symlink and point it to the distribution // executable (the path doesn't matter as long as it is not from the @@ -1963,15 +1900,10 @@ c.Assert(string(data), testutil.Contains, "\"/upper/{,**/}\" r,") // The system apparmor profile of snap-confine was reloaded. - c.Assert(cmd.Calls(), HasLen, 1) - c.Assert(cmd.Calls(), DeepEquals, [][]string{{ - "apparmor_parser", "--replace", - "--write-cache", - "-O", "no-expr-simplify", - "--cache-loc=" + apparmor_sandbox.SystemCacheDir, - "--skip-read-cache", - "--quiet", - profilePath, + c.Assert(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{{ + []string{profilePath}, + apparmor_sandbox.SystemCacheDir, + apparmor_sandbox.SkipReadCache, }}) } @@ -2069,16 +2001,12 @@ if reexec { // The distribution policy was not reloaded because snap-confine executes // from core snap. This is handled separately by per-profile Setup. - c.Assert(cmd.Calls(), HasLen, 0) + c.Assert(s.loadProfilesCalls, HasLen, 0) } else { - c.Assert(cmd.Calls(), DeepEquals, [][]string{{ - "apparmor_parser", "--replace", - "--write-cache", - "-O", "no-expr-simplify", - "--cache-loc=" + apparmor_sandbox.SystemCacheDir, - "--skip-read-cache", - "--quiet", - profilePath, + c.Assert(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{{ + []string{profilePath}, + apparmor_sandbox.SystemCacheDir, + apparmor_sandbox.SkipReadCache, }}) } } @@ -2429,7 +2357,6 @@ } snapInfo := s.InstallSnap(c, tc.opts, "", ifacetest.SambaYamlV1, 1) - s.parserCmd.ForgetCalls() err := s.Backend.Setup(snapInfo, tc.opts, s.Repo, s.meas) c.Assert(err, IsNil) @@ -2567,8 +2494,12 @@ _, err = os.Stat(profile) c.Check(err, IsNil) // apparmor_parser was used to load that file - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "--skip-kernel-load", "--skip-read-cache", "--quiet", updateNSProfile, profile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + { + []string{updateNSProfile, profile}, + fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), + apparmor_sandbox.SkipReadCache | apparmor_sandbox.SkipKernelLoad, + }, }) } @@ -2585,7 +2516,7 @@ for _, opts := range testedConfinementOpts { snapInfo1 := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) snapInfo2 := s.InstallSnap(c, opts, "", ifacetest.SomeSnapYamlV1, 1) - s.parserCmd.ForgetCalls() + s.loadProfilesCalls = nil snap1nsProfile := filepath.Join(dirs.SnapAppArmorDir, "snap-update-ns.samba") snap1AAprofile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") @@ -2602,11 +2533,76 @@ c.Assert(err, IsNil) // expect two batch executions - one for changed profiles, second for unchanged profiles. - c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "-j97", "--skip-kernel-load", "--skip-read-cache", "--quiet", snap1AAprofile, snap2AAprofile}, - {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), "-j97", "--skip-kernel-load", "--quiet", snap1nsProfile, snap2nsProfile}, + c.Check(s.loadProfilesCalls, DeepEquals, []loadProfilesParams{ + { + []string{snap1AAprofile, snap2AAprofile}, + fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), + apparmor_sandbox.SkipReadCache | apparmor_sandbox.ConserveCPU | apparmor_sandbox.SkipKernelLoad, + }, + { + []string{snap1nsProfile, snap2nsProfile}, + fmt.Sprintf("%s/var/cache/apparmor", s.RootDir), + apparmor_sandbox.ConserveCPU | apparmor_sandbox.SkipKernelLoad, + }, }) s.RemoveSnap(c, snapInfo1) s.RemoveSnap(c, snapInfo2) } } + +func (s *backendSuite) TestCoreSnippetOnCoreSystem(c *C) { + dirs.SetRootDir(s.RootDir) + + // NOTE: replace the real template with a shorter variant + restoreTemplate := apparmor.MockTemplate("\n" + + "###SNIPPETS###\n" + + "\n") + defer restoreTemplate() + + expectedContents := ` +# Allow each snaps to access each their own folder on the +# ubuntu-save partition, with write permissions. +/var/lib/snapd/save/snap/@{SNAP_INSTANCE_NAME}/ rw, +/var/lib/snapd/save/snap/@{SNAP_INSTANCE_NAME}/** mrwklix, +` + + tests := []struct { + onClassic bool + classicConfinement bool + jailMode bool + shouldContainSnippet bool + }{ + // XXX: Is it possible for someone to make this nicer? + {onClassic: false, classicConfinement: false, jailMode: false, shouldContainSnippet: true}, + {onClassic: false, classicConfinement: false, jailMode: true, shouldContainSnippet: true}, + + // Rest of the cases the core-specific snippet shouldn't turn up. + {onClassic: false, classicConfinement: true, jailMode: false, shouldContainSnippet: false}, + {onClassic: false, classicConfinement: true, jailMode: true, shouldContainSnippet: false}, + {onClassic: true, classicConfinement: false, jailMode: false, shouldContainSnippet: false}, + {onClassic: true, classicConfinement: true, jailMode: false, shouldContainSnippet: false}, + {onClassic: true, classicConfinement: false, jailMode: true, shouldContainSnippet: false}, + {onClassic: true, classicConfinement: true, jailMode: true, shouldContainSnippet: false}, + } + + for _, t := range tests { + restore := release.MockOnClassic(t.onClassic) + defer restore() + + opts := interfaces.ConfinementOptions{ + Classic: t.classicConfinement, + JailMode: t.jailMode, + } + snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 1) + profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") + if t.shouldContainSnippet { + c.Check(profile, testutil.FileContains, expectedContents, Commentf("Classic %t, JailMode %t", t.onClassic, t.jailMode)) + } else { + c.Check(profile, Not(testutil.FileContains), expectedContents, Commentf("Classic %t, JailMode %t", t.onClassic, t.jailMode)) + } + stat, err := os.Stat(profile) + c.Assert(err, IsNil) + c.Check(stat.Mode(), Equals, os.FileMode(0644)) + s.RemoveSnap(c, snapInfo) + } +} diff -Nru snapd-2.55.5+20.04/interfaces/apparmor/export_test.go snapd-2.57.5+20.04/interfaces/apparmor/export_test.go --- snapd-2.55.5+20.04/interfaces/apparmor/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/apparmor/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,6 +22,7 @@ import ( "os" + apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" ) @@ -30,19 +31,20 @@ NsProfile = nsProfile ProfileGlobs = profileGlobs SnapConfineFromSnapProfile = snapConfineFromSnapProfile - LoadProfiles = loadProfiles - UnloadProfiles = unloadProfiles - MaybeSetNumberOfJobs = maybeSetNumberOfJobs DefaultCoreRuntimeTemplateRules = defaultCoreRuntimeTemplateRules DefaultOtherBaseTemplateRules = defaultOtherBaseTemplateRules ) -func MockRuntimeNumCPU(new func() int) (restore func()) { - old := runtimeNumCPU - runtimeNumCPU = new - return func() { - runtimeNumCPU = old - } +func MockLoadProfiles(f func(fnames []string, cacheDir string, flags apparmor_sandbox.AaParserFlags) error) (restore func()) { + r := testutil.Backup(&loadProfiles) + loadProfiles = f + return r +} + +func MockUnloadProfiles(f func(fnames []string, cacheDir string) error) (restore func()) { + r := testutil.Backup(&unloadProfiles) + unloadProfiles = f + return r } // MockIsRootWritableOverlay mocks the real implementation of osutil.IsRootWritableOverlay @@ -64,14 +66,6 @@ } } -// MockProfilesPath mocks the file read by LoadedProfiles() -func MockProfilesPath(t *testutil.BaseTest, profiles string) { - profilesPath = profiles - t.AddCleanup(func() { - profilesPath = realProfilesPath - }) -} - // MockTemplate replaces apprmor template. // // NOTE: The real apparmor template is long. For testing it is convenient for diff -Nru snapd-2.55.5+20.04/interfaces/apparmor/spec.go snapd-2.57.5+20.04/interfaces/apparmor/spec.go --- snapd-2.55.5+20.04/interfaces/apparmor/spec.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/apparmor/spec.go 2022-10-17 16:25:18.000000000 +0000 @@ -235,6 +235,43 @@ return spec.updateNS.IndexOf(snippet) } +func (spec *Specification) emitLayout(si *snap.Info, layout *snap.Layout) { + emit := spec.AddUpdateNSf + + emit(" # Layout %s\n", layout.String()) + path := si.ExpandSnapVariables(layout.Path) + switch { + case layout.Bind != "": + bind := si.ExpandSnapVariables(layout.Bind) + // Allow bind mounting the layout element. + emit(" mount options=(rbind, rw) \"%s/\" -> \"%s/\",\n", bind, path) + emit(" mount options=(rprivate) -> \"%s/\",\n", path) + emit(" umount \"%s/\",\n", path) + // Allow constructing writable mimic in both bind-mount source and mount point. + GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory + GenWritableProfile(emit, bind, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION + case layout.BindFile != "": + bindFile := si.ExpandSnapVariables(layout.BindFile) + // Allow bind mounting the layout element. + emit(" mount options=(bind, rw) \"%s\" -> \"%s\",\n", bindFile, path) + emit(" mount options=(rprivate) -> \"%s\",\n", path) + emit(" umount \"%s\",\n", path) + // Allow constructing writable mimic in both bind-mount source and mount point. + GenWritableFileProfile(emit, path, 2) // At least / and /some-top-level-directory + GenWritableFileProfile(emit, bindFile, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION + case layout.Type == "tmpfs": + emit(" mount fstype=tmpfs tmpfs -> \"%s/\",\n", path) + emit(" mount options=(rprivate) -> \"%s/\",\n", path) + emit(" umount \"%s/\",\n", path) + // Allow constructing writable mimic to mount point. + GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory + case layout.Symlink != "": + // Allow constructing writable mimic to symlink parent directory. + emit(" \"%s\" rw,\n", path) + GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory + } +} + // AddLayout adds apparmor snippets based on the layout of the snap. // // The per-snap snap-update-ns profiles are composed via a template and @@ -252,24 +289,24 @@ // the data) // Importantly, the above mount operations are happening within the per-snap // mount namespace. -func (spec *Specification) AddLayout(si *snap.Info) { - if len(si.Layout) == 0 { +func (spec *Specification) AddLayout(snapInfo *snap.Info) { + if len(snapInfo.Layout) == 0 { return } // Walk the layout elements in deterministic order, by mount point name. - paths := make([]string, 0, len(si.Layout)) - for path := range si.Layout { + paths := make([]string, 0, len(snapInfo.Layout)) + for path := range snapInfo.Layout { paths = append(paths, path) } sort.Strings(paths) // Get tags describing all apps and hooks. - tags := make([]string, 0, len(si.Apps)+len(si.Hooks)) - for _, app := range si.Apps { + tags := make([]string, 0, len(snapInfo.Apps)+len(snapInfo.Hooks)) + for _, app := range snapInfo.Apps { tags = append(tags, app.SecurityTag()) } - for _, hook := range si.Hooks { + for _, hook := range snapInfo.Hooks { tags = append(tags, hook.SecurityTag()) } @@ -280,49 +317,27 @@ } for _, tag := range tags { for _, path := range paths { - snippet := snippetFromLayout(si.Layout[path]) + snippet := snippetFromLayout(snapInfo.Layout[path]) spec.snippets[tag] = append(spec.snippets[tag], snippet) } sort.Strings(spec.snippets[tag]) } - emit := spec.AddUpdateNSf - // Append update-ns snippets that allow constructing the layout. for _, path := range paths { - l := si.Layout[path] - emit(" # Layout %s\n", l.String()) - path := si.ExpandSnapVariables(l.Path) - switch { - case l.Bind != "": - bind := si.ExpandSnapVariables(l.Bind) - // Allow bind mounting the layout element. - emit(" mount options=(rbind, rw) \"%s/\" -> \"%s/\",\n", bind, path) - emit(" mount options=(rprivate) -> \"%s/\",\n", path) - emit(" umount \"%s/\",\n", path) - // Allow constructing writable mimic in both bind-mount source and mount point. - GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory - GenWritableProfile(emit, bind, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION - case l.BindFile != "": - bindFile := si.ExpandSnapVariables(l.BindFile) - // Allow bind mounting the layout element. - emit(" mount options=(bind, rw) \"%s\" -> \"%s\",\n", bindFile, path) - emit(" mount options=(rprivate) -> \"%s\",\n", path) - emit(" umount \"%s\",\n", path) - // Allow constructing writable mimic in both bind-mount source and mount point. - GenWritableFileProfile(emit, path, 2) // At least / and /some-top-level-directory - GenWritableFileProfile(emit, bindFile, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION - case l.Type == "tmpfs": - emit(" mount fstype=tmpfs tmpfs -> \"%s/\",\n", path) - emit(" mount options=(rprivate) -> \"%s/\",\n", path) - emit(" umount \"%s/\",\n", path) - // Allow constructing writable mimic to mount point. - GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory - case l.Symlink != "": - // Allow constructing writable mimic to symlink parent directory. - emit(" \"%s\" rw,\n", path) - GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory - } + layout := snapInfo.Layout[path] + spec.emitLayout(snapInfo, layout) + } +} + +// AddExtraLayouts adds additional apparmor snippets based on the provided layouts. +// The function is in part identical to AddLayout, except that it considers only the +// layouts passed as parameters instead of those declared in the snap.Info structure. +// XXX: Should we just combine this into AddLayout instead of this separate +// function? +func (spec *Specification) AddExtraLayouts(si *snap.Info, layouts []snap.Layout) { + for _, layout := range layouts { + spec.emitLayout(si, &layout) } } diff -Nru snapd-2.55.5+20.04/interfaces/apparmor/spec_test.go snapd-2.57.5+20.04/interfaces/apparmor/spec_test.go --- snapd-2.55.5+20.04/interfaces/apparmor/spec_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/apparmor/spec_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -506,6 +506,37 @@ c.Assert(updateNS[0], Equals, profile) } +func (s *specSuite) TestApparmorExtraLayouts(c *C) { + snapInfo := snaptest.MockInfo(c, snapTrivial, &snap.SideInfo{Revision: snap.R(42)}) + snapInfo.InstanceKey = "instance" + + restore := apparmor.SetSpecScope(s.spec, []string{"snap.some-snap_instace.app"}) + defer restore() + + extraLayouts := []snap.Layout{ + { + Path: "/test", + Bind: "/usr/home/test", + Mode: 0755, + }, + } + + s.spec.AddExtraLayouts(snapInfo, extraLayouts) + + updateNS := s.spec.UpdateNS() + + // verify that updateNS does indeed add all the additional layout + // lines. This just so happens to be 10 in this case because of reverse + // traversal for the path /usr/home/test + c.Assert(updateNS, HasLen, 10) + + // make sure the extra layout is added + c.Assert(updateNS[0], Equals, " # Layout /test: bind /usr/home/test\n") + c.Assert(updateNS[1], Equals, " mount options=(rbind, rw) \"/usr/home/test/\" -> \"/test/\",\n") + c.Assert(updateNS[2], Equals, " mount options=(rprivate) -> \"/test/\",\n") + // lines 3..9 is the traversal of the prefix for /usr/home/test +} + func (s *specSuite) TestUsesPtraceTrace(c *C) { c.Assert(s.spec.UsesPtraceTrace(), Equals, false) s.spec.SetUsesPtraceTrace() diff -Nru snapd-2.55.5+20.04/interfaces/apparmor/template.go snapd-2.57.5+20.04/interfaces/apparmor/template.go --- snapd-2.55.5+20.04/interfaces/apparmor/template.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/apparmor/template.go 2022-10-17 16:25:18.000000000 +0000 @@ -190,10 +190,12 @@ /usr/lib/os-release k, # systemd native journal API (see sd_journal_print(4)). This should be in - # AppArmor's base abstraction, but until it is, include here. - /run/systemd/journal/socket w, - /run/systemd/journal/stdout rw, # 'r' shouldn't be needed, but journald - # doesn't leak anything so allow + # AppArmor's base abstraction, but until it is, include here. We include + # the base journal path as well as the journal namespace pattern path. Each + # journal namespace for quota groups will be prefixed with 'snap-'. + /run/systemd/journal{,.snap-*}/socket w, + /run/systemd/journal{,.snap-*}/stdout rw, # 'r' shouldn't be needed, but journald + # doesn't leak anything so allow # snapctl and its requirements /usr/bin/snapctl ixr, @@ -285,6 +287,7 @@ /sys/devices/virtual/tty/{console,tty*}/active r, /sys/fs/cgroup/memory/{,user.slice/}memory.limit_in_bytes r, /sys/fs/cgroup/memory/{,**/}snap.@{SNAP_INSTANCE_NAME}{,.*}/memory.limit_in_bytes r, + /sys/fs/cgroup/memory/{,**/}snap.@{SNAP_INSTANCE_NAME}{,.*}/memory.stat r, /sys/fs/cgroup/cpu,cpuacct/{,user.slice/}cpu.cfs_{period,quota}_us r, /sys/fs/cgroup/cpu,cpuacct/{,**/}snap.@{SNAP_INSTANCE_NAME}{,.*}/cpu.cfs_{period,quota}_us r, /sys/fs/cgroup/cpu,cpuacct/{,user.slice/}cpu.shares r, @@ -467,8 +470,7 @@ /run/lock/ r, /run/lock/snap.@{SNAP_INSTANCE_NAME}/ rw, /run/lock/snap.@{SNAP_INSTANCE_NAME}/** mrwklix, - - + ###DEVMODE_SNAP_CONFINE### ` @@ -578,6 +580,7 @@ /{,usr/}bin/mv ixr, /{,usr/}bin/nice ixr, /{,usr/}bin/nohup ixr, + /{,usr/}bin/numfmt ixr, /{,usr/}bin/od ixr, /{,usr/}bin/openssl ixr, # may cause harmless capability block_suspend denial /{,usr/}bin/paste ixr, @@ -623,7 +626,7 @@ /{,usr/}bin/uptime ixr, /{,usr/}bin/vdir ixr, /{,usr/}bin/wc ixr, - /{,usr/}bin/which ixr, + /{,usr/}bin/which{,.debianutils} ixr, /{,usr/}bin/xargs ixr, /{,usr/}bin/xz ixr, /{,usr/}bin/yes ixr, @@ -811,6 +814,16 @@ #capability kill, ` +// coreSnippet contains apparmor rules specific only for +// snaps on native core systems. +// +var coreSnippet = ` +# Allow each snaps to access each their own folder on the +# ubuntu-save partition, with write permissions. +/var/lib/snapd/save/snap/@{SNAP_INSTANCE_NAME}/ rw, +/var/lib/snapd/save/snap/@{SNAP_INSTANCE_NAME}/** mrwklix, +` + // classicTemplate contains apparmor template used for snaps with classic // confinement. This template was Designed by jdstrand: // https://github.com/snapcore/snapd/pull/2366#discussion_r90101320 diff -Nru snapd-2.55.5+20.04/interfaces/backend.go snapd-2.57.5+20.04/interfaces/backend.go --- snapd-2.55.5+20.04/interfaces/backend.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/backend.go 2022-10-17 16:25:18.000000000 +0000 @@ -63,6 +63,12 @@ JailMode bool // Classic flag switches the core snap "chroot" off. Classic bool + // ExtraLayouts is a list of extra mount layouts to add to the + // snap. One example being if the snap is inside a quota group + // with a journal quota set. This will require an additional layout + // as systemd provides a mount namespace which will clash with the + // one snapd sets up. + ExtraLayouts []snap.Layout } // SecurityBackendOptions carries extra flags that affect initialization of the diff -Nru snapd-2.55.5+20.04/interfaces/builtin/accounts_service.go snapd-2.57.5+20.04/interfaces/builtin/accounts_service.go --- snapd-2.55.5+20.04/interfaces/builtin/accounts_service.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/accounts_service.go 2022-10-17 16:25:18.000000000 +0000 @@ -65,7 +65,7 @@ dbus (send) bus=session interface=org.freedesktop.DBus.Introspectable - path=/com/ubuntu/OnlineAccounts{,/**} + path=/org/gnome/OnlineAccounts{,/**} member=Introspect, ` diff -Nru snapd-2.55.5+20.04/interfaces/builtin/acrn_support.go snapd-2.57.5+20.04/interfaces/builtin/acrn_support.go --- snapd-2.55.5+20.04/interfaces/builtin/acrn_support.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/acrn_support.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,58 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 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 + +const acrnSupportSummary = `allows operating managing the ACRN hypervisor` + +const acrnSupportBaseDeclarationSlots = ` + acrn-support: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true +` + +const acrnSupportConnectedPlugAppArmor = ` +# Description: Allow write access to acrn_hsm. +/dev/acrn_hsm rw, +# Allow offlining CPU cores +/sys/devices/system/cpu/cpu[0-9]*/online rw, + +` + +type acrnSupportInterface struct { + commonInterface +} + +var acrnSupportConnectedPlugUDev = []string{ + `KERNEL=="acrn_hsm"`, +} + +func init() { + registerIface(&acrnSupportInterface{commonInterface{ + name: "acrn-support", + summary: acrnSupportSummary, + implicitOnCore: true, + implicitOnClassic: true, + connectedPlugUDev: acrnSupportConnectedPlugUDev, + baseDeclarationSlots: acrnSupportBaseDeclarationSlots, + connectedPlugAppArmor: acrnSupportConnectedPlugAppArmor, + }}) +} diff -Nru snapd-2.55.5+20.04/interfaces/builtin/acrn_support_test.go snapd-2.57.5+20.04/interfaces/builtin/acrn_support_test.go --- snapd-2.55.5+20.04/interfaces/builtin/acrn_support_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/acrn_support_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,119 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/udev" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type acrnSupportInterfaceSuite struct { + testutil.BaseTest + + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&acrnSupportInterfaceSuite{ + iface: builtin.MustInterface("acrn-support"), +}) + +const acrnSupportConsumerYaml = `name: consumer +version: 0 +apps: + app: + plugs: [acrn-support] +` + +const acrnSupportCoreYaml = `name: core +version: 0 +type: os +slots: + acrn-support: +` + +func (s *acrnSupportInterfaceSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + s.plug, s.plugInfo = MockConnectedPlug(c, acrnSupportConsumerYaml, nil, "acrn-support") + s.slot, s.slotInfo = MockConnectedSlot(c, acrnSupportCoreYaml, nil, "acrn-support") +} + +func (s *acrnSupportInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "acrn-support") +} + +func (s *acrnSupportInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *acrnSupportInterfaceSuite) TestSanitizePlug(c *C) { + c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) +} + +func (s *acrnSupportInterfaceSuite) TestAppArmorSpec(c *C) { + spec := &apparmor.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), Equals, ` +# Description: Allow write access to acrn_hsm. +/dev/acrn_hsm rw, +# Allow offlining CPU cores +/sys/devices/system/cpu/cpu[0-9]*/online rw, + +`) +} + +func (s *acrnSupportInterfaceSuite) TestUDevSpec(c *C) { + spec := &udev.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.Snippets(), HasLen, 2) + c.Assert(spec.Snippets()[0], Equals, `# acrn-support +KERNEL=="acrn_hsm", TAG+="snap_consumer_app"`) + c.Assert(spec.Snippets(), testutil.Contains, fmt.Sprintf(`TAG=="snap_consumer_app", RUN+="%s/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`, dirs.DistroLibExecDir)) +} + +func (s *acrnSupportInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Assert(si.ImplicitOnCore, Equals, true) + c.Assert(si.ImplicitOnClassic, Equals, true) + c.Assert(si.Summary, Equals, `allows operating managing the ACRN hypervisor`) + c.Assert(si.BaseDeclarationSlots, testutil.Contains, "acrn-support") +} + +func (s *acrnSupportInterfaceSuite) TestAutoConnect(c *C) { + c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true) +} + +func (s *acrnSupportInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff -Nru snapd-2.55.5+20.04/interfaces/builtin/block_devices.go snapd-2.57.5+20.04/interfaces/builtin/block_devices.go --- snapd-2.55.5+20.04/interfaces/builtin/block_devices.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/block_devices.go 2022-10-17 16:25:18.000000000 +0000 @@ -59,13 +59,13 @@ /sys/devices/**/nvme/**/dev r, # Access to raw devices, not individual partitions -/dev/hd[a-t] rw, # IDE, MFM, RLL -/dev/sd{,[a-h]}[a-z] rw, # SCSI -/dev/sdi[a-v] rw, # SCSI continued -/dev/i2o/hd{,[a-c]}[a-z] rw, # I2O hard disk -/dev/i2o/hdd[a-x] rw, # I2O hard disk continued -/dev/mmcblk[0-9]{,[0-9],[0-9][0-9]} rw, # MMC (up to 1000 devices) -/dev/vd[a-z] rw, # virtio +/dev/hd[a-t] rwk, # IDE, MFM, RLL +/dev/sd{,[a-h]}[a-z] rwk, # SCSI +/dev/sdi[a-v] rwk, # SCSI continued +/dev/i2o/hd{,[a-c]}[a-z] rwk, # I2O hard disk +/dev/i2o/hdd[a-x] rwk, # I2O hard disk continued +/dev/mmcblk[0-9]{,[0-9],[0-9][0-9]} rwk, # MMC (up to 1000 devices) +/dev/vd[a-z] rwk, # virtio # Allow /dev/nvmeXnY namespace block devices. Please note this grants access to all # NVMe namespace block devices and that the numeric suffix on the character device @@ -78,13 +78,13 @@ # controller's identifier. Do not assume any particular device relationship # based on their names. If you do, you may irrevocably erase data on an # unintended device. -/dev/nvme{[0-9],[1-9][0-9]}n{[1-9],[1-5][0-9],6[0-3]} rw, # NVMe (up to 100 devices, with 1-63 namespaces) +/dev/nvme{[0-9],[1-9][0-9]}n{[1-9],[1-5][0-9],6[0-3]} rwk, # NVMe (up to 100 devices, with 1-63 namespaces) # Allow /dev/nvmeX controller character devices. These character devices allow # manipulation of the block devices that we also allow above, so grouping this # access here makes sense, whereas access to individual partitions is delegated # to the raw-volume interface. -/dev/nvme{[0-9],[1-9][0-9]} rw, # NVMe (up to 100 devices) +/dev/nvme{[0-9],[1-9][0-9]} rwk, # NVMe (up to 100 devices) # SCSI device commands, et al capability sys_rawio, diff -Nru snapd-2.55.5+20.04/interfaces/builtin/block_devices_test.go snapd-2.57.5+20.04/interfaces/builtin/block_devices_test.go --- snapd-2.55.5+20.04/interfaces/builtin/block_devices_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/block_devices_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -85,7 +85,7 @@ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `# Description: Allow write access to raw disk block devices.`) - c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/sd{,[a-h]}[a-z] rw,`) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/sd{,[a-h]}[a-z] rwk,`) } func (s *blockDevicesInterfaceSuite) TestUDevSpec(c *C) { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/browser_support.go snapd-2.57.5+20.04/interfaces/builtin/browser_support.go --- snapd-2.55.5+20.04/interfaces/builtin/browser_support.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/browser_support.go 2022-10-17 16:25:18.000000000 +0000 @@ -72,6 +72,10 @@ /run/user/[0-9]*/snap.@{SNAP_INSTANCE_NAME}/{,.}com.google.Chrome.*/SS r, /run/user/[0-9]*/snap.@{SNAP_INSTANCE_NAME}/{,.}com.microsoft.Edge.*/SS r, +# Allow access to Jupyter notebooks. +# This is temporary and will be reverted once LP: #1959417 is fixed upstream. +owner @{HOME}/.local/share/jupyter/** rw, + # Allow reading platform files /run/udev/data/+platform:* r, diff -Nru snapd-2.55.5+20.04/interfaces/builtin/content.go snapd-2.57.5+20.04/interfaces/builtin/content.go --- snapd-2.55.5+20.04/interfaces/builtin/content.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/content.go 2022-10-17 16:25:18.000000000 +0000 @@ -92,14 +92,11 @@ } // Error if "read" or "write" are present alongside "source". - // TODO: use slot.Lookup() once PR 4510 lands. - var unused map[string]interface{} - if err := slot.Attr("source", &unused); err == nil { - var unused []interface{} - if err := slot.Attr("read", &unused); err == nil { + if _, found := slot.Lookup("source"); found { + if _, found := slot.Lookup("read"); found { return fmt.Errorf(`move the "read" attribute into the "source" section`) } - if err := slot.Attr("write", &unused); err == nil { + if _, found := slot.Lookup("write"); found { return fmt.Errorf(`move the "write" attribute into the "source" section`) } } diff -Nru snapd-2.55.5+20.04/interfaces/builtin/content_test.go snapd-2.57.5+20.04/interfaces/builtin/content_test.go --- snapd-2.55.5+20.04/interfaces/builtin/content_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/content_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -987,10 +987,6 @@ expectedMnt := []osutil.MountEntry{{ Name: "/var/snap/producer/2/directory", Dir: "/var/snap/consumer/common/import/directory", - Options: []string{"bind", "ro"}, - }, { - Name: "/var/snap/producer/2/directory", - Dir: "/var/snap/consumer/common/import/directory-2", Options: []string{"bind"}, }} c.Assert(mountSpec.MountEntries(), DeepEquals, expectedMnt) diff -Nru snapd-2.55.5+20.04/interfaces/builtin/cups_control.go snapd-2.57.5+20.04/interfaces/builtin/cups_control.go --- snapd-2.55.5+20.04/interfaces/builtin/cups_control.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/cups_control.go 2022-10-17 16:25:18.000000000 +0000 @@ -81,7 +81,7 @@ bus=system path=/org/cups/cupsd/Notifier interface=org.cups.cupsd.Notifier - peer=(name=org.freedesktop.DBus,label=unconfined), + peer=(label=unconfined), # Allow daemon to send signals to its snap_daemon processes capability kill, @@ -96,7 +96,7 @@ bus=system path=/org/cups/cupsd/Notifier interface=org.cups.cupsd.Notifier - peer=(name=org.freedesktop.DBus,label=###PLUG_SECURITY_TAGS###), + peer=(label=###PLUG_SECURITY_TAGS###), ` const cupsControlConnectedPlugAppArmor = ` @@ -111,7 +111,7 @@ bus=system path=/org/cups/cupsd/Notifier interface=org.cups.cupsd.Notifier - peer=(name=org.freedesktop.DBus,label=###SLOT_SECURITY_TAGS###), + peer=(label=###SLOT_SECURITY_TAGS###), ` type cupsControlInterface struct { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/cups_control_test.go snapd-2.57.5+20.04/interfaces/builtin/cups_control_test.go --- snapd-2.55.5+20.04/interfaces/builtin/cups_control_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/cups_control_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -106,7 +106,7 @@ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Allow communicating with the cups server for printing and configuration.") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "#include ") - c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(name=org.freedesktop.DBus,label=\"snap.provider.app\"") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(label=\"snap.provider.app\"") c.Assert(spec.SnippetForTag("snap.provider.app"), Not(testutil.Contains), "# Allow daemon access to create the CUPS socket") // provider to consumer on core for PermanentSlot @@ -120,7 +120,7 @@ spec = &apparmor.Specification{} c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.providerSlot), IsNil) c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.provider.app"}) - c.Assert(spec.SnippetForTag("snap.provider.app"), testutil.Contains, "peer=(name=org.freedesktop.DBus,label=\"snap.consumer.app\"") + c.Assert(spec.SnippetForTag("snap.provider.app"), testutil.Contains, "peer=(label=\"snap.consumer.app\"") } func (s *cupsControlSuite) TestAppArmorSpecClassic(c *C) { @@ -133,7 +133,7 @@ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Allow communicating with the cups server for printing and configuration.") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "#include ") - c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(name=org.freedesktop.DBus,label=\"{unconfined,/usr/sbin/cupsd,cupsd}\"") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(label=\"{unconfined,/usr/sbin/cupsd,cupsd}\"") c.Assert(spec.SnippetForTag("snap.provider.app"), Not(testutil.Contains), "# Allow daemon access to create the CUPS socket") // core to consumer on classic is empty for PermanentSlot @@ -152,7 +152,7 @@ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Allow communicating with the cups server for printing and configuration.") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "#include ") - c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(name=org.freedesktop.DBus,label=\"snap.provider.app\"") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(label=\"snap.provider.app\"") c.Assert(spec.SnippetForTag("snap.provider.app"), Not(testutil.Contains), "# Allow daemon access to create the CUPS socket") // provider to consumer on classic for PermanentSlot @@ -166,7 +166,7 @@ spec = &apparmor.Specification{} c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.providerSlot), IsNil) c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.provider.app"}) - c.Assert(spec.SnippetForTag("snap.provider.app"), testutil.Contains, "peer=(name=org.freedesktop.DBus,label=\"snap.consumer.app\"") + c.Assert(spec.SnippetForTag("snap.provider.app"), testutil.Contains, "peer=(label=\"snap.consumer.app\"") } func (s *cupsControlSuite) TestStaticInfo(c *C) { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/custom_device_test.go snapd-2.57.5+20.04/interfaces/builtin/custom_device_test.go --- snapd-2.55.5+20.04/interfaces/builtin/custom_device_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/custom_device_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,6 +25,7 @@ . "gopkg.in/check.v1" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" "github.com/snapcore/snapd/interfaces/builtin" @@ -507,7 +508,9 @@ // The last line of the snippet is about snap-device-helper actionLine := snippets[rulesCount] - c.Assert(actionLine, Matches, `^TAG=="snap_consumer_app", RUN\+="/usr/lib/snapd/snap-device-helper .*`, testLabel) + c.Assert(actionLine, Matches, + fmt.Sprintf(`^TAG=="snap_consumer_app", RUN\+="%s/snap-device-helper .*`, dirs.DistroLibExecDir), + testLabel) } } diff -Nru snapd-2.55.5+20.04/interfaces/builtin/desktop.go snapd-2.57.5+20.04/interfaces/builtin/desktop.go --- snapd-2.55.5+20.04/interfaces/builtin/desktop.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/desktop.go 2022-10-17 16:25:18.000000000 +0000 @@ -97,7 +97,7 @@ bus=session interface=org.gtk.Actions member=Changed - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(label=unconfined), # notifications dbus (send) diff -Nru snapd-2.55.5+20.04/interfaces/builtin/desktop_legacy.go snapd-2.57.5+20.04/interfaces/builtin/desktop_legacy.go --- snapd-2.55.5+20.04/interfaces/builtin/desktop_legacy.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/desktop_legacy.go 2022-10-17 16:25:18.000000000 +0000 @@ -58,6 +58,9 @@ # https://gitlab.gnome.org/GNOME/at-spi2-core/-/issues/43 owner /{,var/}run/user/[0-9]*/at-spi/bus* rw, +# Allow access to the socket used by speech-dispatcher +owner /{,var/}run/user/[0-9]*/speech-dispatcher/speechd.sock rw, + # Allow the accessibility services in the user session to send us any events dbus (receive) bus=accessibility diff -Nru snapd-2.55.5+20.04/interfaces/builtin/display_control.go snapd-2.57.5+20.04/interfaces/builtin/display_control.go --- snapd-2.55.5+20.04/interfaces/builtin/display_control.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/display_control.go 2022-10-17 16:25:18.000000000 +0000 @@ -83,6 +83,9 @@ peer=(label=unconfined), /sys/class/backlight/ r, + +# Allow changing backlight +/sys/devices/**/**/drm/card[0-9]/card[0-9]*/*_backlight/brightness w, ` type displayControlInterface struct { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/dm_crypt.go snapd-2.57.5+20.04/interfaces/builtin/dm_crypt.go --- snapd-2.55.5+20.04/interfaces/builtin/dm_crypt.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/dm_crypt.go 2022-10-17 16:25:18.000000000 +0000 @@ -60,7 +60,11 @@ /{,usr/}bin/umount ixr, # mount/umount (via libmount) track some mount info in these files -/run/mount/utab* wrlk, +/{,var/}run/mount/utab* wrlk, + +# Allow access to the file locking mechanism +/{,var/}run/cryptsetup/ r, +/{,var/}run/cryptsetup/* rwk, ` const dmCryptConnectedPlugSecComp = ` diff -Nru snapd-2.55.5+20.04/interfaces/builtin/dm_crypt_test.go snapd-2.57.5+20.04/interfaces/builtin/dm_crypt_test.go --- snapd-2.55.5+20.04/interfaces/builtin/dm_crypt_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/dm_crypt_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -85,13 +85,15 @@ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/dev/mapper/control") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/dev/dm-[0-9]*") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/run/systemd/seats/*") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{,var/}run/cryptsetup/ r,") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{,var/}run/cryptsetup/* rwk,") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{,run/}media/{,**}") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "mount options=(ro,nosuid,nodev) /dev/dm-[0-9]*") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "mount options=(rw,nosuid,nodev) /dev/dm-[0-9]*") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{,usr/}sbin/cryptsetup ixr,") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{,usr/}bin/mount ixr,") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{,usr/}bin/umount ixr,") - c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/run/mount/utab* wrlk,") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{,var/}run/mount/utab* wrlk,") } func (s *DmCryptInterfaceSuite) TestUDevSpec(c *C) { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/dsp_test.go snapd-2.57.5+20.04/interfaces/builtin/dsp_test.go --- snapd-2.55.5+20.04/interfaces/builtin/dsp_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/dsp_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -74,7 +74,6 @@ s.noFlavorSlot, s.noFlavorSlotInfo = MockConnectedSlot(c, gadgetDspSlotYaml, nil, "dsp-no-flavor") s.ambarellaSlot, s.ambarellaSlotInfo = MockConnectedSlot(c, gadgetDspSlotYaml, nil, "dsp-ambarella") s.plug, s.plugInfo = MockConnectedPlug(c, dspMockPlugSnapInfoYaml, nil, "dsp") - } func (s *dspSuite) TestName(c *C) { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/dummy.go snapd-2.57.5+20.04/interfaces/builtin/dummy.go --- snapd-2.55.5+20.04/interfaces/builtin/dummy.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/dummy.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,101 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2018 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" - "github.com/snapcore/snapd/interfaces/apparmor" - "github.com/snapcore/snapd/snap" -) - -type dummyInterface struct{} - -const dummyInterfaceSummary = `allows testing without providing any additional permissions` -const dummyInterfaceBaseDeclarationSlots = ` - dummy: - allow-installation: - slot-snap-type: - - app - deny-auto-connection: true -` - -func (iface *dummyInterface) String() string { - return iface.Name() -} - -// Name returns the name of the dummy interface. -func (iface *dummyInterface) Name() string { - return "dummy" -} - -func (iface *dummyInterface) StaticInfo() interfaces.StaticInfo { - return interfaces.StaticInfo{ - Summary: dummyInterfaceSummary, - BaseDeclarationSlots: dummyInterfaceBaseDeclarationSlots, - } -} - -func (iface *dummyInterface) BeforePreparePlug(plug *snap.PlugInfo) error { - return nil -} - -func (iface *dummyInterface) BeforePrepareSlot(slot *snap.SlotInfo) error { - return nil -} - -func (iface *dummyInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error { - var value string - if err := plug.Attr("before-connect", &value); err != nil { - return err - } - value = fmt.Sprintf("plug-changed(%s)", value) - return plug.SetAttr("before-connect", value) -} - -func (iface *dummyInterface) BeforeConnectSlot(slot *interfaces.ConnectedSlot) error { - var num int64 - if err := slot.Attr("producer-num-1", &num); err != nil { - return err - } - var value string - if err := slot.Attr("before-connect", &value); err != nil { - return err - } - value = fmt.Sprintf("slot-changed(%s)", value) - return slot.SetAttr("before-connect", value) -} - -func (iface *dummyInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { - return nil -} - -func (iface *dummyInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { - return nil -} - -func (iface *dummyInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool { - return true -} - -func init() { - registerIface(&dummyInterface{}) -} diff -Nru snapd-2.55.5+20.04/interfaces/builtin/empty.go snapd-2.57.5+20.04/interfaces/builtin/empty.go --- snapd-2.55.5+20.04/interfaces/builtin/empty.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/empty.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,101 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 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" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/snap" +) + +type emptyInterface struct{} + +const emptyInterfaceSummary = `allows testing without providing any additional permissions` +const emptyInterfaceBaseDeclarationSlots = ` + empty: + allow-installation: + slot-snap-type: + - app + deny-auto-connection: true +` + +func (iface *emptyInterface) String() string { + return iface.Name() +} + +// Name returns the name of the empty interface. +func (iface *emptyInterface) Name() string { + return "empty" +} + +func (iface *emptyInterface) StaticInfo() interfaces.StaticInfo { + return interfaces.StaticInfo{ + Summary: emptyInterfaceSummary, + BaseDeclarationSlots: emptyInterfaceBaseDeclarationSlots, + } +} + +func (iface *emptyInterface) BeforePreparePlug(plug *snap.PlugInfo) error { + return nil +} + +func (iface *emptyInterface) BeforePrepareSlot(slot *snap.SlotInfo) error { + return nil +} + +func (iface *emptyInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error { + var value string + if err := plug.Attr("before-connect", &value); err != nil { + return err + } + value = fmt.Sprintf("plug-changed(%s)", value) + return plug.SetAttr("before-connect", value) +} + +func (iface *emptyInterface) BeforeConnectSlot(slot *interfaces.ConnectedSlot) error { + var num int64 + if err := slot.Attr("producer-num-1", &num); err != nil { + return err + } + var value string + if err := slot.Attr("before-connect", &value); err != nil { + return err + } + value = fmt.Sprintf("slot-changed(%s)", value) + return slot.SetAttr("before-connect", value) +} + +func (iface *emptyInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + return nil +} + +func (iface *emptyInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + return nil +} + +func (iface *emptyInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool { + return true +} + +func init() { + registerIface(&emptyInterface{}) +} diff -Nru snapd-2.55.5+20.04/interfaces/builtin/hardware_observe.go snapd-2.57.5+20.04/interfaces/builtin/hardware_observe.go --- snapd-2.55.5+20.04/interfaces/builtin/hardware_observe.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/hardware_observe.go 2022-10-17 16:25:18.000000000 +0000 @@ -54,9 +54,6 @@ # 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, # power information /sys/power/{,**} r, @@ -78,11 +75,6 @@ /{,usr/}bin/lscpu ixr, /{,usr/}bin/lsmem ixr, -# lsmem -/sys/devices/system/memory/block_size_bytes r, -/sys/devices/system/memory/memory[0-9]*/removable r, -/sys/devices/system/memory/memory[0-9]*/state r, -/sys/devices/system/memory/memory[0-9]*/valid_zones r, # lsusb # Note: lsusb and its database have to be shipped in the snap if not on classic @@ -100,6 +92,12 @@ /sys/kernel/debug/usb/devices r, @{PROC}/sys/abi/{,*} r, +# hwinfo --short +@{PROC}/ioports r, +@{PROC}/dma r, +@{PROC}/tty/driver/serial r, +@{PROC}/sys/dev/cdrom/info r, + # status of hugepages and transparent_hugepage, but not the pages themselves /sys/kernel/mm/{hugepages,transparent_hugepage}/{,**} r, @@ -137,10 +135,6 @@ # determine if it is running in a chroot. Like above, this is best granted via # system-observe. #ptrace (read) peer=unconfined, - -# some devices use this information to set serial, etc. for Ubuntu Core devices -/sys/devices/virtual/dmi/id/product_name r, -/sys/devices/virtual/dmi/id/sys_vendor r, ` const hardwareObserveConnectedPlugSecComp = ` diff -Nru snapd-2.55.5+20.04/interfaces/builtin/hugepages_control.go snapd-2.57.5+20.04/interfaces/builtin/hugepages_control.go --- snapd-2.55.5+20.04/interfaces/builtin/hugepages_control.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/hugepages_control.go 2022-10-17 16:25:18.000000000 +0000 @@ -62,6 +62,9 @@ /sys/kernel/mm/transparent_hugepage/khugepaged/defrag w, /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_{none,swap} w, /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan w, + +# Allow mounting huge tables (for hypervisors like ACRN) +mount options=ro /dev/hugepages, ` func init() { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/kernel_module_load.go snapd-2.57.5+20.04/interfaces/builtin/kernel_module_load.go --- snapd-2.55.5+20.04/interfaces/builtin/kernel_module_load.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/kernel_module_load.go 2022-10-17 16:25:18.000000000 +0000 @@ -59,6 +59,7 @@ loadNone loadOption = iota loadDenied loadOnBoot + loadDynamic ) type ModuleInfo struct { @@ -95,6 +96,8 @@ load = loadDenied case "on-boot": load = loadOnBoot + case "dynamic": + load = loadDynamic default: return fmt.Errorf(`kernel-module-load "load" value is unrecognized: %q`, loadString) } @@ -139,7 +142,8 @@ return errors.New(`kernel-module-load "options" attribute incompatible with "load: denied"`) } - if !kernelModuleOptionsRegexp.MatchString(moduleInfo.options) { + dynamicLoadingWithAnyOptions := moduleInfo.load == loadDynamic && moduleInfo.options == "*" + if !dynamicLoadingWithAnyOptions && !kernelModuleOptionsRegexp.MatchString(moduleInfo.options) { return fmt.Errorf(`kernel-module-load "options" attribute contains invalid characters: %q`, moduleInfo.options) } @@ -194,8 +198,8 @@ break } fallthrough - case loadNone: - if len(moduleInfo.options) > 0 { + case loadNone, loadDynamic: + if len(moduleInfo.options) > 0 && moduleInfo.options != "*" { // module options might include filesystem paths. Beside // supporting hardcoded paths, it makes sense to support also // paths to files provided by the snap; for this reason, we diff -Nru snapd-2.55.5+20.04/interfaces/builtin/kernel_module_load_test.go snapd-2.57.5+20.04/interfaces/builtin/kernel_module_load_test.go --- snapd-2.55.5+20.04/interfaces/builtin/kernel_module_load_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/kernel_module_load_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -60,6 +60,12 @@ options: param_1=ok param_2=false - name: expandvar options: opt=$FOO path=$SNAP_COMMON/bar + - name: dyn-module1 + load: dynamic + options: opt1=v1 opt2=v2 + - name: dyn-module2 + load: dynamic + options: "*" apps: app: plugs: [kmod] @@ -124,6 +130,10 @@ `kernel-module-load "name" must be a string`, }, { + "modules:\n - name: w3/rd*", + `kernel-module-load "name" attribute is not a valid module name`, + }, + { "modules:\n - name: pcspkr", `kernel-module-load: must specify at least "load" or "options"`, }, @@ -152,6 +162,11 @@ `kernel-module-load "options" attribute contains invalid characters: "no-dashes"`, }, { + // "*" is only allowed for `load: dynamic` + "modules:\n - name: pcspkr\n options: \"*\"", + `kernel-module-load "options" attribute contains invalid characters: "\*"`, + }, + { "modules:\n - name: pcspkr\n load: denied\n options: p1=true", `kernel-module-load "options" attribute incompatible with "load: denied"`, }, @@ -172,9 +187,11 @@ "mymodule1": true, }) c.Check(spec.ModuleOptions(), DeepEquals, map[string]string{ - "mymodule1": "p1=3 p2=true p3", - "mymodule2": "param_1=ok param_2=false", - "expandvar": "opt=$FOO path=/var/snap/consumer/common/bar", + "mymodule1": "p1=3 p2=true p3", + "mymodule2": "param_1=ok param_2=false", + "expandvar": "opt=$FOO path=/var/snap/consumer/common/bar", + "dyn-module1": "opt1=v1 opt2=v2", + // No entry for dyn-module2, which has options set to "*" }) c.Check(spec.DisallowedModules(), DeepEquals, []string{"forbidden"}) } diff -Nru snapd-2.55.5+20.04/interfaces/builtin/kubernetes_support.go snapd-2.57.5+20.04/interfaces/builtin/kubernetes_support.go --- snapd-2.55.5+20.04/interfaces/builtin/kubernetes_support.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/kubernetes_support.go 2022-10-17 16:25:18.000000000 +0000 @@ -68,6 +68,11 @@ # https://github.com/projectcalico/cni-plugin/blob/master/pkg/types/types.go /{,var/}run/calico/ipam.lock rwk, +# manually add java certs here +# see also https://bugs.launchpad.net/apparmor/+bug/1816372 +/etc/ssl/certs/java/{,*} r, +#include + /{,usr/}bin/systemd-run Cxr -> systemd_run, /run/systemd/private r, profile systemd_run (attach_disconnected,mediate_deleted) { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/microceph.go snapd-2.57.5+20.04/interfaces/builtin/microceph.go --- snapd-2.55.5+20.04/interfaces/builtin/microceph.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/microceph.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,51 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 + +const microcephSummary = `allows access to the MicroCeph socket` + +const microcephBaseDeclarationSlots = ` + microceph: + allow-installation: false + deny-connection: true + deny-auto-connection: true +` + +const microcephConnectedPlugAppArmor = ` +# Description: allow access to the MicroCeph control socket. + +/var/snap/microceph/common/state/control.socket rw, +` + +const microcephConnectedPlugSecComp = ` +# Description: allow access to the MicroCeph control socket. + +socket AF_NETLINK - NETLINK_GENERIC +` + +func init() { + registerIface(&commonInterface{ + name: "microceph", + summary: microcephSummary, + baseDeclarationSlots: microcephBaseDeclarationSlots, + connectedPlugAppArmor: microcephConnectedPlugAppArmor, + connectedPlugSecComp: microcephConnectedPlugSecComp, + }) +} diff -Nru snapd-2.55.5+20.04/interfaces/builtin/microceph_test.go snapd-2.57.5+20.04/interfaces/builtin/microceph_test.go --- snapd-2.55.5+20.04/interfaces/builtin/microceph_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/microceph_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,111 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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/apparmor" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/seccomp" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type MicroCephInterfaceSuite struct { + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&MicroCephInterfaceSuite{ + iface: builtin.MustInterface("microceph"), +}) + +const microcephConsumerYaml = `name: consumer +version: 0 +apps: + app: + plugs: [microceph] +` + +const microcephCoreYaml = `name: core +version: 0 +type: os +slots: + microceph: +` + +func (s *MicroCephInterfaceSuite) SetUpTest(c *C) { + s.plug, s.plugInfo = MockConnectedPlug(c, microcephConsumerYaml, nil, "microceph") + s.slot, s.slotInfo = MockConnectedSlot(c, microcephCoreYaml, nil, "microceph") +} + +func (s *MicroCephInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "microceph") +} + +func (s *MicroCephInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) + slot := &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "some-snap"}, + Name: "microceph", + Interface: "microceph", + } + + c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), IsNil) +} + +func (s *MicroCephInterfaceSuite) TestSanitizePlug(c *C) { + c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) +} + +func (s *MicroCephInterfaceSuite) TestAppArmorSpec(c *C) { + spec := &apparmor.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/var/snap/microceph/common/state/control.socket rw,\n") +} + +func (s *MicroCephInterfaceSuite) TestSecCompSpec(c *C) { + spec := &seccomp.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "socket AF_NETLINK - NETLINK_GENERIC\n") +} + +func (s *MicroCephInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Assert(si.ImplicitOnCore, Equals, false) + c.Assert(si.ImplicitOnClassic, Equals, false) + c.Assert(si.Summary, Equals, `allows access to the MicroCeph socket`) + c.Assert(si.BaseDeclarationSlots, testutil.Contains, "microceph") +} + +func (s *MicroCephInterfaceSuite) TestAutoConnect(c *C) { + c.Check(s.iface.AutoConnect(nil, nil), Equals, true) +} + +func (s *MicroCephInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff -Nru snapd-2.55.5+20.04/interfaces/builtin/modem_manager.go snapd-2.57.5+20.04/interfaces/builtin/modem_manager.go --- snapd-2.55.5+20.04/interfaces/builtin/modem_manager.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/modem_manager.go 2022-10-17 16:25:18.000000000 +0000 @@ -249,30 +249,87 @@ ` const modemManagerPermanentSlotDBus = ` - - - - -` - -const modemManagerConnectedPlugDBus = ` + + + + ` const modemManagerPermanentSlotUDev = ` # Concatenation of all ModemManager udev rules # do not edit this file, it will be overwritten on update -ACTION!="add|change|move", GOTO="mm_cinterion_port_types_end" -SUBSYSTEM!="tty", GOTO="mm_cinterion_port_types_end" -ENV{ID_VENDOR_ID}!="1e2d", GOTO="mm_cinterion_port_types_end" +ACTION!="add|change|move|bind", GOTO="mm_cinterion_port_types_end" +SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e2d", GOTO="mm_cinterion_port_types" +GOTO="mm_cinterion_port_types_end" +LABEL="mm_cinterion_port_types" SUBSYSTEMS=="usb", ATTRS{bInterfaceNumber}=="?*", ENV{.MM_USBIFNUM}="$attr{bInterfaceNumber}" -ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0053", ENV{.MM_USBIFNUM}=="01", ENV{ID_MM_CINTERION_PORT_TYPE_GPS}="1" +# PHS8 +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0053", ENV{.MM_USBIFNUM}=="01", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1" + +# PLS8 port types +# ttyACM0 (if #0): AT port +# ttyACM1 (if #2): AT port +# ttyACM2 (if #4): GPS data port +# ttyACM3 (if #6): unknown +# ttyACM4 (if #8): unknown +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="00", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="06", ENV{ID_MM_PORT_IGNORE}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0061", ENV{.MM_USBIFNUM}=="08", ENV{ID_MM_PORT_IGNORE}="1" + +# PLS62 family non-mbim enumeration uses alternate settings for 2G band management +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005b", ENV{ID_MM_CINTERION_MODEM_FAMILY}="imt" +# PLS62 family non-mbim enumeration +# ttyACM0 (if #0): AT port +# ttyACM1 (if #2): AT port +# ttyACM2 (if #4): can be AT or GNSS in some models +# ttyACM3 (if #6): AT port (but just ignore) +# ttyACM4 (if #8): DIAG/QCDM +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005b", ENV{.MM_USBIFNUM}=="00", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005b", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005b", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005b", ENV{.MM_USBIFNUM}=="06", ENV{ID_MM_PORT_IGNORE}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005b", ENV{.MM_USBIFNUM}=="08", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1" + +# PLS62 family mbim enumeration +# ttyACM0 (if #0): AT port +# ttyACM1 (if #2): AT port +# ttyACM2 (if #4): can be AT or GNSS in some models +# ttyACM3 (if #6): AT port (but just ignore) +# ttyACM4 (if #8): DIAG/QCDM +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005d", ENV{.MM_USBIFNUM}=="00", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005d", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005d", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005d", ENV{.MM_USBIFNUM}=="06", ENV{ID_MM_PORT_IGNORE}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="005d", ENV{.MM_USBIFNUM}=="08", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1" + +# PLS63 +# ttyACM0 (if #0): AT port +# ttyACM1 (if #2): AT port +# ttyACM2 (if #4): GPS data port +# ttyACM3 (if #6): DIAG/QCDM +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0069", ENV{.MM_USBIFNUM}=="00", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0069", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0069", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="0069", ENV{.MM_USBIFNUM}=="06", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1" + +# PLS83 +# ttyACM0 (if #0): AT port +# ttyACM1 (if #2): AT port +# ttyACM2 (if #4): GPS data port +# ttyACM3 (if #6): DIAG/QCDM +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="006F", ENV{.MM_USBIFNUM}=="00", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_PRIMARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="006F", ENV{.MM_USBIFNUM}=="02", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_AT_SECONDARY}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="006F", ENV{.MM_USBIFNUM}=="04", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_GPS}="1" +ATTRS{idVendor}=="1e2d", ATTRS{idProduct}=="006F", ENV{.MM_USBIFNUM}=="06", SUBSYSTEM=="tty", ENV{ID_MM_PORT_TYPE_QCDM}="1" LABEL="mm_cinterion_port_types_end" # do not edit this file, it will be overwritten on update @@ -1297,11 +1354,6 @@ return nil } -func (iface *modemManagerInterface) DBusConnectedPlug(spec *dbus.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { - spec.AddSnippet(modemManagerConnectedPlugDBus) - return nil -} - func (iface *modemManagerInterface) AppArmorPermanentSlot(spec *apparmor.Specification, slot *snap.SlotInfo) error { spec.AddSnippet(modemManagerPermanentSlotAppArmor) return nil diff -Nru snapd-2.55.5+20.04/interfaces/builtin/modem_manager_test.go snapd-2.57.5+20.04/interfaces/builtin/modem_manager_test.go --- snapd-2.55.5+20.04/interfaces/builtin/modem_manager_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/modem_manager_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -200,7 +200,7 @@ dbusSpec := &dbus.Specification{} err = dbusSpec.AddConnectedPlug(s.iface, plug, s.slot) c.Assert(err, IsNil) - c.Assert(dbusSpec.SecurityTags(), HasLen, 1) + c.Assert(dbusSpec.SecurityTags(), HasLen, 0) dbusSpec = &dbus.Specification{} err = dbusSpec.AddPermanentSlot(s.iface, s.slotInfo) @@ -235,16 +235,25 @@ } func (s *ModemManagerInterfaceSuite) TestConnectedPlugDBus(c *C) { + release.OnClassic = false + plugSnap := snaptest.MockInfo(c, modemmgrMockPlugSnapInfoYaml, nil) + plug := interfaces.NewConnectedPlug(plugSnap.Plugs["modem-manager"], nil, nil) + + dbusSpec := &dbus.Specification{} + err := dbusSpec.AddConnectedPlug(s.iface, plug, s.slot) + c.Assert(err, IsNil) + c.Assert(dbusSpec.SecurityTags(), DeepEquals, []string(nil)) +} + +func (s *ModemManagerInterfaceSuite) TestConnectedPlugDBusClassic(c *C) { plugSnap := snaptest.MockInfo(c, modemmgrMockPlugSnapInfoYaml, nil) plug := interfaces.NewConnectedPlug(plugSnap.Plugs["modem-manager"], nil, nil) + release.OnClassic = true dbusSpec := &dbus.Specification{} err := dbusSpec.AddConnectedPlug(s.iface, plug, s.slot) c.Assert(err, IsNil) - c.Assert(dbusSpec.SecurityTags(), DeepEquals, []string{"snap.modem-manager.mmcli"}) - snippet := dbusSpec.SnippetForTag("snap.modem-manager.mmcli") - c.Assert(snippet, testutil.Contains, "deny own=\"org.freedesktop.ModemManager1\"") - c.Assert(snippet, testutil.Contains, "deny send_destination=\"org.freedesktop.ModemManager1\"") + c.Assert(dbusSpec.SecurityTags(), DeepEquals, []string(nil)) } func (s *ModemManagerInterfaceSuite) TestInterfaces(c *C) { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/mount_control.go snapd-2.57.5+20.04/interfaces/builtin/mount_control.go --- snapd-2.55.5+20.04/interfaces/builtin/mount_control.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/mount_control.go 2022-10-17 16:25:18.000000000 +0000 @@ -106,22 +106,9 @@ // - "remount" } -// List of allowed filesystem types. This can be extended, keeping in mind that -// the filesystems in the following list were considered either dangerous or -// not relevant for this interface: -// bpf -// cgroup -// cgroup2 -// debugfs -// devpts -// ecryptfs -// hugetlbfs -// overlayfs -// proc -// securityfs -// sysfs -// tracefs -var allowedFSTypes = []string{ +// List of filesystem types to allow if the plug declaration does not +// explicitly specify a filesystem type. +var defaultFSTypes = []string{ "aufs", "autofs", "btrfs", @@ -145,6 +132,23 @@ "xfs", } +// The filesystems in the following list were considered either dangerous or +// not relevant for this interface: +var disallowedFSTypes = []string{ + "bpf", + "cgroup", + "cgroup2", + "debugfs", + "devpts", + "ecryptfs", + "hugetlbfs", + "overlayfs", + "proc", + "securityfs", + "sysfs", + "tracefs", +} + // mountControlInterface allows creating transient and persistent mounts type mountControlInterface struct { commonInterface @@ -300,7 +304,7 @@ if !typeRegexp.MatchString(t) { return fmt.Errorf(`mount-control filesystem type invalid: %q`, t) } - if !strutil.ListContains(allowedFSTypes, t) { + if strutil.ListContains(disallowedFSTypes, t) { return fmt.Errorf(`mount-control forbidden filesystem type: %q`, t) } if t == "tmpfs" { @@ -453,15 +457,15 @@ if len(mountInfo.types) > 0 { types = mountInfo.types } else { - types = allowedFSTypes + types = defaultFSTypes } typeRule = "fstype=(" + strings.Join(types, ",") + ")" } options := strings.Join(mountInfo.options, ",") - emit(" mount %s options=(%s) \"%s\" -> \"%s\",\n", typeRule, options, source, target) - emit(" umount \"%s\",\n", target) + emit(" mount %s options=(%s) \"%s\" -> \"%s{,/}\",\n", typeRule, options, source, target) + emit(" umount \"%s{,/}\",\n", target) return nil }) diff -Nru snapd-2.55.5+20.04/interfaces/builtin/mount_control_test.go snapd-2.57.5+20.04/interfaces/builtin/mount_control_test.go --- snapd-2.55.5+20.04/interfaces/builtin/mount_control_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/mount_control_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -65,6 +65,7 @@ options: [ro] - what: /dev/sda[0-1] where: $SNAP_COMMON/{foo,other,**} + type: [mycustomfs] options: [sync] apps: app: @@ -284,23 +285,25 @@ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `capability sys_admin,`) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/{,usr/}bin/mount ixr,`) - expectedMountLine1 := `mount fstype=(ext2,ext3,ext4) options=(rw,sync) "/dev/sd*" -> "/media/**",` + expectedMountLine1 := `mount fstype=(ext2,ext3,ext4) options=(rw,sync) "/dev/sd*" -> "/media/**{,/}",` c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine1) - expectedMountLine2 := `mount options=(bind) "/usr/**" -> "/var/snap/consumer/common/**",` + expectedMountLine2 := `mount options=(bind) "/usr/**" -> "/var/snap/consumer/common/**{,/}",` c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine2) expectedMountLine3 := `mount fstype=(` + `aufs,autofs,btrfs,ext2,ext3,ext4,hfs,iso9660,jfs,msdos,ntfs,ramfs,` + `reiserfs,squashfs,tmpfs,ubifs,udf,ufs,vfat,zfs,xfs` + - `) options=(ro) "/dev/sda{0,1}" -> "/var/snap/consumer/common/**",` + `) options=(ro) "/dev/sda{0,1}" -> "/var/snap/consumer/common/**{,/}",` c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine3) + expectedUmountLine3 := `umount "/var/snap/consumer/common/**{,/}",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedUmountLine3) - expectedMountLine4 := `mount fstype=(` + - `aufs,autofs,btrfs,ext2,ext3,ext4,hfs,iso9660,jfs,msdos,ntfs,ramfs,` + - `reiserfs,squashfs,tmpfs,ubifs,udf,ufs,vfat,zfs,xfs` + - `) options=(sync) "/dev/sda[0-1]" -> "/var/snap/consumer/common/{foo,other,**}",` + expectedMountLine4 := `mount fstype=(mycustomfs) options=(sync) ` + + `"/dev/sda[0-1]" -> "/var/snap/consumer/common/{foo,other,**}{,/}",` c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine4) + expectedUmountLine4 := `umount "/var/snap/consumer/common/{foo,other,**}{,/}",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedUmountLine4) } func (s *MountControlInterfaceSuite) TestStaticInfo(c *C) { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/mount_observe.go snapd-2.57.5+20.04/interfaces/builtin/mount_observe.go --- snapd-2.55.5+20.04/interfaces/builtin/mount_observe.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/mount_observe.go 2022-10-17 16:25:18.000000000 +0000 @@ -39,16 +39,25 @@ # Needed by 'df'. This is an information leak @{PROC}/mounts r, +# Needed by 'htop' to detect whether it's running under lxc/lxd/docker +@{PROC}/1/mounts r, + owner @{PROC}/@{pid}/mounts r, owner @{PROC}/@{pid}/mountinfo r, owner @{PROC}/@{pid}/mountstats r, /sys/devices/*/block/{,**} r, +# Needed by 'htop' to calculate RAM usage more accurately (and informational purposes, if enabled) +@{PROC}/spl/kstat/zfs/arcstats r, + @{PROC}/swaps r, # This is often out of date but some apps insist on using it /etc/mtab r, /etc/fstab r, + +# some apps also insist on consulting utab +/run/mount/utab r, ` const mountObserveConnectedPlugSecComp = ` diff -Nru snapd-2.55.5+20.04/interfaces/builtin/network_control.go snapd-2.57.5+20.04/interfaces/builtin/network_control.go --- snapd-2.55.5+20.04/interfaces/builtin/network_control.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/network_control.go 2022-10-17 16:25:18.000000000 +0000 @@ -68,6 +68,38 @@ member="SetLink{DefaultRoute,DNSOverTLS,DNS,DNSEx,DNSSEC,DNSSECNegativeTrustAnchors,MulticastDNS,Domains,LLMNR}" peer=(label=unconfined), +# required by resolvectl command +dbus (send) + bus=system + path="/org/freedesktop/resolve1" + interface=org.freedesktop.DBus.Properties + member=Get{,All} + peer=(label=unconfined), + +# required by resolvectl command +dbus (receive) + bus=system + path="/org/freedesktop/resolve1" + interface=org.freedesktop.DBus.Properties + member=PropertiesChanged + peer=(label=unconfined), + +# required by resolvectl command +dbus (send) + bus=system + path="/org/freedesktop/resolve1/link/*" + interface="org.freedesktop.DBus.Properties" + member=Get{,All} + peer=(label=unconfined), + +# required by resolvectl command +dbus (receive) + bus=system + path="/org/freedesktop/resolve1/link/*" + interface="org.freedesktop.DBus.Properties" + member=PropertiesChanged + peer=(label=unconfined), + #include capability net_admin, @@ -131,6 +163,7 @@ /{,usr/}{,s}bin/pppdump ixr, /{,usr/}{,s}bin/pppoe-discovery ixr, #/{,usr/}{,s}bin/pppstats ixr, # needs sys_module +/{,usr/}{,s}bin/resolvectl ixr, /{,usr/}{,s}bin/route ixr, /{,usr/}{,s}bin/routef ixr, /{,usr/}{,s}bin/routel ixr, diff -Nru snapd-2.55.5+20.04/interfaces/builtin/network_manager.go snapd-2.57.5+20.04/interfaces/builtin/network_manager.go --- snapd-2.55.5+20.04/interfaces/builtin/network_manager.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/network_manager.go 2022-10-17 16:25:18.000000000 +0000 @@ -81,6 +81,9 @@ /sys/devices/virtual/net/**/dev_id r, /sys/devices/**/net/**/ifindex r, +# access to bridge sysfs interfaces for bridge settings +/sys/devices/virtual/net/*/bridge/* rw, + /dev/rfkill rw, /run/udev/data/* r, diff -Nru snapd-2.55.5+20.04/interfaces/builtin/opengl.go snapd-2.57.5+20.04/interfaces/builtin/opengl.go --- snapd-2.55.5+20.04/interfaces/builtin/opengl.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/opengl.go 2022-10-17 16:25:18.000000000 +0000 @@ -55,6 +55,7 @@ /var/lib/snapd/hostfs/{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libcudnn{,_adv_infer,_adv_train,_cnn_infer,_cnn_train,_ops_infer,_ops_train}*.so{,.*} rm, /var/lib/snapd/hostfs/{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnvrtc{,-builtins}*.so{,.*} rm, /var/lib/snapd/hostfs/{,usr/}lib{,32,64,x32}/{,@{multiarch}/}libnvToolsExt.so{,.*} rm, +/var/lib/snapd/hostfs/{,usr/}lib{,32,64,x32}/{,@{multiarch}/}nvidia/wine/*.dll rm, # Support reading the Vulkan ICD files /var/lib/snapd/lib/vulkan/ r, @@ -124,6 +125,10 @@ /dev/mali[0-9]* rw, /dev/dma_buf_te rw, +# NXP i.MX driver +# https://github.com/Freescale/kernel-module-imx-gpu-viv +/dev/galcore rw, + # OpenCL ICD files /etc/OpenCL/vendors/ r, /etc/OpenCL/vendors/** r, @@ -132,12 +137,14 @@ @{PROC}/driver/prl_vtg rw, # /sys/devices -/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/config r, -/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/revision r, -/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/boot_vga r, -/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}class r, -/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}device r, -/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}vendor r, +/sys/devices/{,*pcie-controller/,platform/{soc,scb}/*.pcie/}pci[0-9a-f]*/**/config r, +/sys/devices/{,*pcie-controller/,platform/{soc,scb}/*.pcie/}pci[0-9a-f]*/**/revision r, +/sys/devices/{,*pcie-controller/,platform/{soc,scb}/*.pcie/}pci[0-9a-f]*/**/resource r, +/sys/devices/{,*pcie-controller/,platform/{soc,scb}/*.pcie/}pci[0-9a-f]*/**/irq r, +/sys/devices/{,*pcie-controller/,platform/{soc,scb}/*.pcie/}pci[0-9a-f]*/**/boot_vga r, +/sys/devices/{,*pcie-controller/,platform/{soc,scb}/*.pcie/}pci[0-9a-f]*/**/{,subsystem_}class r, +/sys/devices/{,*pcie-controller/,platform/{soc,scb}/*.pcie/}pci[0-9a-f]*/**/{,subsystem_}device r, +/sys/devices/{,*pcie-controller/,platform/{soc,scb}/*.pcie/}pci[0-9a-f]*/**/{,subsystem_}vendor r, /sys/devices/**/drm{,_dp_aux_dev}/** r, # FIXME: this is an information leak and snapd should instead query udev for @@ -174,6 +181,7 @@ `KERNEL=="pvr_sync"`, `KERNEL=="mali[0-9]*"`, `KERNEL=="dma_buf_te"`, + `KERNEL=="galcore"`, } func init() { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/opengl_test.go snapd-2.57.5+20.04/interfaces/builtin/opengl_test.go --- snapd-2.55.5+20.04/interfaces/builtin/opengl_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/opengl_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -83,12 +83,13 @@ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/nvidia* rw,`) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/dri/renderD[0-9]* rw,`) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/mali[0-9]* rw,`) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/dev/galcore rw,`) } func (s *OpenglInterfaceSuite) TestUDevSpec(c *C) { spec := &udev.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) - c.Assert(spec.Snippets(), HasLen, 12) + c.Assert(spec.Snippets(), HasLen, 13) c.Assert(spec.Snippets(), testutil.Contains, `# opengl SUBSYSTEM=="drm", KERNEL=="card[0-9]*", TAG+="snap_consumer_app"`) c.Assert(spec.Snippets(), testutil.Contains, `# opengl @@ -107,6 +108,8 @@ KERNEL=="mali[0-9]*", TAG+="snap_consumer_app"`) c.Assert(spec.Snippets(), testutil.Contains, `# opengl KERNEL=="dma_buf_te", TAG+="snap_consumer_app"`) + c.Assert(spec.Snippets(), testutil.Contains, `# opengl +KERNEL=="galcore", TAG+="snap_consumer_app"`) c.Assert(spec.Snippets(), testutil.Contains, fmt.Sprintf(`TAG=="snap_consumer_app", RUN+="%v/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`, dirs.DistroLibExecDir)) } diff -Nru snapd-2.55.5+20.04/interfaces/builtin/posix_mq.go snapd-2.57.5+20.04/interfaces/builtin/posix_mq.go --- snapd-2.55.5+20.04/interfaces/builtin/posix_mq.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/posix_mq.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package builtin import ( + "errors" "fmt" "regexp" "strings" @@ -153,15 +154,13 @@ func (iface *posixMQInterface) getPermissions(attrs interfaces.Attrer, name string) ([]string, error) { var perms []string - if permsAttr, isSet := attrs.Lookup("permissions"); isSet { - if permsList, err := iface.validatePermissionsAttr(permsAttr); err != nil { - return nil, err - } else { - perms = permsList - } - } else { + err := attrs.Attr("permissions", &perms) + switch { + case errors.Is(err, snap.AttributeNotFoundError{}): // If the permissions have not been specified, use the defaults perms = posixMQDefaultPlugPermissions + case err != nil: + return nil, err } if err := iface.validatePermissionList(perms, name); err != nil { @@ -171,30 +170,47 @@ return perms, nil } -func (iface *posixMQInterface) getPath(attrs interfaces.Attrer, name string) (string, error) { - var path string - - if pathAttr, isSet := attrs.Lookup("path"); isSet { - if pathStr, ok := pathAttr.(string); ok { - path = pathStr - } else { - return "", fmt.Errorf(`posix-mq slot "path" attribute must be a string, not %v`, pathAttr) +func (iface *posixMQInterface) getPaths(attrs interfaces.Attrer, name string) ([]string, error) { + var pathList []string + var pathStr string + + // The path attribute can either be a string or an array of strings + err := attrs.Attr("path", &pathStr) + switch { + case errors.Is(err, snap.AttributeNotFoundError{}): + return nil, fmt.Errorf(`posix-mq slot requires the "path" attribute`) + case err != nil: + // If the attribute exists but reading it as a string didn't work, try reading it as an array + if err = attrs.Attr("path", &pathList); err != nil { + // If that didn't work, the attribute is an invalid type + return nil, err } - } else { - return "", fmt.Errorf(`posix-mq slot requires the "path" attribute`) + default: + // If the path is a single string, turn it into an array + pathList = append(pathList, pathStr) } - // Path must begin with a / - if path[0] != '/' { - path = "/" + path + if len(pathList) == 0 { + return nil, fmt.Errorf(`posix-mq slot requires at least one value in the "path" attribute`) } - if err := iface.validatePath(name, path); err != nil { - return "", err - } + for i, path := range pathList { + if len(path) == 0 { + return nil, fmt.Errorf(`posix-mq slot "path" attribute values cannot be empty`) + } - return path, nil + // Path must begin with a / + if path[0] != '/' { + path = "/" + path + pathList[i] = path + } + if err := iface.validatePath(name, path); err != nil { + return nil, err + } + } + + return pathList, nil } func (iface *posixMQInterface) validatePath(name, path string) error { @@ -207,7 +223,7 @@ } if !cleanSubPath(path) { - return fmt.Errorf(`posix-mq "path" attribute is not a clean path: %v"`, path) + return fmt.Errorf(`posix-mq "path" attribute is not a clean path: %q`, path) } return nil @@ -260,35 +276,44 @@ } // Only ensure that the given path is valid, don't use it here - if _, err := iface.getPath(slot, slot.Name); err != nil { + if _, err := iface.getPaths(slot, slot.Name); err != nil { return err } return nil } +func (iface *posixMQInterface) generateSnippet(name, plugOrSlot string, permissions, paths []string) string { + var snippet strings.Builder + aaPerms := strings.Join(permissions, " ") + + snippet.WriteString(fmt.Sprintf(" # POSIX Message Queue %s: %s\n", plugOrSlot, name)) + for _, path := range paths { + snippet.WriteString(fmt.Sprintf(" mqueue (%s) \"%s\",\n", aaPerms, path)) + } + + return snippet.String() +} + func (iface *posixMQInterface) AppArmorPermanentSlot(spec *apparmor.Specification, slot *snap.SlotInfo) error { if implicitSystemPermanentSlot(slot) { return nil } - path, err := iface.getPath(slot, slot.Name) + paths, err := iface.getPaths(slot, slot.Name) if err != nil { return err } - // Slots always have all permissions enabled for the - // given message queue path - aaPerms := strings.Join(posixMQPlugPermissions, " ") - spec.AddSnippet(fmt.Sprintf(` # POSIX Message Queue management - mqueue (%s) "%s", -`, aaPerms, path)) + // Slots always have all permissions enabled for the given message queue path + snippet := iface.generateSnippet(slot.Name, "slot", posixMQPlugPermissions, paths) + spec.AddSnippet(snippet) return nil } func (iface *posixMQInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { - path, err := iface.getPath(slot, slot.Name()) + paths, err := iface.getPaths(slot, slot.Name()) if err != nil { return err } @@ -303,10 +328,8 @@ perms = append(perms, "open") } - aaPerms := strings.Join(perms, " ") - spec.AddSnippet(fmt.Sprintf(` # POSIX Message Queue plug communication - mqueue (%s) "%s", -`, aaPerms, path)) + snippet := iface.generateSnippet(plug.Name(), "plug", perms, paths) + spec.AddSnippet(snippet) return nil } diff -Nru snapd-2.55.5+20.04/interfaces/builtin/posix_mq_test.go snapd-2.57.5+20.04/interfaces/builtin/posix_mq_test.go --- snapd-2.55.5+20.04/interfaces/builtin/posix_mq_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/posix_mq_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -62,6 +62,27 @@ - read - write + test-path-array: + interface: posix-mq + path: + - /test-array-1 + - /test-array-2 + - /test-array-3 + + test-empty-path-array: + interface: posix-mq + path: [] + + test-one-empty-path-array: + interface: posix-mq + path: + - /test-array-1 + - "" + + test-empty-path: + interface: posix-mq + path: "" + test-invalid-path-1: interface: posix-mq path: ../../test-invalid @@ -73,7 +94,14 @@ test-invalid-path-3: interface: posix-mq path: - - this-is-not-a-string + this-is-not-a-valid-path: true + + test-invalid-path-4: + interface: posix-mq + + test-invalid-path-5: + interface: posix-mq + path: /. test-invalid-perms-1: interface: posix-mq @@ -88,6 +116,13 @@ path: /test-invalid-perms-2 permissions: not-a-list + test-invalid-perms-3: + interface: posix-mq + path: /test-invalid-perms-3 + permissions: + - create + - [not-a-string] + test-label: interface: posix-mq posix-mq: this-is-a-test-label @@ -163,6 +198,19 @@ plugs: [test-all-perms] ` +const pathArrayPlugSnapInfoYaml = `name: consumer +version: 1.0 + +plugs: + test-path-array: + interface: posix-mq + +apps: + app: + command: foo + plugs: [test-path-array] +` + const invalidPerms1PlugSnapInfoYaml = `name: consumer version: 1.0 @@ -190,6 +238,19 @@ plugs: [test-label] ` +const invalidPerms3PlugSnapInfoYaml = `name: consumer +version: 1.0 + +plugs: + test-invalid-perms-3: + interface: posix-mq + +apps: + app: + command: foo + plugs: [test-invalid-perms-3] +` + const testInvalidLabelPlugSnapInfoYaml = `name: consumer version: 1.0 @@ -230,6 +291,20 @@ testAllPermsPlugInfo *snap.PlugInfo testAllPermsPlug *interfaces.ConnectedPlug + testPathArraySlotInfo *snap.SlotInfo + testPathArraySlot *interfaces.ConnectedSlot + testPathArrayPlugInfo *snap.PlugInfo + testPathArrayPlug *interfaces.ConnectedPlug + + testEmptyPathArraySlotInfo *snap.SlotInfo + testEmptyPathArraySlot *interfaces.ConnectedSlot + + testOneEmptyPathArraySlotInfo *snap.SlotInfo + testOneEmptyPathArraySlot *interfaces.ConnectedSlot + + testEmptyPathSlotInfo *snap.SlotInfo + testEmptyPathSlot *interfaces.ConnectedSlot + testInvalidPath1SlotInfo *snap.SlotInfo testInvalidPath1Slot *interfaces.ConnectedSlot @@ -239,6 +314,12 @@ testInvalidPath3SlotInfo *snap.SlotInfo testInvalidPath3Slot *interfaces.ConnectedSlot + testInvalidPath4SlotInfo *snap.SlotInfo + testInvalidPath4Slot *interfaces.ConnectedSlot + + testInvalidPath5SlotInfo *snap.SlotInfo + testInvalidPath5Slot *interfaces.ConnectedSlot + testInvalidPerms1SlotInfo *snap.SlotInfo testInvalidPerms1Slot *interfaces.ConnectedSlot testInvalidPerms1PlugInfo *snap.PlugInfo @@ -247,6 +328,11 @@ testInvalidPerms2SlotInfo *snap.SlotInfo testInvalidPerms2Slot *interfaces.ConnectedSlot + testInvalidPerms3SlotInfo *snap.SlotInfo + testInvalidPerms3Slot *interfaces.ConnectedSlot + testInvalidPerms3PlugInfo *snap.PlugInfo + testInvalidPerms3Plug *interfaces.ConnectedPlug + testLabelSlotInfo *snap.SlotInfo testLabelSlot *interfaces.ConnectedSlot testLabelPlugInfo *snap.PlugInfo @@ -279,6 +365,18 @@ s.testAllPermsSlotInfo = slotSnap.Slots["test-all-perms"] s.testAllPermsSlot = interfaces.NewConnectedSlot(s.testAllPermsSlotInfo, nil, nil) + s.testPathArraySlotInfo = slotSnap.Slots["test-path-array"] + s.testPathArraySlot = interfaces.NewConnectedSlot(s.testPathArraySlotInfo, nil, nil) + + s.testEmptyPathArraySlotInfo = slotSnap.Slots["test-empty-path-array"] + s.testEmptyPathArraySlot = interfaces.NewConnectedSlot(s.testEmptyPathArraySlotInfo, nil, nil) + + s.testOneEmptyPathArraySlotInfo = slotSnap.Slots["test-one-empty-path-array"] + s.testOneEmptyPathArraySlot = interfaces.NewConnectedSlot(s.testOneEmptyPathArraySlotInfo, nil, nil) + + s.testEmptyPathSlotInfo = slotSnap.Slots["test-empty-path"] + s.testEmptyPathSlot = interfaces.NewConnectedSlot(s.testEmptyPathSlotInfo, nil, nil) + s.testInvalidPath1SlotInfo = slotSnap.Slots["test-invalid-path-1"] s.testInvalidPath1Slot = interfaces.NewConnectedSlot(s.testInvalidPath1SlotInfo, nil, nil) @@ -288,12 +386,21 @@ s.testInvalidPath3SlotInfo = slotSnap.Slots["test-invalid-path-3"] s.testInvalidPath3Slot = interfaces.NewConnectedSlot(s.testInvalidPath3SlotInfo, nil, nil) + s.testInvalidPath4SlotInfo = slotSnap.Slots["test-invalid-path-4"] + s.testInvalidPath4Slot = interfaces.NewConnectedSlot(s.testInvalidPath4SlotInfo, nil, nil) + + s.testInvalidPath5SlotInfo = slotSnap.Slots["test-invalid-path-5"] + s.testInvalidPath5Slot = interfaces.NewConnectedSlot(s.testInvalidPath5SlotInfo, nil, nil) + s.testInvalidPerms1SlotInfo = slotSnap.Slots["test-invalid-perms-1"] s.testInvalidPerms1Slot = interfaces.NewConnectedSlot(s.testInvalidPerms1SlotInfo, nil, nil) s.testInvalidPerms2SlotInfo = slotSnap.Slots["test-invalid-perms-2"] s.testInvalidPerms2Slot = interfaces.NewConnectedSlot(s.testInvalidPerms2SlotInfo, nil, nil) + s.testInvalidPerms3SlotInfo = slotSnap.Slots["test-invalid-perms-3"] + s.testInvalidPerms3Slot = interfaces.NewConnectedSlot(s.testInvalidPerms3SlotInfo, nil, nil) + s.testLabelSlotInfo = slotSnap.Slots["test-label"] s.testLabelSlot = interfaces.NewConnectedSlot(s.testLabelSlotInfo, nil, nil) @@ -327,6 +434,14 @@ plugSnap6 := snaptest.MockInfo(c, testInvalidLabelPlugSnapInfoYaml, nil) s.testInvalidLabelPlugInfo = plugSnap6.Plugs["test-invalid-label"] s.testInvalidLabelPlug = interfaces.NewConnectedPlug(s.testInvalidLabelPlugInfo, nil, nil) + + plugSnap7 := snaptest.MockInfo(c, invalidPerms3PlugSnapInfoYaml, nil) + s.testInvalidPerms3PlugInfo = plugSnap7.Plugs["test-invalid-perms-3"] + s.testInvalidPerms3Plug = interfaces.NewConnectedPlug(s.testInvalidPerms3PlugInfo, nil, nil) + + plugSnap8 := snaptest.MockInfo(c, pathArrayPlugSnapInfoYaml, nil) + s.testPathArrayPlugInfo = plugSnap8.Plugs["test-path-array"] + s.testPathArrayPlug = interfaces.NewConnectedPlug(s.testPathArrayPlugInfo, nil, nil) } func (s *PosixMQInterfaceSuite) checkSlotSeccompSnippet(c *C, spec *seccomp.Specification) { @@ -348,9 +463,11 @@ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.producer.app"}) slotSnippet := spec.SnippetForTag("snap.producer.app") + c.Check(slotSnippet, testutil.Contains, `# POSIX Message Queue slot: test-rw`) c.Check(slotSnippet, testutil.Contains, `mqueue (open read write create delete) "/test-rw",`) plugSnippet := spec.SnippetForTag("snap.consumer.app") + c.Check(plugSnippet, testutil.Contains, `# POSIX Message Queue plug: test-rw`) c.Check(plugSnippet, testutil.Contains, `mqueue (read write open) "/test-rw",`) } @@ -381,9 +498,11 @@ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.producer.app"}) slotSnippet := spec.SnippetForTag("snap.producer.app") + c.Check(slotSnippet, testutil.Contains, `# POSIX Message Queue slot: test-default`) c.Check(slotSnippet, testutil.Contains, `mqueue (open read write create delete) "/test-default",`) plugSnippet := spec.SnippetForTag("snap.consumer.app") + c.Check(plugSnippet, testutil.Contains, `# POSIX Message Queue plug: test-default`) c.Check(plugSnippet, testutil.Contains, `mqueue (read write open) "/test-default",`) } @@ -440,6 +559,46 @@ c.Check(plugSnippet, Not(testutil.Contains), "mq_unlink") } +func (s *PosixMQInterfaceSuite) TestPathArrayMQAppArmor(c *C) { + spec := &apparmor.Specification{} + err := spec.AddPermanentSlot(s.iface, s.testPathArraySlotInfo) + c.Assert(err, IsNil) + err = spec.AddConnectedPlug(s.iface, s.testPathArrayPlug, s.testPathArraySlot) + c.Assert(err, IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.producer.app"}) + + slotSnippet := spec.SnippetForTag("snap.producer.app") + c.Check(slotSnippet, testutil.Contains, ` mqueue (open read write create delete) "/test-array-1", + mqueue (open read write create delete) "/test-array-2", + mqueue (open read write create delete) "/test-array-3", +`) + + plugSnippet := spec.SnippetForTag("snap.consumer.app") + c.Check(plugSnippet, testutil.Contains, ` mqueue (read write open) "/test-array-1", + mqueue (read write open) "/test-array-2", + mqueue (read write open) "/test-array-3", +`) +} + +func (s *PosixMQInterfaceSuite) TestPathArrayMQSeccomp(c *C) { + spec := &seccomp.Specification{} + err := spec.AddPermanentSlot(s.iface, s.testPathArraySlotInfo) + c.Assert(err, IsNil) + err = spec.AddConnectedPlug(s.iface, s.testPathArrayPlug, s.testPathArraySlot) + c.Assert(err, IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.producer.app"}) + + s.checkSlotSeccompSnippet(c, spec) + + plugSnippet := spec.SnippetForTag("snap.consumer.app") + c.Check(plugSnippet, testutil.Contains, "mq_open") + c.Check(plugSnippet, testutil.Contains, "mq_notify") + c.Check(plugSnippet, testutil.Contains, "mq_timedreceive") + c.Check(plugSnippet, testutil.Contains, "mq_timedsend") + c.Check(plugSnippet, testutil.Contains, "mq_getsetattr") + c.Check(plugSnippet, Not(testutil.Contains), "mq_unlink") +} + func (s *PosixMQInterfaceSuite) TestAllPermsMQAppArmor(c *C) { spec := &apparmor.Specification{} err := spec.AddPermanentSlot(s.iface, s.testAllPermsSlotInfo) @@ -490,7 +649,7 @@ func (s *PosixMQInterfaceSuite) TestPathStringValidation(c *C) { spec := &apparmor.Specification{} err := spec.AddPermanentSlot(s.iface, s.testInvalidPath3SlotInfo) - c.Check(err, ErrorMatches, `posix-mq slot "path" attribute must be a string, not \[this-is-not-a-string\]`) + c.Check(err, ErrorMatches, `snap "producer" has interface "posix-mq" with invalid value type map\[string\]interface {} for "path" attribute: \*\[\]string`) } func (s *PosixMQInterfaceSuite) TestInvalidPerms1(c *C) { @@ -506,6 +665,15 @@ `posix-mq slot permission "break-everything" not valid, must be one of \[open read write create delete\]`) } +func (s *PosixMQInterfaceSuite) TestInvalidPerms3(c *C) { + spec := &apparmor.Specification{} + err := spec.AddPermanentSlot(s.iface, s.testInvalidPerms3SlotInfo) + c.Assert(err, IsNil) + err = spec.AddConnectedPlug(s.iface, s.testInvalidPerms3Plug, s.testInvalidPerms3Slot) + c.Check(err, ErrorMatches, + `snap "producer" has interface "posix-mq" with invalid value type \[\]interface {} for "permissions" attribute: \*\[\]string`) +} + func (s *PosixMQInterfaceSuite) TestName(c *C) { c.Assert(s.iface.Name(), Equals, "posix-mq") } @@ -514,7 +682,9 @@ // Ensure that the interface does not fail if AppArmor is unsupported restore := apparmor_sandbox.MockLevel(apparmor_sandbox.Unsupported) defer restore() - c.Assert(interfaces.BeforePrepareSlot(s.iface, s.testReadWriteSlotInfo), IsNil) + + c.Check(interfaces.BeforePrepareSlot(s.iface, s.testReadWriteSlotInfo), IsNil) + c.Check(interfaces.BeforePreparePlug(s.iface, s.testReadWritePlugInfo), IsNil) } func (s *PosixMQInterfaceSuite) TestFeatureDetection(c *C) { @@ -546,6 +716,8 @@ s.checkSlotPosixMQAttr(c, s.testReadOnlySlotInfo) c.Assert(interfaces.BeforePrepareSlot(s.iface, s.testAllPermsSlotInfo), IsNil) s.checkSlotPosixMQAttr(c, s.testAllPermsSlotInfo) + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.testPathArraySlotInfo), IsNil) + s.checkSlotPosixMQAttr(c, s.testPathArraySlotInfo) c.Assert(interfaces.BeforePrepareSlot(s.iface, s.testLabelSlotInfo), IsNil) c.Check(s.testLabelSlotInfo.Attrs["posix-mq"], Equals, "this-is-a-test-label") @@ -555,13 +727,23 @@ c.Check(interfaces.BeforePrepareSlot(s.iface, s.testInvalidPath2SlotInfo), ErrorMatches, `posix-mq "path" attribute is invalid: /test-invalid-2"\["`) c.Check(interfaces.BeforePrepareSlot(s.iface, s.testInvalidPath3SlotInfo), ErrorMatches, - `posix-mq slot "path" attribute must be a string, not \[this-is-not-a-string\]`) + `snap "producer" has interface "posix-mq" with invalid value type map\[string\]interface {} for "path" attribute: \*\[\]string`) + c.Check(interfaces.BeforePrepareSlot(s.iface, s.testInvalidPath4SlotInfo), ErrorMatches, + `posix-mq slot requires the "path" attribute`) + c.Check(interfaces.BeforePrepareSlot(s.iface, s.testInvalidPath5SlotInfo), ErrorMatches, + `posix-mq "path" attribute is not a clean path: "/."`) c.Check(interfaces.BeforePrepareSlot(s.iface, s.testInvalidPerms1SlotInfo), ErrorMatches, `posix-mq slot permission "break-everything" not valid, must be one of \[open read write create delete\]`) c.Check(interfaces.BeforePrepareSlot(s.iface, s.testInvalidPerms2SlotInfo), ErrorMatches, - `posix-mq slot "permissions" attribute must be a list of strings, not not-a-list`) + `snap "producer" has interface "posix-mq" with invalid value type string for "permissions" attribute: \*\[\]string`) c.Check(interfaces.BeforePrepareSlot(s.iface, s.testInvalidLabelSlotInfo), ErrorMatches, `posix-mq "posix-mq" attribute must be a string, not \[broken\]`) + c.Check(interfaces.BeforePrepareSlot(s.iface, s.testEmptyPathArraySlotInfo), ErrorMatches, + `posix-mq slot requires at least one value in the "path" attribute`) + c.Check(interfaces.BeforePrepareSlot(s.iface, s.testEmptyPathSlotInfo), ErrorMatches, + `posix-mq slot "path" attribute values cannot be empty`) + c.Check(interfaces.BeforePrepareSlot(s.iface, s.testOneEmptyPathArraySlotInfo), ErrorMatches, + `posix-mq slot "path" attribute values cannot be empty`) } func (s *PosixMQInterfaceSuite) TestSanitizePlug(c *C) { @@ -577,6 +759,8 @@ s.checkPlugPosixMQAttr(c, s.testReadOnlyPlugInfo) c.Assert(interfaces.BeforePreparePlug(s.iface, s.testAllPermsPlugInfo), IsNil) s.checkPlugPosixMQAttr(c, s.testAllPermsPlugInfo) + c.Assert(interfaces.BeforePreparePlug(s.iface, s.testPathArrayPlugInfo), IsNil) + s.checkPlugPosixMQAttr(c, s.testPathArrayPlugInfo) c.Assert(interfaces.BeforePreparePlug(s.iface, s.testInvalidPerms1PlugInfo), IsNil) s.checkPlugPosixMQAttr(c, s.testInvalidPerms1PlugInfo) c.Assert(interfaces.BeforePreparePlug(s.iface, s.testLabelPlugInfo), IsNil) diff -Nru snapd-2.55.5+20.04/interfaces/builtin/process_control.go snapd-2.57.5+20.04/interfaces/builtin/process_control.go --- snapd-2.55.5+20.04/interfaces/builtin/process_control.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/process_control.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,6 +34,8 @@ # signals, cpu affinity and nice. This is reserved because it grants privileged # access to all processes under root or processes running under the same UID # otherwise. +# Note: Scope augmented by allowing read/write for /proc/self_pid/coredump_filter +# (needed by opensearch) # /{,usr/}bin/nice is already in default policy /{,usr/}bin/renice ixr, @@ -46,6 +48,8 @@ signal (send), /{,usr/}bin/kill ixr, /{,usr/}bin/pkill ixr, + +@{PROC}/[0-9]*/coredump_filter wr, ` const processControlConnectedPlugSecComp = ` diff -Nru snapd-2.55.5+20.04/interfaces/builtin/pwm.go snapd-2.57.5+20.04/interfaces/builtin/pwm.go --- snapd-2.55.5+20.04/interfaces/builtin/pwm.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/pwm.go 2022-10-17 16:25:18.000000000 +0000 @@ -137,8 +137,6 @@ registerIface(&pwmInterface{commonInterface{ name: "pwm", summary: pwmSummary, - implicitOnCore: true, - implicitOnClassic: true, baseDeclarationSlots: pwmBaseDeclarationSlots, }}) } diff -Nru snapd-2.55.5+20.04/interfaces/builtin/README.md snapd-2.57.5+20.04/interfaces/builtin/README.md --- snapd-2.55.5+20.04/interfaces/builtin/README.md 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/README.md 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,335 @@ +# Interface policy + +## Plug and slot rules + +Declarative rules are used to control what plugs or slots a snap is +allowed to use, and if a snap is allowed to use a plug/slot, what other +slots/plugs can connect to that plug/slot on this snap. + +These rules are declared as a map which has 6 possible keys: + +- allow-installation +- deny-installation +- allow-connection +- deny-connection +- allow-auto-connection +- deny-auto-connection + +Each of these keys can either have a static value of true/false or can be a +more complex object/list which is “evaluated” by snapd on a device to +determine the actual value, be it true or false. + + +### Base declaration and snap declarations + +The rules defined in the snapd interfaces source code (via setting +`commonInterface.baseDeclarationSlots/Plugs`) form the +“base declaration” and can be overridden by per-snap rules in the +snap store published via the per-snap `snap-declaration` assertions, +which make it possible for a store to alter the policy hardcoded in +snapd. For example, a store assertion is typically used to grant +auto-connection to some plugs of a specific application where this is +deemed reasonable or safe (such as auto-connecting the `camera` +interface in a web-streaming application). + +### Basic evaluation and precedence + +The default value of the `allow-*` keys when not otherwise specified is `true` +and the default value of `deny-*` keys when not otherwise specified is `false`. +Matching `deny-*` constraints override/take precedence over `allow-*` +constraints specified in the same declaration. The order of evaluation of rules +is the following (execution stops as soon as a matching rule is found, meaning +that the topmost elements in this list have higher priority): + +- `deny-*` keys in plug snap-declaration rule +- `allow-*` keys in plug snap-declaration rule +- `deny-*` keys in slot snap-declaration rule +- `allow-*` keys in slot snap-declaration rule +- `deny-*` keys in plug base-declaration rule +- `allow-*` keys in plug base-declaration rule +- `deny-*` keys in slot base-declaration rule +- `allow-*` keys in slot base-declaration rule + +In other words, snap-declaration (store) rules have priority over +base-declaration rules; then, plug rules have priority over slot rules, and +finally, deny rules have priority over allow rules. + + +### allow-installation + +The `allow-installation` key is evaluated when the snap is being installed. If +this evaluates to false, the snap cannot be installed if an interface plug or +slot for which `allow-installation` evaluated to `false` exists in the snap. An +example would be the `snapd-control` interface, which has in the +base-declaration the static `allow-installation: false` rule for plugs: + + snapd-control: + allow-installation: false + deny-auto-connection: true + +If a snap does not plug `snapd-control` then this rule does not apply, but if +the snap does declare a `snapd-control` plug and there are no assertions in the +store for this snap about allowing `snapd-control`, then snap installation will +fail. + +Snap interfaces that have `allow-installation` set to `false` for their plugs +in the base-declaration are said to be “**super-privileged**”, meaning they +cannot be used at all without a snap-declaration assertion. + + +### allow-connection + +The `allow-connection` key controls whether an API/manual connection +is permitted at all and usually is used to ensure that only +“compatible” plugs and slots are connected to each other. A great +example is the content interface, where the following (abbreviated) +rule from the base-declaration is used to ensure that a candidate plug +and slot content interface have matching `content` attribute values: + + allow-connection: + plug-attributes: + content: $SLOT(content) + +This can be read as `allow-connection` evaluating to `true` only when the plug +has an attribute `content` with the same value as the attribute `content` in +the slot. That is to say, these plug and slots are compatible because `content` +does match for the plug and slot: + + # in the snap providing the content: + slots: + foo-content: + interface: content + content: specific-files + + # in the snap consuming the content: + plugs: + foo-content: + interface: content + content: specific-files + +While the following plug and slots are not compatible: + + slots: + foo-content: + interface: content + content: other-files + + plugs: + foo-content: + interface: content + content: specific-files + + +### allow-auto-connection + +The allow-auto-connection key is the final key considered when snapd is +evaluating the automatic connection of interface plugs and slots. If this key +evaluates to `true`, then this plug/slot combination is considered a valid +candidate for automatic connection. In this context allow-connection is ignored. + +An automatic connection will happen normally only if there is one single +candidate combination with a slot for a given plug. + + +### Supported rule constraints + +Each of the keys seen before (`allow/deny-installation`, +`allow/deny-connection`, and `allow/deny-auto-connection`) has a set of +sub-keys that can be used as rules with each constraint. The authoritative +place where this information comes from is inside snapd in the `asserts` +package, specifically the file `ifacedecls.go` is the main place where these +are defined. + +In `allow-connection` or `allow-auto-connection` constraints about snap type, +snap ID and publisher can only be specified for the other side snap (e.g. a +slot-side `allow-connection` constraint can only specify plug-snap-type, +plug-snap-id, plug-snap-publisher). + +For the `plug-snap-type` and `slot-snap-type` rules there are 4 +possible values: `core`, `gadget`, `kernel`, and `app`. The `core` snap +type refers to whichever snap is providing snapd on the system and +therefore the system interface slots, either the `core` snap or `snapd` +snap (typically `core` snap on UC16 devices, `snapd` snap on UC18+ +systems, and either on classic systems depending on re-exec logic). + +The `on-store`, `on-brand`, and `on-model` rules are not generally hardcoded in +the snapd interfaces, but are specified in store assertions; they are known as +“device context constraints” and are primarily used to ensure that a given +rule only applies to a device with a serial assertion (and thus model +assertion) from a given brand. This is because if the assertion and snap from a +brand store were copied to a non-branded device, the assertion could still be +acknowledged by the device and the snap installed, but the assertion would not +operate, and snap connections would not take place as they do on the branded +device. + +The `plug-names` and `slot-names` rules are also only used in store assertions. +They refer to the naming of a plug or slot when that slot is scoped globally +with a name other than the generic interface name. +For example this assertion: + + plugs: + gpio: + allow-auto-connection: + slot-names: [ gpio1 ] + plug-names: [ gpio-red-led ] + +only allows the plugging snap to have its plug named `gpio-red-led` +auto-connected to a gpio slot named `gpio1`. + + +### Rule evaluation + +#### Greedy connection / single slot rule + +The first rule about whether an automatic connection happens between a plug and +a slot has to do with “arity” or how many slots a given plug is being +considered to connect to and vice versa. This is expressed with the +`slots-per-plug` and `plugs-per-slot` rules, with the default value of +`plugs-per-slot` being “`*`” meaning any number of plugs can be connected to a +specific slot. The default value of `slots-per-plug` is “`1`”, however, meaning that a +plug can in general without a special snap-declaration only automatically +connect to one slot. All that is to say, if there are multiple candidate slots, +in the default case a plug will auto-connect to neither of them and +snapd will issue a warning. + +See also [this forum +post](https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438) +which was written when this logic was first implemented. + + +#### Maps and Lists + +The next rule about evaluating snap-declaration assertions is that maps are +treated as logical ANDs where each key in the map must individually evaluate to +`true` and lists are treated as logical ORs where only one of the elements of +the list must evaluate to `true`. With the following example assertion, +order +for the `serial-port` plug in this snap to auto-connect, either the first +element of the list must evaluate to `true` or the second element of the list +must evaluate to `true` (or they could both theoretically evaluate to `true`, +but this is impossible in practice since the slots can only come from gadgets +and so to match both the system would have to have two gadget snaps installed +simultaneously which is impossible). + + plugs: + serial-port: + allow-auto-connection: + - + on-store: + - my-app-store + plug-names: + - serial-rf-nic + slot-attributes: + path: /dev/serial-port-rfnic + slot-names: + - serial-rf-nic + slot-snap-id: + - Cx4J8ADDq8xULNaAjO7mQid75ru4rObB + - + on-store: + - my-app-store + plug-names: + - serial-rf-nic + slot-attributes: + path: /dev/serial-port-rfnic + slot-names: + - serial-rf-nic + slot-snap-id: + - WabnwLoV48BCMj8NoOetmdxFFMxDsPGb + +For the first element of the allow-auto-connection list to evaluate to +`true`, the following things must be true: + +- The device must be on a brand device in `my-app-store` AND +- The plug name must be `serial-rf-nic` AND +- The slot name must be `serial-rf-nic` AND +- The slot must declare an attribute, `path`, with the value + `/dev/serial-port-rfnic` AND +- The slot must come from a snap with a snap ID of + `Cx4J8ADDq8xULNaAjO7mQid75ru4rObB` + +The above is also true for the second element of the allow-auto-connection list. + +An equivalent way to write an assertion that works in exactly the same way would be: + + plugs: + serial-port: + allow-auto-connection: + on-store: + - my-app-store + plug-names: + - serial-rf-nic + slot-attributes: + path: /dev/serial-port-rfnic + slot-names: + - serial-rf-nic + slot-snap-id: + - Cx4J8ADDq8xULNaAjO7mQid75ru4rObB + - WabnwLoV48BCMj8NoOetmdxFFMxDsPGb + +The chief difference here is that instead of having a list of two maps, we +instead have a single map, and the key which changes for the two gadgets is the +`slot-snap-id` which now has two values in a list. In this case, the slot snap +ID must be one of the snap IDs in the list in order for the `slot-snap-id` rule +to evaluate to true. So the following things must be true: + +- The device must be on a brand device in `my-app-store` AND +- The plug name must be `serial-rf-nic` AND +- The slot-name must be `serial-rf-nic` AND +- The slot must declare an attribute, `path`, with the value + `/dev/serial-port-rfnic` AND +- The slot must come from a snap with a snap ID of any of the following elements: + - `Cx4J8ADDq8xULNaAjO7mQid75ru4rObB`, OR + - `WabnwLoV48BCMj8NoOetmdxFFMxDsPGb` + +Lists and maps can also be used as values for attributes under +plug/slot-attributes constraints. A map will match only if the attribute value +contains all the entries in the constraints map with the same values (extra +attribute elements are ignored). +A list will match against a non-list attribute value if the value matches any +of the list elements. A list will match against a list attribute value if all +the elements in the attribute list value in turn match something in the list. +This means, for example, a constraint list of value constraints will match a list +of attribute scalars if the two groups of values match as a set (order doesn't +matter). + + +#### Attribute constraints and special matches and variables + +Plug/slot-attributes string value constraints are interpreted as regexps +(wrapped implicitly in `^$` anchors), unless they are one of the special forms +starting with `$`. + +The special forms starting with `$` currently consist of: + +- `$SLOT_PUBLISHER_ID`, `$PLUG_PUBLISHER_ID`: these can be specified in the + `plug-publisher-id` and `slot-publisher-id` constraints respectively and are + used from the plug side and slot side of a declaration to refer to the + publisher-id of the other side of the connection. +- `$PLUG()`, `$SLOT()`: similar to the above, but used in the `plug-attributes` + and `slot-attributes` constraints for specifying attributes instead of + publisher-ids. +- `$MISSING`: used in the `plug-attributes` and `slot-attributes` constraints + to match when the attribute set to `$MISSING` is not specified in the snap. + +For example, these features are used in the base-declaration for the `content` +interface to express that a connection is only allowed when the attribute value +of the verbatim “content” attribute on the slot side is the same as the plug +side and that auto-connection should only take place by default when the plug +publisher ID is the same as the slot publisher ID (unless this is overridden by +a store assertion): + + 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) + diff -Nru snapd-2.55.5+20.04/interfaces/builtin/removable_media.go snapd-2.57.5+20.04/interfaces/builtin/removable_media.go --- snapd-2.55.5+20.04/interfaces/builtin/removable_media.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/removable_media.go 2022-10-17 16:25:18.000000000 +0000 @@ -41,12 +41,12 @@ # Mount points could be in /run/media//* or /media//* /{,run/}media/*/ r, -/{,run/}media/*/** rwkl, +/{,run/}media/*/** mrwklix, # Allow read-only access to /mnt to enumerate items. /mnt/ r, # Allow write access to anything under /mnt -/mnt/** rwkl, +/mnt/** mrwklix, ` func init() { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/removable_media_test.go snapd-2.57.5+20.04/interfaces/builtin/removable_media_test.go --- snapd-2.55.5+20.04/interfaces/builtin/removable_media_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/removable_media_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -80,7 +80,7 @@ c.Assert(err, IsNil) c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.client-snap.other"}) c.Check(apparmorSpec.SnippetForTag("snap.client-snap.other"), testutil.Contains, "/{,run/}media/*/ r") - c.Check(apparmorSpec.SnippetForTag("snap.client-snap.other"), testutil.Contains, "/mnt/** rwkl,") + c.Check(apparmorSpec.SnippetForTag("snap.client-snap.other"), testutil.Contains, "/mnt/** mrwklix,") } func (s *RemovableMediaInterfaceSuite) TestInterfaces(c *C) { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/serial_port.go snapd-2.57.5+20.04/interfaces/builtin/serial_port.go --- snapd-2.55.5+20.04/interfaces/builtin/serial_port.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/serial_port.go 2022-10-17 16:25:18.000000000 +0000 @@ -75,7 +75,8 @@ // - ttySCX (NXP SC16IS7xx serial devices) // - ttyMSMX (Qualcomm msm7x serial devices) // - ttyHSX (Qualcomm GENI based QTI serial cores) -var serialDeviceNodePattern = regexp.MustCompile("^/dev/tty(mxc|USB|ACM|AMA|XRUSB|S|O|SC|MSM|HS)[0-9]+$") +// - ttyGSX (USB gadget serial devices) +var serialDeviceNodePattern = regexp.MustCompile("^/dev/tty(mxc|USB|ACM|AMA|XRUSB|S|O|SC|MSM|HS|GS)[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 diff -Nru snapd-2.55.5+20.04/interfaces/builtin/serial_port_test.go snapd-2.57.5+20.04/interfaces/builtin/serial_port_test.go --- snapd-2.55.5+20.04/interfaces/builtin/serial_port_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/serial_port_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -62,6 +62,8 @@ testSlot10Info *snap.SlotInfo testSlot11 *interfaces.ConnectedSlot testSlot11Info *snap.SlotInfo + testSlot12 *interfaces.ConnectedSlot + testSlot12Info *snap.SlotInfo testSlotCleaned *interfaces.ConnectedSlot testSlotCleanedInfo *snap.SlotInfo missingPathSlot *interfaces.ConnectedSlot @@ -92,6 +94,8 @@ badPathSlot12Info *snap.SlotInfo badPathSlot13 *interfaces.ConnectedSlot badPathSlot13Info *snap.SlotInfo + badPathSlot100 *interfaces.ConnectedSlot + badPathSlot100Info *snap.SlotInfo badInterfaceSlot *interfaces.ConnectedSlot badInterfaceSlotInfo *snap.SlotInfo @@ -164,6 +168,9 @@ test-port-11: interface: serial-port path: /dev/ttyHS0 + test-port-12: + interface: serial-port + path: /dev/ttyGS0 test-port-unclean: interface: serial-port path: /dev/./././ttyS1//// @@ -206,6 +213,9 @@ path: /dev/ttyHS bad-path-13: interface: serial-port + path: /dev/ttyGS + bad-path-100: + interface: serial-port path: /dev/ttyillegal0 bad-interface: other-interface `, nil) @@ -231,6 +241,8 @@ s.testSlot10 = interfaces.NewConnectedSlot(s.testSlot10Info, nil, nil) s.testSlot11Info = osSnapInfo.Slots["test-port-11"] s.testSlot11 = interfaces.NewConnectedSlot(s.testSlot11Info, nil, nil) + s.testSlot12Info = osSnapInfo.Slots["test-port-12"] + s.testSlot12 = interfaces.NewConnectedSlot(s.testSlot12Info, nil, nil) s.testSlotCleanedInfo = osSnapInfo.Slots["test-port-unclean"] s.testSlotCleaned = interfaces.NewConnectedSlot(s.testSlotCleanedInfo, nil, nil) s.missingPathSlotInfo = osSnapInfo.Slots["missing-path"] @@ -261,6 +273,8 @@ s.badPathSlot12 = interfaces.NewConnectedSlot(s.badPathSlot12Info, nil, nil) s.badPathSlot13Info = osSnapInfo.Slots["bad-path-13"] s.badPathSlot13 = interfaces.NewConnectedSlot(s.badPathSlot13Info, nil, nil) + s.badPathSlot100Info = osSnapInfo.Slots["bad-path-100"] + s.badPathSlot100 = interfaces.NewConnectedSlot(s.badPathSlot100Info, nil, nil) s.badInterfaceSlotInfo = osSnapInfo.Slots["bad-interface"] s.badInterfaceSlot = interfaces.NewConnectedSlot(s.badInterfaceSlotInfo, nil, nil) @@ -365,7 +379,20 @@ } func (s *SerialPortInterfaceSuite) TestSanitizeCoreSnapSlots(c *C) { - for _, slot := range []*snap.SlotInfo{s.testSlot1Info, s.testSlot2Info, s.testSlot3Info, s.testSlot4Info, s.testSlot5Info, s.testSlot6Info, s.testSlot7Info, s.testSlot8Info, s.testSlot9Info, s.testSlot10Info, s.testSlot11Info} { + for _, slot := range []*snap.SlotInfo{ + s.testSlot1Info, + s.testSlot2Info, + s.testSlot3Info, + s.testSlot4Info, + s.testSlot5Info, + s.testSlot6Info, + s.testSlot7Info, + s.testSlot8Info, + s.testSlot9Info, + s.testSlot10Info, + s.testSlot11Info, + s.testSlot12Info, + } { c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), IsNil) } } @@ -375,7 +402,22 @@ c.Assert(interfaces.BeforePrepareSlot(s.iface, s.missingPathSlotInfo), ErrorMatches, `serial-port slot must have a path attribute`) // Slots with incorrect value of the "path" attribute are rejected. - for _, slot := range []*snap.SlotInfo{s.badPathSlot1Info, s.badPathSlot2Info, s.badPathSlot3Info, s.badPathSlot4Info, s.badPathSlot5Info, s.badPathSlot6Info, s.badPathSlot7Info, s.badPathSlot8Info, s.badPathSlot9Info, s.badPathSlot10Info, s.badPathSlot11Info, s.badPathSlot12Info, s.badPathSlot13Info} { + for _, slot := range []*snap.SlotInfo{ + s.badPathSlot1Info, + s.badPathSlot2Info, + s.badPathSlot3Info, + s.badPathSlot4Info, + s.badPathSlot5Info, + s.badPathSlot6Info, + s.badPathSlot7Info, + s.badPathSlot8Info, + s.badPathSlot9Info, + s.badPathSlot10Info, + s.badPathSlot11Info, + s.badPathSlot12Info, + s.badPathSlot13Info, + s.badPathSlot100Info, + } { c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "serial-port path attribute must be a valid device node") } } @@ -534,14 +576,17 @@ expectedSnippet11 := `/dev/ttyHS0 rwk,` checkConnectedPlugSnippet(s.testPlugPort1, s.testSlot11, expectedSnippet11) - expectedSnippet12 := `/dev/tty[A-Z]*[0-9] rwk,` - checkConnectedPlugSnippet(s.testPlugPort1, s.testUDev1, expectedSnippet12) + expectedSnippet12 := `/dev/ttyGS0 rwk,` + checkConnectedPlugSnippet(s.testPlugPort1, s.testSlot12, expectedSnippet12) - expectedSnippet13 := `/dev/tty[A-Z]*[0-9] rwk,` - checkConnectedPlugSnippet(s.testPlugPort2, s.testUDev2, expectedSnippet13) + expectedSnippet100 := `/dev/tty[A-Z]*[0-9] rwk,` + checkConnectedPlugSnippet(s.testPlugPort1, s.testUDev1, expectedSnippet100) - expectedSnippet14 := `/dev/tty[A-Z]*[0-9] rwk,` - checkConnectedPlugSnippet(s.testPlugPort2, s.testUDev3, expectedSnippet14) + expectedSnippet101 := `/dev/tty[A-Z]*[0-9] rwk,` + checkConnectedPlugSnippet(s.testPlugPort2, s.testUDev2, expectedSnippet101) + + expectedSnippet102 := `/dev/tty[A-Z]*[0-9] rwk,` + checkConnectedPlugSnippet(s.testPlugPort2, s.testUDev3, expectedSnippet102) } func (s *SerialPortInterfaceSuite) TestConnectedPlugUDevSnippetsForPath(c *C) { @@ -613,18 +658,23 @@ expectedExtraSnippet11 := fmt.Sprintf(`TAG=="snap_client-snap_app-accessing-3rd-port", RUN+="%v/snap-device-helper $env{ACTION} snap_client-snap_app-accessing-3rd-port $devpath $major:$minor"`, dirs.DistroLibExecDir) checkConnectedPlugSnippet(s.testPlugPort3, s.testSlot11, expectedSnippet11, expectedExtraSnippet11) - // these have product and vendor ids expectedSnippet12 := `# serial-port +SUBSYSTEM=="tty", KERNEL=="ttyGS0", TAG+="snap_client-snap_app-accessing-3rd-port"` + expectedExtraSnippet12 := fmt.Sprintf(`TAG=="snap_client-snap_app-accessing-3rd-port", RUN+="%v/snap-device-helper $env{ACTION} snap_client-snap_app-accessing-3rd-port $devpath $major:$minor"`, dirs.DistroLibExecDir) + checkConnectedPlugSnippet(s.testPlugPort3, s.testSlot12, expectedSnippet12, expectedExtraSnippet12) + + // these have product and vendor ids + expectedSnippet100 := `# serial-port IMPORT{builtin}="usb_id" SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="0001", ATTRS{idProduct}=="0001", TAG+="snap_client-snap_app-accessing-3rd-port"` - expectedExtraSnippet12 := fmt.Sprintf(`TAG=="snap_client-snap_app-accessing-3rd-port", RUN+="%v/snap-device-helper $env{ACTION} snap_client-snap_app-accessing-3rd-port $devpath $major:$minor"`, dirs.DistroLibExecDir) - checkConnectedPlugSnippet(s.testPlugPort3, s.testUDev1, expectedSnippet12, expectedExtraSnippet12) + expectedExtraSnippet100 := fmt.Sprintf(`TAG=="snap_client-snap_app-accessing-3rd-port", RUN+="%v/snap-device-helper $env{ACTION} snap_client-snap_app-accessing-3rd-port $devpath $major:$minor"`, dirs.DistroLibExecDir) + checkConnectedPlugSnippet(s.testPlugPort3, s.testUDev1, expectedSnippet100, expectedExtraSnippet100) - expectedSnippet13 := `# serial-port + expectedSnippet101 := `# serial-port IMPORT{builtin}="usb_id" SUBSYSTEM=="tty", SUBSYSTEMS=="usb", ATTRS{idVendor}=="ffff", ATTRS{idProduct}=="ffff", TAG+="snap_client-snap_app-accessing-3rd-port"` - expectedExtraSnippet13 := fmt.Sprintf(`TAG=="snap_client-snap_app-accessing-3rd-port", RUN+="%v/snap-device-helper $env{ACTION} snap_client-snap_app-accessing-3rd-port $devpath $major:$minor"`, dirs.DistroLibExecDir) - checkConnectedPlugSnippet(s.testPlugPort3, s.testUDev2, expectedSnippet13, expectedExtraSnippet13) + expectedExtraSnippet101 := fmt.Sprintf(`TAG=="snap_client-snap_app-accessing-3rd-port", RUN+="%v/snap-device-helper $env{ACTION} snap_client-snap_app-accessing-3rd-port $devpath $major:$minor"`, dirs.DistroLibExecDir) + checkConnectedPlugSnippet(s.testPlugPort3, s.testUDev2, expectedSnippet101, expectedExtraSnippet101) } func (s *SerialPortInterfaceSuite) TestHotplugDeviceDetected(c *C) { diff -Nru snapd-2.55.5+20.04/interfaces/builtin/shared_memory.go snapd-2.57.5+20.04/interfaces/builtin/shared_memory.go --- snapd-2.55.5+20.04/interfaces/builtin/shared_memory.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/shared_memory.go 2022-10-17 16:25:18.000000000 +0000 @@ -48,7 +48,6 @@ // unless a slot was also granted at some point. const sharedMemoryBaseDeclarationPlugs = ` shared-memory: - allow-installation: true allow-connection: - plug-attributes: @@ -221,7 +220,7 @@ snippetType sharedMemorySnippetType) { emitWritableRule := func(path string) { // Ubuntu 14.04 uses /run/shm instead of the most common /dev/shm - fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" rwk,\n", path) + fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" mrwlk,\n", path) } // All checks were already done in BeforePrepare{Plug,Slot} diff -Nru snapd-2.55.5+20.04/interfaces/builtin/shared_memory_test.go snapd-2.57.5+20.04/interfaces/builtin/shared_memory_test.go --- snapd-2.55.5+20.04/interfaces/builtin/shared_memory_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/shared_memory_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -393,12 +393,12 @@ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.provider.app"}) - c.Check(plugSnippet, testutil.Contains, `"/{dev,run}/shm/bar" rwk,`) + c.Check(plugSnippet, testutil.Contains, `"/{dev,run}/shm/bar" mrwlk,`) c.Check(plugSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro" r,`) // Slot has read-write permissions to all paths - c.Check(slotSnippet, testutil.Contains, `"/{dev,run}/shm/bar" rwk,`) - c.Check(slotSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro" rwk,`) + c.Check(slotSnippet, testutil.Contains, `"/{dev,run}/shm/bar" mrwlk,`) + c.Check(slotSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro" mrwlk,`) wildcardSpec := &apparmor.Specification{} c.Assert(wildcardSpec.AddConnectedPlug(s.iface, s.wildcardPlug, s.wildcardSlot), IsNil) @@ -409,12 +409,12 @@ c.Assert(wildcardSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.provider.app"}) - c.Check(wildcardPlugSnippet, testutil.Contains, `"/{dev,run}/shm/bar*" rwk,`) + c.Check(wildcardPlugSnippet, testutil.Contains, `"/{dev,run}/shm/bar*" mrwlk,`) c.Check(wildcardPlugSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro*" r,`) // Slot has read-write permissions to all paths - c.Check(wildcardSlotSnippet, testutil.Contains, `"/{dev,run}/shm/bar*" rwk,`) - c.Check(wildcardSlotSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro*" rwk,`) + c.Check(wildcardSlotSnippet, testutil.Contains, `"/{dev,run}/shm/bar*" mrwlk,`) + c.Check(wildcardSlotSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro*" mrwlk,`) spec = &apparmor.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.privatePlug, s.privateSlot), IsNil) diff -Nru snapd-2.55.5+20.04/interfaces/builtin/steam_support.go snapd-2.57.5+20.04/interfaces/builtin/steam_support.go --- snapd-2.55.5+20.04/interfaces/builtin/steam_support.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/steam_support.go 2022-10-17 16:25:18.000000000 +0000 @@ -126,12 +126,19 @@ /newroot/** rwkl, /bindfile* rw, -mount options=(rw, rbind) /oldroot/home/** -> /newroot/home/**, +mount options=(rw, rbind) /oldroot/opt/ -> /newroot/opt/, +mount options=(rw, rbind) /oldroot/srv/ -> /newroot/srv/, +mount options=(rw, rbind) /oldroot/run/udev/ -> /newroot/run/udev/, +mount options=(rw, rbind) /oldroot/home/{,**} -> /newroot/home/{,**}, mount options=(rw, rbind) /oldroot/snap/** -> /newroot/snap/**, mount options=(rw, rbind) /oldroot/home/**/usr/ -> /newroot/usr/, mount options=(rw, rbind) /oldroot/home/**/usr/etc/** -> /newroot/etc/**, mount options=(rw, rbind) /oldroot/home/**/usr/etc/ld.so.cache -> /newroot/run/pressure-vessel/ldso/runtime-ld.so.cache, mount options=(rw, rbind) /oldroot/home/**/usr/etc/ld.so.conf -> /newroot/run/pressure-vessel/ldso/runtime-ld.so.conf, +mount options=(rw, rbind) /oldroot/mnt/{,**} -> /newroot/mnt/{,**}, +mount options=(rw, rbind) /oldroot/media/{,**} -> /newroot/media/{,**}, +mount options=(rw, rbind) /oldroot/run/media/ -> /newroot/run/media/, +mount options=(rw, rbind) /oldroot/etc/nvidia/ -> /newroot/etc/nvidia/, mount options=(rw, rbind) /oldroot/etc/machine-id -> /newroot/etc/machine-id, mount options=(rw, rbind) /oldroot/etc/group -> /newroot/etc/group, @@ -154,7 +161,7 @@ mount options=(rw, rbind) /oldroot/home/**/.local/share/icons/ -> /newroot/run/host/user-share/icons/, mount options=(rw, rbind) /oldroot/run/user/[0-9]*/wayland-* -> /newroot/run/pressure-vessel/wayland-*, -mount options=(rw, rbind) /oldroot/tmp/.X11-unix/X* -> /newroot/tmp/.X11-unix/X99, +mount options=(rw, rbind) /oldroot/tmp/.X11-unix/X* -> /newroot/tmp/.X11-unix/X*, mount options=(rw, rbind) /bindfile* -> /newroot/run/pressure-vessel/Xauthority, mount options=(rw, rbind) /bindfile* -> /newroot/run/pressure-vessel/pulse/config, @@ -167,6 +174,9 @@ mount options=(rw, rbind) /oldroot/run/systemd/resolve/io.systemd.Resolve -> /newroot/run/systemd/resolve/io.systemd.Resolve, mount options=(rw, rbind) /bindfile* -> /newroot/run/host/container-manager, +# Allow mounting Nvidia drivers into the sandbox +mount options=(rw, rbind) /oldroot/var/lib/snapd/hostfs/usr/lib/@{multiarch}/** -> /newroot/var/lib/snapd/hostfs/usr/lib/@{multiarch}/**, + # Allow masking of certain directories in the sandbox mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /newroot/home/*/snap/steam/common/.local/share/vulkan/implicit_layer.d/, mount fstype=tmpfs options=(rw, nosuid, nodev) tmpfs -> /newroot/run/pressure-vessel/ldso/, @@ -179,6 +189,9 @@ umount /, # Permissions needed within sandbox root +deny /usr/bin/{chfn,chsh,gpasswd,mount,newgrp,passwd,su,sudo,umount} x, +/usr/bin/** ixr, +/usr/sbin/** ixr, /usr/lib/pressure-vessel/** ixr, /run/host/** mr, /run/pressure-vessel/** mrw, diff -Nru snapd-2.55.5+20.04/interfaces/builtin/system_observe.go snapd-2.57.5+20.04/interfaces/builtin/system_observe.go --- snapd-2.55.5+20.04/interfaces/builtin/system_observe.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/system_observe.go 2022-10-17 16:25:18.000000000 +0000 @@ -19,6 +19,18 @@ package builtin +import ( + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + const systemObserveSummary = `allows observing all processes and drivers` const systemObserveBaseDeclarationSlots = ` @@ -50,6 +62,7 @@ ptrace (read), # Other miscellaneous accesses for observing the system +@{PROC}/cgroups r, @{PROC}/locks r, @{PROC}/modules r, @{PROC}/stat r, @@ -58,34 +71,54 @@ @{PROC}/diskstats r, @{PROC}/kallsyms r, @{PROC}/partitions r, +@{PROC}/pressure/cpu r, +@{PROC}/pressure/io r, +@{PROC}/pressure/memory r, @{PROC}/sys/kernel/panic r, @{PROC}/sys/kernel/panic_on_oops r, +@{PROC}/sys/kernel/sched_autogroup_enabled r, @{PROC}/sys/vm/max_map_count r, @{PROC}/sys/vm/panic_on_oom r, +@{PROC}/sys/vm/swappiness r, # These are not process-specific (/proc/*/... and /proc/*/task/*/...) @{PROC}/*/{,task/,task/*/} r, +@{PROC}/*/{,task/*/}autogroup r, @{PROC}/*/{,task/*/}auxv r, @{PROC}/*/{,task/*/}cgroup r, @{PROC}/*/{,task/*/}cmdline r, @{PROC}/*/{,task/*/}comm r, @{PROC}/*/{,task/*/}exe r, @{PROC}/*/{,task/*/}fdinfo/* r, +@{PROC}/*/{,task/*/}io r, +@{PROC}/*/{,task/*/}oom_score r, +# allow reading of smaps_rollup, which is a summary of the memory use of a process, +# but not smaps which contains a detailed mappings breakdown like +# /proc/self/maps, which we do not allow access to for other processes +@{PROC}/*/{,task/*/}smaps_rollup r, @{PROC}/*/{,task/*/}stat r, @{PROC}/*/{,task/*/}statm r, @{PROC}/*/{,task/*/}status r, @{PROC}/*/{,task/*/}wchan r, +# Allow reading processes security label +@{PROC}/*/{,task/*/}attr/{,apparmor/}current r, + # Allow discovering the os-release of the host /var/lib/snapd/hostfs/etc/os-release rk, /var/lib/snapd/hostfs/usr/lib/os-release rk, +# Allow discovering the Kernel build config +@{PROC}/config.gz r, +/boot/config* r, + # Allow discovering system-wide CFS Bandwidth Control information # https://www.kernel.org/doc/html/latest/scheduler/sched-bwc.html /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us r, /sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us r, /sys/fs/cgroup/cpu,cpuacct/cpu.shares r, /sys/fs/cgroup/cpu,cpuacct/cpu.stat r, +/sys/fs/cgroup/memory/memory.stat r, #include @@ -141,15 +174,53 @@ #@deny ptrace ` +type systemObserveInterface struct { + commonInterface +} + +func (iface *systemObserveInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + spec.AddSnippet(systemObserveConnectedPlugAppArmor) + spec.SetSuppressPtraceTrace() + // Allow mounting boot partition to snap-update-ns + emit := spec.AddUpdateNSf + target := "/boot" + source := "/var/lib/snapd/hostfs" + target + emit(" # Read-only access to %s", target) + // When setting up a read-only bind mount, snap-update-ns first creates a + // plain read/write bind mount, and then remounts it to readonly. + emit(" mount options=(bind,rw) %s/ -> %s/,", source, target) + emit(" mount options=(bind,remount,ro) -> %s/,", target) + emit(" umount %s/,\n", target) + return nil +} + +func (iface *systemObserveInterface) MountPermanentPlug(spec *mount.Specification, plug *snap.PlugInfo) error { + dir := filepath.Join(dirs.GlobalRootDir, "/boot") + if matches, _ := filepath.Glob(filepath.Join(dir, "config*")); len(matches) > 0 { + spec.AddMountEntry(osutil.MountEntry{ + Name: "/var/lib/snapd/hostfs" + dir, + Dir: "/boot", + Options: []string{"bind", "ro"}, + }) + } else { + // TODO: if /boot/config does not exist, we should check whether the + // kernel is being delivered as a snap (this is the case in Ubuntu + // Core) and, if found, we should bind-mount the config file onto the + // expected location. + logger.Debugf("system-observe: /boot/config* not found, skipping mount of /boot/") + } + return nil +} + func init() { - registerIface(&commonInterface{ - name: "system-observe", - summary: systemObserveSummary, - implicitOnCore: true, - implicitOnClassic: true, - baseDeclarationSlots: systemObserveBaseDeclarationSlots, - connectedPlugAppArmor: systemObserveConnectedPlugAppArmor, - connectedPlugSecComp: systemObserveConnectedPlugSecComp, - suppressPtraceTrace: true, + registerIface(&systemObserveInterface{ + commonInterface: commonInterface{ + name: "system-observe", + summary: systemObserveSummary, + implicitOnCore: true, + implicitOnClassic: true, + baseDeclarationSlots: systemObserveBaseDeclarationSlots, + connectedPlugSecComp: systemObserveConnectedPlugSecComp, + }, }) } diff -Nru snapd-2.55.5+20.04/interfaces/builtin/system_observe_test.go snapd-2.57.5+20.04/interfaces/builtin/system_observe_test.go --- snapd-2.55.5+20.04/interfaces/builtin/system_observe_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/system_observe_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,10 +21,15 @@ import ( . "gopkg.in/check.v1" + "os" + "path/filepath" + "strings" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/mount" "github.com/snapcore/snapd/interfaces/seccomp" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" @@ -84,6 +89,14 @@ c.Assert(apparmorSpec.SnippetForTag("snap.other.app2"), testutil.Contains, "ptrace") c.Assert(apparmorSpec.SnippetForTag("snap.other.app2"), testutil.Contains, "@{PROC}/partitions r,") + updateNS := apparmorSpec.UpdateNS() + expectedUpdateNS := ` # Read-only access to /boot + mount options=(bind,rw) /var/lib/snapd/hostfs/boot/ -> /boot/, + mount options=(bind,remount,ro) -> /boot/, + umount /boot/, +` + c.Assert(strings.Join(updateNS[:], "\n"), Equals, expectedUpdateNS) + // connected plugs have a non-nil security snippet for seccomp seccompSpec := &seccomp.Specification{} err = seccompSpec.AddConnectedPlug(s.iface, s.plug, s.slot) @@ -92,6 +105,30 @@ c.Check(seccompSpec.SnippetForTag("snap.other.app2"), testutil.Contains, "ptrace\n") } +func (s *SystemObserveInterfaceSuite) TestMountPermanentPlug(c *C) { + tmpdir := c.MkDir() + dirs.SetRootDir(tmpdir) + + // Create a /boot/config-* file so that the interface will generate a bind + // mount for it + fakeBootDir := filepath.Join(tmpdir, "/boot") + c.Assert(os.MkdirAll(fakeBootDir, 0777), IsNil) + file, err := os.OpenFile(filepath.Join(fakeBootDir, "config-5.10"), os.O_CREATE, 0644) + c.Assert(err, IsNil) + c.Assert(file.Close(), IsNil) + + mountSpec := &mount.Specification{} + c.Assert(mountSpec.AddPermanentPlug(s.iface, s.plugInfo), IsNil) + + entries := mountSpec.MountEntries() + c.Assert(entries, HasLen, 1) + + const hostfs = "/var/lib/snapd/hostfs" + c.Check(entries[0].Name, Equals, filepath.Join(hostfs, dirs.GlobalRootDir, "/boot")) + c.Check(entries[0].Dir, Equals, "/boot") + c.Check(entries[0].Options, DeepEquals, []string{"bind", "ro"}) +} + func (s *SystemObserveInterfaceSuite) TestInterfaces(c *C) { c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) } diff -Nru snapd-2.55.5+20.04/interfaces/builtin/system_packages_doc.go snapd-2.57.5+20.04/interfaces/builtin/system_packages_doc.go --- snapd-2.55.5+20.04/interfaces/builtin/system_packages_doc.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/system_packages_doc.go 2022-10-17 16:25:18.000000000 +0000 @@ -40,6 +40,8 @@ # Description: can access documentation of system packages. /usr/share/doc/{,**} r, +/usr/share/cups/doc-root/{,**} r, +/usr/share/gimp/2.0/help/{,**} r, /usr/share/gtk-doc/{,**} r, /usr/share/libreoffice/help/{,**} r, /usr/share/xubuntu-docs/{,**} r, @@ -56,6 +58,12 @@ emit(" mount options=(bind) /var/lib/snapd/hostfs/usr/share/doc/ -> /usr/share/doc/,\n") emit(" remount options=(bind, ro) /usr/share/doc/,\n") emit(" umount /usr/share/doc/,\n") + emit(" mount options=(bind) /var/lib/snapd/hostfs/usr/share/cups/doc-root/ -> /usr/share/cups/doc-root/,\n") + emit(" remount options=(bind, ro) /usr/share/cups/doc-root/,\n") + emit(" umount /usr/share/cups/doc-root/,\n") + emit(" mount options=(bind) /var/lib/snapd/hostfs/usr/share/gimp/2.0/help/ -> /usr/share/gimp/2.0/help/,\n") + emit(" remount options=(bind, ro) /usr/share/gimp/2.0/help/,\n") + emit(" umount /usr/share/gimp/2.0/help/,\n") emit(" mount options=(bind) /var/lib/snapd/hostfs/usr/share/gtk-doc/ -> /usr/share/gtk-doc/,\n") emit(" remount options=(bind, ro) /usr/share/gtk-doc/,\n") emit(" umount /usr/share/gtk-doc/,\n") @@ -78,6 +86,16 @@ Options: []string{"bind", "ro"}, }) spec.AddMountEntry(osutil.MountEntry{ + Name: "/var/lib/snapd/hostfs/usr/share/cups/doc-root", + Dir: "/usr/share/cups/doc-root", + Options: []string{"bind", "ro"}, + }) + spec.AddMountEntry(osutil.MountEntry{ + Name: "/var/lib/snapd/hostfs/usr/share/gimp/2.0/help", + Dir: "/usr/share/gimp/2.0/help", + Options: []string{"bind", "ro"}, + }) + spec.AddMountEntry(osutil.MountEntry{ Name: "/var/lib/snapd/hostfs/usr/share/gtk-doc", Dir: "/usr/share/gtk-doc", Options: []string{"bind", "ro"}, diff -Nru snapd-2.55.5+20.04/interfaces/builtin/system_packages_doc_test.go snapd-2.57.5+20.04/interfaces/builtin/system_packages_doc_test.go --- snapd-2.55.5+20.04/interfaces/builtin/system_packages_doc_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/system_packages_doc_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -86,6 +86,8 @@ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Description: can access documentation of system packages.") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/usr/share/doc/{,**} r,") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/usr/share/cups/doc-root/{,**} r,") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/usr/share/gimp/2.0/help/{,**} r,") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/usr/share/libreoffice/help/{,**} r,") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/usr/share/xubuntu-docs/{,**} r,") @@ -95,6 +97,14 @@ c.Check(updateNS, testutil.Contains, " remount options=(bind, ro) /usr/share/doc/,\n") c.Check(updateNS, testutil.Contains, " umount /usr/share/doc/,\n") + c.Check(updateNS, testutil.Contains, " mount options=(bind) /var/lib/snapd/hostfs/usr/share/cups/doc-root/ -> /usr/share/cups/doc-root/,\n") + c.Check(updateNS, testutil.Contains, " remount options=(bind, ro) /usr/share/cups/doc-root/,\n") + c.Check(updateNS, testutil.Contains, " umount /usr/share/cups/doc-root/,\n") + + c.Check(updateNS, testutil.Contains, " mount options=(bind) /var/lib/snapd/hostfs/usr/share/gimp/2.0/help/ -> /usr/share/gimp/2.0/help/,\n") + c.Check(updateNS, testutil.Contains, " remount options=(bind, ro) /usr/share/gimp/2.0/help/,\n") + c.Check(updateNS, testutil.Contains, " umount /usr/share/gimp/2.0/help/,\n") + c.Check(updateNS, testutil.Contains, " mount options=(bind) /var/lib/snapd/hostfs/usr/share/gtk-doc/ -> /usr/share/gtk-doc/,\n") c.Check(updateNS, testutil.Contains, " remount options=(bind, ro) /usr/share/gtk-doc/,\n") c.Check(updateNS, testutil.Contains, " umount /usr/share/gtk-doc/,\n") @@ -123,19 +133,25 @@ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) entries := spec.MountEntries() - c.Assert(entries, HasLen, 4) + c.Assert(entries, HasLen, 6) c.Check(entries[0].Name, Equals, "/var/lib/snapd/hostfs/usr/share/doc") c.Check(entries[0].Dir, Equals, "/usr/share/doc") c.Check(entries[0].Options, DeepEquals, []string{"bind", "ro"}) - c.Check(entries[1].Name, Equals, "/var/lib/snapd/hostfs/usr/share/gtk-doc") - c.Check(entries[1].Dir, Equals, "/usr/share/gtk-doc") + c.Check(entries[1].Name, Equals, "/var/lib/snapd/hostfs/usr/share/cups/doc-root") + c.Check(entries[1].Dir, Equals, "/usr/share/cups/doc-root") c.Check(entries[1].Options, DeepEquals, []string{"bind", "ro"}) - c.Check(entries[2].Name, Equals, "/var/lib/snapd/hostfs/usr/share/libreoffice/help") - c.Check(entries[2].Dir, Equals, "/usr/share/libreoffice/help") + c.Check(entries[2].Name, Equals, "/var/lib/snapd/hostfs/usr/share/gimp/2.0/help") + c.Check(entries[2].Dir, Equals, "/usr/share/gimp/2.0/help") c.Check(entries[2].Options, DeepEquals, []string{"bind", "ro"}) - c.Check(entries[3].Name, Equals, "/var/lib/snapd/hostfs/usr/share/xubuntu-docs") - c.Check(entries[3].Dir, Equals, "/usr/share/xubuntu-docs") + c.Check(entries[3].Name, Equals, "/var/lib/snapd/hostfs/usr/share/gtk-doc") + c.Check(entries[3].Dir, Equals, "/usr/share/gtk-doc") c.Check(entries[3].Options, DeepEquals, []string{"bind", "ro"}) + c.Check(entries[4].Name, Equals, "/var/lib/snapd/hostfs/usr/share/libreoffice/help") + c.Check(entries[4].Dir, Equals, "/usr/share/libreoffice/help") + c.Check(entries[4].Options, DeepEquals, []string{"bind", "ro"}) + c.Check(entries[5].Name, Equals, "/var/lib/snapd/hostfs/usr/share/xubuntu-docs") + c.Check(entries[5].Dir, Equals, "/usr/share/xubuntu-docs") + c.Check(entries[5].Options, DeepEquals, []string{"bind", "ro"}) entries = spec.UserMountEntries() c.Assert(entries, HasLen, 0) diff -Nru snapd-2.55.5+20.04/interfaces/builtin/u2f_devices.go snapd-2.57.5+20.04/interfaces/builtin/u2f_devices.go --- snapd-2.55.5+20.04/interfaces/builtin/u2f_devices.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/u2f_devices.go 2022-10-17 16:25:18.000000000 +0000 @@ -125,7 +125,7 @@ { Name: "SoloKeys", VendorIDPattern: "1209", - ProductIDPattern: "5070|50b0", + ProductIDPattern: "5070|50b0|beee", }, { Name: "OnlyKey", diff -Nru snapd-2.55.5+20.04/interfaces/builtin/unity7.go snapd-2.57.5+20.04/interfaces/builtin/unity7.go --- snapd-2.55.5+20.04/interfaces/builtin/unity7.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/builtin/unity7.go 2022-10-17 16:25:18.000000000 +0000 @@ -317,7 +317,7 @@ bus=session interface=org.gtk.Actions member=Changed - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(label=unconfined), dbus (receive) bus=session @@ -335,7 +335,7 @@ bus=session interface=org.gtk.Menus member=Changed - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(label=unconfined), # Ubuntu menus dbus (send) @@ -362,7 +362,7 @@ path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}} interface=com.canonical.dbusmenu member="{LayoutUpdated,ItemsPropertiesUpdated}" - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(label=unconfined), dbus (receive) bus=session @@ -430,7 +430,7 @@ path=/{StatusNotifierItem,org/ayatana/NotificationItem/*} interface=org.kde.StatusNotifierItem member="New{AttentionIcon,Icon,IconThemePath,OverlayIcon,Status,Title,ToolTip}" - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(label=unconfined), dbus (receive) bus=session @@ -444,7 +444,7 @@ path=/{StatusNotifierItem/menu,org/ayatana/NotificationItem/*/Menu} interface=com.canonical.dbusmenu member="{LayoutUpdated,ItemsPropertiesUpdated}" - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(label=unconfined), dbus (receive) bus=session @@ -489,7 +489,7 @@ path=/org/ayatana/NotificationItem/* interface=org.kde.StatusNotifierItem member=XAyatanaNew* - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(label=unconfined), # unity launcher dbus (send) @@ -497,14 +497,14 @@ path=/com/canonical/unity/launcherentry/[0-9]* interface=com.canonical.Unity.LauncherEntry member=Update - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(label=unconfined), dbus (send) bus=session path=/com/canonical/unity/launcherentry/[0-9]* interface=com.canonical.dbusmenu member="{LayoutUpdated,ItemsPropertiesUpdated}" - peer=(name=org.freedesktop.DBus, label=unconfined), + peer=(label=unconfined), dbus (receive) bus=session diff -Nru snapd-2.55.5+20.04/interfaces/dbus/backend.go snapd-2.57.5+20.04/interfaces/dbus/backend.go --- snapd-2.55.5+20.04/interfaces/dbus/backend.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/dbus/backend.go 2022-10-17 16:25:18.000000000 +0000 @@ -89,6 +89,7 @@ for _, srv := range []string{ "io.snapcraft.Launcher.service", + "io.snapcraft.Prompt.service", "io.snapcraft.Settings.service", } { dst := filepath.Join("/usr/share/dbus-1/services/", srv) diff -Nru snapd-2.55.5+20.04/interfaces/dbus/backend_test.go snapd-2.57.5+20.04/interfaces/dbus/backend_test.go --- snapd-2.55.5+20.04/interfaces/dbus/backend_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/dbus/backend_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -305,6 +305,7 @@ for _, fn := range []string{ "io.snapcraft.Launcher.service", + "io.snapcraft.Prompt.service", "io.snapcraft.Settings.service", } { content := fmt.Sprintf("content of %s for snap %s", fn, coreOrSnapdSnap.InstanceName()) @@ -315,6 +316,7 @@ var expectedDBusConfigFiles = []string{ "/usr/share/dbus-1/services/io.snapcraft.Launcher.service", + "/usr/share/dbus-1/services/io.snapcraft.Prompt.service", "/usr/share/dbus-1/services/io.snapcraft.Settings.service", "/usr/share/dbus-1/session.d/snapd.session-services.conf", "/usr/share/dbus-1/system.d/snapd.system-services.conf", diff -Nru snapd-2.55.5+20.04/interfaces/kmod/export_test.go snapd-2.57.5+20.04/interfaces/kmod/export_test.go --- snapd-2.55.5+20.04/interfaces/kmod/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/kmod/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -19,6 +19,16 @@ package kmod +import ( + "github.com/snapcore/snapd/testutil" +) + +func MockLoadModule(f func(module string, options []string) error) (restore func()) { + r := testutil.Backup(&kmodLoadModule) + kmodLoadModule = f + return r +} + func (b *Backend) LoadModules(modules []string) { b.loadModules(modules) } diff -Nru snapd-2.55.5+20.04/interfaces/kmod/kmod.go snapd-2.57.5+20.04/interfaces/kmod/kmod.go --- snapd-2.55.5+20.04/interfaces/kmod/kmod.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/kmod/kmod.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,7 +20,11 @@ package kmod import ( - "os/exec" + kmod_wrapper "github.com/snapcore/snapd/osutil/kmod" +) + +var ( + kmodLoadModule = kmod_wrapper.LoadModule ) // loadModules loads given list of modules via modprobe. @@ -34,6 +38,6 @@ } for _, mod := range modules { // ignore errors which are logged by loadModule() via syslog - _ = exec.Command("modprobe", "--syslog", mod).Run() + _ = kmodLoadModule(mod, []string{}) } } diff -Nru snapd-2.55.5+20.04/interfaces/kmod/kmod_test.go snapd-2.57.5+20.04/interfaces/kmod/kmod_test.go --- snapd-2.55.5+20.04/interfaces/kmod/kmod_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/kmod/kmod_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,7 +25,6 @@ "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/ifacetest" "github.com/snapcore/snapd/interfaces/kmod" - "github.com/snapcore/snapd/testutil" ) type kmodSuite struct { @@ -44,8 +43,16 @@ } func (s *kmodSuite) TestModprobeCall(c *C) { - cmd := testutil.MockCommand(c, "modprobe", "") - defer cmd.Restore() + type CallRecord struct { + module string + options []string + } + var calls []CallRecord + restore := kmod.MockLoadModule(func(module string, options []string) error { + calls = append(calls, CallRecord{module, options}) + return nil + }) + defer restore() b, ok := s.Backend.(*kmod.Backend) c.Assert(ok, Equals, true) @@ -53,15 +60,19 @@ "module1", "module2", }) - c.Assert(cmd.Calls(), DeepEquals, [][]string{ - {"modprobe", "--syslog", "module1"}, - {"modprobe", "--syslog", "module2"}, + c.Assert(calls, DeepEquals, []CallRecord{ + {"module1", []string{}}, + {"module2", []string{}}, }) } func (s *kmodSuite) TestNoModprobeCallWhenPreseeding(c *C) { - cmd := testutil.MockCommand(c, "modprobe", "") - defer cmd.Restore() + loadModuleCalls := 0 + restore := kmod.MockLoadModule(func(module string, options []string) error { + loadModuleCalls++ + return nil + }) + defer restore() b := kmod.Backend{} opts := &interfaces.SecurityBackendOptions{ @@ -70,5 +81,5 @@ c.Assert(b.Initialize(opts), IsNil) b.LoadModules([]string{"module1"}) - c.Assert(cmd.Calls(), HasLen, 0) + c.Assert(loadModuleCalls, Equals, 0) } diff -Nru snapd-2.55.5+20.04/interfaces/mount/backend.go snapd-2.57.5+20.04/interfaces/mount/backend.go --- snapd-2.55.5+20.04/interfaces/mount/backend.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/mount/backend.go 2022-10-17 16:25:18.000000000 +0000 @@ -65,6 +65,7 @@ } spec.(*Specification).AddOvername(snapInfo) spec.(*Specification).AddLayout(snapInfo) + spec.(*Specification).AddExtraLayouts(confinement.ExtraLayouts) content := deriveContent(spec.(*Specification), snapInfo) // synchronize the content with the filesystem glob := fmt.Sprintf("snap.%s.*fstab", snapName) diff -Nru snapd-2.55.5+20.04/interfaces/mount/spec.go snapd-2.57.5+20.04/interfaces/mount/spec.go --- snapd-2.55.5+20.04/interfaces/mount/spec.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/mount/spec.go 2022-10-17 16:25:18.000000000 +0000 @@ -29,6 +29,7 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" ) // Specification assists in collecting mount entries associated with an interface. @@ -140,6 +141,15 @@ } } +// AddExtraLayouts adds mount entries based on additional layouts that +// are provided for the snap. +func (spec *Specification) AddExtraLayouts(layouts []snap.Layout) { + for _, layout := range layouts { + entry := mountEntryFromLayout(&layout) + spec.layout = append(spec.layout, entry) + } +} + // MountEntries returns a copy of the added mount entries. func (spec *Specification) MountEntries() []osutil.MountEntry { result := make([]osutil.MountEntry, 0, len(spec.overname)+len(spec.layout)+len(spec.general)) @@ -149,33 +159,127 @@ result = append(result, spec.overname...) result = append(result, spec.layout...) result = append(result, spec.general...) - unclashMountEntries(result) - return result + return unclashMountEntries(result) } // UserMountEntries returns a copy of the added user mount entries. func (spec *Specification) UserMountEntries() []osutil.MountEntry { result := make([]osutil.MountEntry, len(spec.user)) copy(result, spec.user) - unclashMountEntries(result) - return result + return unclashMountEntries(result) +} + +// Assuming that two mount entries have the same source, target and type, this +// function computes the mount options that should be used when performing the +// mount, so that the most permissive options are kept. +// The following flags are considered (of course the operation is commutative): +// - "ro" + "rw" = "rw" +// - "bind" + "rbind" = "rbind +func mergeOptions(options ...[]string) []string { + mergedOptions := make([]string, 0, len(options[0])) + foundWritableEntry := false + foundRBindEntry := false + firstEntryIsBindMount := false + for i, opts := range options { + isReadOnly := false + isRBind := false + for _, o := range opts { + switch o { + case "ro": + isReadOnly = true + case "rbind": + isRBind = true + fallthrough + case "bind": + // We know that the passed entries will either be all + // bind-mounts, or none will be a bind-mount (because + // unclashMountEntries() invokes us only if the source, target, + // and FS type are the same). That's why we check only the + // first entry here. + if i == 0 { + firstEntryIsBindMount = true + } + case "rw", "async": + // these are default options for mount, do nothing + default: + // write all other options + if !strutil.ListContains(mergedOptions, o) { + mergedOptions = append(mergedOptions, o) + } + } + } + if !isReadOnly { + foundWritableEntry = true + } + if isRBind { + foundRBindEntry = true + } + } + + if !foundWritableEntry { + mergedOptions = append(mergedOptions, "ro") + } + + if firstEntryIsBindMount { + if foundRBindEntry { + mergedOptions = append(mergedOptions, "rbind") + } else { + mergedOptions = append(mergedOptions, "bind") + } + } + + return mergedOptions } // unclashMountEntries renames mount points if they clash with other entries. // // Subsequent entries get suffixed with -2, -3, etc. // The initial entry is unaltered (and does not become -1). -func unclashMountEntries(entries []osutil.MountEntry) { - count := make(map[string]int, len(entries)) +func unclashMountEntries(entries []osutil.MountEntry) []osutil.MountEntry { + result := make([]osutil.MountEntry, 0, len(entries)) + + // The clashingEntry structure contains the information about different + // mount entries which use the same mount point. + type clashingEntry struct { + // Index in the `entries` array to the first entry of this clashing group + FirstIndex int + // Number of entries having this same mount point + Count int + // Merged options for the entries on this mount point + Options []string + } + entriesByMountPoint := make(map[string]*clashingEntry, len(entries)) for i := range entries { - path := entries[i].Dir - count[path]++ - if c := count[path]; c > 1 { - newDir := fmt.Sprintf("%s-%d", entries[i].Dir, c) + mountPoint := entries[i].Dir + entryInMap, found := entriesByMountPoint[mountPoint] + if !found { + index := len(result) + result = append(result, entries[i]) + entriesByMountPoint[mountPoint] = &clashingEntry{ + FirstIndex: index, + Count: 1, + } + continue + } + // If the source and the FS type is the same, we do not consider + // this to be a clash, and instead will try to combine the mount + // flags in a way that fulfils the permissions required by all + // requesting entries + firstEntry := &result[entryInMap.FirstIndex] + if firstEntry.Name == entries[i].Name && firstEntry.Type == entries[i].Type && + // Only merge entries that have no origin, or snap-update-ns will + // get confused + firstEntry.XSnapdOrigin() == "" && entries[i].XSnapdOrigin() == "" { + firstEntry.Options = mergeOptions(firstEntry.Options, entries[i].Options) + } else { + entryInMap.Count++ + newDir := fmt.Sprintf("%s-%d", entries[i].Dir, entryInMap.Count) logger.Noticef("renaming mount entry for directory %q to %q to avoid a clash", entries[i].Dir, newDir) entries[i].Dir = newDir + result = append(result, entries[i]) } } + return result } // Implementation of methods required by interfaces.Specification diff -Nru snapd-2.55.5+20.04/interfaces/mount/spec_test.go snapd-2.57.5+20.04/interfaces/mount/spec_test.go --- snapd-2.55.5+20.04/interfaces/mount/spec_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/mount/spec_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -164,6 +164,21 @@ }) } +func (s *specSuite) TestMountEntryFromExtraLayouts(c *C) { + extraLayouts := []snap.Layout{ + { + Path: "/test", + Bind: "/usr/home/test", + Mode: 0755, + }, + } + + s.spec.AddExtraLayouts(extraLayouts) + c.Assert(s.spec.MountEntries(), DeepEquals, []osutil.MountEntry{ + {Dir: "/test", Name: "/usr/home/test", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}, + }) +} + func (s *specSuite) TestParallelInstanceMountEntryFromLayout(c *C) { snapInfo := snaptest.MockInfo(c, snapWithLayout, &snap.SideInfo{Revision: snap.R(42)}) snapInfo.InstanceKey = "instance" @@ -206,6 +221,53 @@ }) } +func (s *specSuite) TestSpecificationMergedClash(c *C) { + defaultEntry := osutil.MountEntry{ + Dir: "/usr/foo", + Type: "tmpfs", + Name: "/here", + } + for _, td := range []struct { + // Options for all the clashing mount entries + Options [][]string + // Expected options for the merged mount entry + ExpectedOptions []string + }{ + { + // If all entries are read-only, the merged entry is also RO + Options: [][]string{{"noatime", "ro"}, {"ro"}}, + ExpectedOptions: []string{"noatime", "ro"}, + }, + { + // If one entry is rbind, the recursiveness is preserved + Options: [][]string{{"bind", "rw"}, {"rbind", "ro"}}, + ExpectedOptions: []string{"rbind"}, + }, + { + // With simple bind, no recursiveness is added + Options: [][]string{{"bind", "noatime"}, {"bind", "noexec"}}, + ExpectedOptions: []string{"noatime", "noexec", "bind"}, + }, + { + // Ordinary flags are preserved + Options: [][]string{{"noexec", "noatime"}, {"noatime", "nomand"}, {"nodev"}}, + ExpectedOptions: []string{"noexec", "noatime", "nomand", "nodev"}, + }, + } { + for _, options := range td.Options { + entry := defaultEntry + entry.Options = options + s.spec.AddMountEntry(entry) + } + c.Check(s.spec.MountEntries(), DeepEquals, []osutil.MountEntry{ + {Dir: "/usr/foo", Name: "/here", Type: "tmpfs", Options: td.ExpectedOptions}, + }, Commentf("Clashing entries: %q", td.Options)) + + // reset the spec after each iteration, or flags will leak + s.spec = &mount.Specification{} + } +} + func (s *specSuite) TestParallelInstanceMountEntriesNoInstanceKey(c *C) { snapInfo := &snap.Info{SideInfo: snap.SideInfo{RealName: "foo", Revision: snap.R(42)}} s.spec.AddOvername(snapInfo) diff -Nru snapd-2.55.5+20.04/interfaces/policy/basedeclaration_test.go snapd-2.57.5+20.04/interfaces/policy/basedeclaration_test.go --- snapd-2.55.5+20.04/interfaces/policy/basedeclaration_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/policy/basedeclaration_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -149,7 +149,7 @@ "packagekit-control": true, "pkcs11": true, "snapd-control": true, - "dummy": true, + "empty": true, } // these simply auto-connect, anything else doesn't @@ -781,7 +781,7 @@ "docker-support": {"core"}, "desktop-launch": {"core"}, "dsp": {"core", "gadget"}, - "dummy": {"app"}, + "empty": {"app"}, "fwupd": {"app", "core"}, "gpio": {"core", "gadget"}, "gpio-control": {"core"}, @@ -835,6 +835,7 @@ "custom-device": nil, "docker": nil, "lxd": nil, + "microceph": nil, "pkcs11": nil, "posix-mq": nil, "shared-memory": nil, @@ -890,6 +891,12 @@ c.Assert(err, Not(IsNil)) c.Assert(err, ErrorMatches, "installation not allowed by \"lxd\" slot rule of interface \"lxd\"") + // test microceph specially + ic = s.installSlotCand(c, "microceph", snap.TypeApp, ``) + err = ic.Check() + c.Assert(err, Not(IsNil)) + c.Assert(err, ErrorMatches, "installation not allowed by \"microceph\" slot rule of interface \"microceph\"") + // test shared-memory specially ic = s.installSlotCand(c, "shared-memory", snap.TypeApp, ``) err = ic.Check() @@ -995,6 +1002,7 @@ "location-observe": true, "lxd": true, "maliit": true, + "microceph": true, "mir": true, "online-accounts-service": true, "posix-mq": true, @@ -1159,7 +1167,7 @@ } } -func (s *baseDeclSuite) TestSanity(c *C) { +func (s *baseDeclSuite) TestValidity(c *C) { all := builtin.Interfaces() // these interfaces have rules both for the slots and plugs side diff -Nru snapd-2.55.5+20.04/interfaces/repo.go snapd-2.57.5+20.04/interfaces/repo.go --- snapd-2.55.5+20.04/interfaces/repo.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/repo.go 2022-10-17 16:25:18.000000000 +0000 @@ -645,7 +645,7 @@ r.m.Lock() defer r.m.Unlock() - // Sanity check + // Validity check if plugSnapName == "" { return fmt.Errorf("cannot disconnect, plug snap name is empty") } diff -Nru snapd-2.55.5+20.04/interfaces/repo_test.go snapd-2.57.5+20.04/interfaces/repo_test.go --- snapd-2.55.5+20.04/interfaces/repo_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/repo_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -2121,7 +2121,7 @@ coreSlot := &snap.SlotInfo{ Snap: s.coreSnap, - Name: "dummy-slot", + Name: "test-slot", Interface: "interface", HotplugKey: "1234", } @@ -2153,7 +2153,7 @@ c.Assert(s.testRepo.AddPlug(s.plug), IsNil) coreSlot := &snap.SlotInfo{ Snap: s.coreSnap, - Name: "dummy-slot", + Name: "test-slot", Interface: "interface", HotplugKey: "1234", Attrs: map[string]interface{}{"a": "b"}, @@ -2178,7 +2178,7 @@ c.Assert(s.testRepo.AddPlug(s.plug), IsNil) coreSlot := &snap.SlotInfo{ Snap: s.coreSnap, - Name: "dummy-slot", + Name: "test-slot", Interface: "interface", HotplugKey: "1234", } @@ -2188,6 +2188,6 @@ c.Assert(err, IsNil) slot, err := s.testRepo.UpdateHotplugSlotAttrs("interface", "1234", map[string]interface{}{"c": "d"}) - c.Assert(err, ErrorMatches, `internal error: cannot update slot dummy-slot while connected`) + c.Assert(err, ErrorMatches, `internal error: cannot update slot test-slot while connected`) c.Assert(slot, IsNil) } diff -Nru snapd-2.55.5+20.04/interfaces/systemd/backend.go snapd-2.57.5+20.04/interfaces/systemd/backend.go --- snapd-2.55.5+20.04/interfaces/systemd/backend.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/systemd/backend.go 2022-10-17 16:25:18.000000000 +0000 @@ -25,7 +25,6 @@ "fmt" "os" "path/filepath" - "time" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" @@ -82,7 +81,7 @@ if b.preseed { systemd = sysd.NewEmulationMode(dirs.GlobalRootDir) } else { - systemd = sysd.New(sysd.SystemMode, &dummyReporter{}) + systemd = sysd.New(sysd.SystemMode, &noopReporter{}) } // We need to be carefully here and stop all removed service units before @@ -107,7 +106,7 @@ if !b.preseed { // If we have new services here which aren't started yet the restart // operation will start them. - if err := systemd.Restart(changed, 10*time.Second); err != nil { + if err := systemd.Restart(changed); err != nil { logger.Noticef("cannot restart services %q: %s", changed, err) } } @@ -128,7 +127,7 @@ // for completeness. systemd = sysd.NewEmulationMode(dirs.GlobalRootDir) } else { - systemd = sysd.New(sysd.SystemMode, &dummyReporter{}) + systemd = sysd.New(sysd.SystemMode, &noopReporter{}) } // Remove all the files matching snap glob glob := serviceName(snapName, "*") @@ -140,7 +139,7 @@ logger.Noticef("cannot disable services %q: %s", removed, err) } if !b.preseed { - if err := systemd.Stop(removed, 5*time.Second); err != nil { + if err := systemd.Stop(removed); err != nil { logger.Noticef("cannot stop services %q: %s", removed, err) } } @@ -205,14 +204,14 @@ } } if len(stopUnits) > 0 { - if err := systemd.Stop(stopUnits, 5*time.Second); err != nil { + if err := systemd.Stop(stopUnits); err != nil { logger.Noticef("cannot stop services %q: %s", stopUnits, err) } } return nil } -type dummyReporter struct{} +type noopReporter struct{} -func (dr *dummyReporter) Notify(msg string) { +func (dr *noopReporter) Notify(msg string) { } diff -Nru snapd-2.55.5+20.04/interfaces/system_key.go snapd-2.57.5+20.04/interfaces/system_key.go --- snapd-2.55.5+20.04/interfaces/system_key.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/system_key.go 2022-10-17 16:25:18.000000000 +0000 @@ -299,7 +299,7 @@ // SystemKeysMatch returns whether the given system keys match. func SystemKeysMatch(systemKey1, systemKey2 interface{}) (bool, error) { - // sanity check + // precondition check _, ok1 := systemKey1.(*systemKey) _, ok2 := systemKey2.(*systemKey) if !(ok1 && ok2) { diff -Nru snapd-2.55.5+20.04/interfaces/udev/backend_test.go snapd-2.57.5+20.04/interfaces/udev/backend_test.go --- snapd-2.55.5+20.04/interfaces/udev/backend_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/udev/backend_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -91,7 +91,7 @@ func (s *backendSuite) TestInstallingSnapWritesAndLoadsRules(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { @@ -117,11 +117,11 @@ func (s *backendSuite) TestInstallingSnapWithHookWritesAndLoadsRules(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } s.Iface.UDevPermanentPlugCallback = func(spec *udev.Specification, slot *snap.PlugInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { @@ -149,7 +149,7 @@ func (s *backendSuite) TestSecurityIsStable(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { @@ -166,7 +166,7 @@ func (s *backendSuite) TestRemovingSnapRemovesAndReloadsRules(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { @@ -223,7 +223,7 @@ return nil } s.Iface.UDevPermanentPlugCallback = func(spec *udev.Specification, slot *snap.PlugInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { @@ -283,7 +283,7 @@ return nil } s.Iface.UDevPermanentPlugCallback = func(spec *udev.Specification, slot *snap.PlugInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { @@ -310,16 +310,16 @@ func (s *backendSuite) TestCombineSnippetsWithActualSnippets(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules") if opts.DevMode || opts.Classic { - c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#dummy\n") + c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#sample\n") } else { - c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\ndummy\n") + c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\nsample\n") } stat, err := os.Stat(fname) c.Assert(err, IsNil) @@ -331,7 +331,7 @@ func (s *backendSuite) TestControlsDeviceCgroup(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") spec.SetControlsDeviceCgroup() return nil } @@ -346,16 +346,16 @@ func (s *backendSuite) TestCombineSnippetsWithActualSnippetsWithNewline(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy1\ndummy2") + spec.AddSnippet("sample1\nsample2") return nil } for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.samba.rules") if opts.DevMode || opts.Classic { - c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#dummy1\n#dummy2\n") + c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#sample1\n#sample2\n") } else { - c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\ndummy1\ndummy2\n") + c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\nsample1\nsample2\n") } stat, err := os.Stat(fname) c.Assert(err, IsNil) @@ -366,16 +366,16 @@ func (s *backendSuite) TestCombineSnippetsWithActualSnippetsWhenPlugNoApps(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentPlugCallback = func(spec *udev.Specification, slot *snap.PlugInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.PlugNoAppsYaml, 0) fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.foo.rules") if opts.DevMode || opts.Classic { - c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#dummy\n") + c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#sample\n") } else { - c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\ndummy\n") + c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\nsample\n") } stat, err := os.Stat(fname) c.Assert(err, IsNil) @@ -387,16 +387,16 @@ func (s *backendSuite) TestCombineSnippetsWithActualSnippetsWhenSlotNoApps(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { snapInfo := s.InstallSnap(c, opts, "", ifacetest.SlotNoAppsYaml, 0) fname := filepath.Join(dirs.SnapUdevRulesDir, "70-snap.foo.rules") if opts.DevMode || opts.Classic { - c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#dummy\n") + c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\n# udev tagging/device cgroups disabled with non-strict mode snaps\n#sample\n") } else { - c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\ndummy\n") + c.Check(fname, testutil.FileEquals, "# This file is automatically generated.\nsample\n") } stat, err := os.Stat(fname) c.Assert(err, IsNil) @@ -419,7 +419,7 @@ func (s *backendSuite) TestUpdatingSnapToOneWithoutSlots(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { @@ -446,7 +446,7 @@ func (s *backendSuite) TestUpdatingSnapWithoutSlotsToOneWithoutSlots(c *C) { // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { @@ -471,7 +471,7 @@ // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { spec.TriggerSubsystem("input") - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { @@ -496,7 +496,7 @@ // NOTE: Hand out a permanent snippet so that .rules file is generated. s.Iface.UDevPermanentSlotCallback = func(spec *udev.Specification, slot *snap.SlotInfo) error { spec.TriggerSubsystem("input/joystick") - spec.AddSnippet("dummy") + spec.AddSnippet("sample") return nil } for _, opts := range testedConfinementOpts { diff -Nru snapd-2.55.5+20.04/interfaces/udev/spec_test.go snapd-2.57.5+20.04/interfaces/udev/spec_test.go --- snapd-2.55.5+20.04/interfaces/udev/spec_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/udev/spec_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -146,7 +146,7 @@ restore := release.MockReleaseInfo(&release.OS{ID: "fedora"}) defer restore() dirs.SetRootDir("") - // sanity + // validity c.Check(dirs.DistroLibExecDir, Equals, "/usr/libexec/snapd") s.testTagDevice(c, "/usr/libexec/snapd") } diff -Nru snapd-2.55.5+20.04/interfaces/udev/udev.go snapd-2.57.5+20.04/interfaces/udev/udev.go --- snapd-2.55.5+20.04/interfaces/udev/udev.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/udev/udev.go 2022-10-17 16:25:18.000000000 +0000 @@ -74,24 +74,29 @@ return fmt.Errorf("cannot run udev triggers: %s", err) } - // FIXME: track if also should trigger the joystick property if it - // wasn't already since we are not able to detect interfaces that are - // removed and set subsystemTriggers correctly. When we can, remove - // this. Allows joysticks to be removed from the device cgroup on - // interface disconnect. - inputJoystickTriggered := false + mustTriggerForInputSubsystem := false + mustTriggerForInputKeys := false for _, subsystem := range subsystemTriggers { - if subsystem == "input/joystick" { - // If one of the interfaces said it uses the input - // subsystem for joysticks, then trigger the joystick - // events in a way that is specific to joysticks to not - // block other inputs. - if err = udevadmTrigger("--property-match=ID_INPUT_JOYSTICK=1"); err != nil { - return fmt.Errorf("cannot run udev triggers for joysticks: %s", err) - } - inputJoystickTriggered = true - } else if subsystem == "input/key" { + if subsystem == "input/key" { + mustTriggerForInputKeys = true + } else if subsystem == "input" { + mustTriggerForInputSubsystem = true + } + // no `else` branch: we already triggered udevadm for all other + // subsystems before by running it with the `--subsystem-nomatch=input` + // option, so there's no need to do anything here. + } + + if mustTriggerForInputSubsystem { + // Trigger for the whole input subsystem + if err := udevadmTrigger("--subsystem-match=input"); err != nil { + return fmt.Errorf("cannot run udev triggers for input subsystem: %s", err) + } + } else { + // More specific triggers, to avoid blocking keyboards and mice + + if mustTriggerForInputKeys { // If one of the interfaces said it uses the input // subsystem for input keys, then trigger the keys // events in a way that is specific to input keys @@ -99,25 +104,12 @@ if err = udevadmTrigger("--property-match=ID_INPUT_KEY=1", "--property-match=ID_INPUT_KEYBOARD!=1"); err != nil { return fmt.Errorf("cannot run udev triggers for keys: %s", err) } - } else if subsystem != "" { - // If one of the interfaces said it uses a subsystem, - // then do it too. - if err := udevadmTrigger("--subsystem-match=" + subsystem); err != nil { - return fmt.Errorf("cannot run udev triggers for %s subsystem: %s", subsystem, err) - } - - if subsystem == "input" { - inputJoystickTriggered = true - } } - } - - // FIXME: if not already triggered, trigger the joystick property if it - // wasn't already since we are not able to detect interfaces that are - // removed and set subsystemTriggers correctly. When we can, remove - // this. Allows joysticks to be removed from the device cgroup on - // interface disconnect. - if !inputJoystickTriggered { + // FIXME: if not already triggered, trigger the joystick property if it + // wasn't already since we are not able to detect interfaces that are + // removed and set subsystemTriggers correctly. When we can, remove + // this. Allows joysticks to be removed from the device cgroup on + // interface disconnect. if err := udevadmTrigger("--property-match=ID_INPUT_JOYSTICK=1"); err != nil { return fmt.Errorf("cannot run udev triggers for joysticks: %s", err) } diff -Nru snapd-2.55.5+20.04/interfaces/udev/udev_test.go snapd-2.57.5+20.04/interfaces/udev/udev_test.go --- snapd-2.55.5+20.04/interfaces/udev/udev_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/interfaces/udev/udev_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -189,7 +189,6 @@ {"udevadm", "control", "--reload-rules"}, {"udevadm", "trigger", "--subsystem-nomatch=input"}, {"udevadm", "trigger", "--subsystem-match=input"}, - {"udevadm", "trigger", "--subsystem-match=tty"}, {"udevadm", "settle", "--timeout=10"}, }) } diff -Nru snapd-2.55.5+20.04/kernel/fde/cmd_helper.go snapd-2.57.5+20.04/kernel/fde/cmd_helper.go --- snapd-2.55.5+20.04/kernel/fde/cmd_helper.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/kernel/fde/cmd_helper.go 2022-10-17 16:25:18.000000000 +0000 @@ -79,7 +79,8 @@ } } - // TODO: put this into a new "systemd/run" package + // TODO: use the new systemd.Run() interface once it supports + // running without dbus (i.e. supports running without --pipe) cmd := exec.Command( "systemd-run", "--collect", @@ -96,6 +97,9 @@ // making sure that people using the hook know that we do not // want them to mess around outside of just providing unseal. "--property=SystemCallFilter=~@mount", + // We do not need any systemd unit dependencies for this + // call. + "--property=DefaultDependencies=no", // WORKAROUNDS // workaround the lack of "--pipe" fmt.Sprintf("--property=StandardInput=file:%s/%s.stdin", runDir, name), diff -Nru snapd-2.55.5+20.04/kernel/fde/fde.go snapd-2.57.5+20.04/kernel/fde/fde.go --- snapd-2.55.5+20.04/kernel/fde/fde.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/kernel/fde/fde.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,6 +34,11 @@ "github.com/snapcore/snapd/osutil" ) +// DeviceSetupHookPartitionOffset defines the free space that is reserved +// at the start of a device-setup based partition for future use (like +// to simulate LUKS keyslot like setup). +const DeviceSetupHookPartitionOffset = uint64(1 * 1024 * 1024) + // HasRevealKey return true if the current system has a "fde-reveal-key" // binary (usually used in the initrd). // @@ -194,3 +199,9 @@ return nil } + +// EncryptedDeviceMapperName returns the name to use in device mapper for a +// device that is encrypted using FDE hooks +func EncryptedDeviceMapperName(name string) string { + return name + "-device-locked" +} diff -Nru snapd-2.55.5+20.04/kernel/fde/fde_test.go snapd-2.57.5+20.04/kernel/fde/fde_test.go --- snapd-2.55.5+20.04/kernel/fde/fde_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/kernel/fde/fde_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -516,6 +516,7 @@ "systemd-run", "--collect", "--service-type=exec", "--quiet", "--property=RuntimeMaxSec=2m0s", "--property=SystemCallFilter=~@mount", + "--property=DefaultDependencies=no", fmt.Sprintf("--property=StandardInput=file:%s/run/fde-reveal-key/fde-reveal-key.stdin", root), fmt.Sprintf("--property=StandardOutput=file:%s/run/fde-reveal-key/fde-reveal-key.stdout", root), fmt.Sprintf("--property=StandardError=file:%s/run/fde-reveal-key/fde-reveal-key.stderr", root), @@ -683,3 +684,14 @@ c.Assert(fde.IsHardwareEncryptedDeviceMapperName(t), Equals, false) } } + +func (s *fdeSuite) TestEncryptedDeviceMapperName(c *C) { + for _, str := range []string{ + "ubuntu-data", + "ubuntu-save", + "foo", + "other", + } { + c.Assert(fde.EncryptedDeviceMapperName(str), Equals, str+"-device-locked") + } +} diff -Nru snapd-2.55.5+20.04/logger/export_test.go snapd-2.57.5+20.04/logger/export_test.go --- snapd-2.55.5+20.04/logger/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/logger/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014,2015,2017 Canonical Ltd + * Copyright (C) 2014-2022 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,12 @@ package logger +import ( + "time" + + "github.com/snapcore/snapd/testutil" +) + func GetLogger() Logger { lock.Lock() defer lock.Unlock() @@ -42,3 +48,9 @@ procCmdlineUseDefaultMockInTests = old } } + +func MockTimeNow(f func() time.Time) (restore func()) { + restore = testutil.Backup(&timeNow) + timeNow = f + return restore +} diff -Nru snapd-2.55.5+20.04/logger/logger.go snapd-2.57.5+20.04/logger/logger.go --- snapd-2.55.5+20.04/logger/logger.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/logger/logger.go 2022-10-17 16:25:18.000000000 +0000 @@ -26,6 +26,7 @@ "log" "os" "sync" + "time" "github.com/snapcore/snapd/osutil" ) @@ -174,3 +175,12 @@ m, _ := osutil.KernelCommandLineKeyValues("snapd.debug") return m["snapd.debug"] == "1" } + +var timeNow = time.Now + +// StartupStageTimestamp produce snap startup timings message. +func StartupStageTimestamp(stage string) { + now := timeNow() + Debugf(`-- snap startup {"stage":"%s", "time":"%v.%06d"}`, + stage, now.Unix(), (now.UnixNano()/1e3)%1e6) +} diff -Nru snapd-2.55.5+20.04/logger/logger_test.go snapd-2.57.5+20.04/logger/logger_test.go --- snapd-2.55.5+20.04/logger/logger_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/logger/logger_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,12 +21,15 @@ import ( "bytes" + "encoding/json" "io/ioutil" "log" "os" "path/filepath" "runtime" + "strings" "testing" + "time" . "gopkg.in/check.v1" @@ -148,3 +151,32 @@ l.Debug("xyzzy") c.Check(buf.String(), testutil.Contains, `DEBUG: xyzzy`) } + +func (s *LogSuite) TestStartupTimestampMsg(c *C) { + os.Setenv("SNAPD_DEBUG", "1") + defer os.Unsetenv("SNAPD_DEBUG") + + type msgTimestamp struct { + Stage string `json:"stage"` + Time string `json:"time"` + } + + now := time.Date(2022, time.May, 16, 10, 43, 12, 22312000, time.UTC) + logger.MockTimeNow(func() time.Time { + return now + }) + logger.StartupStageTimestamp("foo to bar") + msg := strings.TrimSpace(s.logbuf.String()) + c.Assert(msg, Matches, `.* DEBUG: -- snap startup \{"stage":"foo to bar", "time":"1652697792.022312"\}$`) + + var m msgTimestamp + start := strings.LastIndex(msg, "{") + c.Assert(start, Not(Equals), -1) + stamp := msg[start:] + err := json.Unmarshal([]byte(stamp), &m) + c.Assert(err, IsNil) + c.Check(m, Equals, msgTimestamp{ + Stage: "foo to bar", + Time: "1652697792.022312", + }) +} diff -Nru snapd-2.55.5+20.04/.mailmap snapd-2.57.5+20.04/.mailmap --- snapd-2.55.5+20.04/.mailmap 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/.mailmap 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,2 @@ +Sergio Cazzolato sergio-j-cazzolato +John R. Lenton John Lenton diff -Nru snapd-2.55.5+20.04/mkversion.sh snapd-2.57.5+20.04/mkversion.sh --- snapd-2.55.5+20.04/mkversion.sh 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/mkversion.sh 2022-10-17 16:25:18.000000000 +0000 @@ -131,7 +131,16 @@ $v EOF +MOD=-mod=vendor +if [ "$GO111MODULE" = "off" ] ; then + MOD=-- +elif [ ! -d "$GO_GENERATE_BUILDDIR/vendor/github.com" ] ; then + MOD=-- +fi +fmts=$(cd "$GO_GENERATE_BUILDDIR" ; go run $MOD ./asserts/info) + cat < "$PKG_BUILDDIR/data/info" VERSION=$v -SNAPD_APPARMOR_REEXEC=0 +SNAPD_APPARMOR_REEXEC=1 +${fmts} EOF diff -Nru snapd-2.55.5+20.04/osutil/cp.go snapd-2.57.5+20.04/osutil/cp.go --- snapd-2.55.5+20.04/osutil/cp.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/cp.go 2022-10-17 16:25:18.000000000 +0000 @@ -142,6 +142,7 @@ if err != nil { return fmt.Errorf("cannot create atomic file: %v", err) } + fout.SetModTime(fi.ModTime()) defer func() { if cerr := fout.Cancel(); cerr != ErrCannotCancel && err == nil { err = fmt.Errorf("cannot cancel temporary file copy %s: %v", fout.Name(), cerr) diff -Nru snapd-2.55.5+20.04/osutil/cp_test.go snapd-2.57.5+20.04/osutil/cp_test.go --- snapd-2.55.5+20.04/osutil/cp_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/cp_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -305,6 +305,23 @@ } +func (s *cpSuite) TestAtomicWriteFileCopyPreservesModTime(c *C) { + t := time.Date(2010, time.January, 1, 13, 0, 0, 0, time.UTC) + c.Assert(os.Chtimes(s.f1, t, t), IsNil) + + err := osutil.AtomicWriteFileCopy(s.f2, s.f1, 0) + c.Assert(err, IsNil) + c.Assert(s.f2, testutil.FileEquals, s.data) + + finfo, err := os.Stat(s.f1) + c.Assert(err, IsNil) + m1 := finfo.ModTime() + finfo, err = os.Stat(s.f2) + c.Assert(err, IsNil) + m2 := finfo.ModTime() + c.Assert(m1.Equal(m2), Equals, true) +} + func (s *cpSuite) TestAtomicWriteFileCopyOverwrites(c *C) { err := ioutil.WriteFile(s.f2, []byte("this is f2 content"), 0644) c.Assert(err, IsNil) diff -Nru snapd-2.55.5+20.04/osutil/disks/blockdev.go snapd-2.57.5+20.04/osutil/disks/blockdev.go --- snapd-2.55.5+20.04/osutil/disks/blockdev.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/blockdev.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,18 +34,15 @@ return 0, osutil.OutputErr(out, err) } nospace := strings.TrimSpace(string(out)) - sz, err := strconv.Atoi(nospace) + sz, err := strconv.ParseUint(nospace, 10, 64) if err != nil { return 0, fmt.Errorf("cannot parse blockdev %s result size %q: %v", cmd, nospace, err) } - return uint64(sz), nil + return sz, nil } -func blockDeviceSizeInSectors(devpath string) (uint64, error) { - // the size is always reported in 512-byte sectors, even if the device does - // not have a physical sector size of 512 - // XXX: consider using /sys/block//size directly - return blockdevSizeCmd("--getsz", devpath) +func blockDeviceSize(devpath string) (uint64, error) { + return blockdevSizeCmd("--getsize64", devpath) } func blockDeviceSectorSize(devpath string) (uint64, error) { @@ -55,12 +52,6 @@ return 0, err } - // ensure that the sector size is a multiple of 512, since we rely on that - // when we calculate the size in sectors, as blockdev --getsz always returns - // the size in 512-byte sectors - if sz%512 != 0 { - return 0, fmt.Errorf("sector size (%d) is not a multiple of 512", sz) - } if sz == 0 { // in some other places we are using the sector size as a divisor (to // convert from bytes to sectors), so it's essential that 0 is treated diff -Nru snapd-2.55.5+20.04/osutil/disks/disks_darwin.go snapd-2.57.5+20.04/osutil/disks/disks_darwin.go --- snapd-2.55.5+20.04/osutil/disks/disks_darwin.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/disks_darwin.go 2022-10-17 16:25:18.000000000 +0000 @@ -66,3 +66,15 @@ var diskFromPartitionDeviceNode = func(node string) (Disk, error) { return nil, osutil.ErrDarwin } + +func PartitionUUIDFromMountPoint(mountpoint string, opts *Options) (string, error) { + return "", osutil.ErrDarwin +} + +func PartitionUUID(node string) (string, error) { + return "", osutil.ErrDarwin +} + +func SectorSize(devname string) (uint64, error) { + return 0, osutil.ErrDarwin +} diff -Nru snapd-2.55.5+20.04/osutil/disks/disks_linux.go snapd-2.57.5+20.04/osutil/disks/disks_linux.go --- snapd-2.55.5+20.04/osutil/disks/disks_linux.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/disks_linux.go 2022-10-17 16:25:18.000000000 +0000 @@ -47,8 +47,8 @@ return diskFromMountPointImpl(mountpoint, opts) } -var abstractCalculateLastUsableLBA = func(device string) (uint64, error) { - return CalculateLastUsableLBA(device) +var abstractCalculateLastUsableLBA = func(device string, diskSize uint64, sectorSize uint64) (uint64, error) { + return CalculateLastUsableLBA(device, diskSize, sectorSize) } func parseDeviceMajorMinor(s string) (int, int, error) { @@ -276,7 +276,7 @@ return nil, err } - disk, err := diskFromPartUDevProps(props, nil) + disk, err := diskFromPartUDevProps(props) if err != nil { return nil, fmt.Errorf("cannot find disk from partition device node %s: %v", node, err) } @@ -352,13 +352,7 @@ // TODO: this could be implemented by reading the "size" file in sysfs // instead of using blockdev - // The size of the disk is always given by using blockdev, but blockdev - // returns the size in 512-byte blocks, so for bytes we have to multiply by - // 512. - num512Sectors, err := blockDeviceSizeInSectors(d.devname) - // if err is non-nil, numSectors will be 0 and thus 0*512 will still be - // zero - return num512Sectors * 512, err + return blockDeviceSize(d.devname) } // TODO: remove this code in favor of abstractCalculateLastUsableLBA() @@ -422,7 +416,15 @@ } // calculated is last LBA - calculated, err := abstractCalculateLastUsableLBA(d.devname) + byteSize, err := d.SizeInBytes() + if err != nil { + return 0, err + } + sectorSize, err := d.SectorSize() + if err != nil { + return 0, err + } + calculated, err := abstractCalculateLastUsableLBA(d.devname, byteSize, sectorSize) // end (or size) LBA is the last LBA + 1 return calculated + 1, err } @@ -431,79 +433,74 @@ return d.schema } -func diskFromPartUDevProps(props map[string]string, opts *Options) (*disk, error) { - if opts != nil && opts.IsDecryptedDevice { - // verify that the mount point is indeed a mapper device, it should: - // 1. have DEVTYPE == disk from udev - // 2. have dm files in the sysfs entry for the maj:min of the device - if props["DEVTYPE"] != "disk" { - // not a decrypted device - return nil, fmt.Errorf("not a decrypted device: devtype is not disk (is %s)", props["DEVTYPE"]) - } - - // TODO:UC20: currently, we effectively parse the DM_UUID env variable - // that is set for the mapper device volume, but doing so is - // actually wrong, since the value of DM_UUID is an - // implementation detail that depends on the subsystem - // "owner" of the device such that the prefix is considered - // the owner and the suffix is private data owned by the - // subsystem. In our case, in UC20 initramfs, we have the - // device "owned" by systemd-cryptsetup, so we should ideally - // parse that the first part of DM_UUID matches CRYPT- and - // then use `cryptsetup status` (since CRYPT indicates it is - // "owned" by cryptsetup) to get more information on the - // device sufficient for our purposes to find the encrypted - // device/partition underneath the mapper. - // However we don't currently have cryptsetup in the initrd, - // so we can't do that yet :-( - - // TODO:UC20: these files are also likely readable through udev env - // properties, but it's unclear if reading there is reliable - // or not, given that these variables have been observed to - // be missing from the initrd previously, and are not - // available at all during userspace on UC20 for some reason - errFmt := "not a decrypted device: could not read device mapper metadata: %v" - - if props["MAJOR"] == "" { - return nil, fmt.Errorf("incomplete udev output missing required property \"MAJOR\"") - } - if props["MINOR"] == "" { - return nil, fmt.Errorf("incomplete udev output missing required property \"MAJOR\"") - } - - majmin := props["MAJOR"] + ":" + props["MINOR"] - - dmDir := filepath.Join(dirs.SysfsDir, "dev", "block", majmin, "dm") - dmUUID, err := ioutil.ReadFile(filepath.Join(dmDir, "uuid")) - if err != nil { - return nil, fmt.Errorf(errFmt, err) - } - dmUUID = bytes.TrimSpace(dmUUID) - - dmName, err := ioutil.ReadFile(filepath.Join(dmDir, "name")) - if err != nil { - return nil, fmt.Errorf(errFmt, err) - } - dmName = bytes.TrimSpace(dmName) - - matchedHandler := false - for _, resolver := range deviceMapperBackResolvers { - if dev, ok := resolver(dmUUID, dmName); ok { - props, err = udevPropertiesForName(dev) - if err != nil { - return nil, fmt.Errorf("cannot get udev properties for encrypted partition %s: %v", dev, err) - } - - matchedHandler = true - break +func parentPartitionPropsForOptions(props map[string]string) (map[string]string, error) { + // verify that the mount point is indeed a mapper device, it should: + // 1. have DEVTYPE == disk from udev + // 2. have dm files in the sysfs entry for the maj:min of the device + if props["DEVTYPE"] != "disk" { + // not a decrypted device + return nil, fmt.Errorf("not a decrypted device: devtype is not disk (is %s)", props["DEVTYPE"]) + } + + // TODO:UC20: currently, we effectively parse the DM_UUID env variable + // that is set for the mapper device volume, but doing so is + // actually wrong, since the value of DM_UUID is an + // implementation detail that depends on the subsystem + // "owner" of the device such that the prefix is considered + // the owner and the suffix is private data owned by the + // subsystem. In our case, in UC20 initramfs, we have the + // device "owned" by systemd-cryptsetup, so we should ideally + // parse that the first part of DM_UUID matches CRYPT- and + // then use `cryptsetup status` (since CRYPT indicates it is + // "owned" by cryptsetup) to get more information on the + // device sufficient for our purposes to find the encrypted + // device/partition underneath the mapper. + // However we don't currently have cryptsetup in the initrd, + // so we can't do that yet :-( + + // TODO:UC20: these files are also likely readable through udev env + // properties, but it's unclear if reading there is reliable + // or not, given that these variables have been observed to + // be missing from the initrd previously, and are not + // available at all during userspace on UC20 for some reason + errFmt := "not a decrypted device: could not read device mapper metadata: %v" + + if props["MAJOR"] == "" { + return nil, fmt.Errorf("incomplete udev output missing required property \"MAJOR\"") + } + if props["MINOR"] == "" { + return nil, fmt.Errorf("incomplete udev output missing required property \"MAJOR\"") + } + + majmin := props["MAJOR"] + ":" + props["MINOR"] + + dmDir := filepath.Join(dirs.SysfsDir, "dev", "block", majmin, "dm") + dmUUID, err := ioutil.ReadFile(filepath.Join(dmDir, "uuid")) + if err != nil { + return nil, fmt.Errorf(errFmt, err) + } + dmUUID = bytes.TrimSpace(dmUUID) + + dmName, err := ioutil.ReadFile(filepath.Join(dmDir, "name")) + if err != nil { + return nil, fmt.Errorf(errFmt, err) + } + dmName = bytes.TrimSpace(dmName) + + for _, resolver := range deviceMapperBackResolvers { + if dev, ok := resolver(dmUUID, dmName); ok { + props, err = udevPropertiesForName(dev) + if err != nil { + return nil, fmt.Errorf("cannot get udev properties for encrypted partition %s: %v", dev, err) } - } - - if !matchedHandler { - return nil, fmt.Errorf("internal error: no back resolver supports decrypted device mapper with UUID %q and name %q", dmUUID, dmName) + return props, nil } } + return nil, fmt.Errorf("internal error: no back resolver supports decrypted device mapper with UUID %q and name %q", dmUUID, dmName) +} + +func diskFromPartUDevProps(props map[string]string) (*disk, error) { // ID_PART_ENTRY_DISK will give us the major and minor of the disk that this // partition originated from if this mount point is indeed for a partition if props["ID_PART_ENTRY_DISK"] == "" { @@ -577,42 +574,58 @@ return d, nil } -// diskFromMountPointImpl returns a Disk for the underlying mount source of the -// specified mount point. For mount points which have sources that are not -// partitions, and thus are a part of a disk, the returned Disk refers to the -// volume/disk of the mount point itself. -func diskFromMountPointImpl(mountpoint string, opts *Options) (*disk, error) { +func partitionPropsFromMountPoint(mountpoint string) (source string, props map[string]string, err error) { // first get the mount entry for the mountpoint mounts, err := osutil.LoadMountInfo() if err != nil { - return nil, err + return "", nil, err } - var partMountPointSource string // loop over the mount entries in reverse order to prevent shadowing of a // particular mount on top of another one for i := len(mounts) - 1; i >= 0; i-- { if mounts[i].MountDir == mountpoint { - partMountPointSource = mounts[i].MountSource + source = mounts[i].MountSource break } } - if partMountPointSource == "" { - return nil, fmt.Errorf("cannot find mountpoint %q", mountpoint) + if source == "" { + return "", nil, fmt.Errorf("cannot find mountpoint %q", mountpoint) } // now we have the partition for this mountpoint, we need to tie that back // to a disk with a major minor, so query udev with the mount source path // of the mountpoint for properties - props, err := udevPropertiesForName(partMountPointSource) + props, err = udevPropertiesForName(source) if err != nil && props == nil { // only fail here if props is nil, if it's available we validate it // below - return nil, fmt.Errorf("cannot find disk for partition %s: %v", partMountPointSource, err) + return "", nil, fmt.Errorf("cannot process udev properties of %s: %v", source, err) } + return source, props, nil +} - disk, err := diskFromPartUDevProps(props, opts) +// diskFromMountPointImpl returns a Disk for the underlying mount source of the +// specified mount point. For mount points which have sources that are not +// partitions, and thus are a part of a disk, the returned Disk refers to the +// volume/disk of the mount point itself. +func diskFromMountPointImpl(mountpoint string, opts *Options) (*disk, error) { + source, props, err := partitionPropsFromMountPoint(mountpoint) if err != nil { - return nil, fmt.Errorf("cannot find disk from mountpoint source %s of %s: %v", partMountPointSource, mountpoint, err) + return nil, err + } + + if opts != nil && opts.IsDecryptedDevice { + props, err = parentPartitionPropsForOptions(props) + if err != nil { + return nil, fmt.Errorf("cannot process properties of %v parent device: %v", source, err) + } + } + + disk, err := diskFromPartUDevProps(props) + if err != nil { + // TODO: leave the inclusion of mpointpoint source in the error + // to the caller + return nil, fmt.Errorf("cannot find disk from mountpoint source %s of %s: %v", source, mountpoint, err) } return disk, nil } @@ -929,3 +942,44 @@ } return disks, nil } + +// PartitionUUIDFromMountPoint returns the UUID of the partition which is a +// source of a given mount point. +func PartitionUUIDFromMountPoint(mountpoint string, opts *Options) (string, error) { + _, props, err := partitionPropsFromMountPoint(mountpoint) + if err != nil { + return "", err + } + + if opts != nil && opts.IsDecryptedDevice { + props, err = parentPartitionPropsForOptions(props) + if err != nil { + return "", err + } + } + partUUID := props["ID_PART_ENTRY_UUID"] + if partUUID == "" { + partDev := filepath.Join("/dev", props["DEVNAME"]) + return "", fmt.Errorf("cannot get required partition UUID udev property for device %s", partDev) + } + return partUUID, nil +} + +// PartitionUUID returns the UUID of a given partition +func PartitionUUID(node string) (string, error) { + props, err := udevPropertiesForName(node) + if err != nil && props == nil { + // only fail here if props is nil, if it's available we validate it + // below + return "", fmt.Errorf("cannot process udev properties: %v", err) + } + partUUID := props["ID_PART_ENTRY_UUID"] + if partUUID == "" { + return "", fmt.Errorf("cannot get required udev partition UUID property") + } + return partUUID, nil +} + +func SectorSize(devname string) (uint64, error) { + return blockDeviceSectorSize(devname) +} diff -Nru snapd-2.55.5+20.04/osutil/disks/disks_linux_test.go snapd-2.57.5+20.04/osutil/disks/disks_linux_test.go --- snapd-2.55.5+20.04/osutil/disks/disks_linux_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/disks_linux_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -503,7 +503,7 @@ opts := &disks.Options{IsDecryptedDevice: true} _, err := disks.DiskFromMountPoint("/run/mnt/point", opts) - c.Assert(err, ErrorMatches, `cannot find disk from mountpoint source /dev/vda4 of /run/mnt/point: not a decrypted device: devtype is not disk \(is partition\)`) + c.Assert(err, ErrorMatches, `cannot process properties of /dev/vda4 parent device: not a decrypted device: devtype is not disk \(is partition\)`) } func (s *diskSuite) TestDiskFromMountPointUnhappyIsDecryptedDeviceNoSysfs(c *C) { @@ -531,7 +531,7 @@ opts := &disks.Options{IsDecryptedDevice: true} _, err := disks.DiskFromMountPoint("/run/mnt/point", opts) - c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot find disk from mountpoint source /dev/mapper/something of /run/mnt/point: not a decrypted device: could not read device mapper metadata: open %s/dev/block/252:0/dm/uuid: no such file or directory`, dirs.SysfsDir)) + c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot process properties of /dev/mapper/something parent device: not a decrypted device: could not read device mapper metadata: open %s/dev/block/252:0/dm/uuid: no such file or directory`, dirs.SysfsDir)) } func (s *diskSuite) TestDiskFromMountPointHappySinglePartitionIgnoresNonPartitionsInSysfs(c *C) { @@ -761,7 +761,7 @@ }() _, err = disks.DiskFromMountPoint("/run/mnt/point", opts) - c.Assert(err, ErrorMatches, `cannot find disk from mountpoint source /dev/mapper/something of /run/mnt/point: internal error: no back resolver supports decrypted device mapper with UUID "CRYPT-LUKS2-5a522809c87e4dfa81a88dc5667d1304-something" and name "something"`) + c.Assert(err, ErrorMatches, `cannot process properties of /dev/mapper/something parent device: internal error: no back resolver supports decrypted device mapper with UUID "CRYPT-LUKS2-5a522809c87e4dfa81a88dc5667d1304-something" and name "something"`) // but when it is available it works disks.RegisterDeviceMapperBackResolver("crypt-luks2", disks.CryptLuks2DeviceMapperBackResolver) @@ -851,7 +851,7 @@ // first try without the handler fails _, err = disks.DiskFromMountPoint("/run/mnt/point", opts) - c.Assert(err, ErrorMatches, `cannot find disk from mountpoint source /dev/mapper/something-device-locked of /run/mnt/point: internal error: no back resolver supports decrypted device mapper with UUID "5a522809-c87e-4dfa-81a8-8dc5667d1304" and name "something-device-locked"`) + c.Assert(err, ErrorMatches, `cannot process properties of /dev/mapper/something-device-locked parent device: internal error: no back resolver supports decrypted device mapper with UUID "5a522809-c87e-4dfa-81a8-8dc5667d1304" and name "something-device-locked"`) // next try with the FDE package handler works disks.RegisterDeviceMapperBackResolver("device-unlock-kernel-fde", fde.DeviceUnlockKernelHookDeviceMapperBackResolver) @@ -1555,17 +1555,9 @@ c.Assert(d.Schema(), Equals, "gpt") c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") - endSectors, err := d.UsableSectorsEnd() - c.Assert(err, IsNil) - c.Assert(endSectors, Equals, uint64(43)) - c.Assert(sfdiskCmd.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--version"}, - }) - sfdiskCmd.ForgetCalls() - blockDevCmd := testutil.MockCommand(c, "blockdev", ` -if [ "$1" = "--getsz" ]; then - echo 10000 +if [ "$1" = "--getsize64" ]; then + echo 5120000 elif [ "$1" = "--getss" ]; then echo 512 else @@ -1575,11 +1567,24 @@ `) defer blockDevCmd.Restore() + endSectors, err := d.UsableSectorsEnd() + c.Assert(err, IsNil) + c.Assert(endSectors, Equals, uint64(43)) + c.Assert(sfdiskCmd.Calls(), DeepEquals, [][]string{ + {"sfdisk", "--version"}, + }) + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getsize64", "/dev/sda"}, + {"blockdev", "--getss", "/dev/sda"}, + }) + blockDevCmd.ForgetCalls() + sfdiskCmd.ForgetCalls() + sz, err := d.SizeInBytes() c.Assert(err, IsNil) c.Assert(sz, Equals, uint64(10000*512)) c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getsz", "/dev/sda"}, + {"blockdev", "--getsize64", "/dev/sda"}, }) blockDevCmd.ForgetCalls() @@ -1732,8 +1737,8 @@ sfdiskCmd.ForgetCalls() blockDevCmd := testutil.MockCommand(c, "blockdev", ` -if [ "$1" = "--getsz" ]; then - echo 10000 +if [ "$1" = "--getsize64" ]; then + echo 5120000 elif [ "$1" = "--getss" ]; then echo 4096 else @@ -1745,12 +1750,9 @@ sz, err := d.SizeInBytes() c.Assert(err, IsNil) - // the size is still the result of --getsz * 512, we don't use the native - // sector size at all here since blockdev doesn't use the native sector size - // at all - c.Assert(sz, Equals, uint64(10000*512)) + c.Assert(sz, Equals, uint64(5120000)) c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getsz", "/dev/sda"}, + {"blockdev", "--getsize64", "/dev/sda"}, }) blockDevCmd.ForgetCalls() @@ -1766,64 +1768,6 @@ c.Assert(sfdiskCmd.Calls(), HasLen, 0) } -func (s *diskSuite) TestDiskSizeRelatedMethodsGPTNon512MultipleSectorSizeError(c *C) { - restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { - c.Assert(typeOpt, Equals, "--name") - c.Assert(dev, Equals, "sda") - return map[string]string{ - "MAJOR": "1", - "MINOR": "2", - "DEVTYPE": "disk", - "DEVNAME": "/dev/sda", - "ID_PART_TABLE_UUID": "foo", - "ID_PART_TABLE_TYPE": "gpt", - "DEVPATH": "/devices/foo/sda", - }, nil - }) - defer restore() - - sfdiskCmd := testutil.MockCommand(c, "sfdisk", ` -if [ "$1" = --version ]; then - echo 'sfdisk from util-linux 2.34.1' - exit 0 -fi -echo '{ - "partitiontable": { - "unit": "sectors", - "lastlba": 42 - } -}' -`) - defer sfdiskCmd.Restore() - - d, err := disks.DiskFromDeviceName("sda") - c.Assert(err, IsNil) - c.Assert(d.Schema(), Equals, "gpt") - c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") - - // the end of sectors end does not query the size of the sectors - endSectors, err := d.UsableSectorsEnd() - c.Assert(err, IsNil) - c.Assert(endSectors, Equals, uint64(43)) - - sfdiskCmd.ForgetCalls() - - blockDevCmd := testutil.MockCommand(c, "blockdev", ` -echo 513 -`) - defer blockDevCmd.Restore() - - // but getting the sector size itself fails - _, err = d.SectorSize() - c.Assert(err, ErrorMatches, `sector size \(513\) is not a multiple of 512`) - - c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getss", "/dev/sda"}, - }) - - c.Assert(sfdiskCmd.Calls(), HasLen, 0) -} - func (s *diskSuite) TestDiskSizeRelatedMethodsDOS(c *C) { restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { c.Assert(typeOpt, Equals, "--name") @@ -1844,8 +1788,8 @@ defer sfdiskCmd.Restore() blockDevCmd := testutil.MockCommand(c, "blockdev", ` -if [ "$1" = "--getsz" ]; then - echo 10000 +if [ "$1" = "--getsize64" ]; then + echo 5120000 elif [ "$1" = "--getss" ]; then echo 512 else @@ -1867,7 +1811,7 @@ c.Assert(endSectors, Equals, uint64(10000)) c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getsz", "/dev/sda"}, + {"blockdev", "--getsize64", "/dev/sda"}, {"blockdev", "--getss", "/dev/sda"}, }) @@ -1879,7 +1823,7 @@ c.Assert(sz, Equals, uint64(10000*512)) c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getsz", "/dev/sda"}, + {"blockdev", "--getsize64", "/dev/sda"}, }) // we never used sfdisk @@ -1906,8 +1850,8 @@ defer sfdiskCmd.Restore() blockDevCmd := testutil.MockCommand(c, "blockdev", ` -if [ "$1" = "--getsz" ]; then - echo 10000 +if [ "$1" = "--getsize64" ]; then + echo 5120000 elif [ "$1" = "--getss" ]; then echo 4096 else @@ -1927,7 +1871,7 @@ c.Assert(endSectors, Equals, uint64(10000*512/4096)) c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getsz", "/dev/sda"}, + {"blockdev", "--getsize64", "/dev/sda"}, {"blockdev", "--getss", "/dev/sda"}, }) @@ -1939,7 +1883,7 @@ c.Assert(sz, Equals, uint64(10000*512)) c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getsz", "/dev/sda"}, + {"blockdev", "--getsize64", "/dev/sda"}, }) // we never used sfdisk @@ -2022,3 +1966,125 @@ c.Assert(d[2].KernelDeviceNode(), Equals, "/dev/sda") c.Assert(d[3].KernelDeviceNode(), Equals, "/dev/sdb") } + +func (s *diskSuite) TestPartitionUUIDFromMopuntPointErrs(c *C) { + restore := osutil.MockMountInfo(``) + defer restore() + + _, err := disks.PartitionUUIDFromMountPoint("/run/mnt/blah", nil) + c.Assert(err, ErrorMatches, "cannot find mountpoint \"/run/mnt/blah\"") + + restore = osutil.MockMountInfo(`130 30 42:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/vda4 rw +`) + defer restore() + + restore = disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "/dev/vda4") + return map[string]string{ + "DEVNAME": "vda4", + "prop": "hello", + }, nil + }) + defer restore() + + _, err = disks.PartitionUUIDFromMountPoint("/run/mnt/point", nil) + c.Assert(err, ErrorMatches, "cannot get required partition UUID udev property for device /dev/vda4") +} + +func (s *diskSuite) TestPartitionUUIDFromMountPointPlain(c *C) { + restore := osutil.MockMountInfo(`130 30 42:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/vda4 rw +`) + defer restore() + restore = disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "/dev/vda4") + return map[string]string{ + "DEVTYPE": "disk", + "ID_PART_ENTRY_UUID": "foo-uuid", + }, nil + }) + defer restore() + + uuid, err := disks.PartitionUUIDFromMountPoint("/run/mnt/point", nil) + c.Assert(err, IsNil) + c.Assert(uuid, Equals, "foo-uuid") +} + +func (s *diskSuite) TestPartitionUUIDFromMopuntPointDecrypted(c *C) { + restore := osutil.MockMountInfo(`130 30 42:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/mapper/something rw +`) + defer restore() + restore = disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + switch dev { + case "/dev/mapper/something": + return map[string]string{ + "DEVTYPE": "disk", + "MAJOR": "242", + "MINOR": "1", + }, nil + case "/dev/disk/by-uuid/5a522809-c87e-4dfa-81a8-8dc5667d1304": + return map[string]string{ + "ID_PART_ENTRY_UUID": "foo-uuid", + }, nil + default: + c.Errorf("unexpected udev device properties requested: %s", dev) + return nil, fmt.Errorf("unexpected udev device: %s", dev) + } + }) + defer restore() + + // mock the sysfs dm uuid and name files + dmDir := filepath.Join(filepath.Join(dirs.SysfsDir, "dev", "block"), "242:1", "dm") + err := os.MkdirAll(dmDir, 0755) + c.Assert(err, IsNil) + + b := []byte("something") + err = ioutil.WriteFile(filepath.Join(dmDir, "name"), b, 0644) + c.Assert(err, IsNil) + + b = []byte("CRYPT-LUKS2-5a522809c87e4dfa81a88dc5667d1304-something") + err = ioutil.WriteFile(filepath.Join(dmDir, "uuid"), b, 0644) + c.Assert(err, IsNil) + + uuid, err := disks.PartitionUUIDFromMountPoint("/run/mnt/point", &disks.Options{ + IsDecryptedDevice: true, + }) + c.Assert(err, IsNil) + c.Assert(uuid, Equals, "foo-uuid") +} + +func (s *diskSuite) TestPartitionUUID(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + switch dev { + case "/dev/vda4": + return map[string]string{ + "ID_PART_ENTRY_UUID": "foo-uuid", + }, nil + case "/dev/no-uuid": + return map[string]string{ + "no-uuid": "no-uuid", + }, nil + case "/dev/mock-failure": + return nil, fmt.Errorf("mock failure") + default: + c.Errorf("unexpected udev device properties requested: %s", dev) + return nil, fmt.Errorf("unexpected udev device: %s", dev) + } + }) + defer restore() + + uuid, err := disks.PartitionUUID("/dev/vda4") + c.Assert(err, IsNil) + c.Assert(uuid, Equals, "foo-uuid") + + uuid, err = disks.PartitionUUID("/dev/no-uuid") + c.Assert(err, ErrorMatches, "cannot get required udev partition UUID property") + c.Check(uuid, Equals, "") + + uuid, err = disks.PartitionUUID("/dev/mock-failure") + c.Assert(err, ErrorMatches, "cannot process udev properties: mock failure") + c.Check(uuid, Equals, "") +} diff -Nru snapd-2.55.5+20.04/osutil/disks/gpt.go snapd-2.57.5+20.04/osutil/disks/gpt.go --- snapd-2.55.5+20.04/osutil/disks/gpt.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/gpt.go 2022-10-17 16:25:18.000000000 +0000 @@ -78,9 +78,9 @@ return nil } -func LoadGPTHeader(devfd *os.File) (GPTHeader, error) { +func LoadGPTHeader(devfd *os.File, sectorSize uint64) (GPTHeader, error) { var header GPTHeader - rawHeader := make([]byte, 512) + rawHeader := make([]byte, sectorSize) read, err := devfd.Read(rawHeader) if err != nil { return GPTHeader{}, err @@ -100,26 +100,26 @@ return header, nil } -func ReadGPTHeader(device string) (GPTHeader, error) { +func ReadGPTHeader(device string, sectorSize uint64) (GPTHeader, error) { devfd, err := os.Open(device) if err != nil { return GPTHeader{}, err } defer devfd.Close() - if _, err := devfd.Seek(512, os.SEEK_SET); err != nil { + if _, err := devfd.Seek(int64(sectorSize), os.SEEK_SET); err != nil { return GPTHeader{}, err } - header, main_err := LoadGPTHeader(devfd) + header, main_err := LoadGPTHeader(devfd, sectorSize) if main_err != nil { // Read the backup header - _, err := devfd.Seek(-512, os.SEEK_END) + _, err := devfd.Seek(-int64(sectorSize), os.SEEK_END) if err != nil { return GPTHeader{}, main_err } - header, err = LoadGPTHeader(devfd) + header, err = LoadGPTHeader(devfd, sectorSize) if err != nil { return GPTHeader{}, main_err } @@ -128,18 +128,19 @@ return header, nil } -func CalculateLastUsableLBA(device string) (uint64, error) { - header, err := ReadGPTHeader(device) - if err != nil { - return 0, err - } - sectors, err := blockDeviceSizeInSectors(device) +func CalculateLastUsableLBA(device string, diskSize uint64, sectorSize uint64) (uint64, error) { + header, err := ReadGPTHeader(device, sectorSize) if err != nil { return 0, err } + sectors := diskSize / sectorSize + tableSize := uint64(header.NEntries) * uint64(header.EntrySize) + if tableSize < 16*1024 { + tableSize = 16 * 1024 + } // Rounded up division for number of sectors - tableSizeInSectors := (tableSize + 511) / 512 + tableSizeInSectors := (tableSize + sectorSize - 1) / sectorSize // | | // | Last Usable LBA | sectors - tableSizeInSectors - 2 diff -Nru snapd-2.55.5+20.04/osutil/disks/gpt_test.go snapd-2.57.5+20.04/osutil/disks/gpt_test.go --- snapd-2.55.5+20.04/osutil/disks/gpt_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/gpt_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -31,19 +31,44 @@ "github.com/snapcore/snapd/osutil/disks" ) +type tableSizeType int + +const ( + Normal tableSizeType = 0 + Big = 1 + Small = 2 +) + type gptSuite struct { - image string - size uint64 + image string + size uint64 + blockSize uint64 + tableSize tableSizeType } -var _ = Suite(&gptSuite{}) +var _ = Suite(&gptSuite{blockSize: 512, tableSize: Normal}) +var _ = Suite(&gptSuite{blockSize: 512, tableSize: Small}) +var _ = Suite(&gptSuite{blockSize: 512, tableSize: Big}) +var _ = Suite(&gptSuite{blockSize: 4096, tableSize: Normal}) +var _ = Suite(&gptSuite{blockSize: 4096, tableSize: Small}) +var _ = Suite(&gptSuite{blockSize: 4096, tableSize: Big}) func (s *gptSuite) SetUpTest(c *C) { tmpdir := c.MkDir() - header, err := os.Open("testdata/gpt_header") + suffix := "" + if s.blockSize == 4096 { + suffix = suffix + "_4k" + } + if s.tableSize == Small { + suffix = suffix + "_small" + } + if s.tableSize == Big { + suffix = suffix + "_big" + } + header, err := os.Open("testdata/gpt_header" + suffix) c.Assert(err, IsNil) defer header.Close() - footer, err := os.Open("testdata/gpt_footer") + footer, err := os.Open("testdata/gpt_footer" + suffix) c.Assert(err, IsNil) defer footer.Close() s.image = filepath.Join(tmpdir, "image.img") @@ -53,24 +78,24 @@ _, err = io.Copy(image, header) c.Assert(err, IsNil) // 128M - 1 block - _, err = image.Seek((128*1024*2-1)*512, os.SEEK_SET) + _, err = image.Seek((128*1024*1024/int64(s.blockSize)-1)*int64(s.blockSize), os.SEEK_SET) c.Assert(err, IsNil) io.Copy(image, footer) stat, err := os.Stat(s.image) c.Assert(err, IsNil) size := stat.Size() - c.Assert(size%512, Equals, int64(0)) - s.size = uint64(size) / 512 + c.Assert(size%int64(s.blockSize), Equals, int64(0)) + s.size = uint64(size) / s.blockSize } func (s *gptSuite) TestReadFirstLBA(c *C) { f, err := os.Open(s.image) c.Assert(err, IsNil) - _, err = f.Seek(512, 0) + _, err = f.Seek(int64(s.blockSize), 0) c.Assert(err, IsNil) - gptHeader, err := disks.LoadGPTHeader(f) + gptHeader, err := disks.LoadGPTHeader(f, s.blockSize) c.Assert(err, IsNil) c.Assert(uint64(gptHeader.CurrentLBA), Equals, uint64(1)) @@ -80,10 +105,10 @@ func (s *gptSuite) TestReadLastLBA(c *C) { f, err := os.Open(s.image) c.Assert(err, IsNil) - _, err = f.Seek(-512, 2) + _, err = f.Seek(-int64(s.blockSize), 2) c.Assert(err, IsNil) - gptHeader, err := disks.LoadGPTHeader(f) + gptHeader, err := disks.LoadGPTHeader(f, s.blockSize) c.Assert(err, IsNil) c.Assert(uint64(gptHeader.CurrentLBA), Equals, s.size-1) @@ -94,7 +119,7 @@ f, err := os.OpenFile(s.image, os.O_RDWR, 0777) c.Assert(err, IsNil) defer f.Close() - _, err = f.Seek(512, 0) + _, err = f.Seek(int64(s.blockSize), 0) c.Assert(err, IsNil) _, err = f.Write([]byte("NOTGPT")) c.Assert(err, IsNil) @@ -105,10 +130,10 @@ f, err := os.Open(s.image) c.Assert(err, IsNil) - _, err = f.Seek(512, 0) + _, err = f.Seek(int64(s.blockSize), 0) c.Assert(err, IsNil) - _, err = disks.LoadGPTHeader(f) + _, err = disks.LoadGPTHeader(f, s.blockSize) c.Assert(err, ErrorMatches, `GPT Header does not start with the magic string`) } @@ -116,7 +141,7 @@ f, err := os.OpenFile(s.image, os.O_RDWR, 0777) c.Assert(err, IsNil) defer f.Close() - _, err = f.Seek(512+8, 0) + _, err = f.Seek(int64(s.blockSize)+8, 0) c.Assert(err, IsNil) err = binary.Write(f, binary.LittleEndian, uint32(0x12345678)) c.Assert(err, IsNil) @@ -127,10 +152,10 @@ f, err := os.Open(s.image) c.Assert(err, IsNil) - _, err = f.Seek(512, 0) + _, err = f.Seek(int64(s.blockSize), 0) c.Assert(err, IsNil) - _, err = disks.LoadGPTHeader(f) + _, err = disks.LoadGPTHeader(f, s.blockSize) c.Assert(err, ErrorMatches, `GPT header revision is not 1.0`) } @@ -138,7 +163,7 @@ f, err := os.OpenFile(s.image, os.O_RDWR, 0777) c.Assert(err, IsNil) defer f.Close() - _, err = f.Seek(512+8+4, 0) + _, err = f.Seek(int64(s.blockSize)+8+4, 0) c.Assert(err, IsNil) err = binary.Write(f, binary.LittleEndian, newsize) c.Assert(err, IsNil) @@ -149,22 +174,22 @@ f, err := os.Open(s.image) c.Assert(err, IsNil) - _, err = f.Seek(512, 0) + _, err = f.Seek(int64(s.blockSize), 0) c.Assert(err, IsNil) - _, err = disks.LoadGPTHeader(f) + _, err = disks.LoadGPTHeader(f, s.blockSize) c.Assert(err, ErrorMatches, `GPT header size is smaller than the minimum valid size`) } func (s *gptSuite) TestBigSize(c *C) { - s.messSize(c, 514) + s.messSize(c, uint32(s.blockSize)+3) f, err := os.Open(s.image) c.Assert(err, IsNil) - _, err = f.Seek(512, 0) + _, err = f.Seek(int64(s.blockSize), 0) c.Assert(err, IsNil) - _, err = disks.LoadGPTHeader(f) + _, err = disks.LoadGPTHeader(f, s.blockSize) c.Assert(err, ErrorMatches, `GPT header size is larger than the maximum supported size`) } @@ -172,12 +197,12 @@ f, err := os.OpenFile(s.image, os.O_RDWR, 0777) c.Assert(err, IsNil) defer f.Close() - _, err = f.Seek(512+8+4+4, 0) + _, err = f.Seek(int64(s.blockSize)+8+4+4, 0) c.Assert(err, IsNil) var crc uint32 err = binary.Read(f, binary.LittleEndian, &crc) c.Assert(err, IsNil) - _, err = f.Seek(512+8+4+4, 0) + _, err = f.Seek(int64(s.blockSize)+8+4+4, 0) c.Assert(err, IsNil) crc = crc + 1 err = binary.Write(f, binary.LittleEndian, crc) @@ -189,15 +214,15 @@ f, err := os.Open(s.image) c.Assert(err, IsNil) - _, err = f.Seek(512, 0) + _, err = f.Seek(int64(s.blockSize), 0) c.Assert(err, IsNil) - _, err = disks.LoadGPTHeader(f) + _, err = disks.LoadGPTHeader(f, s.blockSize) c.Assert(err, ErrorMatches, `GPT header CRC32 checksum failed: [0-9]+ != [0-9]+`) } func (s *gptSuite) TestReadFile(c *C) { - gptHeader, err := disks.ReadGPTHeader(s.image) + gptHeader, err := disks.ReadGPTHeader(s.image, s.blockSize) c.Assert(err, IsNil) // Check that we got the first header @@ -207,7 +232,7 @@ func (s *gptSuite) TestReadFileFallback(c *C) { s.messSignature(c) - gptHeader, err := disks.ReadGPTHeader(s.image) + gptHeader, err := disks.ReadGPTHeader(s.image, s.blockSize) c.Assert(err, IsNil) // Check that we got the alternate header @@ -219,7 +244,7 @@ f, err := os.OpenFile(s.image, os.O_RDWR, 0777) c.Assert(err, IsNil) defer f.Close() - _, err = f.Seek(-512+8, 2) + _, err = f.Seek(-int64(s.blockSize)+8, 2) c.Assert(err, IsNil) err = binary.Write(f, binary.LittleEndian, uint32(0x12345678)) c.Assert(err, IsNil) @@ -228,29 +253,46 @@ func (s *gptSuite) TestReadFileFail(c *C) { s.messSignature(c) s.messAlternateRevision(c) - _, err := disks.ReadGPTHeader(s.image) + _, err := disks.ReadGPTHeader(s.image, s.blockSize) // Check that we get the error from the main header c.Assert(err, ErrorMatches, `GPT Header does not start with the magic string`) } func (s *gptSuite) TestCalculateSize(c *C) { - calculated, err := disks.CalculateLastUsableLBA(s.image) - c.Assert(err, IsNil) - gptHeader, err := disks.ReadGPTHeader(s.image) + calculated, err := disks.CalculateLastUsableLBA(s.image, 128*1024*1024, s.blockSize) c.Assert(err, IsNil) - c.Assert(uint64(gptHeader.LastUsableLBA), Equals, calculated) + if s.tableSize == Small { + size := 128 * 1024 * 1024 / int64(s.blockSize) + alternateHeader := size - 1 + alternateTable := alternateHeader - 16*1024/int64(s.blockSize) + lastUsable := alternateTable - 1 + c.Assert(calculated, Equals, uint64(lastUsable)) + } else { + gptHeader, err := disks.ReadGPTHeader(s.image, s.blockSize) + c.Assert(err, IsNil) + c.Assert(calculated, Equals, uint64(gptHeader.LastUsableLBA)) + } } func (s *gptSuite) TestCalculateSizeResized(c *C) { err := exec.Command("truncate", "--size", "256M", s.image).Run() c.Assert(err, IsNil) - calculated, err := disks.CalculateLastUsableLBA(s.image) - c.Assert(err, IsNil) - gptHeader, err := disks.ReadGPTHeader(s.image) + calculated, err := disks.CalculateLastUsableLBA(s.image, 256*1024*1024, s.blockSize) c.Assert(err, IsNil) - // We added 128*1024*2 sectors, we expect that exact value added - c.Assert(uint64(gptHeader.LastUsableLBA)+128*1024*2, Equals, calculated) + + if s.tableSize == Small { + size := 256 * 1024 * 1024 / int64(s.blockSize) + alternateHeader := size - 1 + alternateTable := alternateHeader - 16*1024/int64(s.blockSize) + lastUsable := alternateTable - 1 + c.Assert(calculated, Equals, uint64(lastUsable)) + } else { + gptHeader, err := disks.ReadGPTHeader(s.image, s.blockSize) + c.Assert(err, IsNil) + // We added 128*1024*2 sectors, we expect that exact value added + c.Assert(calculated, Equals, uint64(gptHeader.LastUsableLBA)+128*1024*1024/s.blockSize) + } } diff -Nru snapd-2.55.5+20.04/osutil/disks/mockdisk_linux.go snapd-2.57.5+20.04/osutil/disks/mockdisk_linux.go --- snapd-2.55.5+20.04/osutil/disks/mockdisk_linux.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/mockdisk_linux.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,7 +22,7 @@ func MockCalculateLastUsableLBA(value uint64, err error) (restore func()) { old := abstractCalculateLastUsableLBA - abstractCalculateLastUsableLBA = func(device string) (uint64, error) { + abstractCalculateLastUsableLBA = func(device string, diskSize uint64, sectorSize uint64) (uint64, error) { return value, err } diff -Nru snapd-2.55.5+20.04/osutil/disks/size.go snapd-2.57.5+20.04/osutil/disks/size.go --- snapd-2.55.5+20.04/osutil/disks/size.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/size.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2021 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 disks - -import ( - "fmt" - "os/exec" - "strconv" - "strings" - - "github.com/snapcore/snapd/osutil" -) - -// Size returns the size of the given block device, e.g. /dev/sda1 in -// bytes as reported by the kernels BLKGETSIZE ioctl. -func Size(partDevice string) (uint64, error) { - // Use blockdev command instead of calling the ioctl directly since - // on 32bit systems it's a pain to get a 64bit value from a ioctl. - raw, err := exec.Command("blockdev", "--getsz", partDevice).CombinedOutput() - if err != nil { - return 0, fmt.Errorf("cannot get disk size: %v", osutil.OutputErr(raw, err)) - } - output := strings.TrimSpace(string(raw)) - partBlocks, err := strconv.ParseUint(output, 10, 64) - if err != nil { - return 0, fmt.Errorf("cannot parse disk size output: %v", err) - } - - return uint64(partBlocks) * 512, nil -} diff -Nru snapd-2.55.5+20.04/osutil/disks/size_test.go snapd-2.57.5+20.04/osutil/disks/size_test.go --- snapd-2.55.5+20.04/osutil/disks/size_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/size_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,60 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2021 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 disks_test - -import ( - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/osutil/disks" - "github.com/snapcore/snapd/testutil" -) - -type sizeSuite struct{} - -var _ = Suite(&sizeSuite{}) - -func (ts *sizeSuite) TestSizeHappy(c *C) { - mockBlockdev := testutil.MockCommand(c, "blockdev", "echo 1024") - defer mockBlockdev.Restore() - - size, err := disks.Size("/dev/some-device") - c.Assert(err, IsNil) - c.Check(size, Equals, uint64(1024*512)) - - c.Check(mockBlockdev.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getsz", "/dev/some-device"}, - }) -} - -func (ts *sizeSuite) TestSizeErrFailure(c *C) { - mockBlockdev := testutil.MockCommand(c, "blockdev", "echo some-error-message; exit 1") - defer mockBlockdev.Restore() - - _, err := disks.Size("/dev/some-device") - c.Check(err, ErrorMatches, "cannot get disk size: some-error-message") -} - -func (ts *sizeSuite) TestSizeErrSizeParsing(c *C) { - mockBlockdev := testutil.MockCommand(c, "blockdev", "echo NaN") - defer mockBlockdev.Restore() - - _, err := disks.Size("/dev/some-device") - c.Check(err, ErrorMatches, `cannot parse disk size output: strconv.ParseUint: parsing "NaN": invalid syntax`) -} diff -Nru snapd-2.55.5+20.04/osutil/disks/testdata/generate.sh snapd-2.57.5+20.04/osutil/disks/testdata/generate.sh --- snapd-2.55.5+20.04/osutil/disks/testdata/generate.sh 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/disks/testdata/generate.sh 2022-10-17 16:25:18.000000000 +0000 @@ -2,9 +2,44 @@ set -eu -truncate --size 128M image -echo "label: gpt" | sfdisk image -dd if=image of=gpt_header bs=512 count=2 -# 128M - 1 block -dd if=image skip=$((128*1024*2-1)) of=gpt_footer bs=512 count=1 -rm image +gen() { + suffix="" + + if [ "${2}" = 4096 ]; then + suffix="${suffix}_4k" + fi + if [ "${1}" != normal ]; then + suffix="${suffix}_${1}" + fi + + case "${1}" in + big) + table_length=256 + ;; + small) + table_length=32 + ;; + normal) + table_length=128 + ;; + esac + + truncate --size 128M image + loop=$(losetup --sector-size "${2}" --show -f image) + sfdisk "${loop}" <. - * - */ - -package osutil - -// MaybeInjectFault is a dummy implementation for builds with fault injection -// disabled. -func MaybeInjectFault(tag string) { - // dummy -} diff -Nru snapd-2.55.5+20.04/osutil/faultinject_dummy_test.go snapd-2.57.5+20.04/osutil/faultinject_dummy_test.go --- snapd-2.55.5+20.04/osutil/faultinject_dummy_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/faultinject_dummy_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,54 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- -//go:build !faultinject -// +build !faultinject - -/* - * Copyright (C) 2021 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 ( - "os" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/testutil" -) - -type testhelperDummyFaultInjectionSuite struct { - testutil.BaseTest -} - -var _ = Suite(&testhelperDummyFaultInjectionSuite{}) - -func (s *testhelperDummyFaultInjectionSuite) SetUpTest(c *C) { - s.BaseTest.SetUpTest(c) - - oldSnappyTesting := os.Getenv("SNAPPY_TESTING") - s.AddCleanup(func() { os.Setenv("SNAPPY_TESTING", oldSnappyTesting) }) - s.AddCleanup(func() { os.Unsetenv("SNAPD_FAULT_INJECT") }) -} - -func (s *testhelperDummyFaultInjectionSuite) TestDummyFaultInject(c *C) { - os.Setenv("SNAPPY_TESTING", "1") - - os.Setenv("SNAPD_FAULT_INJECT", "tag:reboot,othertag:panic,funtag:reboot") - osutil.MaybeInjectFault("tag") - osutil.MaybeInjectFault("othertag") - osutil.MaybeInjectFault("funtag") -} diff -Nru snapd-2.55.5+20.04/osutil/faultinject_fake.go snapd-2.57.5+20.04/osutil/faultinject_fake.go --- snapd-2.55.5+20.04/osutil/faultinject_fake.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/faultinject_fake.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,28 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !faultinject +// +build !faultinject + +/* + * Copyright (C) 2021 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 + +// MaybeInjectFault is an empty implementation for builds with fault injection +// disabled. +func MaybeInjectFault(tag string) { + // intentionally empty +} diff -Nru snapd-2.55.5+20.04/osutil/faultinject_fake_test.go snapd-2.57.5+20.04/osutil/faultinject_fake_test.go --- snapd-2.55.5+20.04/osutil/faultinject_fake_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/faultinject_fake_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,54 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !faultinject +// +build !faultinject + +/* + * Copyright (C) 2021 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 ( + "os" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/testutil" +) + +type testhelperFakeFaultInjectionSuite struct { + testutil.BaseTest +} + +var _ = Suite(&testhelperFakeFaultInjectionSuite{}) + +func (s *testhelperFakeFaultInjectionSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + oldSnappyTesting := os.Getenv("SNAPPY_TESTING") + s.AddCleanup(func() { os.Setenv("SNAPPY_TESTING", oldSnappyTesting) }) + s.AddCleanup(func() { os.Unsetenv("SNAPD_FAULT_INJECT") }) +} + +func (s *testhelperFakeFaultInjectionSuite) TestFakeFaultInject(c *C) { + os.Setenv("SNAPPY_TESTING", "1") + + os.Setenv("SNAPD_FAULT_INJECT", "tag:reboot,othertag:panic,funtag:reboot") + osutil.MaybeInjectFault("tag") + osutil.MaybeInjectFault("othertag") + osutil.MaybeInjectFault("funtag") +} diff -Nru snapd-2.55.5+20.04/osutil/io.go snapd-2.57.5+20.04/osutil/io.go --- snapd-2.55.5+20.04/osutil/io.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/io.go 2022-10-17 16:25:18.000000000 +0000 @@ -27,6 +27,7 @@ "os" "path/filepath" "syscall" + "time" "github.com/snapcore/snapd/osutil/sys" "github.com/snapcore/snapd/randutil" @@ -57,6 +58,7 @@ tmpname string uid sys.UserID gid sys.GroupID + mtime time.Time closed bool renamed bool } @@ -145,6 +147,11 @@ const NoChown = sys.FlagID +// SetModTime sets the given modification time on the created file. +func (aw *AtomicFile) SetModTime(t time.Time) { + aw.mtime = t +} + func (aw *AtomicFile) commit() error { if aw.uid != NoChown || aw.gid != NoChown { if err := chown(aw.File, aw.uid, aw.gid); err != nil { @@ -171,6 +178,12 @@ return err } + if !aw.mtime.IsZero() { + if err := os.Chtimes(aw.tmpname, time.Now(), aw.mtime); err != nil { + return err + } + } + if err := os.Rename(aw.tmpname, aw.target); err != nil { return err } diff -Nru snapd-2.55.5+20.04/osutil/io_test.go snapd-2.57.5+20.04/osutil/io_test.go --- snapd-2.55.5+20.04/osutil/io_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/io_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -26,6 +26,7 @@ "os" "path/filepath" "strings" + "time" . "gopkg.in/check.v1" @@ -236,6 +237,21 @@ c.Check(osutil.FileExists(fn), Equals, false) } +func (ts *AtomicWriteTestSuite) TestAtomicFileModTime(c *C) { + d := c.MkDir() + p := filepath.Join(d, "foo") + + aw, err := osutil.NewAtomicFile(p, 0644, 0, osutil.NoChown, osutil.NoChown) + c.Assert(err, IsNil) + t := time.Date(2010, time.January, 1, 13, 0, 0, 0, time.UTC) + aw.SetModTime(t) + c.Assert(aw.Commit(), IsNil) + + finfo, err := os.Stat(p) + c.Assert(err, IsNil) + c.Assert(finfo.ModTime().Equal(t), Equals, true) +} + func (ts *AtomicWriteTestSuite) TestAtomicFileCommitAs(c *C) { d := c.MkDir() initialTarget := filepath.Join(d, "foo") diff -Nru snapd-2.55.5+20.04/osutil/kmod/export_test.go snapd-2.57.5+20.04/osutil/kmod/export_test.go --- snapd-2.55.5+20.04/osutil/kmod/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/kmod/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,34 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 ( + "github.com/snapcore/snapd/testutil" +) + +var ( + ModprobeCommand = modprobeCommand +) + +func MockModprobeCommand(f func(args ...string) error) (restore func()) { + r := testutil.Backup(&modprobeCommand) + modprobeCommand = f + return r +} diff -Nru snapd-2.55.5+20.04/osutil/kmod/kmod.go snapd-2.57.5+20.04/osutil/kmod/kmod.go --- snapd-2.55.5+20.04/osutil/kmod/kmod.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/kmod/kmod.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,48 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 ( + "fmt" + "os/exec" + + "github.com/snapcore/snapd/osutil" +) + +var modprobeCommand = func(args ...string) error { + allArgs := append([]string{"--syslog"}, args...) + err := exec.Command("modprobe", allArgs...).Run() + if err != nil { + exitCode, err := osutil.ExitCode(err) + if err != nil { + return err + } + return fmt.Errorf("modprobe failed with exit status %d (see syslog for details)", exitCode) + } + return nil +} + +func LoadModule(module string, options []string) error { + return modprobeCommand(append([]string{module}, options...)...) +} + +func UnloadModule(module string) error { + return modprobeCommand("-r", module) +} diff -Nru snapd-2.55.5+20.04/osutil/kmod/kmod_test.go snapd-2.57.5+20.04/osutil/kmod/kmod_test.go --- snapd-2.55.5+20.04/osutil/kmod/kmod_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/kmod/kmod_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,131 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 ( + "errors" + "os" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/osutil/kmod" + "github.com/snapcore/snapd/testutil" +) + +func TestRun(t *testing.T) { TestingT(t) } + +type kmodSuite struct { + testutil.BaseTest +} + +var _ = Suite(&kmodSuite{}) + +func (s *kmodSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) +} + +func (s *kmodSuite) TearDownTest(c *C) { + s.BaseTest.TearDownTest(c) +} + +func (s *kmodSuite) TestModprobeCommandNotFound(c *C) { + originalPath := os.Getenv("PATH") + defer func() { + os.Setenv("PATH", originalPath) + }() + + os.Unsetenv("PATH") + err := kmod.ModprobeCommand("name", "opt1=v1", "opt2=v2") + c.Check(err, ErrorMatches, `exec: "modprobe": executable file not found in \$PATH`) +} + +func (s *kmodSuite) TestModprobeCommandFailure(c *C) { + cmd := testutil.MockCommand(c, "modprobe", "exit 1") + defer cmd.Restore() + + err := kmod.ModprobeCommand("name", "opt1=v1", "opt2=v2") + c.Check(err, ErrorMatches, `modprobe failed with exit status 1 \(see syslog for details\)`) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"modprobe", "--syslog", "name", "opt1=v1", "opt2=v2"}, + }) +} + +func (s *kmodSuite) TestModprobeCommandHappy(c *C) { + cmd := testutil.MockCommand(c, "modprobe", "") + defer cmd.Restore() + + err := kmod.ModprobeCommand("name", "opt1=v1", "opt2=v2") + c.Check(err, IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"modprobe", "--syslog", "name", "opt1=v1", "opt2=v2"}, + }) +} + +func (s *kmodSuite) TestLoadModule(c *C) { + var returnValue error + var receivedArguments []string + restore := kmod.MockModprobeCommand(func(args ...string) error { + receivedArguments = args + return returnValue + }) + defer restore() + + for _, testData := range []struct { + moduleName string + moduleOptions []string + expectedArgs []string + expectedError error + }{ + {"mymodule", nil, []string{"mymodule"}, nil}, + {"mymodule", []string{"just one"}, []string{"mymodule", "just one"}, nil}, + {"mod1", []string{"opt1=v1", "opt2=v2"}, []string{"mod1", "opt1=v1", "opt2=v2"}, nil}, + {"mod2", []string{}, []string{"mod2"}, errors.New("some error")}, + } { + returnValue = testData.expectedError + err := kmod.LoadModule(testData.moduleName, testData.moduleOptions) + c.Check(err, Equals, testData.expectedError) + c.Check(receivedArguments, DeepEquals, testData.expectedArgs) + } +} + +func (s *kmodSuite) TestUnloadModule(c *C) { + var returnValue error + var receivedArguments []string + restore := kmod.MockModprobeCommand(func(args ...string) error { + receivedArguments = args + return returnValue + }) + defer restore() + + for _, testData := range []struct { + moduleName string + expectedArgs []string + expectedError error + }{ + {"mymodule", []string{"-r", "mymodule"}, nil}, + {"mod2", []string{"-r", "mod2"}, errors.New("some error")}, + } { + returnValue = testData.expectedError + err := kmod.UnloadModule(testData.moduleName) + c.Check(err, Equals, testData.expectedError) + c.Check(receivedArguments, DeepEquals, testData.expectedArgs) + } +} diff -Nru snapd-2.55.5+20.04/osutil/udev/.gitignore snapd-2.57.5+20.04/osutil/udev/.gitignore --- snapd-2.55.5+20.04/osutil/udev/.gitignore 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/osutil/udev/.gitignore 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +go-udev +*.log +*.swp diff -Nru snapd-2.55.5+20.04/overlord/assertstate/assertmgr.go snapd-2.57.5+20.04/overlord/assertstate/assertmgr.go --- snapd-2.55.5+20.04/overlord/assertstate/assertmgr.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/assertstate/assertmgr.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2017 Canonical Ltd + * Copyright (C) 2016-2022 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 @@ -102,9 +102,10 @@ } modelAs := deviceCtx.Model() + expectedProv := snapsup.ExpectedProvenance err = doFetch(st, snapsup.UserID, deviceCtx, func(f asserts.Fetcher) error { - if err := snapasserts.FetchSnapAssertions(f, sha3_384); err != nil { + if err := snapasserts.FetchSnapAssertions(f, sha3_384, expectedProv); err != nil { return err } @@ -134,14 +135,21 @@ } db := DB(st) - err = snapasserts.CrossCheck(snapsup.InstanceName(), sha3_384, snapSize, snapsup.SideInfo, db) + signedProv, err := snapasserts.CrossCheck(snapsup.InstanceName(), sha3_384, expectedProv, snapSize, snapsup.SideInfo, modelAs, db) if err != nil { - // TODO: trigger a global sanity check + // TODO: trigger a global validity check // that will generate the changes to deal with this // for things like snap-decl revocation and renames? return err } + // we have an authorized snap-revision with matching hash for + // the blob, double check that the snap metadata provenance + // matches + if err := snapasserts.CheckProvenance(snapsup.SnapPath, signedProv); err != nil { + return err + } + // TODO: set DeveloperID from assertions return nil } diff -Nru snapd-2.55.5+20.04/overlord/assertstate/assertstate.go snapd-2.57.5+20.04/overlord/assertstate/assertstate.go --- snapd-2.55.5+20.04/overlord/assertstate/assertstate.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/assertstate/assertstate.go 2022-10-17 16:25:18.000000000 +0000 @@ -23,6 +23,7 @@ package assertstate import ( + "errors" "fmt" "strings" @@ -289,6 +290,24 @@ return a.(*asserts.Account), nil } +// PublisherStoreAccount returns the store account information from the publisher assertion. +func PublisherStoreAccount(st *state.State, snapID string) (snap.StoreAccount, error) { + if snapID == "" { + return snap.StoreAccount{}, nil + } + + pubAcct, err := Publisher(st, snapID) + if err != nil { + return snap.StoreAccount{}, fmt.Errorf("cannot find publisher details: %v", err) + } + return snap.StoreAccount{ + ID: pubAcct.AccountID(), + Username: pubAcct.Username(), + DisplayName: pubAcct.DisplayName(), + Validation: pubAcct.Validation(), + }, nil +} + // Store returns the store assertion with the given name/id if it is // present in the system assertion database. func Store(s *state.State, store string) (*asserts.Store, error) { @@ -314,7 +333,13 @@ } explicitAliases := decl.Aliases() if len(explicitAliases) != 0 { - return explicitAliases, nil + aliasesForApps := make(map[string]string, len(explicitAliases)) + for alias, app := range explicitAliases { + if _, ok := info.Apps[app]; ok { + aliasesForApps[alias] = app + } + } + return aliasesForApps, nil } // XXX: old header fallback, just to keep edge working while we fix the // store, to remove before next release! @@ -688,11 +713,11 @@ // by RefreshValidationSetAssertions. var tr ValidationSetTracking trerr := GetValidationSet(st, accountID, name, &tr) - if trerr != nil && trerr != state.ErrNoState { + if trerr != nil && !errors.Is(trerr, state.ErrNoState) { return nil, 0, trerr } // not tracked, update the assertion - if trerr == state.ErrNoState { + if errors.Is(trerr, state.ErrNoState) { // update with pool atSeq.Sequence = vs.Sequence() atSeq.Revision = vs.Revision() @@ -747,13 +772,21 @@ return vs, latest, err } +// TryEnforceValidationSets tries to fetch the given validation sets and enforce them (together with currently tracked validation sets) against installed snaps, +// but doesn't update tracking information. It may return snapasserts.ValidationSetsValidationError which can be used to install/remove snaps as required +// to satisfy validation sets constraints. +func TryEnforceValidationSets(st *state.State, validationSets []string, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + // TODO + return fmt.Errorf("not implemented") +} + // EnforceValidationSet tries to fetch the given validation set and enforce it. // If all validation sets constrains are satisfied, the current validation sets // tracking state is saved in validation sets history. -func EnforceValidationSet(st *state.State, accountID, name string, sequence, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { +func EnforceValidationSet(st *state.State, accountID, name string, sequence, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (*ValidationSetTracking, error) { _, current, err := validationSetAssertionForEnforce(st, accountID, name, sequence, userID, snaps, ignoreValidation) if err != nil { - return err + return nil, err } tr := ValidationSetTracking{ @@ -766,20 +799,21 @@ } UpdateValidationSet(st, &tr) - return addCurrentTrackingToValidationSetsHistory(st) + err = addCurrentTrackingToValidationSetsHistory(st) + return &tr, err } // MonitorValidationSet tries to fetch the given validation set and monitor it. // The current validation sets tracking state is saved in validation sets history. -func MonitorValidationSet(st *state.State, accountID, name string, sequence int, userID int) error { +func MonitorValidationSet(st *state.State, accountID, name string, sequence int, userID int) (*ValidationSetTracking, error) { pinned := sequence > 0 opts := ResolveOptions{AllowLocalFallback: true} as, local, err := validationSetAssertionForMonitor(st, accountID, name, sequence, pinned, userID, &opts) if err != nil { - return fmt.Errorf("cannot get validation set assertion for %v: %v", ValidationSetKey(accountID, name), err) + return nil, fmt.Errorf("cannot get validation set assertion for %v: %v", ValidationSetKey(accountID, name), err) } - tr := ValidationSetTracking{ + tr := &ValidationSetTracking{ AccountID: accountID, Name: name, Mode: Monitor, @@ -789,8 +823,8 @@ LocalOnly: local, } - UpdateValidationSet(st, &tr) - return addCurrentTrackingToValidationSetsHistory(st) + UpdateValidationSet(st, tr) + return tr, addCurrentTrackingToValidationSetsHistory(st) } // TemporaryDB returns a temporary database stacked on top of the assertions diff -Nru snapd-2.55.5+20.04/overlord/assertstate/assertstate_test.go snapd-2.57.5+20.04/overlord/assertstate/assertstate_test.go --- snapd-2.55.5+20.04/overlord/assertstate/assertstate_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/assertstate/assertstate_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2021 Canonical Ltd + * Copyright (C) 2016-2022 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,8 +25,6 @@ "crypto" "errors" "fmt" - "io/ioutil" - "path/filepath" "sort" "strings" "testing" @@ -494,7 +492,16 @@ return string(d) } -func (s *assertMgrSuite) prereqSnapAssertions(c *C, revisions ...int) { +func (s *assertMgrSuite) makeTestSnap(c *C, r int, extra string) string { + yaml := `name: foo +version: %d +%s +` + yaml = fmt.Sprintf(yaml, r, extra) + return snaptest.MakeTestSnapWithFiles(c, yaml, nil) +} + +func (s *assertMgrSuite) prereqSnapAssertions(c *C, revisions ...int) (paths map[int]string, digests map[int]string) { headers := map[string]interface{}{ "series": "16", "snap-id": "snap-id-1", @@ -507,11 +514,20 @@ err = s.storeSigning.Add(snapDecl) c.Assert(err, IsNil) + paths = make(map[int]string) + digests = make(map[int]string) + for _, rev := range revisions { + snapPath := s.makeTestSnap(c, rev, "") + digest, sz, err := asserts.SnapFileSHA3_384(snapPath) + c.Assert(err, IsNil) + paths[rev] = snapPath + digests[rev] = digest + headers = map[string]interface{}{ "snap-id": "snap-id-1", - "snap-sha3-384": makeDigest(rev), - "snap-size": fmt.Sprintf("%d", len(fakeSnap(rev))), + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", sz), "snap-revision": fmt.Sprintf("%d", rev), "developer-id": s.dev1Acct.AccountID(), "timestamp": time.Now().Format(time.RFC3339), @@ -521,17 +537,19 @@ err = s.storeSigning.Add(snapRev) c.Assert(err, IsNil) } + + return paths, digests } func (s *assertMgrSuite) TestDoFetch(c *C) { - s.prereqSnapAssertions(c, 10) + _, digests := s.prereqSnapAssertions(c, 10) s.state.Lock() defer s.state.Unlock() ref := &asserts.Ref{ Type: asserts.SnapRevisionType, - PrimaryKey: []string{makeDigest(10)}, + PrimaryKey: []string{digests[10]}, } err := assertstate.DoFetch(s.state, 0, s.trivialDeviceCtx, func(f asserts.Fetcher) error { @@ -545,14 +563,14 @@ } func (s *assertMgrSuite) TestFetchIdempotent(c *C) { - s.prereqSnapAssertions(c, 10, 11) + _, digests := s.prereqSnapAssertions(c, 10, 11) s.state.Lock() defer s.state.Unlock() ref := &asserts.Ref{ Type: asserts.SnapRevisionType, - PrimaryKey: []string{makeDigest(10)}, + PrimaryKey: []string{digests[10]}, } fetching := func(f asserts.Fetcher) error { return f.Fetch(ref) @@ -563,7 +581,7 @@ ref = &asserts.Ref{ Type: asserts.SnapRevisionType, - PrimaryKey: []string{makeDigest(11)}, + PrimaryKey: []string{digests[11]}, } err = assertstate.DoFetch(s.state, 0, s.trivialDeviceCtx, fetching) @@ -701,19 +719,15 @@ } func (s *assertMgrSuite) TestValidateSnap(c *C) { - s.prereqSnapAssertions(c, 10) - - tempdir := c.MkDir() - snapPath := filepath.Join(tempdir, "foo.snap") - err := ioutil.WriteFile(snapPath, fakeSnap(10), 0644) - c.Assert(err, IsNil) + paths, digests := s.prereqSnapAssertions(c, 10) + snapPath := paths[10] s.state.Lock() defer s.state.Unlock() // have a model and the store assertion available storeAs := s.setupModelAndStore(c) - err = s.storeSigning.Add(storeAs) + err := s.storeSigning.Add(storeAs) c.Assert(err, IsNil) chg := s.state.NewChange("install", "...") @@ -739,7 +753,7 @@ snapRev, err := assertstate.DB(s.state).Find(asserts.SnapRevisionType, map[string]string{ "snap-id": "snap-id-1", - "snap-sha3-384": makeDigest(10), + "snap-sha3-384": digests[10], }) c.Assert(err, IsNil) c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10) @@ -752,12 +766,9 @@ } func (s *assertMgrSuite) TestValidateSnapStoreNotFound(c *C) { - s.prereqSnapAssertions(c, 10) + paths, digests := s.prereqSnapAssertions(c, 10) - tempdir := c.MkDir() - snapPath := filepath.Join(tempdir, "foo.snap") - err := ioutil.WriteFile(snapPath, fakeSnap(10), 0644) - c.Assert(err, IsNil) + snapPath := paths[10] s.state.Lock() defer s.state.Unlock() @@ -788,7 +799,7 @@ snapRev, err := assertstate.DB(s.state).Find(asserts.SnapRevisionType, map[string]string{ "snap-id": "snap-id-1", - "snap-sha3-384": makeDigest(10), + "snap-sha3-384": digests[10], }) c.Assert(err, IsNil) c.Check(snapRev.(*asserts.SnapRevision).SnapRevision(), Equals, 10) @@ -817,10 +828,7 @@ } func (s *assertMgrSuite) TestValidateSnapNotFound(c *C) { - tempdir := c.MkDir() - snapPath := filepath.Join(tempdir, "foo.snap") - err := ioutil.WriteFile(snapPath, fakeSnap(33), 0644) - c.Assert(err, IsNil) + snapPath := s.makeTestSnap(c, 33, "") s.state.Lock() defer s.state.Unlock() @@ -850,12 +858,9 @@ } func (s *assertMgrSuite) TestValidateSnapCrossCheckFail(c *C) { - s.prereqSnapAssertions(c, 10) + paths, _ := s.prereqSnapAssertions(c, 10) - tempdir := c.MkDir() - snapPath := filepath.Join(tempdir, "foo.snap") - err := ioutil.WriteFile(snapPath, fakeSnap(10), 0644) - c.Assert(err, IsNil) + snapPath := paths[10] s.state.Lock() defer s.state.Unlock() @@ -884,6 +889,197 @@ c.Assert(chg.Err(), ErrorMatches, `(?s).*cannot install "f", snap "f" is undergoing a rename to "foo".*`) } +func (s *assertMgrSuite) TestValidateDelegatedSnap(c *C) { + snapPath := s.makeTestSnap(c, 10, `provenance: delegated-prov`) + digest, sz, err := asserts.SnapFileSHA3_384(snapPath) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "revision-authority": []interface{}{ + map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{"delegated-prov"}, + "on-store": []interface{}{"my-brand-store"}, + "on-model": []interface{}{"my-brand/my-model"}, + }, + }, + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapDecl) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "series": "16", + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "provenance": "delegated-prov", + "snap-size": fmt.Sprintf("%d", sz), + "snap-revision": "10", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + snapRev, err := s.dev1Signing.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapRev) + c.Assert(err, IsNil) + + s.state.Lock() + defer s.state.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + err = s.storeSigning.Add(storeAs) + c.Assert(err, IsNil) + + chg := s.state.NewChange("install", "...") + t := s.state.NewTask("validate-snap", "Fetch and check snap assertions") + snapsup := snapstate.SnapSetup{ + SnapPath: snapPath, + UserID: 0, + SideInfo: &snap.SideInfo{ + RealName: "foo", + SnapID: "snap-id-1", + Revision: snap.R(10), + }, + ExpectedProvenance: "delegated-prov", + } + t.Set("snap-setup", snapsup) + chg.AddTask(t) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + + snapRev1, err := assertstate.DB(s.state).Find(asserts.SnapRevisionType, map[string]string{ + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "provenance": "delegated-prov", + }) + c.Assert(err, IsNil) + c.Check(snapRev1.(*asserts.SnapRevision).SnapRevision(), Equals, 10) + + // store assertion was also fetched + _, err = assertstate.DB(s.state).Find(asserts.StoreType, map[string]string{ + "store": "my-brand-store", + }) + c.Assert(err, IsNil) +} + +func (s *assertMgrSuite) TestValidateDelegatedSnapProvenanceMismatch(c *C) { + err := s.testValidateDelegatedSnapMismatch(c, `provenance: delegated-prov-other`, "delegated-prov-other", "delegated-prov", map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{"delegated-prov"}, + }) + c.Check(err, ErrorMatches, `(?s).*cannot verify snap "foo", no matching signatures found.*`) +} + +func (s *assertMgrSuite) TestValidateDelegatedSnapStoreProvenanceMismatch(c *C) { + // this is a scenario where a store is serving information matching + // the assertions which themselves don't match the snap + err := s.testValidateDelegatedSnapMismatch(c, `provenance: delegated-prov-other`, "delegated-prov", "delegated-prov", map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{"delegated-prov"}, + }) + c.Check(err, ErrorMatches, `(?s).*snap ".*foo.*\.snap" has been signed under provenance "delegated-prov" different from the metadata one: "delegated-prov-other".*`) +} + +func (s *assertMgrSuite) testValidateDelegatedSnapMismatch(c *C, provenanceFrag, expectedProv, revProvenance string, revisionAuthority map[string]interface{}) error { + snapPath := s.makeTestSnap(c, 10, provenanceFrag) + digest, sz, err := asserts.SnapFileSHA3_384(snapPath) + c.Assert(err, IsNil) + + headers := map[string]interface{}{ + "series": "16", + "snap-id": "snap-id-1", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "revision-authority": []interface{}{ + revisionAuthority, + }, + "timestamp": time.Now().Format(time.RFC3339), + } + snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapDecl) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": s.dev1Acct.AccountID(), + "series": "16", + "snap-id": "snap-id-1", + "snap-sha3-384": digest, + "snap-size": fmt.Sprintf("%d", sz), + "snap-revision": "10", + "developer-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + } + if revProvenance != "" { + headers["provenance"] = revProvenance + } + snapRev, err := s.dev1Signing.Sign(asserts.SnapRevisionType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapRev) + c.Assert(err, IsNil) + + s.state.Lock() + defer s.state.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + err = s.storeSigning.Add(storeAs) + c.Assert(err, IsNil) + + chg := s.state.NewChange("install", "...") + t := s.state.NewTask("validate-snap", "Fetch and check snap assertions") + snapsup := snapstate.SnapSetup{ + SnapPath: snapPath, + UserID: 0, + SideInfo: &snap.SideInfo{ + RealName: "foo", + SnapID: "snap-id-1", + Revision: snap.R(10), + }, + ExpectedProvenance: expectedProv, + } + t.Set("snap-setup", snapsup) + chg.AddTask(t) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + return chg.Err() +} + +func (s *assertMgrSuite) TestValidateDelegatedSnapDeviceMismatch(c *C) { + err := s.testValidateDelegatedSnapMismatch(c, `provenance: delegated-prov`, "delegated-prov", "delegated-prov", map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{"delegated-prov"}, + "on-store": []interface{}{"other-store"}, + }) + c.Check(err, ErrorMatches, `(?s).*snap "foo" revision assertion with provenance "delegated-prov" is not signed by an authority authorized on this device: .*`) +} + +func (s *assertMgrSuite) TestValidateDelegatedSnapDefaultProvenanceMismatch(c *C) { + err := s.testValidateDelegatedSnapMismatch(c, "", "", "delegated-prov", map[string]interface{}{ + "account-id": s.dev1Acct.AccountID(), + "provenance": []interface{}{"delegated-prov"}, + "on-store": []interface{}{"my-brand-store"}, + }) + c.Check(err, ErrorMatches, `(?s).*cannot verify snap "foo", no matching signatures found.*`) +} + func (s *assertMgrSuite) validationSetAssert(c *C, name, sequence, revision string, snapPresence, requiredRevision string) *asserts.ValidationSet { snaps := []interface{}{map[string]interface{}{ "id": "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", @@ -2164,6 +2360,10 @@ "name": "alias2", "target": "cmd2", }, + map[string]interface{}{ + "name": "alias-missing", + "target": "cmd-missing", + }, }, "revision": "1", }) @@ -2174,6 +2374,11 @@ RealName: "foo", SnapID: "foo-id", }, + Apps: map[string]*snap.AppInfo{ + "cmd1": {}, + "cmd2": {}, + // no cmd-missing + }, }) c.Assert(err, IsNil) c.Check(aliases, DeepEquals, map[string]string{ @@ -2204,6 +2409,30 @@ c.Check(acct.Username(), Equals, "developer1") } +func (s *assertMgrSuite) TestPublisherStoreAccount(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // have a declaration in the system db + err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) + err = assertstate.Add(s.state, s.dev1Acct) + c.Assert(err, IsNil) + snapDeclFoo := s.snapDecl(c, "foo", nil) + err = assertstate.Add(s.state, snapDeclFoo) + c.Assert(err, IsNil) + + _, err = assertstate.SnapDeclaration(s.state, "snap-id-other") + c.Check(asserts.IsNotFound(err), Equals, true) + + acct, err := assertstate.PublisherStoreAccount(s.state, "foo-id") + c.Assert(err, IsNil) + c.Check(acct.ID, Equals, s.dev1Acct.AccountID()) + c.Check(acct.Username, Equals, "developer1") + c.Check(acct.DisplayName, Equals, "Developer1") + c.Check(acct.Validation, Equals, s.dev1Acct.Validation()) +} + func (s *assertMgrSuite) TestStore(c *C) { s.state.Lock() defer s.state.Unlock() @@ -2380,7 +2609,7 @@ err = s.storeSigning.Add(vsetAs3) c.Assert(err, IsNil) - // sanity check - sequence 4 not available locally yet + // precondition check - sequence 4 not available locally yet _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ "series": "16", "account-id": s.dev1Acct.AccountID(), @@ -2536,7 +2765,7 @@ err = assertstate.RefreshValidationSetAssertions(s.state, 0, nil) c.Assert(err, IsNil) - // sanity - local assertion vsetAs1 is the latest + // precondition - local assertion vsetAs1 is the latest a, err := assertstate.DB(s.state).FindSequence(asserts.ValidationSetType, map[string]string{ "series": "16", "account-id": s.dev1Acct.AccountID(), @@ -2816,7 +3045,7 @@ assertstate.UpdateValidationSet(s.state, &tr) c.Assert(assertstate.RefreshValidationSetAssertions(s.state, 0, nil), IsNil) - c.Assert(logbuf.String(), Matches, `.*cannot refresh to validation set assertions that do not satisfy installed snaps: validation sets assertions are not met:\n- missing required snaps:\n - foo \(required by sets .*/foo\)\n`) + c.Assert(logbuf.String(), Matches, `.*cannot refresh to validation set assertions that do not satisfy installed snaps: validation sets assertions are not met:\n- missing required snaps:\n - foo \(required at any revision by sets .*/foo\)\n`) a, err := assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ "series": "16", @@ -3100,8 +3329,10 @@ c.Assert(err, NotNil) verr, ok := err.(*snapasserts.ValidationSetsValidationError) c.Assert(ok, Equals, true) - c.Check(verr.MissingSnaps, DeepEquals, map[string][]string{ - "foo": {fmt.Sprintf("%s/bar", s.dev1Acct.AccountID())}, + c.Check(verr.MissingSnaps, DeepEquals, map[string]map[snap.Revision][]string{ + "foo": { + snap.R(1): []string{fmt.Sprintf("%s/bar", s.dev1Acct.AccountID())}, + }, }) // and it hasn't been committed @@ -3357,7 +3588,7 @@ } sequence := 2 - err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) + tracking, err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) // and it has been committed @@ -3372,6 +3603,7 @@ var tr assertstate.ValidationSetTracking c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "bar", &tr), IsNil) + c.Check(tr, DeepEquals, *tracking) c.Check(tr, DeepEquals, assertstate.ValidationSetTracking{ AccountID: s.dev1Acct.AccountID(), @@ -3417,7 +3649,7 @@ } sequence := 2 - err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) + tracking, err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) // and it has been committed @@ -3439,6 +3671,7 @@ PinnedAt: 2, Current: 2, }) + c.Check(tr, DeepEquals, *tracking) // and it was added to the history vshist, err := assertstate.ValidationSetsHistory(st) @@ -3455,7 +3688,7 @@ // not pinned sequence = 0 - err = assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) + tracking, err = assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "bar", &tr), IsNil) @@ -3466,6 +3699,7 @@ PinnedAt: 0, Current: 2, }) + c.Check(tr, DeepEquals, *tracking) } func (s *assertMgrSuite) TestEnforceValidationSetAssertionPinToOlderSequence(c *C) { @@ -3492,7 +3726,7 @@ } sequence := 2 - err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) + tracking, err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) // and it has been committed @@ -3514,10 +3748,11 @@ PinnedAt: 2, Current: 2, }) + c.Check(tr, DeepEquals, *tracking) // pin to older sequence = 1 - err = assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) + tracking, err = assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "bar", &tr), IsNil) @@ -3529,6 +3764,7 @@ // and current points at the latest sequence available Current: 2, }) + c.Check(tr, DeepEquals, *tracking) } func (s *assertMgrSuite) TestEnforceValidationSetAssertionAfterMonitor(c *C) { @@ -3565,7 +3801,7 @@ c.Assert(s.storeSigning.Add(vsetAs), IsNil) sequence := 2 - err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) + tracking, err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) // and it has been committed @@ -3588,6 +3824,7 @@ PinnedAt: 2, Current: 2, }) + c.Check(tr, DeepEquals, *tracking) } func (s *assertMgrSuite) TestEnforceValidationSetAssertionIgnoreValidation(c *C) { @@ -3613,13 +3850,13 @@ sequence := 2 ignoreValidation := map[string]bool{} - err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, ignoreValidation) + _, err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, ignoreValidation) wrongRevErr, ok := err.(*snapasserts.ValidationSetsValidationError) c.Assert(ok, Equals, true) c.Check(wrongRevErr.WrongRevisionSnaps["foo"], NotNil) ignoreValidation["foo"] = true - err = assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, ignoreValidation) + tracking, err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, ignoreValidation) c.Assert(err, IsNil) // and it has been committed @@ -3642,6 +3879,7 @@ PinnedAt: 2, Current: 2, }) + c.Check(tr, DeepEquals, *tracking) } func (s *assertMgrSuite) TestMonitorValidationSet(c *C) { @@ -3662,8 +3900,15 @@ c.Assert(s.storeSigning.Add(vsetAs), IsNil) sequence := 2 - err := assertstate.MonitorValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0) + tr1, err := assertstate.MonitorValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0) c.Assert(err, IsNil) + c.Check(tr1, DeepEquals, &assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 2, + }) // and it has been committed _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ @@ -3720,8 +3965,25 @@ vsetAs2 := s.validationSetAssert(c, "baz", "2", "2", "required", "1") c.Assert(s.storeSigning.Add(vsetAs2), IsNil) - c.Assert(assertstate.MonitorValidationSet(st, s.dev1Acct.AccountID(), "bar", 2, 0), IsNil) - c.Assert(assertstate.MonitorValidationSet(st, s.dev1Acct.AccountID(), "baz", 2, 0), IsNil) + tr1, err := assertstate.MonitorValidationSet(st, s.dev1Acct.AccountID(), "bar", 2, 0) + c.Assert(err, IsNil) + c.Check(tr1, DeepEquals, &assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 2, + }) + + tr2, err := assertstate.MonitorValidationSet(st, s.dev1Acct.AccountID(), "baz", 2, 0) + c.Assert(err, IsNil) + c.Check(tr2, DeepEquals, &assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "baz", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 2, + }) c.Assert(assertstate.ForgetValidationSet(st, s.dev1Acct.AccountID(), "bar"), IsNil) diff -Nru snapd-2.55.5+20.04/overlord/assertstate/helpers.go snapd-2.57.5+20.04/overlord/assertstate/helpers.go --- snapd-2.55.5+20.04/overlord/assertstate/helpers.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/assertstate/helpers.go 2022-10-17 16:25:18.000000000 +0000 @@ -75,7 +75,7 @@ return err } - // TODO: trigger w. caller a global sanity check if a is revoked + // TODO: trigger w. caller a global validity check if a is revoked // (but try to save as much possible still), // or err is a check error return b.CommitTo(db, nil) diff -Nru snapd-2.55.5+20.04/overlord/assertstate/validation_set_tracking.go snapd-2.57.5+20.04/overlord/assertstate/validation_set_tracking.go --- snapd-2.55.5+20.04/overlord/assertstate/validation_set_tracking.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/assertstate/validation_set_tracking.go 2022-10-17 16:25:18.000000000 +0000 @@ -21,6 +21,7 @@ import ( "encoding/json" + "errors" "fmt" "github.com/snapcore/snapd/asserts" @@ -76,7 +77,7 @@ func UpdateValidationSet(st *state.State, tr *ValidationSetTracking) { var vsmap map[string]*json.RawMessage err := st.Get("validation-sets", &vsmap) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { panic("internal error: cannot unmarshal validation set tracking state: " + err.Error()) } if vsmap == nil { @@ -97,7 +98,7 @@ func ForgetValidationSet(st *state.State, accountID, name string) error { var vsmap map[string]*json.RawMessage err := st.Get("validation-sets", &vsmap) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { panic("internal error: cannot unmarshal validation set tracking state: " + err.Error()) } if len(vsmap) == 0 { @@ -139,17 +140,17 @@ // ValidationSets retrieves all ValidationSetTracking data. func ValidationSets(st *state.State) (map[string]*ValidationSetTracking, error) { var vsmap map[string]*ValidationSetTracking - if err := st.Get("validation-sets", &vsmap); err != nil && err != state.ErrNoState { + if err := st.Get("validation-sets", &vsmap); err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } return vsmap, nil } // EnforcedValidationSets returns ValidationSets object with all currently tracked -// validation sets that are in enforcing mode. If extraVs is not nil then it is -// added to the returned set and replaces a validation set with same account/name -// in case one was tracked already. -func EnforcedValidationSets(st *state.State, extraVs *asserts.ValidationSet) (*snapasserts.ValidationSets, error) { +// validation sets that are in enforcing mode. If extraVss is not nil then they are +// added to the returned set and replaces validation sets with same account/name +// in case they were tracked already. +func EnforcedValidationSets(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) { valsets, err := ValidationSets(st) if err != nil { return nil, err @@ -158,8 +159,10 @@ db := DB(st) sets := snapasserts.NewValidationSets() - if extraVs != nil { + skip := make(map[string]bool, len(extraVss)) + for _, extraVs := range extraVss { sets.Add(extraVs) + skip[fmt.Sprintf("%s:%s", extraVs.AccountID(), extraVs.Name())] = true } for _, vs := range valsets { @@ -169,7 +172,7 @@ // if extraVs matches an already enforced validation set, then skip that one, extraVs has been added // before the loop. - if extraVs != nil && extraVs.AccountID() == vs.AccountID && extraVs.Name() == vs.Name { + if skip[fmt.Sprintf("%s:%s", vs.AccountID, vs.Name)] { continue } @@ -252,7 +255,7 @@ // the validations sets tracking history. func validationSetsHistoryTop(st *state.State) (map[string]*ValidationSetTracking, error) { var vshist []*json.RawMessage - if err := st.Get("validation-sets-history", &vshist); err != nil && err != state.ErrNoState { + if err := st.Get("validation-sets-history", &vshist); err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } if len(vshist) == 0 { @@ -270,7 +273,7 @@ // ValidationSetsHistory returns the complete history of validation sets tracking. func ValidationSetsHistory(st *state.State) ([]map[string]*ValidationSetTracking, error) { var vshist []map[string]*ValidationSetTracking - if err := st.Get("validation-sets-history", &vshist); err != nil && err != state.ErrNoState { + if err := st.Get("validation-sets-history", &vshist); err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } return vshist, nil diff -Nru snapd-2.55.5+20.04/overlord/assertstate/validation_set_tracking_test.go snapd-2.57.5+20.04/overlord/assertstate/validation_set_tracking_test.go --- snapd-2.55.5+20.04/overlord/assertstate/validation_set_tracking_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/assertstate/validation_set_tracking_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -27,6 +27,7 @@ "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/testutil" ) type validationSetTrackingSuite struct { @@ -184,7 +185,7 @@ // non-existing err = assertstate.GetValidationSet(s.st, "foo", "baz", &res) - c.Assert(err, Equals, state.ErrNoState) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) } func (s *validationSetTrackingSuite) mockAssert(c *C, name, sequence, presence string) asserts.Assertion { @@ -246,7 +247,7 @@ vs3 := s.mockAssert(c, "baz", "5", "invalid") c.Assert(assertstate.Add(s.st, vs3), IsNil) - valsets, err := assertstate.EnforcedValidationSets(s.st, nil) + valsets, err := assertstate.EnforcedValidationSets(s.st) c.Assert(err, IsNil) // foo and bar are in conflict, use this as an indirect way of checking @@ -256,6 +257,80 @@ c.Check(err, ErrorMatches, `validation sets are in conflict:\n- cannot constrain snap "snap-b" as both invalid \(.*/bar\) and required at any revision \(.*/foo\)`) } +func (s *validationSetTrackingSuite) TestEnforcedValidationSetsWithExtraSets(c *C) { + s.st.Lock() + defer s.st.Unlock() + + tr := assertstate.ValidationSetTracking{ + AccountID: s.dev1acct.AccountID(), + Name: "foo", + Mode: assertstate.Enforce, + Current: 2, + } + assertstate.UpdateValidationSet(s.st, &tr) + + tr = assertstate.ValidationSetTracking{ + AccountID: s.dev1acct.AccountID(), + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 1, + Current: 3, + } + assertstate.UpdateValidationSet(s.st, &tr) + + vs1 := s.mockAssert(c, "foo", "2", "optional") + c.Assert(assertstate.Add(s.st, vs1), IsNil) + + vs2 := s.mockAssert(c, "bar", "1", "required") + c.Assert(assertstate.Add(s.st, vs2), IsNil) + + valsets, err := assertstate.EnforcedValidationSets(s.st) + c.Assert(err, IsNil) + + err = valsets.Conflict() + c.Assert(err, IsNil) + + // use extra validation sets that trigger conflicts to verify they are + // considered by EnforcedValidationSets. + + // extra validation set "foo" replaces vs from the state + extra1 := s.mockAssert(c, "foo", "9", "required") + valsets, err = assertstate.EnforcedValidationSets(s.st, extra1.(*asserts.ValidationSet)) + c.Assert(err, IsNil) + + err = valsets.Conflict() + c.Assert(err, IsNil) + + // extra validations set "baz" is not tracked, it augments computed validation sets (and creates a conflict) + extra2 := s.mockAssert(c, "baz", "9", "invalid") + valsets, err = assertstate.EnforcedValidationSets(s.st, extra1.(*asserts.ValidationSet), extra2.(*asserts.ValidationSet)) + c.Assert(err, IsNil) + err = valsets.Conflict() + c.Check(err, ErrorMatches, `validation sets are in conflict:\n- cannot constrain snap "snap-b" as both invalid \(.*/baz\) and required at any revision \(.*/foo\)`) + + // extra validations set "baz" is not tracked, it augments computed validation sets (no conflict this time) + extra2 = s.mockAssert(c, "baz", "9", "optional") + valsets, err = assertstate.EnforcedValidationSets(s.st, extra1.(*asserts.ValidationSet), extra2.(*asserts.ValidationSet)) + c.Assert(err, IsNil) + err = valsets.Conflict() + c.Assert(err, IsNil) + + // extra validations set replace both foo and bar vs from the state + extra1 = s.mockAssert(c, "foo", "9", "required") + extra2 = s.mockAssert(c, "bar", "9", "invalid") + valsets, err = assertstate.EnforcedValidationSets(s.st, extra1.(*asserts.ValidationSet), extra2.(*asserts.ValidationSet)) + c.Assert(err, IsNil) + err = valsets.Conflict() + c.Check(err, ErrorMatches, `validation sets are in conflict:\n- cannot constrain snap "snap-b" as both invalid \(.*/bar\) and required at any revision \(.*/foo\)`) + + // no conflict once both are invalid + extra1 = s.mockAssert(c, "foo", "9", "invalid") + valsets, err = assertstate.EnforcedValidationSets(s.st, extra1.(*asserts.ValidationSet), extra2.(*asserts.ValidationSet)) + c.Assert(err, IsNil) + err = valsets.Conflict() + c.Check(err, IsNil) +} + func (s *validationSetTrackingSuite) TestAddToValidationSetsHistory(c *C) { s.st.Lock() defer s.st.Unlock() @@ -414,7 +489,7 @@ s.st.Lock() defer s.st.Unlock() - c.Assert(assertstate.RestoreValidationSetsTracking(s.st), Equals, state.ErrNoState) + c.Assert(assertstate.RestoreValidationSetsTracking(s.st), testutil.ErrorIs, state.ErrNoState) } func (s *validationSetTrackingSuite) TestRestoreValidationSetsTracking(c *C) { diff -Nru snapd-2.55.5+20.04/overlord/auth/auth.go snapd-2.57.5+20.04/overlord/auth/auth.go --- snapd-2.55.5+20.04/overlord/auth/auth.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/auth/auth.go 2022-10-17 16:25:18.000000000 +0000 @@ -139,7 +139,7 @@ var authStateData AuthState err := st.Get("auth", &authStateData) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { authStateData = AuthState{} } else if err != nil { return nil, err @@ -193,7 +193,7 @@ var authStateData AuthState err := st.Get("auth", &authStateData) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil, ErrInvalidUser } if err != nil { @@ -221,7 +221,7 @@ var authStateData AuthState err := st.Get("auth", &authStateData) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil, nil } if err != nil { @@ -250,7 +250,7 @@ var authStateData AuthState err := st.Get("auth", &authStateData) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil, ErrInvalidUser } if err != nil { @@ -271,7 +271,7 @@ var authStateData AuthState err := st.Get("auth", &authStateData) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return ErrInvalidUser } if err != nil { diff -Nru snapd-2.55.5+20.04/overlord/cmdstate/cmdmgr.go snapd-2.57.5+20.04/overlord/cmdstate/cmdmgr.go --- snapd-2.55.5+20.04/overlord/cmdstate/cmdmgr.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/cmdstate/cmdmgr.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package cmdstate import ( + "errors" "strings" "time" @@ -51,7 +52,7 @@ defer st.Unlock() var ignore bool - if err := t.Get("ignore", &ignore); err != nil && err != state.ErrNoState { + if err := t.Get("ignore", &ignore); err != nil && !errors.Is(err, state.ErrNoState) { return err } if ignore { @@ -67,10 +68,10 @@ err := t.Get("timeout", &tout) // timeout is optional and might not be set - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { tout = defaultExecTimeout } diff -Nru snapd-2.55.5+20.04/overlord/configstate/config/helpers.go snapd-2.57.5+20.04/overlord/configstate/config/helpers.go --- snapd-2.55.5+20.04/overlord/configstate/config/helpers.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/config/helpers.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,6 +22,7 @@ import ( "bytes" "encoding/json" + "errors" "fmt" "regexp" "sort" @@ -141,7 +142,7 @@ func GetSnapConfig(st *state.State, snapName string) (*json.RawMessage, error) { var config map[string]*json.RawMessage err := st.Get("config", &config) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil, nil } if err != nil { @@ -160,7 +161,7 @@ err := st.Get("config", &config) // empty nil snapcfg should be an empty message, but deal with "null" as well. isNil := snapcfg == nil || len(*snapcfg) == 0 || bytes.Compare(*snapcfg, []byte("null")) == 0 - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { if isNil { // bail out early return nil @@ -187,7 +188,7 @@ // Get current configuration of the snap from state err := st.Get("config", &config) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil } else if err != nil { return fmt.Errorf("internal error: cannot unmarshal configuration: %v", err) @@ -198,7 +199,7 @@ } err = st.Get("revision-config", &revisionConfig) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { revisionConfig = make(map[string]map[string]*json.RawMessage) } else if err != nil { return err @@ -221,14 +222,14 @@ var revisionConfig map[string]map[string]*json.RawMessage // snap => revision => configuration err := st.Get("revision-config", &revisionConfig) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil } else if err != nil { return fmt.Errorf("internal error: cannot unmarshal revision-config: %v", err) } err = st.Get("config", &config) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { config = make(map[string]*json.RawMessage) } else if err != nil { return fmt.Errorf("internal error: cannot unmarshal configuration: %v", err) @@ -250,7 +251,7 @@ func DiscardRevisionConfig(st *state.State, snapName string, rev snap.Revision) error { var revisionConfig map[string]map[string]*json.RawMessage // snap => revision => configuration err := st.Get("revision-config", &revisionConfig) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil } else if err != nil { return fmt.Errorf("internal error: cannot unmarshal revision-config: %v", err) @@ -273,7 +274,7 @@ var config map[string]map[string]*json.RawMessage // snap => key => value err := st.Get("config", &config) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil } else if err != nil { return fmt.Errorf("internal error: cannot unmarshal configuration: %v", err) diff -Nru snapd-2.55.5+20.04/overlord/configstate/config/helpers_test.go snapd-2.57.5+20.04/overlord/configstate/config/helpers_test.go --- snapd-2.55.5+20.04/overlord/configstate/config/helpers_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/config/helpers_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -149,7 +149,7 @@ buf, err := json.Marshal(nil) c.Assert(err, IsNil) empty2 := (*json.RawMessage)(&buf) - // sanity check + // validity check c.Check(bytes.Compare(*empty2, []byte(`null`)), Equals, 0) for _, emptyCfg := range []*json.RawMessage{nil, &empty1, empty2, {}} { diff -Nru snapd-2.55.5+20.04/overlord/configstate/config/transaction.go snapd-2.57.5+20.04/overlord/configstate/config/transaction.go --- snapd-2.55.5+20.04/overlord/configstate/config/transaction.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/config/transaction.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,6 +22,7 @@ import ( "bytes" "encoding/json" + "errors" "fmt" "reflect" "sort" @@ -55,7 +56,7 @@ // 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 { + if errors.Is(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)) @@ -318,7 +319,7 @@ // 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 { + if errors.Is(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)) diff -Nru snapd-2.55.5+20.04/overlord/configstate/configcore/cloud.go snapd-2.57.5+20.04/overlord/configstate/configcore/cloud.go --- snapd-2.55.5+20.04/overlord/configstate/configcore/cloud.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/configcore/cloud.go 2022-10-17 16:25:18.000000000 +0000 @@ -23,6 +23,7 @@ import ( "encoding/json" + "errors" "io/ioutil" "os" @@ -39,7 +40,7 @@ defer st.Unlock() var seeded bool err := tr.State().Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return false, err } return seeded, nil diff -Nru snapd-2.55.5+20.04/overlord/configstate/configcore/ctrlaltdel.go snapd-2.57.5+20.04/overlord/configstate/configcore/ctrlaltdel.go --- snapd-2.55.5+20.04/overlord/configstate/configcore/ctrlaltdel.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/configcore/ctrlaltdel.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,134 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 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 configcore + +import ( + "fmt" + + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/sysconfig" + "github.com/snapcore/snapd/systemd" +) + +// Systemd unit name to control ctrl-alt-del behaviour +const ( + ctrlAltDelTarget = "ctrl-alt-del.target" +) + +// Supported actions for core.system.ctrl-alt-del-action +const ( + ctrlAltDelReboot = "reboot" + ctrlAltDelNone = "none" +) + +func init() { + supportedConfigurations["core.system.ctrl-alt-del-action"] = true +} + +type sysdCtrlAltDelLogger struct{} + +func (l *sysdCtrlAltDelLogger) Notify(status string) { + fmt.Fprintf(Stderr, "ctrl-alt-del: %s\n", status) +} + +// switchCtrlAltDelAction configures the systemd handling of the special +// ctrl-alt-del keyboard sequence. This function supports configuring +// systemd to trigger a reboot, or ignore the key sequence. +func switchCtrlAltDelAction(action string, opts *fsOnlyContext) error { + if action != "reboot" && action != "none" { + return fmt.Errorf("invalid action %q supplied for system.ctrl-alt-del-action option", action) + } + + // The opts argument tells us if the real rootfs is mounted and + // systemd is running normally. + // opts != nil: The direct rootfs path is supplied because the system is not + // ready so we cannot use systemctl to modify unit files + // without supplying the root path. + // opts == nil: No rootfs path is supplied because we are running with + // rootfs mounted, so we can use systemctl normally. This + // case is used for normal runtime changes. + var sysd systemd.Systemd + if opts != nil { + // Use systemctl for direct unit file manipulations (support a + // subset of unit operations such as enable/disable/mark/unmask) + // See: "systemctl --root=" + sysd = systemd.NewEmulationMode(opts.RootDir) + } else { + // Use systemctl with access to all the unit operations + sysd = systemd.New(systemd.SystemMode, &sysdCtrlAltDelLogger{}) + + // Make sure the ctrl-alt-del.target unit is in the expected state. + // (1) The required unit should be present (file exist under /{run,etc,lib}/systemd/system). + // (2) The Enable state for a unit typically means automatic startup on boot. The + // expected state for reboot.target (ctrl-alt-del.target) is 'disabled'. + // The ctrl-alt-del.target unit is an alias for reboot.target. This means that + // if reboot.target is enabled, a ctrl-alt-del.target symlink will be created + // under /etc/systemd/system, which is not needed and will prevent masking + status, err := sysd.Status([]string{ctrlAltDelTarget}) + if err != nil { + return err + } + switch { + case len(status) != 1: + return fmt.Errorf("internal error: expected status of target %s, got %v", ctrlAltDelTarget, status) + case !status[0].Installed: + return fmt.Errorf("internal error: target %s not installed", ctrlAltDelTarget) + case status[0].Enabled: + return fmt.Errorf("internal error: target %s should not be enabled", ctrlAltDelTarget) + } + } + + // It is safe to mask an already masked unit, and unmask an already unmasked unit. The code + // will not try to optimize this because this will require knowledge of the initial + // "unset" state, which complicates the problem unnecessarily, for not much benefit. + switch action { + case ctrlAltDelNone: + // the unit is masked and cannot be started + if err := sysd.Mask(ctrlAltDelTarget); err != nil { + return err + } + case ctrlAltDelReboot: + // the unit is no longer masked and thus can be started on demand causing a reboot + if err := sysd.Unmask(ctrlAltDelTarget); err != nil { + return err + } + default: + // We already checked the action against the list of defined actions. This is an + // internal double check to see if any of the actions are unhandled with in this switch. + return fmt.Errorf("internal error: action %s unhandled on this platform", action) + } + return nil +} + +func handleCtrlAltDelConfiguration(_ sysconfig.Device, tr config.ConfGetter, opts *fsOnlyContext) error { + output, err := coreCfg(tr, "system.ctrl-alt-del-action") + if err != nil { + return err + } + // The coreCfg() function returns an empty string ("") if + // the config key was unset (not found). We only react on + // explicitly set actions. + if output != "" { + if err := switchCtrlAltDelAction(output, opts); err != nil { + return err + } + } + return nil +} diff -Nru snapd-2.55.5+20.04/overlord/configstate/configcore/ctrlaltdel_test.go snapd-2.57.5+20.04/overlord/configstate/configcore/ctrlaltdel_test.go --- snapd-2.55.5+20.04/overlord/configstate/configcore/ctrlaltdel_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/configcore/ctrlaltdel_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,199 @@ +// -*- 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 configcore_test + +import ( + "fmt" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/overlord/configstate/configcore" + "github.com/snapcore/snapd/snap" +) + +type unitStates int + +// The possible unit states we should test for to make sure +// appropriate error messages are displayed +const ( + unitStateNone unitStates = iota + unitStateMulti + unitStateUninstalled + unitStateDisabled + unitStateEnabled + // Ubuntu Core <= 18 has an earlier version of systemd and the + // UnitFileState for a masked unit is returned as 'bad'. + // LoadState (unused by us) returns 'masked'. + unitStateMaskedv1 + // Ubuntu Core > 18 has a later version of systemd and the + // UnitFileState for a masked unit is returned as 'masked'. + unitStateMaskedv2 +) + +type ctrlaltdelSuite struct { + configcoreSuite + unit unitStates +} + +var _ = Suite(&ctrlaltdelSuite{}) + +func (s *ctrlaltdelSuite) SetUpTest(c *C) { + s.configcoreSuite.SetUpTest(c) + s.systemctlOutput = func(args ...string) []byte { + var output []byte + // 'args' represents the arguments passed in for the systemctl Status call. + // The test context is specific to the ctrlaltdel handler, which only uses + // the Status call on the 'ctrl-alt-del.target' unit. + // args[0]: The systemctl command 'show' + // args[1]: The list of properties '--properties=Id,ActiveState,...' + // args[2]: The requested unit ctrl-alt-del.target + if args[0] == "show" { + switch s.unit { + case unitStateMulti: + // This test is a little artificial, as we know the ctrl-alt-del handler + // only requests a single unit. The check error does not depend on the unit + // name requested, but only on the fact that the units requested and the + // number of replies do not match. + output = []byte("Id=ctrl-alt-del.target\nActiveState=inactive\nUnitFileState=enabled\nNames=ctrl-alt-del.target\n" + + "\n" + + fmt.Sprintf("Id=%s\nActiveState=inactive\nUnitFileState=enabled\nNames=%[1]s\n", args[2])) + case unitStateUninstalled: + output = []byte(fmt.Sprintf("Id=%s\nActiveState=inactive\nUnitFileState=\nNames=%[1]s\n", args[2])) + case unitStateDisabled: + output = []byte(fmt.Sprintf("Id=%s\nActiveState=inactive\nUnitFileState=disabled\nNames=%[1]s\n", args[2])) + case unitStateEnabled: + output = []byte(fmt.Sprintf("Id=%s\nActiveState=inactive\nUnitFileState=enabled\nNames=%[1]s\n", args[2])) + case unitStateMaskedv1: + output = []byte(fmt.Sprintf("Id=%s\nActiveState=inactive\nUnitFileState=bad\nNames=%[1]s\n", args[2])) + case unitStateMaskedv2: + output = []byte(fmt.Sprintf("Id=%s\nActiveState=inactive\nUnitFileState=masked\nNames=%[1]s\n", args[2])) + default: + // No output returned by systemctl + } + } + return output + } + s.unit = unitStateNone + c.Assert(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "etc"), 0755), IsNil) + s.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) +} + +// Only "none" or "reboot" are valid action states +func (s *ctrlaltdelSuite) TestCtrlAltDelInvalidAction(c *C) { + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + changes: map[string]interface{}{ + "system.ctrl-alt-del-action": "xxx", + }, + }) + c.Check(err, ErrorMatches, `invalid action "xxx" supplied for system.ctrl-alt-del-action option`) +} + +// Only the status properties of a single matching unit (ctrl-alt-del.target) is expected +func (s *ctrlaltdelSuite) TestCtrlAltDelInvalidSystemctlReply(c *C) { + s.unit = unitStateMulti + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + changes: map[string]interface{}{ + "system.ctrl-alt-del-action": "none", + }, + }) + c.Check(err, ErrorMatches, "cannot get unit status: got more results than expected") +} + +// The ctrl-alt-del.target unit is expected to be installed in the filesystem +func (s *ctrlaltdelSuite) TestCtrlAltDelInvalidInstalledState(c *C) { + s.unit = unitStateUninstalled + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + changes: map[string]interface{}{ + "system.ctrl-alt-del-action": "none", + }, + }) + c.Check(err, ErrorMatches, `internal error: target ctrl-alt-del.target not installed`) +} + +// The ctrl-alt-del.target unit may not be in the enabled state +func (s *ctrlaltdelSuite) TestCtrlAltDelInvalidEnabledState(c *C) { + s.unit = unitStateEnabled + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + changes: map[string]interface{}{ + "system.ctrl-alt-del-action": "none", + }, + }) + c.Check(err, ErrorMatches, `internal error: target ctrl-alt-del.target should not be enabled`) +} + +// The ctrl-alt-del.target unit may be in: +// (1) Disabled state (reboot action) +// (2) Masked state (none action) as returned for Ubuntu Core 16 and 18 +// (3) Masked state (none action) as returned for Ubuntu Core 20 +func (s *ctrlaltdelSuite) TestCtrlAltDelValidDisabledState(c *C) { + for _, state := range []unitStates{unitStateDisabled, unitStateMaskedv1, unitStateMaskedv2} { + s.unit = state + for _, opt := range []string{"reboot", "none"} { + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + changes: map[string]interface{}{"system.ctrl-alt-del-action": opt}, + }) + c.Assert(err, IsNil) + c.Check(s.systemctlArgs, HasLen, 2) + c.Check(s.systemctlArgs[0], DeepEquals, []string{"show", "--property=Id,ActiveState,UnitFileState,Names", "ctrl-alt-del.target"}) + switch opt { + case "reboot": + c.Check(s.systemctlArgs[1], DeepEquals, []string{"unmask", "ctrl-alt-del.target"}) + case "none": + c.Check(s.systemctlArgs[1], DeepEquals, []string{"mask", "ctrl-alt-del.target"}) + default: + c.Fatalf("unreachable") + } + s.systemctlArgs = nil + } + } +} + +func (s *ctrlaltdelSuite) TestFilesystemOnlyApplyNone(c *C) { + conf := configcore.PlainCoreConfig(map[string]interface{}{ + "system.ctrl-alt-del-action": "none", + }) + tmpDir := c.MkDir() + c.Assert(configcore.FilesystemOnlyApply(coreDev, tmpDir, conf), IsNil) + + c.Check(s.systemctlArgs, DeepEquals, [][]string{ + {"--root", tmpDir, "mask", "ctrl-alt-del.target"}, + }) +} + +func (s *ctrlaltdelSuite) TestFilesystemOnlyApplyReboot(c *C) { + // slightly strange test as this is the default + conf := configcore.PlainCoreConfig(map[string]interface{}{ + "system.ctrl-alt-del-action": "reboot", + }) + tmpDir := c.MkDir() + c.Assert(configcore.FilesystemOnlyApply(coreDev, tmpDir, conf), IsNil) + + c.Check(s.systemctlArgs, DeepEquals, [][]string{ + {"--root", tmpDir, "unmask", "ctrl-alt-del.target"}, + }) +} diff -Nru snapd-2.55.5+20.04/overlord/configstate/configcore/handlers.go snapd-2.57.5+20.04/overlord/configstate/configcore/handlers.go --- snapd-2.55.5+20.04/overlord/configstate/configcore/handlers.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/configcore/handlers.go 2022-10-17 16:25:18.000000000 +0000 @@ -79,6 +79,9 @@ // system.power-key-action addFSOnlyHandler(nil, handlePowerButtonConfiguration, coreOnly) + // system.disable-ctrl-alt-del + addFSOnlyHandler(nil, handleCtrlAltDelConfiguration, coreOnly) + // pi-config.* addFSOnlyHandler(nil, handlePiConfiguration, coreOnly) diff -Nru snapd-2.55.5+20.04/overlord/configstate/configcore/services.go snapd-2.57.5+20.04/overlord/configstate/configcore/services.go --- snapd-2.55.5+20.04/overlord/configstate/configcore/services.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/configcore/services.go 2022-10-17 16:25:18.000000000 +0000 @@ -24,7 +24,6 @@ "io/ioutil" "os" "path/filepath" - "time" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" @@ -73,7 +72,7 @@ return err } if opts == nil { - return sysd.Stop(units, 5*time.Minute) + return sysd.Stop(units) } } else { err := os.Remove(sshCanary) @@ -186,7 +185,7 @@ } // mask triggered a reload already if opts == nil { - return sysd.Stop(units, 5*time.Minute) + return sysd.Stop(units) } } else { if err := sysd.Unmask(serviceName); err != nil { diff -Nru snapd-2.55.5+20.04/overlord/configstate/configcore/swap.go snapd-2.57.5+20.04/overlord/configstate/configcore/swap.go --- snapd-2.55.5+20.04/overlord/configstate/configcore/swap.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/configcore/swap.go 2022-10-17 16:25:18.000000000 +0000 @@ -24,7 +24,6 @@ "io/ioutil" "os" "path/filepath" - "time" "github.com/mvo5/goconfigparser" @@ -146,8 +145,7 @@ // restart the swap service sysd := systemd.NewUnderRoot(dirs.GlobalRootDir, systemd.SystemMode, &backlightSysdLogger{}) - // TODO: what's an appropriate amount of time to wait here? - if err := sysd.Restart([]string{"swapfile.service"}, 60*time.Second); err != nil { + if err := sysd.Restart([]string{"swapfile.service"}); err != nil { return err } } diff -Nru snapd-2.55.5+20.04/overlord/configstate/configcore/vitality.go snapd-2.57.5+20.04/overlord/configstate/configcore/vitality.go --- snapd-2.55.5+20.04/overlord/configstate/configcore/vitality.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/configcore/vitality.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,6 +22,7 @@ package configcore import ( + "errors" "fmt" "strings" @@ -86,7 +87,7 @@ err := snapstate.Get(st, instanceName, &snapst) // not installed, vitality-score will be applied when the snap // gets installed - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { continue } if err != nil { diff -Nru snapd-2.55.5+20.04/overlord/configstate/configstate.go snapd-2.57.5+20.04/overlord/configstate/configstate.go --- snapd-2.55.5+20.04/overlord/configstate/configstate.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/configstate.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,6 +22,7 @@ package configstate import ( + "errors" "fmt" "os" "time" @@ -61,7 +62,7 @@ var snapst snapstate.SnapState err := snapstate.Get(st, snapName, &snapst) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } @@ -177,7 +178,7 @@ if preloadGadget != nil { dev, gi, err := preloadGadget() if err != nil { - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { // nothing to do return nil } @@ -194,7 +195,7 @@ func systemAlreadyConfigured(st *state.State) (bool, error) { var seeded bool - if err := st.Get("seeded", &seeded); err != nil && err != state.ErrNoState { + if err := st.Get("seeded", &seeded); err != nil && !errors.Is(err, state.ErrNoState) { return false, err } if seeded { diff -Nru snapd-2.55.5+20.04/overlord/configstate/configstate_test.go snapd-2.57.5+20.04/overlord/configstate/configstate_test.go --- snapd-2.55.5+20.04/overlord/configstate/configstate_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/configstate_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -148,7 +148,7 @@ c.Check(err, IsNil) c.Check(patch, DeepEquals, test.patch) } else { - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) c.Check(patch, IsNil) } c.Check(useDefaults, Equals, test.useDefaults) diff -Nru snapd-2.55.5+20.04/overlord/configstate/hooks.go snapd-2.57.5+20.04/overlord/configstate/hooks.go --- snapd-2.55.5+20.04/overlord/configstate/hooks.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/configstate/hooks.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,7 @@ package configstate import ( + "errors" "fmt" "github.com/snapcore/snapd/overlord/configstate/config" @@ -79,7 +80,7 @@ var patch map[string]interface{} var useDefaults bool - if err := h.context.Get("use-defaults", &useDefaults); err != nil && err != state.ErrNoState { + if err := h.context.Get("use-defaults", &useDefaults); err != nil && !errors.Is(err, state.ErrNoState) { return err } @@ -93,7 +94,7 @@ } patch, err = snapstate.ConfigDefaults(st, deviceCtx, instanceName) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } // core is handled internally and does not need a configure @@ -109,7 +110,7 @@ } } } else { - if err := h.context.Get("patch", &patch); err != nil && err != state.ErrNoState { + if err := h.context.Get("patch", &patch); err != nil && !errors.Is(err, state.ErrNoState) { return err } } diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicectx.go snapd-2.57.5+20.04/overlord/devicestate/devicectx.go --- snapd-2.55.5+20.04/overlord/devicestate/devicectx.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicectx.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,6 +20,8 @@ package devicestate import ( + "errors" + "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" @@ -39,7 +41,7 @@ if err == nil { return remodCtx, nil } - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } modelAs, err := findModel(st) @@ -88,6 +90,10 @@ return dc.model.Base() } +func (dc groundDeviceContext) Gadget() string { + return dc.model.Gadget() +} + func (dc groundDeviceContext) RunMode() bool { return dc.systemMode == "run" } diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicemgr.go snapd-2.57.5+20.04/overlord/devicestate/devicemgr.go --- snapd-2.55.5+20.04/overlord/devicestate/devicemgr.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicemgr.go 2022-10-17 16:25:18.000000000 +0000 @@ -33,8 +33,10 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/kernel/fde" "github.com/snapcore/snapd/logger" @@ -51,6 +53,7 @@ "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapfile" @@ -65,6 +68,8 @@ var ( cloudInitStatus = sysconfig.CloudInitStatus restrictCloudInit = sysconfig.RestrictCloudInit + + secbootMarkSuccessful = secboot.MarkSuccessful ) // EarlyConfig is a hook set by configstate that can process early configuration @@ -99,7 +104,9 @@ ensureSeedInConfigRan bool - ensureInstalledRan bool + ensureInstalledRan bool + ensureFactoryResetRan bool + ensurePostFactoryResetRan bool ensureTriedRecoverySystemRan bool @@ -169,6 +176,7 @@ runner.AddHandler("mark-preseeded", m.doMarkPreseeded, nil) runner.AddHandler("mark-seeded", m.doMarkSeeded, nil) runner.AddHandler("setup-run-system", m.doSetupRunSystem, nil) + runner.AddHandler("factory-reset-run-system", m.doFactoryResetRunSystem, nil) runner.AddHandler("restart-system-to-run-mode", m.doRestartSystemToRunMode, nil) runner.AddHandler("prepare-remodeling", m.doPrepareRemodeling, nil) runner.AddCleanup("prepare-remodeling", m.cleanupRemodel) @@ -483,7 +491,7 @@ var seeded bool err = m.state.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } @@ -510,7 +518,7 @@ var storeID, gadget string model, err := m.Model() - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } if err == nil { @@ -556,6 +564,23 @@ } hasPrepareDeviceHook = (gadgetInfo.Hooks["prepare-device"] != nil) } + if device.KeyID == "" && model.Grade() != "" { + // UC20+ devices support factory reset + serial, err := m.maybeRestoreAfterReset(device) + if err != nil { + return err + } + if serial != nil { + device.KeyID = serial.DeviceKey().ID() + device.Serial = serial.Serial() + if err := m.setDevice(device); err != nil { + return fmt.Errorf("cannot set device for restored serial and key: %v", err) + } + logger.Noticef("restored serial %v for %v/%v signed with key %v", + device.Serial, device.Brand, device.Model, device.KeyID) + return nil + } + } // have some backoff between full retries if m.ensureOperationalShouldBackoff(time.Now()) { @@ -599,6 +624,52 @@ return nil } +// maybeRestoreAfterReset attempts to restore the serial assertion with a +// matching key in a post-factory reset scenario. It is possible that it is +// called when the device was unregistered, but when doing so, the device key is +// removed. +func (m *DeviceManager) maybeRestoreAfterReset(device *auth.DeviceState) (*asserts.Serial, error) { + // there should be a serial assertion for the current model + serials, err := assertstate.DB(m.state).FindMany(asserts.SerialType, map[string]string{ + "brand-id": device.Brand, + "model": device.Model, + }) + if err != nil { + if asserts.IsNotFound(err) { + // no serial assertion + return nil, nil + } + return nil, err + } + for _, serial := range serials { + serialAs := serial.(*asserts.Serial) + deviceKeyID := serialAs.DeviceKey().ID() + logger.Debugf("processing candidate serial assertion for %v/%v signed with key %v", + device.Brand, device.Model, deviceKeyID) + // serial assertion is signed with the device key, its ID is in + // the header; factory-reset would have restored the serial + // assertion and a matching device key, OTOH when the device is + // unregistered we explicitly remove the key, hence should this + // code process such serial assertion, there will be no matching + // key for it + err = m.withKeypairMgr(func(kpmgr asserts.KeypairManager) error { + _, err := kpmgr.Get(deviceKeyID) + return err + }) + if err != nil { + if asserts.IsKeyNotFound(err) { + // there is no key matching this serial assertion, + // perhaps device was unregistered at some point + continue + } + return nil, err + } + return serialAs, nil + } + // none of the assertions has a matching key + return nil, nil +} + var startTime time.Time func init() { @@ -608,7 +679,7 @@ func (m *DeviceManager) setTimeOnce(name string, t time.Time) error { var prev time.Time err := m.state.Get(name, &prev) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } if !prev.IsZero() { @@ -737,7 +808,7 @@ var seeded bool err := m.state.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } if seeded { @@ -813,7 +884,7 @@ if !m.bootOkRan { deviceCtx, err := DeviceCtx(m.state, nil, nil) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } if err == nil { @@ -821,6 +892,10 @@ return err } } + if err := secbootMarkSuccessful(); err != nil { + return err + } + m.bootOkRan = true } @@ -844,7 +919,7 @@ var seeded bool err := m.state.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } @@ -1003,6 +1078,27 @@ return nil } +// hasInstallDeviceHook returns whether the gadget has an install-device hook. +// It can return an error if the device has no gadget snap +func (m *DeviceManager) hasInstallDeviceHook(model *asserts.Model) (bool, error) { + gadgetInfo, err := snapstate.CurrentInfo(m.state, model.Gadget()) + if err != nil { + return false, fmt.Errorf("device is seeded in install mode but has no gadget snap: %v", err) + } + hasInstallDeviceHook := (gadgetInfo.Hooks["install-device"] != nil) + return hasInstallDeviceHook, nil +} + +func (m *DeviceManager) installDeviceHookTask(model *asserts.Model) *state.Task { + summary := i18n.G("Run install-device hook") + hooksup := &hookstate.HookSetup{ + // TODO: add a reasonable timeout for the install-device hook + Snap: model.Gadget(), + Hook: "install-device", + } + return hookstate.HookTask(m.state, summary, hooksup, nil) +} + func (m *DeviceManager) ensureInstalled() error { m.state.Lock() defer m.state.Unlock() @@ -1021,72 +1117,138 @@ var seeded bool err := m.state.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } if !seeded { return nil } - if m.changeInFlight("install-system") { - return nil - } - perfTimings := timings.New(map[string]string{"ensure": "install-system"}) model, err := m.Model() - if err != nil && err != state.ErrNoState { - return err - } if err != nil { - return fmt.Errorf("internal error: core device brand and model are set but there is no model assertion") + if errors.Is(err, state.ErrNoState) { + return fmt.Errorf("internal error: core device brand and model are set but there is no model assertion") + } + return err } - // check if the gadget has an install-device hook - var hasInstallDeviceHook bool - - gadgetInfo, err := snapstate.CurrentInfo(m.state, model.Gadget()) + // check if the gadget has an install-device hook, do this before + // we mark ensureInstalledRan as true, as this can fail if no gadget + // snap is present + hasInstallDeviceHook, err := m.hasInstallDeviceHook(model) if err != nil { - return fmt.Errorf("internal error: device is seeded in install mode but has no gadget snap: %v", err) + return fmt.Errorf("internal error: %v", err) } - hasInstallDeviceHook = (gadgetInfo.Hooks["install-device"] != nil) m.ensureInstalledRan = true - var prev *state.Task + // Create both setup-run-system and restart-system-to-run-mode tasks as they + // will run unconditionally. They will be chained together with optionally the + // install-device hook task. setupRunSystem := m.state.NewTask("setup-run-system", i18n.G("Setup system for run mode")) + restartSystem := m.state.NewTask("restart-system-to-run-mode", i18n.G("Ensure next boot to run mode")) + prev := setupRunSystem tasks := []*state.Task{setupRunSystem} addTask := func(t *state.Task) { t.WaitFor(prev) tasks = append(tasks, t) prev = t } - prev = setupRunSystem // add the install-device hook before ensure-next-boot-to-run-mode if it // exists in the snap - var installDevice *state.Task if hasInstallDeviceHook { - summary := i18n.G("Run install-device hook") - hooksup := &hookstate.HookSetup{ - // TODO: what's a reasonable timeout for the install-device hook? - Snap: model.Gadget(), - Hook: "install-device", - } - installDevice = hookstate.HookTask(m.state, summary, hooksup, nil) + installDevice := m.installDeviceHookTask(model) + + // reference used by snapctl reboot + installDevice.Set("restart-task", restartSystem.ID()) addTask(installDevice) } - restartSystem := m.state.NewTask("restart-system-to-run-mode", i18n.G("Ensure next boot to run mode")) addTask(restartSystem) - if installDevice != nil { + chg := m.state.NewChange("install-system", i18n.G("Install the system")) + chg.AddAll(state.NewTaskSet(tasks...)) + + state.TagTimingsWithChange(perfTimings, chg) + perfTimings.Save(m.state) + + return nil +} + +func (m *DeviceManager) ensureFactoryReset() error { + m.state.Lock() + defer m.state.Unlock() + + if release.OnClassic { + return nil + } + + if m.ensureFactoryResetRan { + return nil + } + + if m.SystemMode(SysHasModeenv) != "factory-reset" { + return nil + } + + var seeded bool + err := m.state.Get("seeded", &seeded) + if err != nil && !errors.Is(err, state.ErrNoState) { + return err + } + if !seeded { + return nil + } + + perfTimings := timings.New(map[string]string{"ensure": "factory-reset"}) + + model, err := m.Model() + if err != nil { + if errors.Is(err, state.ErrNoState) { + return fmt.Errorf("internal error: core device brand and model are set but there is no model assertion") + } + return err + } + + // We perform this check before setting ensureFactoryResetRan in + // case this should fail. This should in theory not be possible as + // the same type of check is made during install-mode. + hasInstallDeviceHook, err := m.hasInstallDeviceHook(model) + if err != nil { + return fmt.Errorf("internal error: %v", err) + } + + m.ensureFactoryResetRan = true + + // Create both factory-reset-run-system and restart-system-to-run-mode tasks as they + // will run unconditionally. They will be chained together with optionally the + // install-device hook task. + factoryReset := m.state.NewTask("factory-reset-run-system", i18n.G("Perform factory reset of the system")) + restartSystem := m.state.NewTask("restart-system-to-run-mode", i18n.G("Ensure next boot to run mode")) + + prev := factoryReset + tasks := []*state.Task{factoryReset} + addTask := func(t *state.Task) { + t.WaitFor(prev) + tasks = append(tasks, t) + prev = t + } + + if hasInstallDeviceHook { + installDevice := m.installDeviceHookTask(model) + // reference used by snapctl reboot installDevice.Set("restart-task", restartSystem.ID()) + addTask(installDevice) } - chg := m.state.NewChange("install-system", i18n.G("Install the system")) + addTask(restartSystem) + + chg := m.state.NewChange("factory-reset", i18n.G("Perform factory reset")) chg.AddAll(state.NewTaskSet(tasks...)) state.TagTimingsWithChange(perfTimings, chg) @@ -1110,14 +1272,14 @@ if err == nil { return opTime, nil } - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return opTime, err } // start-of-operation-time not set yet, use seed-time if available var seedTime time.Time err = m.state.Get("seed-time", &seedTime) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return opTime, err } if err == nil { @@ -1151,7 +1313,7 @@ if !m.ensureSeedInConfigRan { // get global seeded option var seeded bool - if err := m.state.Get("seeded", &seeded); err != nil && err != state.ErrNoState { + if err := m.state.Get("seeded", &seeded); err != nil && !errors.Is(err, state.ErrNoState) { return err } if !seeded { @@ -1178,7 +1340,7 @@ // state is locked by the caller var triedSystems []string - if err := m.state.Get("tried-systems", &triedSystems); err != nil && err != state.ErrNoState { + if err := m.state.Get("tried-systems", &triedSystems); err != nil && !errors.Is(err, state.ErrNoState) { return err } if strutil.ListContains(triedSystems, label) { @@ -1206,7 +1368,7 @@ defer m.state.Unlock() deviceCtx, err := DeviceCtx(m.state, nil, nil) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } outcome, label, err := boot.InspectTryRecoverySystemOutcome(deviceCtx) @@ -1241,6 +1403,67 @@ return nil } +var bootMarkFactoryResetComplete = boot.MarkFactoryResetComplete + +func (m *DeviceManager) ensurePostFactoryReset() error { + m.state.Lock() + defer m.state.Unlock() + + if release.OnClassic { + return nil + } + + if m.ensurePostFactoryResetRan { + return nil + } + + mode := m.SystemMode(SysHasModeenv) + if mode != "run" { + return nil + } + + var seeded bool + err := m.state.Get("seeded", &seeded) + if err != nil && !errors.Is(err, state.ErrNoState) { + return err + } + if !seeded { + return nil + } + + m.ensurePostFactoryResetRan = true + + factoryResetMarker := filepath.Join(dirs.SnapDeviceDir, "factory-reset") + if !osutil.FileExists(factoryResetMarker) { + // marker is gone already + return nil + } + + encrypted := true + // XXX have a helper somewhere for this? + if !osutil.FileExists(filepath.Join(dirs.SnapFDEDir, "marker")) { + encrypted = false + } + + // verify the marker + if err := verifyFactoryResetMarkerInRun(factoryResetMarker, encrypted); err != nil { + return fmt.Errorf("cannot verify factory reset marker: %v", err) + } + + // if encrypted, rotates the fallback keys on disk + if err := bootMarkFactoryResetComplete(encrypted); err != nil { + return fmt.Errorf("cannot complete factory reset: %v", err) + } + + if encrypted { + if err := rotateEncryptionKeys(); err != nil { + return fmt.Errorf("cannot transition encryption keys: %v", err) + } + } + + return os.Remove(factoryResetMarker) +} + type ensureError struct { errs []error } @@ -1294,6 +1517,14 @@ if err := m.ensureTriedRecoverySystem(); err != nil { errs = append(errs, err) } + + if err := m.ensureFactoryReset(); err != nil { + errs = append(errs, err) + } + + if err := m.ensurePostFactoryReset(); err != nil { + errs = append(errs, err) + } } if len(errs) > 0 { @@ -1319,7 +1550,7 @@ func (m *DeviceManager) withSaveDir(f func() error) error { // we use the model to check whether this is a UC20 device model, err := m.Model() - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return fmt.Errorf("internal error: cannot access save dir before a model is set") } if err != nil { @@ -1363,7 +1594,7 @@ // to deal with that, this code typically will return the old location // until a restart model, err := m.Model() - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return fmt.Errorf("internal error: cannot access device keypair manager before a model is set") } if err != nil { @@ -1508,7 +1739,7 @@ // SystemModeInfo returns details about the current system mode the device is in. func (m *DeviceManager) SystemModeInfo() (*SystemModeInfo, error) { deviceCtx, err := DeviceCtx(m.state, nil, nil) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil, fmt.Errorf("cannot report system mode information before device model is acknowledged") } if err != nil { @@ -1517,7 +1748,7 @@ var seeded bool err = m.state.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } @@ -1781,7 +2012,7 @@ } privKey, err := scb.DeviceManager.keyPair() - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil, fmt.Errorf("internal error: inconsistent state with serial but no device key") } if err != nil { @@ -1934,3 +2165,99 @@ func (h fdeSetupHandler) Error(err error) (bool, error) { return false, nil } + +var ( + secbootEnsureRecoveryKey = secboot.EnsureRecoveryKey + secbootRemoveRecoveryKeys = secboot.RemoveRecoveryKeys +) + +// EnsureRecoveryKeys makes sure appropriate recovery keys exist and +// returns them. Usually a single recovery key is created/used, but +// older systems might return both a recovery key for ubuntu-data and a +// reinstall key for ubuntu-save. +func (m *DeviceManager) EnsureRecoveryKeys() (*client.SystemRecoveryKeysResponse, error) { + fdeDir := dirs.SnapFDEDir + mode := m.SystemMode(SysAny) + if mode == "install" { + fdeDir = boot.InstallHostFDEDataDir + } else if mode != "run" { + return nil, fmt.Errorf("cannot ensure recovery keys from system mode %q", mode) + } + sysKeys := &client.SystemRecoveryKeysResponse{} + // backward compatibility + reinstallKeyFile := filepath.Join(fdeDir, "reinstall.key") + if osutil.FileExists(reinstallKeyFile) { + rkey, err := keys.RecoveryKeyFromFile(device.RecoveryKeyUnder(fdeDir)) + if err != nil { + return nil, err + } + sysKeys.RecoveryKey = rkey.String() + + reinstallKey, err := keys.RecoveryKeyFromFile(reinstallKeyFile) + if err != nil { + return nil, err + } + sysKeys.ReinstallKey = reinstallKey.String() + return sysKeys, nil + } + if !device.HasEncryptedMarkerUnder(fdeDir) { + return nil, fmt.Errorf("system does not use disk encryption") + } + dataMountPoints, err := boot.HostUbuntuDataForMode(m.SystemMode(SysHasModeenv)) + if err != nil { + return nil, fmt.Errorf("cannot determine ubuntu-data mount point: %v", err) + } + if len(dataMountPoints) == 0 { + // shouldn't happen as the marker file is under ubuntu-data + return nil, fmt.Errorf("cannot ensure recovery keys without any ubuntu-data mount points") + } + recoveryKeyDevices := []secboot.RecoveryKeyDevice{ + { + Mountpoint: dataMountPoints[0], + // TODO ubuntu-data key in install mode? key isn't + // available in the keyring nor exists on disk + }, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + AuthorizingKeyFile: device.SaveKeyUnder(dirs.SnapFDEDirUnder(filepath.Join(dataMountPoints[0], "system-data"))), + }, + } + rkey, err := secbootEnsureRecoveryKey(device.RecoveryKeyUnder(fdeDir), recoveryKeyDevices) + if err != nil { + return nil, err + } + sysKeys.RecoveryKey = rkey.String() + return sysKeys, nil +} + +// RemoveRecoveryKeys removes and disables all recovery keys. +func (m *DeviceManager) RemoveRecoveryKeys() error { + mode := m.SystemMode(SysAny) + if mode != "run" { + return fmt.Errorf("cannot remove recovery keys from system mode %q", mode) + } + if !device.HasEncryptedMarkerUnder(dirs.SnapFDEDir) { + return fmt.Errorf("system does not use disk encryption") + } + dataMountPoints, err := boot.HostUbuntuDataForMode(m.SystemMode(SysHasModeenv)) + if err != nil { + return fmt.Errorf("cannot determine ubuntu-data mount point: %v", err) + } + recoveryKeyDevices := make(map[secboot.RecoveryKeyDevice]string, 2) + rkey := device.RecoveryKeyUnder(dirs.SnapFDEDir) + recoveryKeyDevices[secboot.RecoveryKeyDevice{ + Mountpoint: dataMountPoints[0], + // authorization from keyring + }] = rkey + // reinstall.key is deprecated, there is no path helper for it + reinstallKeyFile := filepath.Join(dirs.SnapFDEDir, "reinstall.key") + if !osutil.FileExists(reinstallKeyFile) { + reinstallKeyFile = rkey + } + recoveryKeyDevices[secboot.RecoveryKeyDevice{ + Mountpoint: boot.InitramfsUbuntuSaveDir, + AuthorizingKeyFile: device.SaveKeyUnder(dirs.SnapFDEDirUnder(filepath.Join(dataMountPoints[0], "system-data"))), + }] = reinstallKeyFile + + return secbootRemoveRecoveryKeys(recoveryKeyDevices) +} diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicestate_bootconfig_test.go snapd-2.57.5+20.04/overlord/devicestate/devicestate_bootconfig_test.go --- snapd-2.55.5+20.04/overlord/devicestate/devicestate_bootconfig_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicestate_bootconfig_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -105,7 +105,7 @@ s.state.Lock() tsk := s.state.NewTask("update-managed-boot-config", "update boot config") - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(tsk) chg.Set("system-restart-immediate", true) s.state.Unlock() @@ -267,7 +267,7 @@ // be extra sure c.Check(remodCtx.ForRemodeling(), Equals, true) tsk := s.state.NewTask("update-managed-boot-config", "update boot config") - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(tsk) remodCtx.Init(chg) // replace the bootloader with something that always fails diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicestate_gadget_test.go snapd-2.57.5+20.04/overlord/devicestate/devicestate_gadget_test.go --- snapd-2.55.5+20.04/overlord/devicestate/devicestate_gadget_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicestate_gadget_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -36,6 +36,7 @@ "github.com/snapcore/snapd/bootloader/bootloadertest" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" @@ -254,7 +255,7 @@ SideInfo: si, Type: snap.TypeGadget, }) - chg = s.state.NewChange("dummy", "...") + chg = s.state.NewChange("sample", "...") chg.AddTask(tsk) return chg, tsk @@ -502,7 +503,7 @@ SideInfo: si, Type: snap.TypeGadget, }) - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(t) s.state.Unlock() @@ -559,7 +560,7 @@ SideInfo: si, Type: snap.TypeGadget, }) - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(t) s.state.Unlock() @@ -610,7 +611,7 @@ SideInfo: si, Type: snap.TypeGadget, }) - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(t) s.state.Unlock() @@ -639,7 +640,7 @@ s.state.Set("seeded", true) t := s.state.NewTask("update-gadget-assets", "update gadget") - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(t) s.state.Unlock() @@ -705,11 +706,29 @@ {"content.img", "updated content"}, }) + r := gadget.MockVolumeStructureToLocationMap(func(_ gadget.GadgetData, _ gadget.Model, _ map[string]*gadget.LaidOutVolume) (map[string]map[int]gadget.StructureLocation, error) { + return map[string]map[int]gadget.StructureLocation{ + "pc": { + 0: { + Device: "/dev/foo", + Offset: quantity.OffsetMiB, + }, + }, + }, nil + }) + defer r() + expectedRollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34") updaterForStructureCalls := 0 - restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, _ gadget.ContentUpdateObserver) (gadget.Updater, error) { + restore := gadget.MockUpdaterForStructure(func(loc gadget.StructureLocation, ps *gadget.LaidOutStructure, rootDir, rollbackDir string, _ gadget.ContentUpdateObserver) (gadget.Updater, error) { updaterForStructureCalls++ + c.Assert(loc, Equals, gadget.StructureLocation{ + Device: "/dev/foo", + Offset: quantity.OffsetMiB, + RootMountPoint: "", + }) + c.Assert(ps.Name, Equals, "foo") c.Assert(rootDir, Equals, updateInfo.MountDir()) c.Assert(filepath.Join(rootDir, "content.img"), testutil.FileEquals, "updated content") @@ -736,7 +755,7 @@ SideInfo: si, Type: snap.TypeGadget, }) - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(t) s.state.Unlock() @@ -927,7 +946,7 @@ SideInfo: si, Type: snap.TypeGadget, }) - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(t) s.state.Unlock() @@ -1016,7 +1035,7 @@ SideInfo: siNext, Type: snap.TypeKernel, }) - chg = s.state.NewChange("dummy", "...") + chg = s.state.NewChange("sample", "...") chg.AddTask(tsk) return chg, tsk @@ -1135,7 +1154,7 @@ SideInfo: &updateSi, Type: snap.TypeGadget, }) - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(tsk) s.state.Unlock() @@ -1445,7 +1464,7 @@ }) terr := s.state.NewTask("error-trigger", "provoking total undo") terr.WaitFor(tsk) - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(tsk) chg.AddTask(terr) chg.Set("system-restart-immediate", true) @@ -1554,7 +1573,7 @@ }) terr := s.state.NewTask("error-trigger", "provoking total undo") terr.WaitFor(tsk) - chg := s.state.NewChange("dummy", "...") + chg := s.state.NewChange("sample", "...") chg.AddTask(tsk) chg.AddTask(terr) s.state.Unlock() diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicestate.go snapd-2.57.5+20.04/overlord/devicestate/devicestate.go --- snapd-2.55.5+20.04/overlord/devicestate/devicestate.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicestate.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2019 Canonical Ltd + * Copyright (C) 2016-2022 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,6 +23,7 @@ import ( "context" + "errors" "fmt" "path/filepath" "sort" @@ -135,10 +136,10 @@ return true, nil } - // Check model exists, for sanity. We always have a model, either + // Check model exists, for validity. We always have a model, either // seeded or a generic one that ships with snapd. _, err := findModel(st) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return false, nil } if err != nil { @@ -146,7 +147,7 @@ } _, err = findSerial(st, nil) - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return false, nil } if err != nil { @@ -230,7 +231,7 @@ return nil } - // do basic validity checks on the gadget against its model constraints + // do basic precondition checks on the gadget against its model constraints _, err := gadget.ReadInfoFromSnapFile(snapf, deviceCtx.Model()) return err } @@ -522,7 +523,7 @@ // find the first task that carries snap setup sup, err := snapstate.TaskSnapSetup(t) if err != nil { - if err != state.ErrNoState { + if !errors.Is(err, state.ErrNoState) { return nil, err } // try the next one @@ -849,7 +850,7 @@ func Remodel(st *state.State, new *asserts.Model) (*state.Change, error) { var seeded bool err := st.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } if !seeded { @@ -862,7 +863,7 @@ } if _, err := findSerial(st, nil); err != nil { - if err == state.ErrNoState { + if errors.Is(err, state.ErrNoState) { return nil, fmt.Errorf("cannot remodel without a serial") } return nil, err @@ -879,7 +880,7 @@ if current.Grade() != new.Grade() { if current.Grade() == asserts.ModelGradeUnset && new.Grade() != asserts.ModelGradeUnset { // a case of pre-UC20 -> UC20 remodel - return nil, fmt.Errorf("cannot remodel to Ubuntu Core 20 models yet") + return nil, fmt.Errorf("cannot remodel from pre-UC20 to UC20+ models") } return nil, fmt.Errorf("cannot remodel from grade %v to grade %v", current.Grade(), new.Grade()) } @@ -930,7 +931,7 @@ } // ensure a new session accounting for the new brand store st.Unlock() - _, err := sto.EnsureDeviceSession() + err := sto.EnsureDeviceSession() st.Lock() if err != nil { return nil, fmt.Errorf("cannot get a store session based on the new model assertion: %v", err) @@ -1031,7 +1032,7 @@ } func createRecoverySystemTasks(st *state.State, label string, snapSetupTasks []string) (*state.TaskSet, error) { - // sanity check, the directory should not exist yet + // precondition check, the directory should not exist yet systemDirectory := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", label) exists, _, err := osutil.DirExists(systemDirectory) if err != nil { @@ -1060,7 +1061,7 @@ func CreateRecoverySystem(st *state.State, label string) (*state.Change, error) { var seeded bool err := st.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } if !seeded { diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicestate_install_mode_test.go snapd-2.57.5+20.04/overlord/devicestate/devicestate_install_mode_test.go --- snapd-2.55.5+20.04/overlord/devicestate/devicestate_install_mode_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicestate_install_mode_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,9 +22,11 @@ import ( "bytes" "compress/gzip" + "crypto" "fmt" "io/ioutil" "os" + "os/exec" "path/filepath" "time" @@ -32,6 +34,8 @@ "gopkg.in/tomb.v2" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/bootloadertest" @@ -39,6 +43,8 @@ "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/install" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" @@ -50,6 +56,10 @@ "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" + "github.com/snapcore/snapd/seed" + "github.com/snapcore/snapd/seed/seedtest" + "github.com/snapcore/snapd/seed/seedwriter" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/sysconfig" @@ -79,6 +89,12 @@ classic := false s.deviceMgrBaseSuite.setupBaseTest(c, classic) + // restore dirs after os-release mock is cleaned up + s.AddCleanup(func() { dirs.SetRootDir(dirs.GlobalRootDir) }) + s.AddCleanup(release.MockReleaseInfo(&release.OS{ID: "ubuntu"})) + // reload directory paths to match our mocked os-release + dirs.SetRootDir(dirs.GlobalRootDir) + s.ConfigureTargetSystemOptsPassed = nil s.ConfigureTargetSystemErr = nil restore := devicestate.MockSysconfigConfigureTargetSystem(func(mod *asserts.Model, opts *sysconfig.Options) error { @@ -195,11 +211,8 @@ } var ( - dataEncryptionKey = secboot.EncryptionKey{'d', 'a', 't', 'a', 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} - dataRecoveryKey = secboot.RecoveryKey{'r', 'e', 'c', 'o', 'v', 'e', 'r', 'y', 10, 11, 12, 13, 14, 15, 16, 17} - - saveKey = secboot.EncryptionKey{'s', 'a', 'v', 'e', 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} - reinstallKey = secboot.RecoveryKey{'r', 'e', 'i', 'n', 's', 't', 'a', 'l', 'l', 11, 12, 13, 14, 15, 16, 17} + dataEncryptionKey = keys.EncryptionKey{'d', 'a', 't', 'a', 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + saveKey = keys.EncryptionKey{'s', 'a', 'v', 'e', 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} ) func (s *deviceMgrInstallModeSuite) doRunChangeTestWithEncryption(c *C, grade string, tc encTestCase) error { @@ -223,21 +236,15 @@ brOpts = options installSealingObserver = obs installRunCalled++ - var keysForRoles map[string]*install.EncryptionKeySet + var keyForRole map[string]keys.EncryptionKey if tc.encrypt { - keysForRoles = map[string]*install.EncryptionKeySet{ - gadget.SystemData: { - Key: dataEncryptionKey, - RecoveryKey: dataRecoveryKey, - }, - gadget.SystemSave: { - Key: saveKey, - RecoveryKey: reinstallKey, - }, + keyForRole = map[string]keys.EncryptionKey{ + gadget.SystemData: dataEncryptionKey, + gadget.SystemSave: saveKey, } } return &install.InstalledSystemSideData{ - KeysForRoles: keysForRoles, + KeyForRole: keyForRole, }, nil }) defer restore() @@ -432,7 +439,7 @@ c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) } -func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookExpTasks(c *C) { +func (s *deviceMgrInstallModeSuite) TestInstallRestoresPreseedArtifact(c *C) { restore := release.MockOnClassic(false) defer restore() @@ -441,23 +448,23 @@ }) defer restore() - hooksCalled := []*hookstate.Context{} - restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { - ctx.Lock() - defer ctx.Unlock() - - hooksCalled = append(hooksCalled, ctx) - return nil, nil + var applyPreseedCalled int + restoreApplyPreseed := devicestate.MockMaybeApplyPreseededData(func(st *state.State, ubuntuSeedDir, sysLabel, writableDir string) (bool, error) { + applyPreseedCalled++ + c.Check(ubuntuSeedDir, Equals, filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-seed")) + c.Check(sysLabel, Equals, "20200105") + c.Check(writableDir, Equals, filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-data/system-data")) + return true, nil }) - defer restore() + defer restoreApplyPreseed() err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), - []byte("mode=install\n"), 0644) + []byte("mode=install\nrecovery_system=20200105\n"), 0644) c.Assert(err, IsNil) s.state.Lock() s.makeMockInstallModel(c, "dangerous") - s.makeMockInstalledPcGadget(c, "install-device-hook-content", "") + s.makeMockInstalledPcGadget(c, "", "") devicestate.SetSystemMode(s.mgr, "install") s.state.Unlock() @@ -469,43 +476,12 @@ installSystem := s.findInstallSystem() c.Check(installSystem.Err(), IsNil) - tasks := installSystem.Tasks() - c.Assert(tasks, HasLen, 3) - setupRunSystemTask := tasks[0] - installDevice := tasks[1] - restartSystemToRunModeTask := tasks[2] - - c.Assert(setupRunSystemTask.Kind(), Equals, "setup-run-system") - c.Assert(restartSystemToRunModeTask.Kind(), Equals, "restart-system-to-run-mode") - c.Assert(installDevice.Kind(), Equals, "run-hook") - - // setup-run-system has no pre-reqs - c.Assert(setupRunSystemTask.WaitTasks(), HasLen, 0) - - // install-device has a pre-req of setup-run-system - waitTasks := installDevice.WaitTasks() - c.Assert(waitTasks, HasLen, 1) - c.Assert(waitTasks[0].ID(), Equals, setupRunSystemTask.ID()) - - // install-device restart-task references to restart-system-to-run-mode - var restartTask string - err = installDevice.Get("restart-task", &restartTask) - c.Assert(err, IsNil) - c.Check(restartTask, Equals, restartSystemToRunModeTask.ID()) - - // restart-system-to-run-mode has a pre-req of install-device - waitTasks = restartSystemToRunModeTask.WaitTasks() - c.Assert(waitTasks, HasLen, 1) - c.Assert(waitTasks[0].ID(), Equals, installDevice.ID()) - // we did request a restart through restartSystemToRunModeTask c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) - - c.Assert(hooksCalled, HasLen, 1) - c.Assert(hooksCalled[0].HookName(), Equals, "install-device") + c.Check(applyPreseedCalled, Equals, 1) } -func (s *deviceMgrInstallModeSuite) testInstallWithInstallDeviceHookSnapctlReboot(c *C, arg string, rst restart.RestartType) { +func (s *deviceMgrInstallModeSuite) TestInstallRestoresPreseedArtifactError(c *C) { restore := release.MockOnClassic(false) defer restore() @@ -514,22 +490,20 @@ }) defer restore() - restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { - c.Assert(ctx.HookName(), Equals, "install-device") - - // snapctl reboot --halt - _, _, err := ctlcmd.Run(ctx, []string{"reboot", arg}, 0) - return nil, err + var applyPreseedCalled int + restoreApplyPreseed := devicestate.MockMaybeApplyPreseededData(func(st *state.State, ubuntuSeedDir, sysLabel, writableDir string) (bool, error) { + applyPreseedCalled++ + return false, fmt.Errorf("boom") }) - defer restore() + defer restoreApplyPreseed() err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), - []byte("mode=install\n"), 0644) + []byte("mode=install\nrecovery_system=20200105\n"), 0644) c.Assert(err, IsNil) s.state.Lock() s.makeMockInstallModel(c, "dangerous") - s.makeMockInstalledPcGadget(c, "install-device-hook-content", "") + s.makeMockInstalledPcGadget(c, "", "") devicestate.SetSystemMode(s.mgr, "install") s.state.Unlock() @@ -539,299 +513,582 @@ defer s.state.Unlock() installSystem := s.findInstallSystem() - c.Check(installSystem.Err(), IsNil) + c.Check(installSystem.Err(), ErrorMatches, "cannot perform the following tasks:\\n- Ensure next boot to run mode \\(boom\\)") - // we did end up requesting the right shutdown - c.Check(s.restartRequests, DeepEquals, []restart.RestartType{rst}) + c.Check(s.restartRequests, HasLen, 0) + c.Check(applyPreseedCalled, Equals, 1) } -func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookSnapctlRebootHalt(c *C) { - s.testInstallWithInstallDeviceHookSnapctlReboot(c, "--halt", restart.RestartSystemHaltNow) +type fakeSeed struct { + modeSnaps []*seed.Snap + essentialSnaps []*seed.Snap } -func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookSnapctlRebootPoweroff(c *C) { - s.testInstallWithInstallDeviceHookSnapctlReboot(c, "--poweroff", restart.RestartSystemPoweroffNow) +func (fakeSeed) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Batch) error) error { + return nil } -func (s *deviceMgrInstallModeSuite) TestInstallWithBrokenInstallDeviceHookUnhappy(c *C) { - restore := release.MockOnClassic(false) - defer restore() - - restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { - return nil, nil - }) - defer restore() - - hooksCalled := []*hookstate.Context{} - restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { - ctx.Lock() - defer ctx.Unlock() +func (fakeSeed) Model() *asserts.Model { + return nil +} - hooksCalled = append(hooksCalled, ctx) - return []byte("hook exited broken"), fmt.Errorf("hook broken") - }) - defer restore() +func (fakeSeed) Brand() (*asserts.Account, error) { + return nil, nil +} - err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), - []byte("mode=install\n"), 0644) - c.Assert(err, IsNil) +func (fakeSeed) LoadEssentialMeta(essentialTypes []snap.Type, tm timings.Measurer) error { + return nil +} - s.state.Lock() - s.makeMockInstallModel(c, "dangerous") - s.makeMockInstalledPcGadget(c, "install-device-hook-content", "") - devicestate.SetSystemMode(s.mgr, "install") - s.state.Unlock() +func (fakeSeed) LoadEssentialMetaWithSnapHandler([]snap.Type, seed.SnapHandler, timings.Measurer) error { + return nil +} - s.settle(c) +func (fakeSeed) LoadMeta(string, seed.SnapHandler, timings.Measurer) error { + return nil +} - s.state.Lock() - defer s.state.Unlock() +func (fakeSeed) UsesSnapdSnap() bool { + return true +} - installSystem := s.findInstallSystem() - c.Check(installSystem.Err(), ErrorMatches, `cannot perform the following tasks: -- Run install-device hook \(run hook \"install-device\": hook exited broken\)`) +func (fakeSeed) SetParallelism(n int) {} - tasks := installSystem.Tasks() - c.Assert(tasks, HasLen, 3) - setupRunSystemTask := tasks[0] - installDevice := tasks[1] - restartSystemToRunModeTask := tasks[2] +func (f *fakeSeed) EssentialSnaps() []*seed.Snap { + return f.essentialSnaps +} - c.Assert(setupRunSystemTask.Kind(), Equals, "setup-run-system") - c.Assert(installDevice.Kind(), Equals, "run-hook") - c.Assert(restartSystemToRunModeTask.Kind(), Equals, "restart-system-to-run-mode") +func (f *fakeSeed) ModeSnaps(mode string) ([]*seed.Snap, error) { + return f.modeSnaps, nil +} - // install-device is in Error state - c.Assert(installDevice.Status(), Equals, state.ErrorStatus) +func (f *fakeSeed) NumSnaps() int { + return 0 +} - // setup-run-system is in Done (it has no undo handler) - c.Assert(setupRunSystemTask.Status(), Equals, state.DoneStatus) +func (f *fakeSeed) Iter(func(sn *seed.Snap) error) error { + return nil +} - // restart-system-to-run-mode is in Hold - c.Assert(restartSystemToRunModeTask.Status(), Equals, state.HoldStatus) +func (s *deviceMgrInstallModeSuite) mockPreseedAssertion(c *C, brandID, modelName, series, preseedAsPath, sysLabel string, digest string, snaps []interface{}) { + headers := map[string]interface{}{ + "type": "preseed", + "authority-id": brandID, + "series": series, + "brand-id": brandID, + "model": modelName, + "system-label": sysLabel, + "artifact-sha3-384": digest, + "timestamp": time.Now().UTC().Format(time.RFC3339), + "snaps": snaps, + } + + signer := s.brands.Signing(brandID) + preseedAs, err := signer.Sign(asserts.PreseedType, headers, nil, "") + if err != nil { + panic(err) + } - // we didn't request a restart since restartsystemToRunMode didn't run - c.Check(s.restartRequests, HasLen, 0) + f, err := os.Create(preseedAsPath) + defer f.Close() + c.Assert(err, IsNil) + enc := asserts.NewEncoder(f) + c.Assert(enc.Encode(preseedAs), IsNil) - c.Assert(hooksCalled, HasLen, 1) - c.Assert(hooksCalled[0].HookName(), Equals, "install-device") + // other-brand account key needs to be explicitly added to the serialized preseed assertion + // if needed by some of the unhappy-scenario tests (normally my-brand is used). + if brandID == "other-brand" { + for _, as := range s.brands.AccountsAndKeys("other-brand") { + c.Assert(enc.Encode(as), IsNil) + } + } } -func (s *deviceMgrInstallModeSuite) TestInstallSetupRunSystemTaskNoRestarts(c *C) { - restore := release.MockOnClassic(false) - defer restore() +func (s *deviceMgrInstallModeSuite) setupCore20Seed(ts *seedtest.TestingSeed20, c *C) *asserts.Model { + gadgetYaml := ` +volumes: + volume-id: + bootloader: grub + structure: + - name: ubuntu-seed + role: system-seed + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + size: 1G + - name: ubuntu-data + role: system-data + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 2G +` + makeSnap := func(yamlKey string) { + var files [][]string + if yamlKey == "pc=20" { + files = append(files, []string{"meta/gadget.yaml", gadgetYaml}) + } + ts.MakeAssertedSnap(c, seedtest.SampleSnapYaml[yamlKey], files, snap.R(1), "canonical", ts.StoreSigning.Database) + } - restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { - return nil, nil - }) - defer restore() + makeSnap("snapd") + makeSnap("pc-kernel=20") + makeSnap("core20") + makeSnap("pc=20") + optSnapPath := snaptest.MakeTestSnapWithFiles(c, seedtest.SampleSnapYaml["optional20-a"], nil) - err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), - []byte("mode=install\n"), 0644) - c.Assert(err, IsNil) + model := map[string]interface{}{ + "display-name": "my model", + "architecture": "amd64", + "base": "core20", + "grade": "dangerous", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": ts.AssertedSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": ts.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "snapd", + "id": ts.AssertedSnapID("snapd"), + "type": "snapd", + }, + map[string]interface{}{ + "name": "core20", + "id": ts.AssertedSnapID("core20"), + "type": "base", + }}, + } - s.state.Lock() - defer s.state.Unlock() + return ts.MakeSeed(c, "20220401", "my-brand", "my-model", model, []*seedwriter.OptionsSnap{{Path: optSnapPath}}) +} - s.makeMockInstallModel(c, "dangerous") - s.makeMockInstalledPcGadget(c, "", "") - devicestate.SetSystemMode(s.mgr, "install") +type dumpDirContents struct { + c *C + dir string +} - // also set the system as installed so that the install-system change - // doesn't get automatically added and we can craft our own change with just - // the setup-run-system task and not with the restart-system-to-run-mode - // task - devicestate.SetInstalledRan(s.mgr, true) +func (d *dumpDirContents) CheckCommentString() string { + cmd := exec.Command("find", d.dir) + data, err := cmd.CombinedOutput() + d.c.Assert(err, IsNil) + return fmt.Sprintf("writable dir contents:\n%s", data) +} - s.state.Unlock() - defer s.state.Lock() +func (s *deviceMgrInstallModeSuite) TestMaybeApplyPreseededData(c *C) { + st := s.state - s.settle(c) + mockTarCmd := testutil.MockCommand(c, "tar", "") + defer mockTarCmd.Restore() - s.state.Lock() - defer s.state.Unlock() + ubuntuSeedDir := dirs.SnapSeedDir + sysLabel := "20220401" + writableDir := filepath.Join(c.MkDir(), "run/mnt/ubuntu-data/system-data") + preseedArtifact := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed.tgz") - // make sure there is no install-system change that snuck in underneath us - installSystem := s.findInstallSystem() - c.Check(installSystem, IsNil) + restore := seed.MockTrusted(s.storeSigning.Trusted) + defer restore() - t := s.state.NewTask("setup-run-system", "setup run system") - chg := s.state.NewChange("install-system", "install the system") - chg.AddTask(t) + // now create a minimal uc20 seed dir with snaps/assertions + ss := &seedtest.SeedSnaps{ + StoreSigning: s.storeSigning, + Brands: s.brands, + } - // now let the change run - s.state.Unlock() - defer s.state.Lock() + seed20 := &seedtest.TestingSeed20{ + SeedSnaps: *ss, + SeedDir: ubuntuSeedDir, + } - s.settle(c) + model := s.setupCore20Seed(seed20, c) - s.state.Lock() - defer s.state.Unlock() + c.Assert(os.MkdirAll(writableDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(preseedArtifact, nil, 0644), IsNil) + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapSeedDir, "snaps"), 0755), IsNil) + c.Assert(os.MkdirAll(dirs.SnapBlobDir, 0755), IsNil) - // now we should have the install-system change - installSystem = s.findInstallSystem() - c.Check(installSystem, Not(IsNil)) - c.Check(installSystem.Err(), IsNil) + st.Lock() + defer st.Unlock() - tasks := installSystem.Tasks() - c.Assert(tasks, HasLen, 1) - setupRunSystemTask := tasks[0] + c.Assert(devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "my-brand", + Model: "my-model", + // no serial in install mode + }), IsNil) - c.Assert(setupRunSystemTask.Kind(), Equals, "setup-run-system") + assertstatetest.AddMany(st, s.brands.AccountsAndKeys("my-brand")...) + assertstatetest.AddMany(st, model) - // we did not request a restart (since that is done in restart-system-to-run-mode) - c.Check(s.restartRequests, HasLen, 0) -} + snaps := []interface{}{ + map[string]interface{}{"name": "snapd", "id": seed20.AssertedSnapID("snapd"), "revision": "1"}, + map[string]interface{}{"name": "core20", "id": seed20.AssertedSnapID("core20"), "revision": "1"}, + map[string]interface{}{"name": "pc-kernel", "id": seed20.AssertedSnapID("pc-kernel"), "revision": "1"}, + map[string]interface{}{"name": "pc", "id": seed20.AssertedSnapID("pc"), "revision": "1"}, + map[string]interface{}{"name": "optional20-a"}, + } + sha3_384, _, err := osutil.FileDigest(preseedArtifact, crypto.SHA3_384) + c.Assert(err, IsNil) + digest, err := asserts.EncodeDigest(crypto.SHA3_384, sha3_384) + c.Assert(err, IsNil) -func (s *deviceMgrInstallModeSuite) TestInstallModeNotInstallmodeNoChg(c *C) { - restore := release.MockOnClassic(false) - defer restore() + preseedAsPath := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed") + s.mockPreseedAssertion(c, model.BrandID(), model.Model(), "16", preseedAsPath, sysLabel, digest, snaps) - s.state.Lock() - devicestate.SetSystemMode(s.mgr, "") - s.state.Unlock() + // set a specific mod time on one of the snaps to verify it's preserved when the blob gets copied. + pastTime, err := time.Parse(time.RFC3339, "2020-01-01T10:00:00Z") + c.Assert(err, IsNil) + c.Assert(os.Chtimes(filepath.Join(ubuntuSeedDir, "snaps", "snapd_1.snap"), pastTime, pastTime), IsNil) - s.settle(c) + // restore root dir, otherwise paths referencing GlobalRootDir, such as from placeInfo.MountFile() get confused + // in the test. + dirs.SetRootDir("/") + preseeded, err := devicestate.MaybeApplyPreseededData(st, ubuntuSeedDir, sysLabel, writableDir) + c.Assert(err, IsNil) + c.Check(preseeded, Equals, true) - s.state.Lock() - defer s.state.Unlock() + c.Check(mockTarCmd.Calls(), DeepEquals, [][]string{ + {"tar", "--extract", "--preserve-permissions", "--preserve-order", "--gunzip", "--directory", writableDir, "-f", preseedArtifact}, + }) - // the install-system change is *not* created (not in install mode) - installSystem := s.findInstallSystem() - c.Assert(installSystem, IsNil) + for _, seedSnap := range []struct { + name string + blob string + }{ + {"snapd/1", "snapd_1.snap"}, + {"core20/1", "core20_1.snap"}, + {"pc-kernel/1", "pc-kernel_1.snap"}, + {"pc/1", "pc_1.snap"}, + {"optional20-a/x1", "optional20-a_x1.snap"}, + } { + c.Assert(osutil.FileExists(filepath.Join(writableDir, "/snap", seedSnap.name)), Equals, true, &dumpDirContents{c, writableDir}) + c.Assert(osutil.FileExists(filepath.Join(writableDir, dirs.SnapBlobDir, seedSnap.blob)), Equals, true, &dumpDirContents{c, writableDir}) + } + + // verify that modtime of the copied snap blob was preserved + finfo, err := os.Stat(filepath.Join(writableDir, dirs.SnapBlobDir, "snapd_1.snap")) + c.Assert(err, IsNil) + c.Check(finfo.ModTime().Equal(pastTime), Equals, true) } -func (s *deviceMgrInstallModeSuite) TestInstallModeNotClassic(c *C) { - restore := release.MockOnClassic(true) - defer restore() +func (s *deviceMgrInstallModeSuite) TestMaybeApplyPreseededDataSnapMismatch(c *C) { + st := s.state - s.state.Lock() - devicestate.SetSystemMode(s.mgr, "install") - s.state.Unlock() + mockTarCmd := testutil.MockCommand(c, "tar", "") + defer mockTarCmd.Restore() - s.settle(c) + snapPath1 := filepath.Join(dirs.GlobalRootDir, "essential-snap_1.snap") + snapPath2 := filepath.Join(dirs.GlobalRootDir, "mode-snap_3.snap") + c.Assert(ioutil.WriteFile(snapPath1, nil, 0644), IsNil) + c.Assert(ioutil.WriteFile(snapPath2, nil, 0644), IsNil) + + restore := devicestate.MockSeedOpen(func(seedDir, label string) (seed.Seed, error) { + return &fakeSeed{ + essentialSnaps: []*seed.Snap{{Path: snapPath1, SideInfo: &snap.SideInfo{RealName: "essential-snap", Revision: snap.R(1), SnapID: "id111111111111111111111111111111"}}}, + modeSnaps: []*seed.Snap{{Path: snapPath2, SideInfo: &snap.SideInfo{RealName: "mode-snap", Revision: snap.R(3), SnapID: "id222222222222222222222222222222"}}, + {Path: snapPath2, SideInfo: &snap.SideInfo{RealName: "mode-snap2"}}}, + }, nil + }) + defer restore() - s.state.Lock() - defer s.state.Unlock() + ubuntuSeedDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-seed") + sysLabel := "20220105" + writableDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-data/system-data") + preseedArtifact := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed.tgz") + c.Assert(os.MkdirAll(filepath.Join(ubuntuSeedDir, "systems", sysLabel), 0755), IsNil) + c.Assert(os.MkdirAll(writableDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(preseedArtifact, nil, 0644), IsNil) - // the install-system change is *not* created (we're on classic) - installSystem := s.findInstallSystem() - c.Assert(installSystem, IsNil) -} + st.Lock() + defer st.Unlock() + model := s.makeMockInstallModel(c, "dangerous") -func (s *deviceMgrInstallModeSuite) TestInstallDangerous(c *C) { - err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{tpm: false, bypass: false, encrypt: false}) + sha3_384, _, err := osutil.FileDigest(preseedArtifact, crypto.SHA3_384) c.Assert(err, IsNil) -} - -func (s *deviceMgrInstallModeSuite) TestInstallDangerousWithTPM(c *C) { - err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{ - tpm: true, bypass: false, encrypt: true, trustedBootloader: true, - }) + digest, err := asserts.EncodeDigest(crypto.SHA3_384, sha3_384) c.Assert(err, IsNil) - c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) -} -func (s *deviceMgrInstallModeSuite) TestInstallDangerousBypassEncryption(c *C) { - err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{tpm: false, bypass: true, encrypt: false}) - c.Assert(err, IsNil) -} + preseedAsPath := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed") -func (s *deviceMgrInstallModeSuite) TestInstallDangerousWithTPMBypassEncryption(c *C) { - err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{tpm: true, bypass: true, encrypt: false}) - c.Assert(err, IsNil) -} + for _, tc := range []struct { + snapName string + rev string + snapID string + err string + }{ + {"essential-snap", "2", "id111111111111111111111111111111", `snap "essential-snap" has wrong revision 1 \(expected: 2\)`}, + {"essential-snap", "1", "id000000000000000000000000000000", `snap "essential-snap" has wrong snap id "id111111111111111111111111111111" \(expected: "id000000000000000000000000000000"\)`}, + {"mode-snap", "4", "id222222222222222222222222222222", `snap "mode-snap" has wrong revision 3 \(expected: 4\)`}, + {"mode-snap", "3", "id000000000000000000000000000000", `snap "mode-snap" has wrong snap id "id222222222222222222222222222222" \(expected: "id000000000000000000000000000000"\)`}, + {"mode-snap2", "3", "id000000000000000000000000000000", `snap "mode-snap2" has wrong revision unset \(expected: 3\)`}, + {"extra-snap", "1", "id000000000000000000000000000000", `seed has 3 snaps but 4 snaps are required by preseed assertion`}, + } { -func (s *deviceMgrInstallModeSuite) TestInstallSigned(c *C) { - err := s.doRunChangeTestWithEncryption(c, "signed", encTestCase{tpm: false, bypass: false, encrypt: false}) - c.Assert(err, IsNil) + preseedAsSnaps := []interface{}{ + map[string]interface{}{"name": "essential-snap", "id": "id111111111111111111111111111111", "revision": "1"}, + map[string]interface{}{"name": "mode-snap", "id": "id222222222222222222222222222222", "revision": "3"}, + map[string]interface{}{"name": "mode-snap2"}, + } + + var found bool + for i, ps := range preseedAsSnaps { + if ps.(map[string]interface{})["name"] == tc.snapName { + preseedAsSnaps[i] = map[string]interface{}{"name": tc.snapName, "id": tc.snapID, "revision": tc.rev} + found = true + break + } + } + if !found { + preseedAsSnaps = append(preseedAsSnaps, map[string]interface{}{"name": tc.snapName, "id": tc.snapID, "revision": tc.rev}) + } + + s.mockPreseedAssertion(c, model.BrandID(), model.Model(), "16", preseedAsPath, sysLabel, digest, preseedAsSnaps) + _, err = devicestate.MaybeApplyPreseededData(st, ubuntuSeedDir, sysLabel, writableDir) + c.Assert(err, ErrorMatches, tc.err) + } + + // mode-snap is presend in the seed but missing in the preseed assertion; add other-snap to preseed assertion + // to satisfy the check for number of snaps. + preseedAsSnaps := []interface{}{ + map[string]interface{}{"name": "essential-snap", "id": "id111111111111111111111111111111", "revision": "1"}, + map[string]interface{}{"name": "other-snap", "id": "id333222222222222222222222222222", "revision": "2"}, + map[string]interface{}{"name": "mode-snap2"}, + } + s.mockPreseedAssertion(c, model.BrandID(), model.Model(), "16", preseedAsPath, sysLabel, digest, preseedAsSnaps) + _, err = devicestate.MaybeApplyPreseededData(st, ubuntuSeedDir, sysLabel, writableDir) + c.Assert(err, ErrorMatches, `snap "mode-snap" not present in the preseed assertion`) } -func (s *deviceMgrInstallModeSuite) TestInstallSignedWithTPM(c *C) { - err := s.doRunChangeTestWithEncryption(c, "signed", encTestCase{ - tpm: true, bypass: false, encrypt: true, trustedBootloader: true, +func (s *deviceMgrInstallModeSuite) TestMaybeApplyPreseededSysLabelMismatch(c *C) { + st := s.state + + mockTarCmd := testutil.MockCommand(c, "tar", "") + defer mockTarCmd.Restore() + + snapPath1 := filepath.Join(dirs.GlobalRootDir, "essential-snap_1.snap") + c.Assert(ioutil.WriteFile(snapPath1, nil, 0644), IsNil) + + restore := devicestate.MockSeedOpen(func(seedDir, label string) (seed.Seed, error) { + return &fakeSeed{ + essentialSnaps: []*seed.Snap{{Path: snapPath1, SideInfo: &snap.SideInfo{RealName: "essential-snap", Revision: snap.R(1)}}}, + }, nil }) - c.Assert(err, IsNil) - c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) -} + defer restore() -func (s *deviceMgrInstallModeSuite) TestInstallSignedBypassEncryption(c *C) { - err := s.doRunChangeTestWithEncryption(c, "signed", encTestCase{tpm: false, bypass: true, encrypt: false}) + ubuntuSeedDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-seed") + sysLabel := "20220105" + writableDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-data/system-data") + preseedArtifact := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed.tgz") + c.Assert(os.MkdirAll(filepath.Join(ubuntuSeedDir, "systems", sysLabel), 0755), IsNil) + c.Assert(os.MkdirAll(writableDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(preseedArtifact, nil, 0644), IsNil) + + st.Lock() + defer st.Unlock() + model := s.makeMockInstallModel(c, "dangerous") + + snaps := []interface{}{ + map[string]interface{}{"name": "essential-snap", "id": "id111111111111111111111111111111", "revision": "1"}, + } + sha3_384, _, err := osutil.FileDigest(preseedArtifact, crypto.SHA3_384) + c.Assert(err, IsNil) + digest, err := asserts.EncodeDigest(crypto.SHA3_384, sha3_384) c.Assert(err, IsNil) -} -func (s *deviceMgrInstallModeSuite) TestInstallSecured(c *C) { - err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{tpm: false, bypass: false, encrypt: false}) - c.Assert(err, ErrorMatches, "(?s).*cannot encrypt device storage as mandated by model grade secured:.*TPM not available.*") + preseedAsPath := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed") + s.mockPreseedAssertion(c, model.BrandID(), model.Model(), "16", preseedAsPath, "wrong-label", digest, snaps) + + _, err = devicestate.MaybeApplyPreseededData(st, ubuntuSeedDir, sysLabel, writableDir) + c.Assert(err, ErrorMatches, `preseed assertion system label "wrong-label" doesn't match system label "20220105"`) } -func (s *deviceMgrInstallModeSuite) TestInstallSecuredWithTPM(c *C) { - err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ - tpm: true, bypass: false, encrypt: true, trustedBootloader: true, +func (s *deviceMgrInstallModeSuite) TestMaybeApplyPreseededDataWrongDigest(c *C) { + st := s.state + + mockTarCmd := testutil.MockCommand(c, "tar", "") + defer mockTarCmd.Restore() + + snapPath1 := filepath.Join(dirs.GlobalRootDir, "essential-snap_1.snap") + c.Assert(ioutil.WriteFile(snapPath1, nil, 0644), IsNil) + + restore := devicestate.MockSeedOpen(func(seedDir, label string) (seed.Seed, error) { + return &fakeSeed{ + essentialSnaps: []*seed.Snap{{Path: snapPath1, SideInfo: &snap.SideInfo{RealName: "essential-snap", Revision: snap.R(1)}}}, + }, nil }) - c.Assert(err, IsNil) - c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) + defer restore() + + ubuntuSeedDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-seed") + sysLabel := "20220105" + writableDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-data/system-data") + preseedArtifact := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed.tgz") + c.Assert(os.MkdirAll(filepath.Join(ubuntuSeedDir, "systems", sysLabel), 0755), IsNil) + c.Assert(os.MkdirAll(writableDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(preseedArtifact, nil, 0644), IsNil) + + st.Lock() + defer st.Unlock() + model := s.makeMockInstallModel(c, "dangerous") + + snaps := []interface{}{ + map[string]interface{}{"name": "essential-snap", "id": "id111111111111111111111111111111", "revision": "1"}, + } + + wrongDigest := "DGOnW4ReT30BEH2FLkwkhcUaUKqqlPxhmV5xu-6YOirDcTgxJkrbR_traaaY1fAE" + preseedAsPath := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed") + s.mockPreseedAssertion(c, model.BrandID(), model.Model(), "16", preseedAsPath, sysLabel, wrongDigest, snaps) + + _, err := devicestate.MaybeApplyPreseededData(st, ubuntuSeedDir, sysLabel, writableDir) + c.Assert(err, ErrorMatches, `invalid preseed artifact digest`) } -func (s *deviceMgrInstallModeSuite) TestInstallDangerousEncryptionWithTPMNoTrustedAssets(c *C) { - err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{ - tpm: true, bypass: false, encrypt: true, trustedBootloader: false, +func (s *deviceMgrInstallModeSuite) TestMaybeApplyPreseededModelMismatch(c *C) { + st := s.state + + mockTarCmd := testutil.MockCommand(c, "tar", "") + defer mockTarCmd.Restore() + + snapPath1 := filepath.Join(dirs.GlobalRootDir, "essential-snap_1.snap") + c.Assert(ioutil.WriteFile(snapPath1, nil, 0644), IsNil) + + restore := devicestate.MockSeedOpen(func(seedDir, label string) (seed.Seed, error) { + return &fakeSeed{ + essentialSnaps: []*seed.Snap{{Path: snapPath1, SideInfo: &snap.SideInfo{RealName: "essential-snap", Revision: snap.R(1)}}}, + }, nil }) - c.Assert(err, IsNil) - c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) -} + defer restore() -func (s *deviceMgrInstallModeSuite) TestInstallDangerousNoEncryptionWithTrustedAssets(c *C) { - err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{ - tpm: false, bypass: false, encrypt: false, trustedBootloader: true, + ubuntuSeedDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-seed") + sysLabel := "20220105" + writableDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-data/system-data") + preseedArtifact := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed.tgz") + c.Assert(os.MkdirAll(filepath.Join(ubuntuSeedDir, "systems", sysLabel), 0755), IsNil) + c.Assert(os.MkdirAll(writableDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(preseedArtifact, nil, 0644), IsNil) + + st.Lock() + defer st.Unlock() + + s.brands.Register("other-brand", brandPrivKey3, map[string]interface{}{ + "display-name": "other publisher", }) + + model := s.makeMockInstallModel(c, "dangerous") + + snaps := []interface{}{ + map[string]interface{}{"name": "essential-snap", "id": "id111111111111111111111111111111", "revision": "1"}, + } + + sha3_384, _, err := osutil.FileDigest(preseedArtifact, crypto.SHA3_384) c.Assert(err, IsNil) + digest, err := asserts.EncodeDigest(crypto.SHA3_384, sha3_384) + c.Assert(err, IsNil) + + preseedAsPath := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed") + + for _, tc := range []struct { + brandID string + modelName string + series string + err string + }{ + {"other-brand", model.Model(), "16", `preseed assertion brand "other-brand" doesn't match model brand "my-brand"`}, + {model.BrandID(), "other-model", "16", `preseed assertion model "other-model" doesn't match the model "my-model"`}, + {model.BrandID(), model.Model(), "99", `preseed assertion series "99" doesn't match model series "16"`}, + } { + s.mockPreseedAssertion(c, tc.brandID, tc.modelName, tc.series, preseedAsPath, sysLabel, digest, snaps) + _, err := devicestate.MaybeApplyPreseededData(st, ubuntuSeedDir, sysLabel, writableDir) + c.Assert(err, ErrorMatches, tc.err) + } } -func (s *deviceMgrInstallModeSuite) TestInstallSecuredWithTPMAndSave(c *C) { - err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ - tpm: true, bypass: false, encrypt: true, trustedBootloader: true, +func (s *deviceMgrInstallModeSuite) TestMaybeApplyPreseededAssertionMissing(c *C) { + st := s.state + + mockTarCmd := testutil.MockCommand(c, "tar", "") + defer mockTarCmd.Restore() + + snapPath1 := filepath.Join(dirs.GlobalRootDir, "essential-snap_1.snap") + c.Assert(ioutil.WriteFile(snapPath1, nil, 0644), IsNil) + + restore := devicestate.MockSeedOpen(func(seedDir, label string) (seed.Seed, error) { + return &fakeSeed{ + essentialSnaps: []*seed.Snap{{Path: snapPath1, SideInfo: &snap.SideInfo{RealName: "essential-snap", Revision: snap.R(1)}}}, + }, nil }) - c.Assert(err, IsNil) - c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) - c.Check(filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key"), testutil.FileEquals, []byte(saveKey)) - c.Check(filepath.Join(boot.InstallHostFDEDataDir, "reinstall.key"), testutil.FileEquals, reinstallKey[:]) - marker, err := ioutil.ReadFile(filepath.Join(boot.InstallHostFDEDataDir, "marker")) - c.Assert(err, IsNil) - c.Check(marker, HasLen, 32) - c.Check(filepath.Join(boot.InstallHostFDESaveDir, "marker"), testutil.FileEquals, marker) + defer restore() + + ubuntuSeedDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-seed") + sysLabel := "20220105" + writableDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-data/system-data") + preseedArtifact := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed.tgz") + c.Assert(os.MkdirAll(filepath.Join(ubuntuSeedDir, "systems", sysLabel), 0755), IsNil) + c.Assert(os.MkdirAll(writableDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(preseedArtifact, nil, 0644), IsNil) + + st.Lock() + defer st.Unlock() + + s.makeMockInstallModel(c, "dangerous") + + _, err := devicestate.MaybeApplyPreseededData(st, ubuntuSeedDir, sysLabel, writableDir) + c.Assert(err, ErrorMatches, `cannot read preseed assertion:.*`) + + preseedAsPath := filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed") + // empty "preseed" assertion file + c.Assert(ioutil.WriteFile(preseedAsPath, nil, 0644), IsNil) + + _, err = devicestate.MaybeApplyPreseededData(st, ubuntuSeedDir, sysLabel, writableDir) + c.Assert(err, ErrorMatches, `internal error: preseed assertion file is present but preseed assertion not found`) } -func (s *deviceMgrInstallModeSuite) TestInstallSecuredBypassEncryption(c *C) { - err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{tpm: false, bypass: true, encrypt: false}) - c.Assert(err, ErrorMatches, "(?s).*cannot encrypt device storage as mandated by model grade secured:.*TPM not available.*") +func (s *deviceMgrInstallModeSuite) TestMaybeApplyPreseededNoopIfNoArtifact(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + mockTarCmd := testutil.MockCommand(c, "tar", "") + defer mockTarCmd.Restore() + + ubuntuSeedDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-seed") + sysLabel := "20220105" + writableDir := filepath.Join(dirs.GlobalRootDir, "run/mnt/ubuntu-data/system-data") + c.Assert(os.MkdirAll(filepath.Join(ubuntuSeedDir, "systems", sysLabel), 0755), IsNil) + c.Assert(os.MkdirAll(writableDir, 0755), IsNil) + preseeded, err := devicestate.MaybeApplyPreseededData(st, ubuntuSeedDir, sysLabel, writableDir) + c.Assert(err, IsNil) + c.Check(preseeded, Equals, false) + c.Check(mockTarCmd.Calls(), HasLen, 0) } -func (s *deviceMgrInstallModeSuite) TestInstallBootloaderVarSetFails(c *C) { - restore := devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { - c.Check(options.EncryptionType, Equals, secboot.EncryptionTypeNone) - // no keys set - return &install.InstalledSystemSideData{}, nil - }) +func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookExpTasks(c *C) { + restore := release.MockOnClassic(false) defer restore() - restore = devicestate.MockBootEnsureNextBootToRunMode(func(systemLabel string) error { - c.Check(systemLabel, Equals, "1234") - // no keys set - return fmt.Errorf("bootloader goes boom") + restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { + return nil, nil }) defer restore() - restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return fmt.Errorf("no encrypted soup for you") }) + hooksCalled := []*hookstate.Context{} + restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { + ctx.Lock() + defer ctx.Unlock() + + hooksCalled = append(hooksCalled, ctx) + return nil, nil + }) defer restore() err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), - []byte("mode=install\nrecovery_system=1234"), 0644) + []byte("mode=install\n"), 0644) c.Assert(err, IsNil) s.state.Lock() s.makeMockInstallModel(c, "dangerous") - s.makeMockInstalledPcGadget(c, "", "") + s.makeMockInstalledPcGadget(c, "install-device-hook-content", "") devicestate.SetSystemMode(s.mgr, "install") s.state.Unlock() @@ -841,17 +1098,60 @@ defer s.state.Unlock() installSystem := s.findInstallSystem() - c.Check(installSystem.Err(), ErrorMatches, `cannot perform the following tasks: -- Ensure next boot to run mode \(bootloader goes boom\)`) - // no restart request on failure - c.Check(s.restartRequests, HasLen, 0) + c.Check(installSystem.Err(), IsNil) + + tasks := installSystem.Tasks() + c.Assert(tasks, HasLen, 3) + setupRunSystemTask := tasks[0] + installDevice := tasks[1] + restartSystemToRunModeTask := tasks[2] + + c.Assert(setupRunSystemTask.Kind(), Equals, "setup-run-system") + c.Assert(restartSystemToRunModeTask.Kind(), Equals, "restart-system-to-run-mode") + c.Assert(installDevice.Kind(), Equals, "run-hook") + + // setup-run-system has no pre-reqs + c.Assert(setupRunSystemTask.WaitTasks(), HasLen, 0) + + // install-device has a pre-req of setup-run-system + waitTasks := installDevice.WaitTasks() + c.Assert(waitTasks, HasLen, 1) + c.Assert(waitTasks[0].ID(), Equals, setupRunSystemTask.ID()) + + // install-device restart-task references to restart-system-to-run-mode + var restartTask string + err = installDevice.Get("restart-task", &restartTask) + c.Assert(err, IsNil) + c.Check(restartTask, Equals, restartSystemToRunModeTask.ID()) + + // restart-system-to-run-mode has a pre-req of install-device + waitTasks = restartSystemToRunModeTask.WaitTasks() + c.Assert(waitTasks, HasLen, 1) + c.Assert(waitTasks[0].ID(), Equals, installDevice.ID()) + + // we did request a restart through restartSystemToRunModeTask + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) + + c.Assert(hooksCalled, HasLen, 1) + c.Assert(hooksCalled[0].HookName(), Equals, "install-device") } -func (s *deviceMgrInstallModeSuite) testInstallEncryptionSanityChecks(c *C, errMatch string) { +func (s *deviceMgrInstallModeSuite) testInstallWithInstallDeviceHookSnapctlReboot(c *C, arg string, rst restart.RestartType) { restore := release.MockOnClassic(false) defer restore() - restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return nil }) + restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { + return nil, nil + }) + defer restore() + + restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { + c.Assert(ctx.HookName(), Equals, "install-device") + + // snapctl reboot --halt + _, _, err := ctlcmd.Run(ctx, []string{"reboot", arg}, 0) + return nil, err + }) defer restore() err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), @@ -860,7 +1160,7 @@ s.state.Lock() s.makeMockInstallModel(c, "dangerous") - s.makeMockInstalledPcGadget(c, "", "") + s.makeMockInstalledPcGadget(c, "install-device-hook-content", "") devicestate.SetSystemMode(s.mgr, "install") s.state.Unlock() @@ -870,37 +1170,21 @@ defer s.state.Unlock() installSystem := s.findInstallSystem() - c.Check(installSystem.Err(), ErrorMatches, errMatch) - // no restart request on failure - c.Check(s.restartRequests, HasLen, 0) + c.Check(installSystem.Err(), IsNil) + + // we did end up requesting the right shutdown + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{rst}) } -func (s *deviceMgrInstallModeSuite) TestInstallEncryptionSanityChecksNoKeys(c *C) { - restore := devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { - c.Check(options.EncryptionType, Equals, secboot.EncryptionTypeLUKS) - // no keys set - return &install.InstalledSystemSideData{}, nil - }) - defer restore() - s.testInstallEncryptionSanityChecks(c, `(?ms)cannot perform the following tasks: -- Setup system for run mode \(internal error: system encryption keys are unset\)`) +func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookSnapctlRebootHalt(c *C) { + s.testInstallWithInstallDeviceHookSnapctlReboot(c, "--halt", restart.RestartSystemHaltNow) } -func (s *deviceMgrInstallModeSuite) TestInstallEncryptionSanityChecksNoSystemDataKey(c *C) { - restore := devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { - c.Check(options.EncryptionType, Equals, secboot.EncryptionTypeLUKS) - // no keys set - return &install.InstalledSystemSideData{ - // empty map - KeysForRoles: map[string]*install.EncryptionKeySet{}, - }, nil - }) - defer restore() - s.testInstallEncryptionSanityChecks(c, `(?ms)cannot perform the following tasks: -- Setup system for run mode \(internal error: system encryption keys are unset\)`) +func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookSnapctlRebootPoweroff(c *C) { + s.testInstallWithInstallDeviceHookSnapctlReboot(c, "--poweroff", restart.RestartSystemPoweroffNow) } -func (s *deviceMgrInstallModeSuite) mockInstallModeChange(c *C, modelGrade, gadgetDefaultsYaml string) *asserts.Model { +func (s *deviceMgrInstallModeSuite) TestInstallWithBrokenInstallDeviceHookUnhappy(c *C) { restore := release.MockOnClassic(false) defer restore() @@ -909,254 +1193,292 @@ }) defer restore() - s.state.Lock() - mockModel := s.makeMockInstallModel(c, modelGrade) - s.makeMockInstalledPcGadget(c, "", gadgetDefaultsYaml) - s.state.Unlock() - c.Check(mockModel.Grade(), Equals, asserts.ModelGrade(modelGrade)) + hooksCalled := []*hookstate.Context{} + restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { + ctx.Lock() + defer ctx.Unlock() - restore = devicestate.MockBootMakeSystemRunnable(func(model *asserts.Model, bootWith *boot.BootableSet, seal *boot.TrustedAssetsInstallObserver) error { - return nil + hooksCalled = append(hooksCalled, ctx) + return []byte("hook exited broken"), fmt.Errorf("hook broken") }) defer restore() - modeenv := boot.Modeenv{ - Mode: "install", - RecoverySystem: "20191218", - } - c.Assert(modeenv.WriteTo(""), IsNil) - devicestate.SetSystemMode(s.mgr, "install") - - // normally done by snap-bootstrap - err := os.MkdirAll(boot.InitramfsUbuntuBootDir, 0755) + err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), + []byte("mode=install\n"), 0644) c.Assert(err, IsNil) + s.state.Lock() + s.makeMockInstallModel(c, "dangerous") + s.makeMockInstalledPcGadget(c, "install-device-hook-content", "") + devicestate.SetSystemMode(s.mgr, "install") + s.state.Unlock() + s.settle(c) - return mockModel + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Check(installSystem.Err(), ErrorMatches, `cannot perform the following tasks: +- Run install-device hook \(run hook \"install-device\": hook exited broken\)`) + + tasks := installSystem.Tasks() + c.Assert(tasks, HasLen, 3) + setupRunSystemTask := tasks[0] + installDevice := tasks[1] + restartSystemToRunModeTask := tasks[2] + + c.Assert(setupRunSystemTask.Kind(), Equals, "setup-run-system") + c.Assert(installDevice.Kind(), Equals, "run-hook") + c.Assert(restartSystemToRunModeTask.Kind(), Equals, "restart-system-to-run-mode") + + // install-device is in Error state + c.Assert(installDevice.Status(), Equals, state.ErrorStatus) + + // setup-run-system is in Done (it has no undo handler) + c.Assert(setupRunSystemTask.Status(), Equals, state.DoneStatus) + + // restart-system-to-run-mode is in Hold + c.Assert(restartSystemToRunModeTask.Status(), Equals, state.HoldStatus) + + // we didn't request a restart since restartsystemToRunMode didn't run + c.Check(s.restartRequests, HasLen, 0) + + c.Assert(hooksCalled, HasLen, 1) + c.Assert(hooksCalled[0].HookName(), Equals, "install-device") } -func (s *deviceMgrInstallModeSuite) TestInstallModeRunSysconfig(c *C) { - s.mockInstallModeChange(c, "dangerous", "") +func (s *deviceMgrInstallModeSuite) TestInstallSetupRunSystemTaskNoRestarts(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { + return nil, nil + }) + defer restore() + + err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), + []byte("mode=install\n"), 0644) + c.Assert(err, IsNil) s.state.Lock() defer s.state.Unlock() - // the install-system change is created + s.makeMockInstallModel(c, "dangerous") + s.makeMockInstalledPcGadget(c, "", "") + devicestate.SetSystemMode(s.mgr, "install") + + // also set the system as installed so that the install-system change + // doesn't get automatically added and we can craft our own change with just + // the setup-run-system task and not with the restart-system-to-run-mode + // task + devicestate.SetInstalledRan(s.mgr, true) + + s.state.Unlock() + defer s.state.Lock() + + s.settle(c) + + s.state.Lock() + defer s.state.Unlock() + + // make sure there is no install-system change that snuck in underneath us installSystem := s.findInstallSystem() - c.Assert(installSystem, NotNil) + c.Check(installSystem, IsNil) - // and was run successfully + t := s.state.NewTask("setup-run-system", "setup run system") + chg := s.state.NewChange("install-system", "install the system") + chg.AddTask(t) + + // now let the change run + s.state.Unlock() + defer s.state.Lock() + + s.settle(c) + + s.state.Lock() + defer s.state.Unlock() + + // now we should have the install-system change + installSystem = s.findInstallSystem() + c.Check(installSystem, Not(IsNil)) c.Check(installSystem.Err(), IsNil) - c.Check(installSystem.Status(), Equals, state.DoneStatus) - // and sysconfig.ConfigureTargetSystem was run exactly once - c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ - { - AllowCloudInit: true, - TargetRootDir: boot.InstallHostWritableDir, - GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), - }, - }) + tasks := installSystem.Tasks() + c.Assert(tasks, HasLen, 1) + setupRunSystemTask := tasks[0] - // and the special dirs in _writable_defaults were created - for _, dir := range []string{"/etc/udev/rules.d/", "/etc/modules-load.d/", "/etc/modprobe.d/"} { - fullDir := filepath.Join(sysconfig.WritableDefaultsDir(boot.InstallHostWritableDir), dir) - c.Assert(fullDir, testutil.FilePresent) - } + c.Assert(setupRunSystemTask.Kind(), Equals, "setup-run-system") + + // we did not request a restart (since that is done in restart-system-to-run-mode) + c.Check(s.restartRequests, HasLen, 0) } -func (s *deviceMgrInstallModeSuite) TestInstallModeRunSysconfigErr(c *C) { - s.ConfigureTargetSystemErr = fmt.Errorf("error from sysconfig.ConfigureTargetSystem") - s.mockInstallModeChange(c, "dangerous", "") +func (s *deviceMgrInstallModeSuite) TestInstallModeNotInstallmodeNoChg(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + s.state.Lock() + devicestate.SetSystemMode(s.mgr, "") + s.state.Unlock() + + s.settle(c) s.state.Lock() defer s.state.Unlock() - // the install-system was run but errorred as specified in the above mock + // the install-system change is *not* created (not in install mode) installSystem := s.findInstallSystem() - c.Check(installSystem.Err(), ErrorMatches, `(?ms)cannot perform the following tasks: -- Setup system for run mode \(error from sysconfig.ConfigureTargetSystem\)`) - // and sysconfig.ConfigureTargetSystem was run exactly once - c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ - { - AllowCloudInit: true, - TargetRootDir: boot.InstallHostWritableDir, - GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), - }, - }) + c.Assert(installSystem, IsNil) } -func (s *deviceMgrInstallModeSuite) TestInstallModeSupportsCloudInitInDangerous(c *C) { - // pretend we have a cloud-init config on the seed partition - cloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d") - err := os.MkdirAll(cloudCfg, 0755) - c.Assert(err, IsNil) - for _, mockCfg := range []string{"foo.cfg", "bar.cfg"} { - err = ioutil.WriteFile(filepath.Join(cloudCfg, mockCfg), []byte(fmt.Sprintf("%s config", mockCfg)), 0644) - c.Assert(err, IsNil) - } +func (s *deviceMgrInstallModeSuite) TestInstallModeNotClassic(c *C) { + restore := release.MockOnClassic(true) + defer restore() - s.mockInstallModeChange(c, "dangerous", "") + s.state.Lock() + devicestate.SetSystemMode(s.mgr, "install") + s.state.Unlock() - // and did tell sysconfig about the cloud-init files - c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ - { - AllowCloudInit: true, - CloudInitSrcDir: filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d"), - TargetRootDir: boot.InstallHostWritableDir, - GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), - }, - }) + s.settle(c) + + s.state.Lock() + defer s.state.Unlock() + + // the install-system change is *not* created (we're on classic) + installSystem := s.findInstallSystem() + c.Assert(installSystem, IsNil) } -func (s *deviceMgrInstallModeSuite) TestInstallModeSupportsCloudInitGadgetAndSeedConfigSigned(c *C) { - // pretend we have a cloud-init config on the seed partition - cloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d") - err := os.MkdirAll(cloudCfg, 0755) +func (s *deviceMgrInstallModeSuite) TestInstallDangerous(c *C) { + err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{tpm: false, bypass: false, encrypt: false}) c.Assert(err, IsNil) - for _, mockCfg := range []string{"foo.cfg", "bar.cfg"} { - err = ioutil.WriteFile(filepath.Join(cloudCfg, mockCfg), []byte(fmt.Sprintf("%s config", mockCfg)), 0644) - c.Assert(err, IsNil) - } +} - // we also have gadget cloud init too - gadgetDir := filepath.Join(dirs.SnapMountDir, "pc/1/") - err = os.MkdirAll(gadgetDir, 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), nil, 0644) +func (s *deviceMgrInstallModeSuite) TestInstallDangerousWithTPM(c *C) { + err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) c.Assert(err, IsNil) +} - s.mockInstallModeChange(c, "signed", "") - - // sysconfig is told about both configs - c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ - { - AllowCloudInit: true, - TargetRootDir: boot.InstallHostWritableDir, - GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), - CloudInitSrcDir: cloudCfg, - }, - }) +func (s *deviceMgrInstallModeSuite) TestInstallDangerousBypassEncryption(c *C) { + err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{tpm: false, bypass: true, encrypt: false}) + c.Assert(err, IsNil) } -func (s *deviceMgrInstallModeSuite) TestInstallModeSupportsCloudInitBothGadgetAndUbuntuSeedDangerous(c *C) { - // pretend we have a cloud-init config on the seed partition - cloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d") - err := os.MkdirAll(cloudCfg, 0755) +func (s *deviceMgrInstallModeSuite) TestInstallDangerousWithTPMBypassEncryption(c *C) { + err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{tpm: true, bypass: true, encrypt: false}) c.Assert(err, IsNil) - for _, mockCfg := range []string{"foo.cfg", "bar.cfg"} { - err = ioutil.WriteFile(filepath.Join(cloudCfg, mockCfg), []byte(fmt.Sprintf("%s config", mockCfg)), 0644) - c.Assert(err, IsNil) - } +} - // we also have gadget cloud init too - gadgetDir := filepath.Join(dirs.SnapMountDir, "pc/1/") - err = os.MkdirAll(gadgetDir, 0755) +func (s *deviceMgrInstallModeSuite) TestInstallSigned(c *C) { + err := s.doRunChangeTestWithEncryption(c, "signed", encTestCase{tpm: false, bypass: false, encrypt: false}) c.Assert(err, IsNil) - err = ioutil.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), nil, 0644) +} + +func (s *deviceMgrInstallModeSuite) TestInstallSignedWithTPM(c *C) { + err := s.doRunChangeTestWithEncryption(c, "signed", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) c.Assert(err, IsNil) +} - s.mockInstallModeChange(c, "dangerous", "") +func (s *deviceMgrInstallModeSuite) TestInstallSignedBypassEncryption(c *C) { + err := s.doRunChangeTestWithEncryption(c, "signed", encTestCase{tpm: false, bypass: true, encrypt: false}) + c.Assert(err, IsNil) +} - // and did tell sysconfig about the cloud-init files - c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ - { - AllowCloudInit: true, - CloudInitSrcDir: filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d"), - TargetRootDir: boot.InstallHostWritableDir, - GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), - }, - }) +func (s *deviceMgrInstallModeSuite) TestInstallSecured(c *C) { + err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{tpm: false, bypass: false, encrypt: false}) + c.Assert(err, ErrorMatches, "(?s).*cannot encrypt device storage as mandated by model grade secured:.*TPM not available.*") } -func (s *deviceMgrInstallModeSuite) TestInstallModeSignedNoUbuntuSeedCloudInit(c *C) { - // pretend we have no cloud-init config anywhere - s.mockInstallModeChange(c, "signed", "") - - // we didn't pass any cloud-init src dir but still left cloud-init enabled - // if for example a CI-DATA USB drive was provided at runtime - c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ - { - AllowCloudInit: true, - TargetRootDir: boot.InstallHostWritableDir, - GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), - }, +func (s *deviceMgrInstallModeSuite) TestInstallSecuredWithTPM(c *C) { + err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, }) + c.Assert(err, IsNil) } -func (s *deviceMgrInstallModeSuite) TestInstallModeSecuredGadgetCloudConfCloudInit(c *C) { - // pretend we have a cloud.conf from the gadget - gadgetDir := filepath.Join(dirs.SnapMountDir, "pc/1/") - err := os.MkdirAll(gadgetDir, 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), nil, 0644) +func (s *deviceMgrInstallModeSuite) TestInstallDangerousEncryptionWithTPMNoTrustedAssets(c *C) { + err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: false, + }) c.Assert(err, IsNil) +} - err = s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ - tpm: true, bypass: false, encrypt: true, trustedBootloader: true, +func (s *deviceMgrInstallModeSuite) TestInstallDangerousNoEncryptionWithTrustedAssets(c *C) { + err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{ + tpm: false, bypass: false, encrypt: false, trustedBootloader: true, }) c.Assert(err, IsNil) +} - c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ - { - AllowCloudInit: true, - TargetRootDir: boot.InstallHostWritableDir, - GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), - }, +func (s *deviceMgrInstallModeSuite) TestInstallSecuredWithTPMAndSave(c *C) { + err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, }) + c.Assert(err, IsNil) + c.Check(filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key"), testutil.FileEquals, []byte(saveKey)) + marker, err := ioutil.ReadFile(filepath.Join(boot.InstallHostFDEDataDir, "marker")) + c.Assert(err, IsNil) + c.Check(marker, HasLen, 32) + c.Check(filepath.Join(boot.InstallHostFDESaveDir, "marker"), testutil.FileEquals, marker) } -func (s *deviceMgrInstallModeSuite) TestInstallModeSecuredNoUbuntuSeedCloudInit(c *C) { - // pretend we have a cloud-init config on the seed partition with some files - cloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d") - err := os.MkdirAll(cloudCfg, 0755) - c.Assert(err, IsNil) - for _, mockCfg := range []string{"foo.cfg", "bar.cfg"} { - err = ioutil.WriteFile(filepath.Join(cloudCfg, mockCfg), []byte(fmt.Sprintf("%s config", mockCfg)), 0644) - c.Assert(err, IsNil) - } +func (s *deviceMgrInstallModeSuite) TestInstallSecuredBypassEncryption(c *C) { + err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{tpm: false, bypass: true, encrypt: false}) + c.Assert(err, ErrorMatches, "(?s).*cannot encrypt device storage as mandated by model grade secured:.*TPM not available.*") +} - err = s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ - tpm: true, bypass: false, encrypt: true, trustedBootloader: true, +func (s *deviceMgrInstallModeSuite) TestInstallBootloaderVarSetFails(c *C) { + restore := devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { + c.Check(options.EncryptionType, Equals, secboot.EncryptionTypeNone) + // no keys set + return &install.InstalledSystemSideData{}, nil }) - c.Assert(err, IsNil) + defer restore() - // we did tell sysconfig about the ubuntu-seed cloud config dir because it - // exists, but it is up to sysconfig to use the model to determine to ignore - // the files - c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ - { - AllowCloudInit: false, - TargetRootDir: boot.InstallHostWritableDir, - GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), - CloudInitSrcDir: cloudCfg, - }, + restore = devicestate.MockBootEnsureNextBootToRunMode(func(systemLabel string) error { + c.Check(systemLabel, Equals, "1234") + // no keys set + return fmt.Errorf("bootloader goes boom") }) -} + defer restore() -func (s *deviceMgrInstallModeSuite) TestInstallModeWritesModel(c *C) { - // pretend we have a cloud-init config on the seed partition - model := s.mockInstallModeChange(c, "dangerous", "") + restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return fmt.Errorf("no encrypted soup for you") }) + defer restore() - var buf bytes.Buffer - err := asserts.NewEncoder(&buf).Encode(model) + err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), + []byte("mode=install\nrecovery_system=1234"), 0644) c.Assert(err, IsNil) s.state.Lock() + s.makeMockInstallModel(c, "dangerous") + s.makeMockInstalledPcGadget(c, "", "") + devicestate.SetSystemMode(s.mgr, "install") + s.state.Unlock() + + s.settle(c) + + s.state.Lock() defer s.state.Unlock() installSystem := s.findInstallSystem() - c.Assert(installSystem, NotNil) + c.Check(installSystem.Err(), ErrorMatches, `cannot perform the following tasks: +- Ensure next boot to run mode \(bootloader goes boom\)`) + // no restart request on failure + c.Check(s.restartRequests, HasLen, 0) +} - // and was run successfully - c.Check(installSystem.Err(), IsNil) - c.Check(installSystem.Status(), Equals, state.DoneStatus) +func (s *deviceMgrInstallModeSuite) testInstallEncryptionValidityChecks(c *C, errMatch string) { + restore := release.MockOnClassic(false) + defer restore() - c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), testutil.FileEquals, buf.String()) -} + restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return nil }) + defer restore() -func (s *deviceMgrInstallModeSuite) testInstallGadgetNoSave(c *C) { err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), []byte("mode=install\n"), 0644) c.Assert(err, IsNil) @@ -1164,44 +1486,46 @@ s.state.Lock() s.makeMockInstallModel(c, "dangerous") s.makeMockInstalledPcGadget(c, "", "") - info, err := snapstate.CurrentInfo(s.state, "pc") - c.Assert(err, IsNil) - // replace gadget yaml with one that has no ubuntu-save - c.Assert(uc20gadgetYaml, Not(testutil.Contains), "ubuntu-save") - err = ioutil.WriteFile(filepath.Join(info.MountDir(), "meta/gadget.yaml"), []byte(uc20gadgetYaml), 0644) - c.Assert(err, IsNil) devicestate.SetSystemMode(s.mgr, "install") s.state.Unlock() s.settle(c) -} - -func (s *deviceMgrInstallModeSuite) TestInstallWithEncryptionValidatesGadgetErr(c *C) { - restore := release.MockOnClassic(false) - defer restore() - - restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { - return nil, fmt.Errorf("unexpected call") - }) - defer restore() - - // pretend we have a TPM - restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return nil }) - defer restore() - - s.testInstallGadgetNoSave(c) s.state.Lock() defer s.state.Unlock() installSystem := s.findInstallSystem() - c.Check(installSystem.Err(), ErrorMatches, `(?ms)cannot perform the following tasks: -- Setup system for run mode \(cannot use gadget: gadget does not support encrypted data: required partition with system-save role is missing\)`) + c.Check(installSystem.Err(), ErrorMatches, errMatch) // no restart request on failure c.Check(s.restartRequests, HasLen, 0) } -func (s *deviceMgrInstallModeSuite) TestInstallWithoutEncryptionValidatesGadgetWithoutSaveHappy(c *C) { +func (s *deviceMgrInstallModeSuite) TestInstallEncryptionValidityChecksNoKeys(c *C) { + restore := devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { + c.Check(options.EncryptionType, Equals, secboot.EncryptionTypeLUKS) + // no keys set + return &install.InstalledSystemSideData{}, nil + }) + defer restore() + s.testInstallEncryptionValidityChecks(c, `(?ms)cannot perform the following tasks: +- Setup system for run mode \(internal error: system encryption keys are unset\)`) +} + +func (s *deviceMgrInstallModeSuite) TestInstallEncryptionValidityChecksNoSystemDataKey(c *C) { + restore := devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { + c.Check(options.EncryptionType, Equals, secboot.EncryptionTypeLUKS) + // no keys set + return &install.InstalledSystemSideData{ + // empty map + KeyForRole: map[string]keys.EncryptionKey{}, + }, nil + }) + defer restore() + s.testInstallEncryptionValidityChecks(c, `(?ms)cannot perform the following tasks: +- Setup system for run mode \(internal error: system encryption keys are unset\)`) +} + +func (s *deviceMgrInstallModeSuite) mockInstallModeChange(c *C, modelGrade, gadgetDefaultsYaml string) *asserts.Model { restore := release.MockOnClassic(false) defer restore() @@ -1210,331 +1534,1419 @@ }) defer restore() - // pretend we have a TPM - restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return fmt.Errorf("TPM2 not available") }) + s.state.Lock() + mockModel := s.makeMockInstallModel(c, modelGrade) + s.makeMockInstalledPcGadget(c, "", gadgetDefaultsYaml) + s.state.Unlock() + c.Check(mockModel.Grade(), Equals, asserts.ModelGrade(modelGrade)) + + restore = devicestate.MockBootMakeSystemRunnable(func(model *asserts.Model, bootWith *boot.BootableSet, seal *boot.TrustedAssetsInstallObserver) error { + return nil + }) defer restore() - s.testInstallGadgetNoSave(c) + modeenv := boot.Modeenv{ + Mode: "install", + RecoverySystem: "20191218", + } + c.Assert(modeenv.WriteTo(""), IsNil) + devicestate.SetSystemMode(s.mgr, "install") + + // normally done by snap-bootstrap + err := os.MkdirAll(boot.InitramfsUbuntuBootDir, 0755) + c.Assert(err, IsNil) + + s.settle(c) + + return mockModel +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeRunSysconfig(c *C) { + s.mockInstallModeChange(c, "dangerous", "") s.state.Lock() defer s.state.Unlock() + // the install-system change is created installSystem := s.findInstallSystem() - c.Check(installSystem.Err(), IsNil) - c.Check(s.restartRequests, HasLen, 1) -} + c.Assert(installSystem, NotNil) -func (s *deviceMgrInstallModeSuite) TestInstallCheckEncrypted(c *C) { - st := s.state - st.Lock() - defer st.Unlock() + // and was run successfully + c.Check(installSystem.Err(), IsNil) + c.Check(installSystem.Status(), Equals, state.DoneStatus) - mockModel := s.makeModelAssertionInState(c, "canonical", "pc", map[string]interface{}{ - "architecture": "amd64", - "kernel": "pc-kernel", - "gadget": "pc", - }) - devicestatetest.SetDevice(s.state, &auth.DeviceState{ - Brand: "canonical", - Model: "pc", + // and sysconfig.ConfigureTargetSystem was run exactly once + c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ + { + AllowCloudInit: true, + TargetRootDir: boot.InstallHostWritableDir, + GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), + }, }) - deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} - for _, tc := range []struct { - hasFDESetupHook bool - fdeSetupHookFeatures string + // and the special dirs in _writable_defaults were created + for _, dir := range []string{"/etc/udev/rules.d/", "/etc/modules-load.d/", "/etc/modprobe.d/"} { + fullDir := filepath.Join(sysconfig.WritableDefaultsDir(boot.InstallHostWritableDir), dir) + c.Assert(fullDir, testutil.FilePresent) + } +} - hasTPM bool - encryptionType secboot.EncryptionType - }{ - // unhappy: no tpm, no hook - {false, "[]", false, secboot.EncryptionTypeNone}, - // happy: either tpm or hook or both - {false, "[]", true, secboot.EncryptionTypeLUKS}, - {true, "[]", false, secboot.EncryptionTypeLUKS}, - {true, "[]", true, secboot.EncryptionTypeLUKS}, - // happy but device-setup hook - {true, `["device-setup"]`, true, secboot.EncryptionTypeDeviceSetupHook}, - } { - hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { +func (s *deviceMgrInstallModeSuite) TestInstallModeRunSysconfigErr(c *C) { + s.ConfigureTargetSystemErr = fmt.Errorf("error from sysconfig.ConfigureTargetSystem") + s.mockInstallModeChange(c, "dangerous", "") + + s.state.Lock() + defer s.state.Unlock() + + // the install-system was run but errorred as specified in the above mock + installSystem := s.findInstallSystem() + c.Check(installSystem.Err(), ErrorMatches, `(?ms)cannot perform the following tasks: +- Setup system for run mode \(error from sysconfig.ConfigureTargetSystem\)`) + // and sysconfig.ConfigureTargetSystem was run exactly once + c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ + { + AllowCloudInit: true, + TargetRootDir: boot.InstallHostWritableDir, + GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), + }, + }) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeSupportsCloudInitInDangerous(c *C) { + // pretend we have a cloud-init config on the seed partition + cloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d") + err := os.MkdirAll(cloudCfg, 0755) + c.Assert(err, IsNil) + for _, mockCfg := range []string{"foo.cfg", "bar.cfg"} { + err = ioutil.WriteFile(filepath.Join(cloudCfg, mockCfg), []byte(fmt.Sprintf("%s config", mockCfg)), 0644) + c.Assert(err, IsNil) + } + + s.mockInstallModeChange(c, "dangerous", "") + + // and did tell sysconfig about the cloud-init files + c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ + { + AllowCloudInit: true, + CloudInitSrcDir: filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d"), + TargetRootDir: boot.InstallHostWritableDir, + GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), + }, + }) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeSupportsCloudInitGadgetAndSeedConfigSigned(c *C) { + // pretend we have a cloud-init config on the seed partition + cloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d") + err := os.MkdirAll(cloudCfg, 0755) + c.Assert(err, IsNil) + for _, mockCfg := range []string{"foo.cfg", "bar.cfg"} { + err = ioutil.WriteFile(filepath.Join(cloudCfg, mockCfg), []byte(fmt.Sprintf("%s config", mockCfg)), 0644) + c.Assert(err, IsNil) + } + + // we also have gadget cloud init too + gadgetDir := filepath.Join(dirs.SnapMountDir, "pc/1/") + err = os.MkdirAll(gadgetDir, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), nil, 0644) + c.Assert(err, IsNil) + + s.mockInstallModeChange(c, "signed", "") + + // sysconfig is told about both configs + c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ + { + AllowCloudInit: true, + TargetRootDir: boot.InstallHostWritableDir, + GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), + CloudInitSrcDir: cloudCfg, + }, + }) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeSupportsCloudInitBothGadgetAndUbuntuSeedDangerous(c *C) { + // pretend we have a cloud-init config on the seed partition + cloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d") + err := os.MkdirAll(cloudCfg, 0755) + c.Assert(err, IsNil) + for _, mockCfg := range []string{"foo.cfg", "bar.cfg"} { + err = ioutil.WriteFile(filepath.Join(cloudCfg, mockCfg), []byte(fmt.Sprintf("%s config", mockCfg)), 0644) + c.Assert(err, IsNil) + } + + // we also have gadget cloud init too + gadgetDir := filepath.Join(dirs.SnapMountDir, "pc/1/") + err = os.MkdirAll(gadgetDir, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), nil, 0644) + c.Assert(err, IsNil) + + s.mockInstallModeChange(c, "dangerous", "") + + // and did tell sysconfig about the cloud-init files + c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ + { + AllowCloudInit: true, + CloudInitSrcDir: filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d"), + TargetRootDir: boot.InstallHostWritableDir, + GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), + }, + }) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeSignedNoUbuntuSeedCloudInit(c *C) { + // pretend we have no cloud-init config anywhere + s.mockInstallModeChange(c, "signed", "") + + // we didn't pass any cloud-init src dir but still left cloud-init enabled + // if for example a CI-DATA USB drive was provided at runtime + c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ + { + AllowCloudInit: true, + TargetRootDir: boot.InstallHostWritableDir, + GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), + }, + }) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeSecuredGadgetCloudConfCloudInit(c *C) { + // pretend we have a cloud.conf from the gadget + gadgetDir := filepath.Join(dirs.SnapMountDir, "pc/1/") + err := os.MkdirAll(gadgetDir, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), nil, 0644) + c.Assert(err, IsNil) + + err = s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) + c.Assert(err, IsNil) + + c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ + { + AllowCloudInit: true, + TargetRootDir: boot.InstallHostWritableDir, + GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), + }, + }) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeSecuredNoUbuntuSeedCloudInit(c *C) { + // pretend we have a cloud-init config on the seed partition with some files + cloudCfg := filepath.Join(boot.InitramfsUbuntuSeedDir, "data/etc/cloud/cloud.cfg.d") + err := os.MkdirAll(cloudCfg, 0755) + c.Assert(err, IsNil) + for _, mockCfg := range []string{"foo.cfg", "bar.cfg"} { + err = ioutil.WriteFile(filepath.Join(cloudCfg, mockCfg), []byte(fmt.Sprintf("%s config", mockCfg)), 0644) + c.Assert(err, IsNil) + } + + err = s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) + c.Assert(err, IsNil) + + // we did tell sysconfig about the ubuntu-seed cloud config dir because it + // exists, but it is up to sysconfig to use the model to determine to ignore + // the files + c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ + { + AllowCloudInit: false, + TargetRootDir: boot.InstallHostWritableDir, + GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), + CloudInitSrcDir: cloudCfg, + }, + }) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeWritesModel(c *C) { + // pretend we have a cloud-init config on the seed partition + model := s.mockInstallModeChange(c, "dangerous", "") + + var buf bytes.Buffer + err := asserts.NewEncoder(&buf).Encode(model) + c.Assert(err, IsNil) + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Assert(installSystem, NotNil) + + // and was run successfully + c.Check(installSystem.Err(), IsNil) + c.Check(installSystem.Status(), Equals, state.DoneStatus) + + c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), testutil.FileEquals, buf.String()) +} + +func (s *deviceMgrInstallModeSuite) testInstallGadgetNoSave(c *C) { + err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), + []byte("mode=install\n"), 0644) + c.Assert(err, IsNil) + + s.state.Lock() + s.makeMockInstallModel(c, "dangerous") + s.makeMockInstalledPcGadget(c, "", "") + info, err := snapstate.CurrentInfo(s.state, "pc") + c.Assert(err, IsNil) + // replace gadget yaml with one that has no ubuntu-save + c.Assert(uc20gadgetYaml, Not(testutil.Contains), "ubuntu-save") + err = ioutil.WriteFile(filepath.Join(info.MountDir(), "meta/gadget.yaml"), []byte(uc20gadgetYaml), 0644) + c.Assert(err, IsNil) + devicestate.SetSystemMode(s.mgr, "install") + s.state.Unlock() + + s.settle(c) +} + +func (s *deviceMgrInstallModeSuite) TestInstallWithEncryptionValidatesGadgetErr(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { + return nil, fmt.Errorf("unexpected call") + }) + defer restore() + + // pretend we have a TPM + restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return nil }) + defer restore() + + s.testInstallGadgetNoSave(c) + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Check(installSystem.Err(), ErrorMatches, `(?ms)cannot perform the following tasks: +- Setup system for run mode \(cannot use gadget: gadget does not support encrypted data: required partition with system-save role is missing\)`) + // no restart request on failure + c.Check(s.restartRequests, HasLen, 0) +} + +func (s *deviceMgrInstallModeSuite) TestInstallWithoutEncryptionValidatesGadgetWithoutSaveHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { + return nil, nil + }) + defer restore() + + // pretend we have a TPM + restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return fmt.Errorf("TPM2 not available") }) + defer restore() + + s.testInstallGadgetNoSave(c) + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Check(installSystem.Err(), IsNil) + c.Check(s.restartRequests, HasLen, 1) +} + +func (s *deviceMgrInstallModeSuite) TestInstallCheckEncrypted(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + mockModel := s.makeModelAssertionInState(c, "canonical", "pc", map[string]interface{}{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + }) + devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + }) + deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} + + for _, tc := range []struct { + hasFDESetupHook bool + fdeSetupHookFeatures string + + hasTPM bool + encryptionType secboot.EncryptionType + }{ + // unhappy: no tpm, no hook + {false, "[]", false, secboot.EncryptionTypeNone}, + // happy: either tpm or hook or both + {false, "[]", true, secboot.EncryptionTypeLUKS}, + {true, "[]", false, secboot.EncryptionTypeLUKS}, + {true, "[]", true, secboot.EncryptionTypeLUKS}, + // happy but device-setup hook + {true, `["device-setup"]`, true, secboot.EncryptionTypeDeviceSetupHook}, + } { + hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { + ctx.Lock() + defer ctx.Unlock() + ctx.Set("fde-setup-result", []byte(fmt.Sprintf(`{"features":%s}`, tc.fdeSetupHookFeatures))) + return nil, nil + } + rhk := hookstate.MockRunHook(hookInvoke) + defer rhk() + + if tc.hasFDESetupHook { + makeInstalledMockKernelSnap(c, st, kernelYamlWithFdeSetup) + } else { + makeInstalledMockKernelSnap(c, st, kernelYamlNoFdeSetup) + } + restore := devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { + if tc.hasTPM { + return nil + } + return fmt.Errorf("tpm says no") + }) + defer restore() + + encryptionType, err := devicestate.DeviceManagerCheckEncryption(s.mgr, st, deviceCtx) + c.Assert(err, IsNil) + c.Check(encryptionType, Equals, tc.encryptionType, Commentf("%v", tc)) + } +} + +func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedStorageSafety(c *C) { + s.state.Lock() + defer s.state.Unlock() + + restore := devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return nil }) + defer restore() + + var testCases = []struct { + grade, storageSafety string + + expectedEncryption bool + }{ + // we don't test unset here because the assertion assembly + // will ensure it has a default + {"dangerous", "prefer-unencrypted", false}, + {"dangerous", "prefer-encrypted", true}, + {"dangerous", "encrypted", true}, + {"signed", "prefer-unencrypted", false}, + {"signed", "prefer-encrypted", true}, + {"signed", "encrypted", true}, + // secured+prefer-{,un}encrypted is an error at the + // assertion level already so cannot be tested here + {"secured", "encrypted", true}, + } + for _, tc := range testCases { + mockModel := s.makeModelAssertionInState(c, "my-brand", "my-model", map[string]interface{}{ + "display-name": "my model", + "architecture": "amd64", + "base": "core20", + "grade": tc.grade, + "storage-safety": tc.storageSafety, + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": pcKernelSnapID, + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": pcSnapID, + "type": "gadget", + "default-channel": "20", + }}, + }) + deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} + + encryptionType, err := devicestate.DeviceManagerCheckEncryption(s.mgr, s.state, deviceCtx) + c.Assert(err, IsNil) + encrypt := (encryptionType != secboot.EncryptionTypeNone) + c.Check(encrypt, Equals, tc.expectedEncryption) + } +} + +func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedErrors(c *C) { + s.state.Lock() + defer s.state.Unlock() + + restore := devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return fmt.Errorf("tpm says no") }) + defer restore() + + var testCases = []struct { + grade, storageSafety string + + expectedErr string + }{ + // we don't test unset here because the assertion assembly + // will ensure it has a default + { + "dangerous", "encrypted", + "cannot encrypt device storage as mandated by encrypted storage-safety model option: tpm says no", + }, { + "signed", "encrypted", + "cannot encrypt device storage as mandated by encrypted storage-safety model option: tpm says no", + }, { + "secured", "", + "cannot encrypt device storage as mandated by model grade secured: tpm says no", + }, { + "secured", "encrypted", + "cannot encrypt device storage as mandated by model grade secured: tpm says no", + }, + } + for _, tc := range testCases { + mockModel := s.makeModelAssertionInState(c, "my-brand", "my-model", map[string]interface{}{ + "display-name": "my model", + "architecture": "amd64", + "base": "core20", + "grade": tc.grade, + "storage-safety": tc.storageSafety, + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": pcKernelSnapID, + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": pcSnapID, + "type": "gadget", + "default-channel": "20", + }}, + }) + deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} + _, err := devicestate.DeviceManagerCheckEncryption(s.mgr, s.state, deviceCtx) + c.Check(err, ErrorMatches, tc.expectedErr, Commentf("%s %s", tc.grade, tc.storageSafety)) + } +} + +func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedFDEHook(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + s.makeModelAssertionInState(c, "canonical", "pc", map[string]interface{}{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + }) + devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + }) + makeInstalledMockKernelSnap(c, st, kernelYamlWithFdeSetup) + + for _, tc := range []struct { + hookOutput string + expectedErr string + + encryptionType secboot.EncryptionType + }{ + // invalid json + {"xxx", `cannot parse hook output "xxx": invalid character 'x' looking for beginning of value`, secboot.EncryptionTypeNone}, + // no output is invalid + {"", `cannot parse hook output "": unexpected end of JSON input`, secboot.EncryptionTypeNone}, + // specific error + {`{"error":"failed"}`, `cannot use hook: it returned error: failed`, secboot.EncryptionTypeNone}, + {`{}`, `cannot use hook: neither "features" nor "error" returned`, secboot.EncryptionTypeNone}, + // valid + {`{"features":[]}`, "", secboot.EncryptionTypeLUKS}, + {`{"features":["a"]}`, "", secboot.EncryptionTypeLUKS}, + {`{"features":["a","b"]}`, "", secboot.EncryptionTypeLUKS}, + // features must be list of strings + {`{"features":[1]}`, `cannot parse hook output ".*": json: cannot unmarshal number into Go struct.*`, secboot.EncryptionTypeNone}, + {`{"features":1}`, `cannot parse hook output ".*": json: cannot unmarshal number into Go struct.*`, secboot.EncryptionTypeNone}, + {`{"features":"1"}`, `cannot parse hook output ".*": json: cannot unmarshal string into Go struct.*`, secboot.EncryptionTypeNone}, + // valid and switches to "device-setup" + {`{"features":["device-setup"]}`, "", secboot.EncryptionTypeDeviceSetupHook}, + {`{"features":["a","device-setup","b"]}`, "", secboot.EncryptionTypeDeviceSetupHook}, + } { + hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { ctx.Lock() defer ctx.Unlock() - ctx.Set("fde-setup-result", []byte(fmt.Sprintf(`{"features":%s}`, tc.fdeSetupHookFeatures))) + ctx.Set("fde-setup-result", []byte(tc.hookOutput)) return nil, nil } - rhk := hookstate.MockRunHook(hookInvoke) - defer rhk() + rhk := hookstate.MockRunHook(hookInvoke) + defer rhk() + + et, err := devicestate.DeviceManagerCheckFDEFeatures(s.mgr, st) + if tc.expectedErr != "" { + c.Check(err, ErrorMatches, tc.expectedErr, Commentf("%v", tc)) + } else { + c.Check(err, IsNil, Commentf("%v", tc)) + c.Check(et, Equals, tc.encryptionType, Commentf("%v", tc)) + } + } +} + +var checkEncryptionModelHeaders = map[string]interface{}{ + "display-name": "my model", + "architecture": "amd64", + "base": "core20", + "grade": "dangerous", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": pcKernelSnapID, + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": pcSnapID, + "type": "gadget", + "default-channel": "20", + }}, +} + +func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedErrorsLogsTPM(c *C) { + s.state.Lock() + defer s.state.Unlock() + + restore := devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { + return fmt.Errorf("tpm says no") + }) + defer restore() + + logbuf, restore := logger.MockLogger() + defer restore() + + mockModel := s.makeModelAssertionInState(c, "my-brand", "my-model", checkEncryptionModelHeaders) + deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} + _, err := devicestate.DeviceManagerCheckEncryption(s.mgr, s.state, deviceCtx) + c.Check(err, IsNil) + c.Check(logbuf.String(), Matches, "(?s).*: not encrypting device storage as checking TPM gave: tpm says no\n") +} + +func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedErrorsLogsHook(c *C) { + s.state.Lock() + defer s.state.Unlock() + + logbuf, restore := logger.MockLogger() + defer restore() + + mockModel := s.makeModelAssertionInState(c, "my-brand", "my-model", checkEncryptionModelHeaders) + // mock kernel installed but no hook or handle so checkEncryption + // will fail + makeInstalledMockKernelSnap(c, s.state, kernelYamlWithFdeSetup) + + deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} + _, err := devicestate.DeviceManagerCheckEncryption(s.mgr, s.state, deviceCtx) + c.Check(err, IsNil) + c.Check(logbuf.String(), Matches, "(?s).*: not encrypting device storage as querying kernel fde-setup hook did not succeed:.*\n") +} + +func (s *deviceMgrInstallModeSuite) TestInstallHappyLogfiles(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { + return nil, nil + }) + defer restore() + + mockedSnapCmd := testutil.MockCommand(c, "snap", ` +echo "mock output of: $(basename "$0") $*" +`) + defer mockedSnapCmd.Restore() + + err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), + []byte("mode=install\n"), 0644) + c.Assert(err, IsNil) + + s.state.Lock() + // pretend we are seeding + chg := s.state.NewChange("seed", "just for testing") + chg.AddTask(s.state.NewTask("test-task", "the change needs a task")) + s.makeMockInstallModel(c, "dangerous") + s.makeMockInstalledPcGadget(c, "", "") + devicestate.SetSystemMode(s.mgr, "install") + s.state.Unlock() + + s.settle(c) + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Check(installSystem.Err(), IsNil) + c.Check(s.restartRequests, HasLen, 1) + + // logs are created + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data/var/log/install-mode.log.gz"), testutil.FilePresent) + timingsPath := filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data/var/log/install-timings.txt.gz") + c.Check(timingsPath, testutil.FilePresent) + + f, err := os.Open(timingsPath) + c.Assert(err, IsNil) + defer f.Close() + gz, err := gzip.NewReader(f) + c.Assert(err, IsNil) + content, err := ioutil.ReadAll(gz) + c.Assert(err, IsNil) + c.Check(string(content), Equals, `---- Output of: snap changes +mock output of: snap changes + +---- Output of snap debug timings --ensure=seed +mock output of: snap debug timings --ensure=seed + +---- Output of snap debug timings --ensure=install-system +mock output of: snap debug timings --ensure=install-system +`) + + // and the right commands are run + c.Check(mockedSnapCmd.Calls(), DeepEquals, [][]string{ + {"snap", "changes"}, + {"snap", "debug", "timings", "--ensure=seed"}, + {"snap", "debug", "timings", "--ensure=install-system"}, + }) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeWritesTimesyncdClockHappy(c *C) { + now := time.Now() + restore := devicestate.MockTimeNow(func() time.Time { return now }) + defer restore() + + clockTsInSrc := filepath.Join(dirs.GlobalRootDir, "/var/lib/systemd/timesync/clock") + c.Assert(os.MkdirAll(filepath.Dir(clockTsInSrc), 0755), IsNil) + c.Assert(ioutil.WriteFile(clockTsInSrc, nil, 0644), IsNil) + // a month old timestamp file + c.Assert(os.Chtimes(clockTsInSrc, now.AddDate(0, -1, 0), now.AddDate(0, -1, 0)), IsNil) + + s.mockInstallModeChange(c, "dangerous", "") + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Assert(installSystem, NotNil) + + // installation was successful + c.Check(installSystem.Err(), IsNil) + c.Check(installSystem.Status(), Equals, state.DoneStatus) + + clockTsInDst := filepath.Join(boot.InstallHostWritableDir, "/var/lib/systemd/timesync/clock") + fi, err := os.Stat(clockTsInDst) + c.Assert(err, IsNil) + c.Check(fi.ModTime().Round(time.Second), Equals, now.Round(time.Second)) + c.Check(fi.Size(), Equals, int64(0)) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeWritesTimesyncdClockErr(c *C) { + now := time.Now() + restore := devicestate.MockTimeNow(func() time.Time { return now }) + defer restore() + + if os.Geteuid() == 0 { + c.Skip("the test cannot be executed by the root user") + } + + clockTsInSrc := filepath.Join(dirs.GlobalRootDir, "/var/lib/systemd/timesync/clock") + c.Assert(os.MkdirAll(filepath.Dir(clockTsInSrc), 0755), IsNil) + c.Assert(ioutil.WriteFile(clockTsInSrc, nil, 0644), IsNil) + + timesyncDirInDst := filepath.Join(boot.InstallHostWritableDir, "/var/lib/systemd/timesync/") + c.Assert(os.MkdirAll(timesyncDirInDst, 0755), IsNil) + c.Assert(os.Chmod(timesyncDirInDst, 0000), IsNil) + defer os.Chmod(timesyncDirInDst, 0755) + + s.mockInstallModeChange(c, "dangerous", "") + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Assert(installSystem, NotNil) + + // install failed copying the timestamp + c.Check(installSystem.Err(), ErrorMatches, `(?s).*\(cannot seed timesyncd clock: cannot copy clock:.*Permission denied.*`) + c.Check(installSystem.Status(), Equals, state.ErrorStatus) +} + +type resetTestCase struct { + noSave bool + tpm bool + encrypt bool + trustedBootloader bool +} + +func (s *deviceMgrInstallModeSuite) doRunFactoryResetChange(c *C, model *asserts.Model, tc resetTestCase) error { + restore := release.MockOnClassic(false) + defer restore() + bootloaderRootdir := c.MkDir() + + // inject trusted keys + defer sysdb.InjectTrusted([]asserts.Assertion{s.storeSigning.TrustedKey})() + + var brGadgetRoot, brDevice string + var brOpts install.Options + var installFactoryResetCalled int + var installSealingObserver gadget.ContentObserver + restore = devicestate.MockInstallFactoryReset(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, obs gadget.ContentObserver, pertTimings timings.Measurer) (*install.InstalledSystemSideData, error) { + // ensure we can grab the lock here, i.e. that it's not taken + s.state.Lock() + s.state.Unlock() + + c.Check(mod.Grade(), Equals, model.Grade()) + + brGadgetRoot = gadgetRoot + brDevice = device + brOpts = options + installSealingObserver = obs + installFactoryResetCalled++ + var keyForRole map[string]keys.EncryptionKey + if tc.encrypt { + keyForRole = map[string]keys.EncryptionKey{ + gadget.SystemData: dataEncryptionKey, + gadget.SystemSave: saveKey, + } + } + devForRole := map[string]string{ + gadget.SystemData: "/dev/foo-data", + } + if tc.encrypt { + devForRole[gadget.SystemSave] = "/dev/foo-save" + } + c.Assert(os.MkdirAll(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir), 0755), IsNil) + return &install.InstalledSystemSideData{ + KeyForRole: keyForRole, + DeviceForRole: devForRole, + }, nil + }) + defer restore() - if tc.hasFDESetupHook { - makeInstalledMockKernelSnap(c, st, kernelYamlWithFdeSetup) + restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { + if tc.tpm { + return nil } else { - makeInstalledMockKernelSnap(c, st, kernelYamlNoFdeSetup) + return fmt.Errorf("TPM not available") } - restore := devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { - if tc.hasTPM { - return nil - } - return fmt.Errorf("tpm says no") - }) + }) + defer restore() + + if tc.trustedBootloader { + tab := bootloadertest.Mock("trusted", bootloaderRootdir).WithTrustedAssets() + tab.TrustedAssetsList = []string{"trusted-asset"} + bootloader.Force(tab) + s.AddCleanup(func() { bootloader.Force(nil) }) + + err := os.MkdirAll(boot.InitramfsUbuntuSeedDir, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "trusted-asset"), nil, 0644) + c.Assert(err, IsNil) + } + + s.state.Lock() + s.makeMockInstalledPcGadget(c, "", "") + s.state.Unlock() + + var saveKey keys.EncryptionKey + restore = devicestate.MockSecbootTransitionEncryptionKeyChange(func(node string, key keys.EncryptionKey) error { + c.Errorf("unexpected call") + return fmt.Errorf("unexpected call") + }) + defer restore() + restore = devicestate.MockSecbootStageEncryptionKeyChange(func(node string, key keys.EncryptionKey) error { + if tc.encrypt { + c.Check(node, Equals, "/dev/foo-save") + saveKey = key + return nil + } + c.Fail() + return fmt.Errorf("unexpected call") + }) + defer restore() + + var recoveryKeyRemoved bool + defer devicestate.MockSecbootRemoveRecoveryKeys(func(r2k map[secboot.RecoveryKeyDevice]string) error { + if tc.encrypt { + recoveryKeyRemoved = true + c.Check(r2k, DeepEquals, map[secboot.RecoveryKeyDevice]string{ + {Mountpoint: boot.InitramfsUbuntuSaveDir}: filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), + }) + return nil + } + c.Errorf("unexpected call") + return fmt.Errorf("unexpected call") + })() + + bootMakeBootableCalled := 0 + restore = devicestate.MockBootMakeSystemRunnableAfterDataReset(func(makeRunnableModel *asserts.Model, bootWith *boot.BootableSet, seal *boot.TrustedAssetsInstallObserver) error { + c.Check(makeRunnableModel, DeepEquals, model) + c.Check(bootWith.KernelPath, Matches, ".*/var/lib/snapd/snaps/pc-kernel_1.snap") + c.Check(bootWith.BasePath, Matches, ".*/var/lib/snapd/snaps/core20_2.snap") + c.Check(bootWith.RecoverySystemDir, Matches, "/systems/20191218") + c.Check(bootWith.UnpackedGadgetDir, Equals, filepath.Join(dirs.SnapMountDir, "pc/1")) + if tc.encrypt { + c.Check(seal, NotNil) + } else { + c.Check(seal, IsNil) + } + bootMakeBootableCalled++ + + if tc.encrypt { + // those 2 keys are removed + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), + testutil.FileAbsent) + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"), + testutil.FileAbsent) + // but the original ubuntu-save key remains + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + testutil.FilePresent) + } + + // this would be done by boot + if tc.encrypt { + err := ioutil.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"), + []byte("save"), 0644) + c.Check(err, IsNil) + err = ioutil.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), + []byte("new-data"), 0644) + c.Check(err, IsNil) + } + return nil + }) + defer restore() + + if !tc.noSave { + restore := osutil.MockMountInfo(fmt.Sprintf(mountRunMntUbuntuSaveFmt, dirs.GlobalRootDir)) defer restore() + } + + modeenv := boot.Modeenv{ + Mode: "factory-reset", + RecoverySystem: "20191218", + } + c.Assert(modeenv.WriteTo(""), IsNil) + devicestate.SetSystemMode(s.mgr, "factory-reset") + + // normally done by snap-bootstrap when booting info factory-reset + err := os.MkdirAll(boot.InitramfsUbuntuBootDir, 0755) + c.Assert(err, IsNil) + if !tc.noSave { + // since there is no save, there is no mount and no target + // directory created by systemd-mount + err = os.MkdirAll(boot.InitramfsUbuntuSaveDir, 0755) + c.Assert(err, IsNil) + } + + s.settle(c) + + // the factory-reset change is created + s.state.Lock() + defer s.state.Unlock() + factoryReset := s.findFactoryReset() + c.Assert(factoryReset, NotNil) + + // and was run successfully + if err := factoryReset.Err(); err != nil { + // we failed, no further checks needed + return err + } + + c.Assert(factoryReset.Status(), Equals, state.DoneStatus) + + // in the right way + c.Assert(brGadgetRoot, Equals, filepath.Join(dirs.SnapMountDir, "/pc/1")) + c.Assert(brDevice, Equals, "") + if tc.encrypt { + c.Assert(brOpts, DeepEquals, install.Options{ + Mount: true, + EncryptionType: secboot.EncryptionTypeLUKS, + }) + } else { + c.Assert(brOpts, DeepEquals, install.Options{ + Mount: true, + }) + } + if tc.encrypt { + // inteface is not nil + c.Assert(installSealingObserver, NotNil) + // we expect a very specific type + trustedInstallObserver, ok := installSealingObserver.(*boot.TrustedAssetsInstallObserver) + c.Assert(ok, Equals, true, Commentf("unexpected type: %T", installSealingObserver)) + c.Assert(trustedInstallObserver, NotNil) + } else { + c.Assert(installSealingObserver, IsNil) + } + + c.Assert(installFactoryResetCalled, Equals, 1) + c.Assert(bootMakeBootableCalled, Equals, 1) + c.Assert(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) + if tc.encrypt { + c.Assert(saveKey, NotNil) + c.Check(recoveryKeyRemoved, Equals, true) + c.Check(filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key"), testutil.FileEquals, []byte(saveKey)) + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), testutil.FileEquals, "new-data") + // sha3-384 of the mocked ubuntu-save sealed key + c.Check(filepath.Join(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir), "factory-reset"), + testutil.FileEquals, + `{"fallback-save-key-sha3-384":"d192153f0a50e826c6eb400c8711750ed0466571df1d151aaecc8c73095da7ec104318e7bf74d5e5ae2940827bf8402b"} +`) + } else { + c.Check(filepath.Join(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir), "factory-reset"), + testutil.FileEquals, "{}\n") + } + + return nil +} + +func makeDeviceSerialAssertionInDir(c *C, where string, storeStack *assertstest.StoreStack, brands *assertstest.SigningAccounts, model *asserts.Model, key asserts.PrivateKey, serialN string) *asserts.Serial { + encDevKey, err := asserts.EncodePublicKey(key.PublicKey()) + c.Assert(err, IsNil) + serial, err := brands.Signing(model.BrandID()).Sign(asserts.SerialType, map[string]interface{}{ + "brand-id": model.BrandID(), + "model": model.Model(), + "serial": serialN, + "device-key": string(encDevKey), + "device-key-sha3-384": key.PublicKey().ID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + kp, err := asserts.OpenFSKeypairManager(where) + c.Assert(err, IsNil) + c.Assert(kp.Put(key), IsNil) + bs, err := asserts.OpenFSBackstore(where) + c.Assert(err, IsNil) + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: bs, + Trusted: storeStack.Trusted, + OtherPredefined: storeStack.Generic, + }) + c.Assert(err, IsNil) - encryptionType, err := devicestate.DeviceManagerCheckEncryption(s.mgr, st, deviceCtx) - c.Assert(err, IsNil) - c.Check(encryptionType, Equals, tc.encryptionType, Commentf("%v", tc)) - } + b := asserts.NewBatch(nil) + c.Logf("root key ID: %v", storeStack.RootSigning.KeyID) + c.Assert(b.Add(storeStack.StoreAccountKey("")), IsNil) + c.Assert(b.Add(brands.AccountKey(model.BrandID())), IsNil) + c.Assert(b.Add(brands.Account(model.BrandID())), IsNil) + c.Assert(b.Add(serial), IsNil) + c.Assert(b.Add(model), IsNil) + c.Assert(b.CommitTo(db, nil), IsNil) + return serial.(*asserts.Serial) } -func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedStorageSafety(c *C) { +func (s *deviceMgrInstallModeSuite) TestFactoryResetNoEncryptionHappyFull(c *C) { s.state.Lock() - defer s.state.Unlock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() - restore := devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return nil }) + // for debug timinigs + mockedSnapCmd := testutil.MockCommand(c, "snap", ` +echo "mock output of: $(basename "$0") $*" +`) + defer mockedSnapCmd.Restore() + + // pretend snap-bootstrap mounted ubuntu-save + err := os.MkdirAll(boot.InitramfsUbuntuSaveDir, 0755) + c.Assert(err, IsNil) + // and it has some content + serial := makeDeviceSerialAssertionInDir(c, boot.InstallHostDeviceSaveDir, s.storeSigning, s.brands, + model, devKey, "serial-1234") + + logbuf, restore := logger.MockLogger() defer restore() - var testCases = []struct { - grade, storageSafety string + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + tpm: false, encrypt: false, + }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, IsNil) - expectedEncryption bool - }{ - // we don't test unset here because the assertion assembly - // will ensure it has a default - {"dangerous", "prefer-unencrypted", false}, - {"dangerous", "prefer-encrypted", true}, - {"dangerous", "encrypted", true}, - {"signed", "prefer-unencrypted", false}, - {"signed", "prefer-encrypted", true}, - {"signed", "encrypted", true}, - // secured+prefer-{,un}encrypted is an error at the - // assertion level already so cannot be tested here - {"secured", "encrypted", true}, - } - for _, tc := range testCases { - mockModel := s.makeModelAssertionInState(c, "my-brand", "my-model", map[string]interface{}{ - "display-name": "my model", - "architecture": "amd64", - "base": "core20", - "grade": tc.grade, - "storage-safety": tc.storageSafety, - "snaps": []interface{}{ - map[string]interface{}{ - "name": "pc-kernel", - "id": pcKernelSnapID, - "type": "kernel", - "default-channel": "20", - }, - map[string]interface{}{ - "name": "pc", - "id": pcSnapID, - "type": "gadget", - "default-channel": "20", - }}, - }) - deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} + // verify that the serial assertion has been restored + assertsInResetSystem := filepath.Join(boot.InstallHostWritableDir, "var/lib/snapd/assertions") + bs, err := asserts.OpenFSBackstore(assertsInResetSystem) + c.Assert(err, IsNil) + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: bs, + Trusted: s.storeSigning.Trusted, + OtherPredefined: s.storeSigning.Generic, + }) + c.Assert(err, IsNil) + ass, err := db.FindMany(asserts.SerialType, map[string]string{ + "brand-id": serial.BrandID(), + "model": serial.Model(), + "device-key-sha3-384": serial.DeviceKey().ID(), + }) + c.Assert(err, IsNil) + c.Assert(ass, HasLen, 1) + asSerial, _ := ass[0].(*asserts.Serial) + c.Assert(asSerial, NotNil) + c.Assert(asSerial, DeepEquals, serial) - encryptionType, err := devicestate.DeviceManagerCheckEncryption(s.mgr, s.state, deviceCtx) - c.Assert(err, IsNil) - encrypt := (encryptionType != secboot.EncryptionTypeNone) - c.Check(encrypt, Equals, tc.expectedEncryption) - } + kp, err := asserts.OpenFSKeypairManager(assertsInResetSystem) + c.Assert(err, IsNil) + _, err = kp.Get(serial.DeviceKey().ID()) + // the key will not have been found, as this is a device with ubuntu-save + // and key is stored on that partition + c.Assert(asserts.IsKeyNotFound(err), Equals, true) + // which we verify here + kpInSave, err := asserts.OpenFSKeypairManager(boot.InstallHostDeviceSaveDir) + c.Assert(err, IsNil) + _, err = kpInSave.Get(serial.DeviceKey().ID()) + c.Assert(err, IsNil) + + logsPath := filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data/var/log/factory-reset-mode.log.gz") + c.Check(logsPath, testutil.FilePresent) + timingsPath := filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data/var/log/factory-reset-timings.txt.gz") + c.Check(timingsPath, testutil.FilePresent) + // and the right commands are run + c.Check(mockedSnapCmd.Calls(), DeepEquals, [][]string{ + {"snap", "changes"}, + {"snap", "debug", "timings", "--ensure=seed"}, + {"snap", "debug", "timings", "--ensure=factory-reset"}, + }) } -func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedErrors(c *C) { +func (s *deviceMgrInstallModeSuite) TestFactoryResetEncryptionHappyFull(c *C) { s.state.Lock() - defer s.state.Unlock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() - restore := devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { return fmt.Errorf("tpm says no") }) + // for debug timinigs + mockedSnapCmd := testutil.MockCommand(c, "snap", ` +echo "mock output of: $(basename "$0") $*" +`) + defer mockedSnapCmd.Restore() + + // pretend snap-bootstrap mounted ubuntu-save + err := os.MkdirAll(boot.InitramfsUbuntuSaveDir, 0755) + c.Assert(err, IsNil) + snaptest.PopulateDir(boot.InitramfsSeedEncryptionKeyDir, [][]string{ + {"ubuntu-data.recovery.sealed-key", "old-data"}, + {"ubuntu-save.recovery.sealed-key", "old-save"}, + }) + + // and it has some content + serial := makeDeviceSerialAssertionInDir(c, boot.InstallHostDeviceSaveDir, s.storeSigning, s.brands, + model, devKey, "serial-1234") + + err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSaveDir, "device/fde"), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSaveDir, "device/fde/marker"), nil, 0644) + c.Assert(err, IsNil) + + logbuf, restore := logger.MockLogger() defer restore() - var testCases = []struct { - grade, storageSafety string + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + tpm: true, encrypt: true, trustedBootloader: true, + }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, IsNil) - expectedErr string - }{ - // we don't test unset here because the assertion assembly - // will ensure it has a default - { - "dangerous", "encrypted", - "cannot encrypt device storage as mandated by encrypted storage-safety model option: tpm says no", - }, { - "signed", "encrypted", - "cannot encrypt device storage as mandated by encrypted storage-safety model option: tpm says no", - }, { - "secured", "", - "cannot encrypt device storage as mandated by model grade secured: tpm says no", - }, { - "secured", "encrypted", - "cannot encrypt device storage as mandated by model grade secured: tpm says no", - }, - } - for _, tc := range testCases { - mockModel := s.makeModelAssertionInState(c, "my-brand", "my-model", map[string]interface{}{ - "display-name": "my model", - "architecture": "amd64", - "base": "core20", - "grade": tc.grade, - "storage-safety": tc.storageSafety, - "snaps": []interface{}{ - map[string]interface{}{ - "name": "pc-kernel", - "id": pcKernelSnapID, - "type": "kernel", - "default-channel": "20", - }, - map[string]interface{}{ - "name": "pc", - "id": pcSnapID, - "type": "gadget", - "default-channel": "20", - }}, - }) - deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} - _, err := devicestate.DeviceManagerCheckEncryption(s.mgr, s.state, deviceCtx) - c.Check(err, ErrorMatches, tc.expectedErr, Commentf("%s %s", tc.grade, tc.storageSafety)) - } + // verify that the serial assertion has been restored + assertsInResetSystem := filepath.Join(boot.InstallHostWritableDir, "var/lib/snapd/assertions") + bs, err := asserts.OpenFSBackstore(assertsInResetSystem) + c.Assert(err, IsNil) + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: bs, + Trusted: s.storeSigning.Trusted, + OtherPredefined: s.storeSigning.Generic, + }) + c.Assert(err, IsNil) + ass, err := db.FindMany(asserts.SerialType, map[string]string{ + "brand-id": serial.BrandID(), + "model": serial.Model(), + "device-key-sha3-384": serial.DeviceKey().ID(), + }) + c.Assert(err, IsNil) + c.Assert(ass, HasLen, 1) + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), + testutil.FileEquals, "new-data") + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + testutil.FileEquals, "old-save") + // new key was written + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"), + testutil.FileEquals, "save") } -func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedFDEHook(c *C) { - st := s.state - st.Lock() - defer st.Unlock() +func (s *deviceMgrInstallModeSuite) TestFactoryResetEncryptionHappyAfterReboot(c *C) { + s.state.Lock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() - s.makeModelAssertionInState(c, "canonical", "pc", map[string]interface{}{ - "architecture": "amd64", - "kernel": "pc-kernel", - "gadget": "pc", + // for debug timinigs + mockedSnapCmd := testutil.MockCommand(c, "snap", ` +echo "mock output of: $(basename "$0") $*" +`) + defer mockedSnapCmd.Restore() + + // pretend snap-bootstrap mounted ubuntu-save + err := os.MkdirAll(boot.InitramfsUbuntuSaveDir, 0755) + c.Assert(err, IsNil) + snaptest.PopulateDir(boot.InitramfsSeedEncryptionKeyDir, [][]string{ + {"ubuntu-data.recovery.sealed-key", "old-data"}, + {"ubuntu-save.recovery.sealed-key", "old-save"}, + {"ubuntu-save.recovery.sealed-key.factory-reset", "old-factory-reset"}, }) - devicestatetest.SetDevice(s.state, &auth.DeviceState{ - Brand: "canonical", - Model: "pc", + + // and it has some content + serial := makeDeviceSerialAssertionInDir(c, boot.InstallHostDeviceSaveDir, s.storeSigning, s.brands, + model, devKey, "serial-1234") + + err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSaveDir, "device/fde"), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSaveDir, "device/fde/marker"), nil, 0644) + c.Assert(err, IsNil) + + logbuf, restore := logger.MockLogger() + defer restore() + + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + tpm: true, encrypt: true, trustedBootloader: true, }) - makeInstalledMockKernelSnap(c, st, kernelYamlWithFdeSetup) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, IsNil) - for _, tc := range []struct { - hookOutput string - expectedErr string + // verify that the serial assertion has been restored + assertsInResetSystem := filepath.Join(boot.InstallHostWritableDir, "var/lib/snapd/assertions") + bs, err := asserts.OpenFSBackstore(assertsInResetSystem) + c.Assert(err, IsNil) + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: bs, + Trusted: s.storeSigning.Trusted, + OtherPredefined: s.storeSigning.Generic, + }) + c.Assert(err, IsNil) + ass, err := db.FindMany(asserts.SerialType, map[string]string{ + "brand-id": serial.BrandID(), + "model": serial.Model(), + "device-key-sha3-384": serial.DeviceKey().ID(), + }) + c.Assert(err, IsNil) + c.Assert(ass, HasLen, 1) + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), + testutil.FileEquals, "new-data") + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + testutil.FileEquals, "old-save") + // key was replaced + c.Check(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"), + testutil.FileEquals, "save") +} - encryptionType secboot.EncryptionType - }{ - // invalid json - {"xxx", `cannot parse hook output "xxx": invalid character 'x' looking for beginning of value`, secboot.EncryptionTypeNone}, - // no output is invalid - {"", `cannot parse hook output "": unexpected end of JSON input`, secboot.EncryptionTypeNone}, - // specific error - {`{"error":"failed"}`, `cannot use hook: it returned error: failed`, secboot.EncryptionTypeNone}, - {`{}`, `cannot use hook: neither "features" nor "error" returned`, secboot.EncryptionTypeNone}, - // valid - {`{"features":[]}`, "", secboot.EncryptionTypeLUKS}, - {`{"features":["a"]}`, "", secboot.EncryptionTypeLUKS}, - {`{"features":["a","b"]}`, "", secboot.EncryptionTypeLUKS}, - // features must be list of strings - {`{"features":[1]}`, `cannot parse hook output ".*": json: cannot unmarshal number into Go struct.*`, secboot.EncryptionTypeNone}, - {`{"features":1}`, `cannot parse hook output ".*": json: cannot unmarshal number into Go struct.*`, secboot.EncryptionTypeNone}, - {`{"features":"1"}`, `cannot parse hook output ".*": json: cannot unmarshal string into Go struct.*`, secboot.EncryptionTypeNone}, - // valid and switches to "device-setup" - {`{"features":["device-setup"]}`, "", secboot.EncryptionTypeDeviceSetupHook}, - {`{"features":["a","device-setup","b"]}`, "", secboot.EncryptionTypeDeviceSetupHook}, - } { - hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { - ctx.Lock() - defer ctx.Unlock() - ctx.Set("fde-setup-result", []byte(tc.hookOutput)) - return nil, nil - } - rhk := hookstate.MockRunHook(hookInvoke) - defer rhk() +func (s *deviceMgrInstallModeSuite) TestFactoryResetSerialsWithoutKey(c *C) { + s.state.Lock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() - et, err := devicestate.DeviceManagerCheckFDEFeatures(s.mgr, st) - if tc.expectedErr != "" { - c.Check(err, ErrorMatches, tc.expectedErr, Commentf("%v", tc)) - } else { - c.Check(err, IsNil, Commentf("%v", tc)) - c.Check(et, Equals, tc.encryptionType, Commentf("%v", tc)) - } + // pretend snap-bootstrap mounted ubuntu-save + err := os.MkdirAll(boot.InitramfsUbuntuSaveDir, 0755) + c.Assert(err, IsNil) + + kp, err := asserts.OpenFSKeypairManager(boot.InstallHostDeviceSaveDir) + c.Assert(err, IsNil) + // generate the serial assertions + for i := 0; i < 5; i++ { + key, _ := assertstest.GenerateKey(testKeyLength) + makeDeviceSerialAssertionInDir(c, boot.InstallHostDeviceSaveDir, s.storeSigning, s.brands, + model, key, fmt.Sprintf("serial-%d", i)) + // remove the key such that the assert cannot be used + c.Assert(kp.Delete(key.PublicKey().ID()), IsNil) } -} -var checkEncryptionModelHeaders = map[string]interface{}{ - "display-name": "my model", - "architecture": "amd64", - "base": "core20", - "grade": "dangerous", - "snaps": []interface{}{ - map[string]interface{}{ - "name": "pc-kernel", - "id": pcKernelSnapID, - "type": "kernel", - "default-channel": "20", - }, - map[string]interface{}{ - "name": "pc", - "id": pcSnapID, - "type": "gadget", - "default-channel": "20", - }}, + logbuf, restore := logger.MockLogger() + defer restore() + + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + tpm: false, encrypt: false, + }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, IsNil) + + // nothing has been restored in the assertions dir + matches, err := filepath.Glob(filepath.Join(boot.InstallHostWritableDir, "var/lib/snapd/assertions/*/*")) + c.Assert(err, IsNil) + c.Assert(matches, HasLen, 0) +} + +func (s *deviceMgrInstallModeSuite) TestFactoryResetNoSerials(c *C) { + s.state.Lock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() + + // pretend snap-bootstrap mounted ubuntu-save + err := os.MkdirAll(boot.InitramfsUbuntuSaveDir, 0755) + c.Assert(err, IsNil) + + // no serials, no device keys + logbuf, restore := logger.MockLogger() + defer restore() + + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + tpm: false, encrypt: false, + }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, IsNil) + + // nothing has been restored in the assertions dir + matches, err := filepath.Glob(filepath.Join(boot.InstallHostWritableDir, "var/lib/snapd/assertions/*/*")) + c.Assert(err, IsNil) + c.Assert(matches, HasLen, 0) } -func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedErrorsLogsTPM(c *C) { +func (s *deviceMgrInstallModeSuite) TestFactoryResetNoSave(c *C) { s.state.Lock() - defer s.state.Unlock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() - restore := devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { - return fmt.Errorf("tpm says no") + // no ubuntu-save directory, what makes the whole process behave like reinstall + + // no serials, no device keys + logbuf, restore := logger.MockLogger() + defer restore() + + err := s.doRunFactoryResetChange(c, model, resetTestCase{ + tpm: false, encrypt: false, + noSave: true, }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, IsNil) + + // nothing has been restored in the assertions dir as nothing was there + // to begin with + matches, err := filepath.Glob(filepath.Join(boot.InstallHostWritableDir, "var/lib/snapd/assertions/*/*")) + c.Assert(err, IsNil) + c.Assert(matches, HasLen, 0) + + // and we logged why nothing was restored from save + c.Check(logbuf.String(), testutil.Contains, "not restoring from save, ubuntu-save not mounted") +} + +func (s *deviceMgrInstallModeSuite) TestFactoryResetPreviouslyEncrypted(c *C) { + s.state.Lock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() + + // pretend snap-bootstrap mounted ubuntu-save and there is an encryption marker file + err := os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSaveDir, "device/fde"), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSaveDir, "device/fde/marker"), nil, 0644) + c.Assert(err, IsNil) + + // no serials, no device keys + logbuf, restore := logger.MockLogger() defer restore() + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + // no TPM + tpm: false, encrypt: false, + }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, ErrorMatches, `(?s).*cannot perform factory reset using different encryption, the original system was encrypted\)`) +} + +func (s *deviceMgrInstallModeSuite) TestFactoryResetPreviouslyUnencrypted(c *C) { + s.state.Lock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() + + // pretend snap-bootstrap mounted ubuntu-save but there is no encryption marker + err := os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSaveDir, "device/fde"), 0755) + c.Assert(err, IsNil) + + // no serials, no device keys logbuf, restore := logger.MockLogger() defer restore() - mockModel := s.makeModelAssertionInState(c, "my-brand", "my-model", checkEncryptionModelHeaders) - deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} - _, err := devicestate.DeviceManagerCheckEncryption(s.mgr, s.state, deviceCtx) - c.Check(err, IsNil) - c.Check(logbuf.String(), Matches, "(?s).*: not encrypting device storage as checking TPM gave: tpm says no\n") + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + // no TPM + tpm: true, encrypt: false, + }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, ErrorMatches, `(?s).*cannot perform factory reset using different encryption, the original system was unencrypted\)`) } -func (s *deviceMgrInstallModeSuite) TestInstallCheckEncryptedErrorsLogsHook(c *C) { +func (s *deviceMgrInstallModeSuite) TestFactoryResetSerialManyOneValid(c *C) { s.state.Lock() - defer s.state.Unlock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() + + // pretend snap-bootstrap mounted ubuntu-save + err := os.MkdirAll(boot.InitramfsUbuntuSaveDir, 0755) + c.Assert(err, IsNil) + + kp, err := asserts.OpenFSKeypairManager(boot.InstallHostDeviceSaveDir) + c.Assert(err, IsNil) + // generate some invalid the serial assertions + for i := 0; i < 5; i++ { + key, _ := assertstest.GenerateKey(testKeyLength) + makeDeviceSerialAssertionInDir(c, boot.InstallHostDeviceSaveDir, s.storeSigning, s.brands, + model, key, fmt.Sprintf("serial-%d", i)) + // remove the key such that the assert cannot be used + c.Assert(kp.Delete(key.PublicKey().ID()), IsNil) + } + serial := makeDeviceSerialAssertionInDir(c, boot.InstallHostDeviceSaveDir, s.storeSigning, s.brands, + model, devKey, "serial-1234") logbuf, restore := logger.MockLogger() defer restore() - mockModel := s.makeModelAssertionInState(c, "my-brand", "my-model", checkEncryptionModelHeaders) - // mock kernel installed but no hook or handle so checkEncryption - // will fail - makeInstalledMockKernelSnap(c, s.state, kernelYamlWithFdeSetup) + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + tpm: false, encrypt: false, + }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, IsNil) - deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: mockModel} - _, err := devicestate.DeviceManagerCheckEncryption(s.mgr, s.state, deviceCtx) - c.Check(err, IsNil) - c.Check(logbuf.String(), Matches, "(?s).*: not encrypting device storage as querying kernel fde-setup hook did not succeed:.*\n") + // verify that only one serial assertion has been restored + assertsInResetSystem := filepath.Join(boot.InstallHostWritableDir, "var/lib/snapd/assertions") + bs, err := asserts.OpenFSBackstore(assertsInResetSystem) + c.Assert(err, IsNil) + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: bs, + Trusted: s.storeSigning.Trusted, + OtherPredefined: s.storeSigning.Generic, + }) + c.Assert(err, IsNil) + ass, err := db.FindMany(asserts.SerialType, map[string]string{ + "brand-id": serial.BrandID(), + "model": serial.Model(), + "device-key-sha3-384": serial.DeviceKey().ID(), + }) + c.Assert(err, IsNil) + c.Assert(ass, HasLen, 1) + asSerial, _ := ass[0].(*asserts.Serial) + c.Assert(asSerial, NotNil) + c.Assert(asSerial, DeepEquals, serial) } -func (s *deviceMgrInstallModeSuite) TestInstallHappyLogfiles(c *C) { +func (s *deviceMgrInstallModeSuite) findFactoryReset() *state.Change { + for _, chg := range s.state.Changes() { + if chg.Kind() == "factory-reset" { + return chg + } + } + return nil +} + +func (s *deviceMgrInstallModeSuite) TestFactoryResetExpectedTasks(c *C) { restore := release.MockOnClassic(false) defer restore() - restore = devicestate.MockInstallRun(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, _ gadget.ContentObserver, _ timings.Measurer) (*install.InstalledSystemSideData, error) { - return nil, nil + restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { + return fmt.Errorf("TPM not available") }) defer restore() - mockedSnapCmd := testutil.MockCommand(c, "snap", ` -echo "mock output of: $(basename "$0") $*" -`) - defer mockedSnapCmd.Restore() + restore = devicestate.MockInstallFactoryReset(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, obs gadget.ContentObserver, pertTimings timings.Measurer) (*install.InstalledSystemSideData, error) { + c.Assert(os.MkdirAll(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir), 0755), IsNil) + return &install.InstalledSystemSideData{ + DeviceForRole: map[string]string{ + "ubuntu-save": "/dev/foo", + }, + }, nil + }) + defer restore() - err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), - []byte("mode=install\n"), 0644) - c.Assert(err, IsNil) + m := boot.Modeenv{ + Mode: "factory-reset", + RecoverySystem: "1234", + } + c.Assert(m.WriteTo(""), IsNil) s.state.Lock() - // pretend we are seeding - chg := s.state.NewChange("seed", "just for testing") - chg.AddTask(s.state.NewTask("test-task", "the change needs a task")) s.makeMockInstallModel(c, "dangerous") s.makeMockInstalledPcGadget(c, "", "") - devicestate.SetSystemMode(s.mgr, "install") + devicestate.SetSystemMode(s.mgr, "factory-reset") s.state.Unlock() s.settle(c) @@ -1542,97 +2954,183 @@ s.state.Lock() defer s.state.Unlock() - installSystem := s.findInstallSystem() - c.Check(installSystem.Err(), IsNil) - c.Check(s.restartRequests, HasLen, 1) + factoryReset := s.findFactoryReset() + c.Assert(factoryReset, NotNil) + c.Check(factoryReset.Err(), IsNil) - // logs are created - c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data/var/log/install-mode.log.gz"), testutil.FilePresent) - timingsPath := filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data/var/log/install-timings.txt.gz") - c.Check(timingsPath, testutil.FilePresent) + tasks := factoryReset.Tasks() + c.Assert(tasks, HasLen, 2) + factoryResetTask := tasks[0] + restartSystemToRunModeTask := tasks[1] - f, err := os.Open(timingsPath) - c.Assert(err, IsNil) - defer f.Close() - gz, err := gzip.NewReader(f) - c.Assert(err, IsNil) - content, err := ioutil.ReadAll(gz) - c.Assert(err, IsNil) - c.Check(string(content), Equals, `---- Output of: snap changes -mock output of: snap changes + c.Assert(factoryResetTask.Kind(), Equals, "factory-reset-run-system") + c.Assert(restartSystemToRunModeTask.Kind(), Equals, "restart-system-to-run-mode") ----- Output of snap debug timings --ensure=seed -mock output of: snap debug timings --ensure=seed + // factory-reset has no pre-reqs + c.Assert(factoryResetTask.WaitTasks(), HasLen, 0) ----- Output of snap debug timings --ensure=install-system -mock output of: snap debug timings --ensure=install-system -`) + // restart-system-to-run-mode has a pre-req of factory-reset + waitTasks := restartSystemToRunModeTask.WaitTasks() + c.Assert(waitTasks, HasLen, 1) + c.Assert(waitTasks[0].ID(), Equals, factoryResetTask.ID()) - // and the right commands are run - c.Check(mockedSnapCmd.Calls(), DeepEquals, [][]string{ - {"snap", "changes"}, - {"snap", "debug", "timings", "--ensure=seed"}, - {"snap", "debug", "timings", "--ensure=install-system"}, - }) + // we did request a restart through restartSystemToRunModeTask + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) } -func (s *deviceMgrInstallModeSuite) TestInstallModeWritesTimesyncdClockHappy(c *C) { - now := time.Now() - restore := devicestate.MockTimeNow(func() time.Time { return now }) +func (s *deviceMgrInstallModeSuite) TestFactoryResetInstallDeviceHook(c *C) { + restore := release.MockOnClassic(false) defer restore() - clockTsInSrc := filepath.Join(dirs.GlobalRootDir, "/var/lib/systemd/timesync/clock") - c.Assert(os.MkdirAll(filepath.Dir(clockTsInSrc), 0755), IsNil) - c.Assert(ioutil.WriteFile(clockTsInSrc, nil, 0644), IsNil) - // a month old timestamp file - c.Assert(os.Chtimes(clockTsInSrc, now.AddDate(0, -1, 0), now.AddDate(0, -1, 0)), IsNil) + restore = devicestate.MockSecbootCheckTPMKeySealingSupported(func() error { + return fmt.Errorf("TPM not available") + }) + defer restore() - s.mockInstallModeChange(c, "dangerous", "") + hooksCalled := []*hookstate.Context{} + restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { + ctx.Lock() + defer ctx.Unlock() + + hooksCalled = append(hooksCalled, ctx) + return nil, nil + }) + defer restore() + + restore = devicestate.MockInstallFactoryReset(func(mod gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, obs gadget.ContentObserver, pertTimings timings.Measurer) (*install.InstalledSystemSideData, error) { + c.Assert(os.MkdirAll(dirs.SnapDeviceDirUnder(boot.InstallHostWritableDir), 0755), IsNil) + return &install.InstalledSystemSideData{ + DeviceForRole: map[string]string{ + "ubuntu-save": "/dev/foo", + }, + }, nil + }) + defer restore() + + m := boot.Modeenv{ + Mode: "factory-reset", + RecoverySystem: "1234", + } + c.Assert(m.WriteTo(""), IsNil) + + s.state.Lock() + s.makeMockInstallModel(c, "dangerous") + s.makeMockInstalledPcGadget(c, "install-device-hook-content", "") + devicestate.SetSystemMode(s.mgr, "factory-reset") + s.state.Unlock() + + s.settle(c) s.state.Lock() defer s.state.Unlock() - installSystem := s.findInstallSystem() - c.Assert(installSystem, NotNil) + factoryReset := s.findFactoryReset() + c.Check(factoryReset.Err(), IsNil) - // installation was successful - c.Check(installSystem.Err(), IsNil) - c.Check(installSystem.Status(), Equals, state.DoneStatus) + tasks := factoryReset.Tasks() + c.Assert(tasks, HasLen, 3) + factoryResetTask := tasks[0] + installDeviceTask := tasks[1] + restartSystemTask := tasks[2] + + c.Assert(factoryResetTask.Kind(), Equals, "factory-reset-run-system") + c.Assert(installDeviceTask.Kind(), Equals, "run-hook") + c.Assert(restartSystemTask.Kind(), Equals, "restart-system-to-run-mode") - clockTsInDst := filepath.Join(boot.InstallHostWritableDir, "/var/lib/systemd/timesync/clock") - fi, err := os.Stat(clockTsInDst) + // factory-reset-run-system has no pre-reqs + c.Assert(factoryResetTask.WaitTasks(), HasLen, 0) + + // install-device has a pre-req of factory-reset-run-system + waitTasks := installDeviceTask.WaitTasks() + c.Assert(waitTasks, HasLen, 1) + c.Check(waitTasks[0].ID(), Equals, factoryResetTask.ID()) + + // install-device restart-task references to restart-system-to-run-mode + var restartTask string + err := installDeviceTask.Get("restart-task", &restartTask) c.Assert(err, IsNil) - c.Check(fi.ModTime().Round(time.Second), Equals, now.Round(time.Second)) - c.Check(fi.Size(), Equals, int64(0)) + c.Check(restartTask, Equals, restartSystemTask.ID()) + + // restart-system-to-run-mode has a pre-req of install-device + waitTasks = restartSystemTask.WaitTasks() + c.Assert(waitTasks, HasLen, 1) + c.Check(waitTasks[0].ID(), Equals, installDeviceTask.ID()) + + // we did request a restart through restartSystemToRunModeTask + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) + + c.Assert(hooksCalled, HasLen, 1) + c.Assert(hooksCalled[0].HookName(), Equals, "install-device") } -func (s *deviceMgrInstallModeSuite) TestInstallModeWritesTimesyncdClockErr(c *C) { - now := time.Now() - restore := devicestate.MockTimeNow(func() time.Time { return now }) +func (s *deviceMgrInstallModeSuite) TestFactoryResetRunSysconfig(c *C) { + s.state.Lock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() + + // pretend snap-bootstrap mounted ubuntu-save + err := os.MkdirAll(boot.InitramfsUbuntuSaveDir, 0755) + c.Assert(err, IsNil) + + logbuf, restore := logger.MockLogger() defer restore() - if os.Geteuid() == 0 { - c.Skip("the test cannot be executed by the root user") + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + tpm: false, + }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, IsNil) + + // and sysconfig.ConfigureTargetSystem was run exactly once + c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ + { + AllowCloudInit: true, + TargetRootDir: boot.InstallHostWritableDir, + GadgetDir: filepath.Join(dirs.SnapMountDir, "pc/1/"), + }, + }) + + // and the special dirs in _writable_defaults were created + for _, dir := range []string{"/etc/udev/rules.d/", "/etc/modules-load.d/", "/etc/modprobe.d/"} { + fullDir := filepath.Join(sysconfig.WritableDefaultsDir(boot.InstallHostWritableDir), dir) + c.Assert(fullDir, testutil.FilePresent) } +} + +func (s *deviceMgrInstallModeSuite) TestFactoryResetWritesTimesyncdClock(c *C) { + now := time.Now() + restore := devicestate.MockTimeNow(func() time.Time { return now }) + defer restore() clockTsInSrc := filepath.Join(dirs.GlobalRootDir, "/var/lib/systemd/timesync/clock") c.Assert(os.MkdirAll(filepath.Dir(clockTsInSrc), 0755), IsNil) c.Assert(ioutil.WriteFile(clockTsInSrc, nil, 0644), IsNil) + // a month old timestamp file + c.Assert(os.Chtimes(clockTsInSrc, now.AddDate(0, -1, 0), now.AddDate(0, -1, 0)), IsNil) - timesyncDirInDst := filepath.Join(boot.InstallHostWritableDir, "/var/lib/systemd/timesync/") - c.Assert(os.MkdirAll(timesyncDirInDst, 0755), IsNil) - c.Assert(os.Chmod(timesyncDirInDst, 0000), IsNil) - defer os.Chmod(timesyncDirInDst, 0755) + s.state.Lock() + model := s.makeMockInstallModel(c, "dangerous") + s.state.Unlock() - s.mockInstallModeChange(c, "dangerous", "") + // pretend snap-bootstrap mounted ubuntu-save + err := os.MkdirAll(boot.InitramfsUbuntuSaveDir, 0755) + c.Assert(err, IsNil) + + logbuf, restore := logger.MockLogger() + defer restore() + + err = s.doRunFactoryResetChange(c, model, resetTestCase{ + tpm: false, + }) + c.Logf("logs:\n%v", logbuf.String()) + c.Assert(err, IsNil) s.state.Lock() defer s.state.Unlock() - installSystem := s.findInstallSystem() - c.Assert(installSystem, NotNil) - - // install failed copying the timestamp - c.Check(installSystem.Err(), ErrorMatches, `(?s).*\(cannot seed timesyncd clock: cannot copy clock:.*Permission denied.*`) - c.Check(installSystem.Status(), Equals, state.ErrorStatus) + clockTsInDst := filepath.Join(boot.InstallHostWritableDir, "/var/lib/systemd/timesync/clock") + fi, err := os.Stat(clockTsInDst) + c.Assert(err, IsNil) + c.Check(fi.ModTime().Round(time.Second), Equals, now.Round(time.Second)) + c.Check(fi.Size(), Equals, int64(0)) } diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicestate_recovery_keys_test.go snapd-2.57.5+20.04/overlord/devicestate/devicestate_recovery_keys_test.go --- snapd-2.55.5+20.04/overlord/devicestate/devicestate_recovery_keys_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicestate_recovery_keys_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -0,0 +1,212 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2022 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 devicestate_test + +import ( + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/overlord/devicestate" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" +) + +var _ = Suite(&deviceMgrRecoveryKeysSuite{}) + +type deviceMgrRecoveryKeysSuite struct { + deviceMgrBaseSuite +} + +func (s *deviceMgrRecoveryKeysSuite) SetUpTest(c *C) { + if (keys.RecoveryKey{}).String() == "not-implemented" { + c.Skip("needs working secboot recovery key") + } + s.deviceMgrBaseSuite.setupBaseTest(c, false) + + devicestate.SetSystemMode(s.mgr, "run") +} + +func mockSnapFDEFile(c *C, fname string, data []byte) { + p := filepath.Join(dirs.SnapFDEDir, fname) + err := os.MkdirAll(filepath.Dir(p), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(p, data, 0644) + c.Assert(err, IsNil) +} + +func mockSystemRecoveryKeys(c *C, alsoReinstall bool) { + // same inputs/outputs as secboot:crypt_test.go in this test + rkeystr, err := hex.DecodeString("e1f01302c5d43726a9b85b4a8d9c7f6e") + c.Assert(err, IsNil) + mockSnapFDEFile(c, "recovery.key", []byte(rkeystr)) + + if alsoReinstall { + skeystr := "1234567890123456" + mockSnapFDEFile(c, "reinstall.key", []byte(skeystr)) + } +} + +func (s *deviceMgrRecoveryKeysSuite) TestEnsureRecoveryKeysBackwardCompat(c *C) { + mockSystemRecoveryKeys(c, true) + + keys, err := s.mgr.EnsureRecoveryKeys() + c.Assert(err, IsNil) + + c.Assert(keys, DeepEquals, &client.SystemRecoveryKeysResponse{ + RecoveryKey: "61665-00531-54469-09783-47273-19035-40077-28287", + ReinstallKey: "12849-13363-13877-14391-12345-12849-13363-13877", + }) +} + +func (s *deviceMgrRecoveryKeysSuite) TestEnsureRecoveryKey(c *C) { + _, err := s.mgr.EnsureRecoveryKeys() + c.Check(err, ErrorMatches, `system does not use disk encryption`) + + rkeystr, err := hex.DecodeString("e1f01302c5d43726a9b85b4a8d9c7f6e") + c.Assert(err, IsNil) + defer devicestate.MockSecbootEnsureRecoveryKey(func(keyFile string, rkeyDevs []secboot.RecoveryKeyDevice) (keys.RecoveryKey, error) { + c.Check(keyFile, Equals, filepath.Join(dirs.SnapFDEDir, "recovery.key")) + c.Check(rkeyDevs, DeepEquals, []secboot.RecoveryKeyDevice{ + {Mountpoint: boot.InitramfsDataDir}, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + AuthorizingKeyFile: filepath.Join(boot.InitramfsDataDir, "system-data/var/lib/snapd/device/fde/ubuntu-save.key"), + }, + }) + + var rkey keys.RecoveryKey + copy(rkey[:], []byte(rkeystr)) + return rkey, nil + })() + mockSnapFDEFile(c, "marker", nil) + + keys, err := s.mgr.EnsureRecoveryKeys() + c.Assert(err, IsNil) + + c.Assert(keys, DeepEquals, &client.SystemRecoveryKeysResponse{ + RecoveryKey: "61665-00531-54469-09783-47273-19035-40077-28287", + }) +} + +func (s *deviceMgrRecoveryKeysSuite) TestEnsureRecoveryKeyInstallMode(c *C) { + devicestate.SetSystemMode(s.mgr, "install") + + rkeystr, err := hex.DecodeString("e1f01302c5d43726a9b85b4a8d9c7f6e") + c.Assert(err, IsNil) + defer devicestate.MockSecbootEnsureRecoveryKey(func(keyFile string, rkeyDevs []secboot.RecoveryKeyDevice) (keys.RecoveryKey, error) { + c.Check(keyFile, Equals, filepath.Join(boot.InstallHostFDEDataDir, "recovery.key")) + c.Check(rkeyDevs, DeepEquals, []secboot.RecoveryKeyDevice{ + { + Mountpoint: filepath.Dir(boot.InstallHostWritableDir), + }, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + AuthorizingKeyFile: filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key"), + }, + }) + + var rkey keys.RecoveryKey + copy(rkey[:], []byte(rkeystr)) + return rkey, nil + })() + + p := filepath.Join(boot.InstallHostFDEDataDir, "marker") + err = os.MkdirAll(filepath.Dir(p), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(p, nil, 0644) + c.Assert(err, IsNil) + + keys, err := s.mgr.EnsureRecoveryKeys() + c.Assert(err, IsNil) + + c.Assert(keys, DeepEquals, &client.SystemRecoveryKeysResponse{ + RecoveryKey: "61665-00531-54469-09783-47273-19035-40077-28287", + }) +} + +func (s *deviceMgrRecoveryKeysSuite) TestEnsureRecoveryKeyRecoverMode(c *C) { + devicestate.SetSystemMode(s.mgr, "recover") + + _, err := s.mgr.EnsureRecoveryKeys() + c.Check(err, ErrorMatches, `cannot ensure recovery keys from system mode "recover"`) +} + +func (s *deviceMgrRecoveryKeysSuite) TestRemoveRecoveryKeys(c *C) { + err := s.mgr.RemoveRecoveryKeys() + c.Check(err, ErrorMatches, `system does not use disk encryption`) + + called := false + rkey := filepath.Join(dirs.SnapFDEDir, "recovery.key") + defer devicestate.MockSecbootRemoveRecoveryKeys(func(r2k map[secboot.RecoveryKeyDevice]string) error { + called = true + c.Check(r2k, DeepEquals, map[secboot.RecoveryKeyDevice]string{ + {Mountpoint: boot.InitramfsDataDir}: rkey, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + AuthorizingKeyFile: filepath.Join(boot.InitramfsDataDir, "system-data/var/lib/snapd/device/fde/ubuntu-save.key"), + }: rkey, + }) + return nil + })() + mockSnapFDEFile(c, "marker", nil) + + err = s.mgr.RemoveRecoveryKeys() + c.Assert(err, IsNil) + c.Check(called, Equals, true) +} + +func (s *deviceMgrRecoveryKeysSuite) TestRemoveRecoveryKeysBackwardCompat(c *C) { + called := false + rkey := filepath.Join(dirs.SnapFDEDir, "recovery.key") + defer devicestate.MockSecbootRemoveRecoveryKeys(func(r2k map[secboot.RecoveryKeyDevice]string) error { + called = true + c.Check(r2k, DeepEquals, map[secboot.RecoveryKeyDevice]string{ + {Mountpoint: boot.InitramfsDataDir}: rkey, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + AuthorizingKeyFile: filepath.Join(boot.InitramfsDataDir, "system-data/var/lib/snapd/device/fde/ubuntu-save.key"), + }: filepath.Join(dirs.SnapFDEDir, "reinstall.key"), + }) + return nil + })() + mockSnapFDEFile(c, "marker", nil) + mockSnapFDEFile(c, "reinstall.key", nil) + + err := s.mgr.RemoveRecoveryKeys() + c.Assert(err, IsNil) + c.Check(called, Equals, true) +} + +func (s *deviceMgrRecoveryKeysSuite) TestRemoveRecoveryKeysOtherModes(c *C) { + for _, mode := range []string{"recover", "install"} { + devicestate.SetSystemMode(s.mgr, mode) + + err := s.mgr.RemoveRecoveryKeys() + c.Check(err, ErrorMatches, fmt.Sprintf(`cannot remove recovery keys from system mode %q`, mode)) + } +} diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicestate_remodel_test.go snapd-2.57.5+20.04/overlord/devicestate/devicestate_remodel_test.go --- snapd-2.55.5+20.04/overlord/devicestate/devicestate_remodel_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicestate_remodel_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2021 Canonical Ltd + * Copyright (C) 2016-2022 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 @@ -154,7 +154,7 @@ {map[string]interface{}{"architecture": "pdp-7"}, "cannot remodel to different architectures yet"}, {map[string]interface{}{"base": "core18"}, "cannot remodel from core to bases yet"}, // pre-UC20 to UC20 - {map[string]interface{}{"base": "core20", "kernel": nil, "gadget": nil, "snaps": mockCore20ModelSnaps}, "cannot remodel to Ubuntu Core 20 models yet"}, + {map[string]interface{}{"base": "core20", "kernel": nil, "gadget": nil, "snaps": mockCore20ModelSnaps}, `cannot remodel from pre-UC20 to UC20\+ models`}, } { mergeMockModelHeaders(cur, t.new) new := s.brands.Model(t.new["brand"].(string), t.new["model"].(string), t.new) @@ -698,9 +698,9 @@ ensureDeviceSession int } -func (sto *freshSessionStore) EnsureDeviceSession() (*auth.DeviceState, error) { +func (sto *freshSessionStore) EnsureDeviceSession() error { sto.ensureDeviceSession += 1 - return nil, nil + return nil } func (s *deviceMgrRemodelSuite) TestRemodelStoreSwitch(c *C) { @@ -1063,7 +1063,7 @@ // nothing in the state _, err := devicestate.DeviceCtx(s.state, nil, nil) - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // have a model assertion model := s.brands.Model("canonical", "pc", map[string]interface{}{ diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicestate_serial_test.go snapd-2.57.5+20.04/overlord/devicestate/devicestate_serial_test.go --- snapd-2.55.5+20.04/overlord/devicestate/devicestate_serial_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicestate_serial_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1359,9 +1359,9 @@ defer s.state.Unlock() // nothing in the state _, err := s.mgr.Model() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) _, err = s.mgr.Serial() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // just brand and model devicestatetest.SetDevice(s.state, &auth.DeviceState{ @@ -1369,9 +1369,9 @@ Model: "pc", }) _, err = s.mgr.Model() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) _, err = s.mgr.Serial() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // have a model assertion model := s.brands.Model("canonical", "pc", map[string]interface{}{ @@ -1386,7 +1386,7 @@ c.Assert(mod.BrandID(), Equals, "canonical") _, err = s.mgr.Serial() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // have a serial as well devicestatetest.SetDevice(s.state, &auth.DeviceState{ @@ -1397,7 +1397,7 @@ _, err = s.mgr.Model() c.Assert(err, IsNil) _, err = s.mgr.Serial() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // have a serial assertion s.makeSerialAssertionInState(c, "canonical", "pc", "8989") @@ -1434,9 +1434,9 @@ // nothing in the state _, err := scb.Model() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) _, err = scb.Serial() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // just brand and model devicestatetest.SetDevice(s.state, &auth.DeviceState{ @@ -1444,9 +1444,9 @@ Model: "pc", }) _, err = scb.Model() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) _, err = scb.Serial() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // have a model assertion model := s.brands.Model("canonical", "pc", map[string]interface{}{ @@ -1461,7 +1461,7 @@ c.Assert(mod.BrandID(), Equals, "canonical") _, err = scb.Serial() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // have a serial as well devicestatetest.SetDevice(s.state, &auth.DeviceState{ @@ -1472,7 +1472,7 @@ _, err = scb.Model() c.Assert(err, IsNil) _, err = scb.Serial() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // have a serial assertion s.makeSerialAssertionInState(c, "canonical", "pc", "8989") @@ -1560,7 +1560,7 @@ // nothing in the state _, err := scb.ProxyStore() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) // have a store referenced tr := config.NewTransaction(s.state) @@ -1569,7 +1569,7 @@ c.Assert(err, IsNil) _, err = scb.ProxyStore() - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) operatorAcct := assertstest.NewAccount(s.storeSigning, "foo-operator", nil, "") @@ -2207,3 +2207,116 @@ becomeOperational = s.findBecomeOperationalChange() c.Assert(becomeOperational, IsNil) } + +func (s *deviceMgrSerialSuite) TestDeviceSerialRestoreHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + log, restore := logger.MockLogger() + defer restore() + + // setup state as will be done by first-boot + s.state.Lock() + defer s.state.Unlock() + + // in this case is just marked seeded without snaps + s.state.Set("seeded", true) + + // save is available (where device keys are kept) + devicestate.SetSaveAvailable(s.mgr, true) + + // this is the regular assertions DB + c.Assert(os.MkdirAll(dirs.SnapAssertsDBDir, 0755), IsNil) + // this is the ubuntu-save is bind mounted under /var/lib/snapd/save, + // there is a device directory under it + c.Assert(os.MkdirAll(dirs.SnapDeviceSaveDir, 0755), IsNil) + + bs, err := asserts.OpenFSBackstore(dirs.SnapAssertsDBDir) + c.Assert(err, IsNil) + + // the test suite uses a memory backstore DB, but we need to look at the + // filesystem + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: bs, + Trusted: s.storeSigning.Trusted, + OtherPredefined: s.storeSigning.Generic, + }) + c.Assert(err, IsNil) + // cleanup is done by the suite + assertstate.ReplaceDB(s.state, db) + err = db.Add(s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) + + model := s.makeModelAssertionInState(c, "my-brand", "pc-20", map[string]interface{}{ + "architecture": "amd64", + // UC20 + "base": "core20", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": snaptest.AssertedSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": snaptest.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + }, + }) + + devicestatetest.MockGadget(c, s.state, "pc", snap.R(2), nil) + + // the mock has written key under snap asserts dir, but when ubuntu-save + // exists, the key is written under ubuntu-save/device, thus + // factory-reset never restores is to the asserts dir + kp, err := asserts.OpenFSKeypairManager(dirs.SnapAssertsDBDir) + c.Assert(err, IsNil) + + otherKey, _ := assertstest.GenerateKey(testKeyLength) + // an assertion for which there is no corresponding device key + makeDeviceSerialAssertionInDir(c, dirs.SnapAssertsDBDir, s.storeSigning, s.brands, + model, otherKey, "serial-other-key") + c.Assert(kp.Delete(otherKey.PublicKey().ID()), IsNil) + // an assertion which has a device key, which needs to be moved to the + // right location + makeDeviceSerialAssertionInDir(c, dirs.SnapAssertsDBDir, s.storeSigning, s.brands, + model, devKey, "serial-1234") + c.Assert(kp.Delete(devKey.PublicKey().ID()), IsNil) + // write the key under a location which corresponds to ubuntu-save/device + kp, err = asserts.OpenFSKeypairManager(dirs.SnapDeviceSaveDir) + c.Assert(err, IsNil) + c.Assert(kp.Put(devKey), IsNil) + + devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "my-brand", + Model: "pc-20", + }) + + s.state.Unlock() + s.se.Ensure() + s.state.Lock() + + // no need for the operational change + becomeOperational := s.findBecomeOperationalChange() + c.Check(becomeOperational, IsNil) + + device, err := devicestatetest.Device(s.state) + c.Assert(err, IsNil) + c.Check(device.Brand, Equals, "my-brand") + c.Check(device.Model, Equals, "pc-20") + // serial was restored + c.Check(device.Serial, Equals, "serial-1234") + // key ID was restored + c.Check(device.KeyID, Equals, devKey.PublicKey().ID()) + // key is present + _, err = devicestate.KeypairManager(s.mgr).Get(devKey.PublicKey().ID()) + c.Check(err, IsNil) + // no session yet + c.Check(device.SessionMacaroon, Equals, "") + // and something was logged + c.Check(log.String(), testutil.Contains, + fmt.Sprintf("restored serial serial-1234 for my-brand/pc-20 signed with key %v", devKey.PublicKey().ID())) +} diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicestate_systems_test.go snapd-2.57.5+20.04/overlord/devicestate/devicestate_systems_test.go --- snapd-2.55.5+20.04/overlord/devicestate/devicestate_systems_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicestate_systems_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -1063,7 +1063,7 @@ var triedSystems []string s.state.Lock() err = s.state.Get("tried-systems", &triedSystems) - c.Assert(err, Equals, state.ErrNoState) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) // also logged c.Check(s.logbuf.String(), testutil.Contains, `tried recovery system outcome error: recovery system "1234" was tried, but is not present in the modeenv CurrentRecoverySystems`) s.state.Unlock() @@ -1093,7 +1093,7 @@ var triedSystems []string s.state.Lock() err = s.state.Get("tried-systems", &triedSystems) - c.Assert(err, Equals, state.ErrNoState) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) c.Check(s.logbuf.String(), testutil.Contains, `tried recovery system "1234" failed`) s.state.Unlock() @@ -1113,7 +1113,7 @@ s.state.Lock() defer s.state.Unlock() err = s.state.Get("tried-systems", &triedSystems) - c.Assert(err, Equals, state.ErrNoState) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) // bootenv got cleared m, err = s.bootloader.GetBootVars("try_recovery_system", "recovery_system_status") c.Assert(err, IsNil) @@ -1495,7 +1495,7 @@ var triedSystemsAfterFinalize []string err = s.state.Get("tried-systems", &triedSystemsAfterFinalize) - c.Assert(err, Equals, state.ErrNoState) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) modeenvAfterFinalize, err := boot.ReadModeenv("") c.Assert(err, IsNil) @@ -1523,8 +1523,8 @@ barSnap := snaptest.MakeTestSnapWithFiles(c, "name: bar\nversion: 1.0\nbase: core20", nil) s.state.Lock() // fake downloads are a nop - tSnapsup1 := s.state.NewTask("fake-download", "dummy task carrying snap setup") - tSnapsup2 := s.state.NewTask("fake-download", "dummy task carrying snap setup") + tSnapsup1 := s.state.NewTask("fake-download", "test task carrying snap setup") + tSnapsup2 := s.state.NewTask("fake-download", "test task carrying snap setup") // both snaps are asserted snapsupFoo := snapstate.SnapSetup{ SideInfo: &snap.SideInfo{RealName: "foo", SnapID: s.ss.AssertedSnapID("foo"), Revision: snap.R(99)}, @@ -1564,7 +1564,7 @@ }) tss.WaitFor(tSnapsup1) tss.WaitFor(tSnapsup2) - // add the dummy tasks to the change + // add the test tasks to the change chg := s.state.NewChange("create-recovery-system", "create recovery system") chg.AddTask(tSnapsup1) chg.AddTask(tSnapsup2) @@ -1703,7 +1703,7 @@ s.state.Lock() defer s.state.Unlock() // fake downloads are a nop - tSnapsup1 := s.state.NewTask("fake-download", "dummy task carrying snap setup") + tSnapsup1 := s.state.NewTask("fake-download", "test task carrying snap setup") // both snaps are asserted snapsupFoo := snapstate.SnapSetup{ SideInfo: &snap.SideInfo{RealName: "foo", SnapID: s.ss.AssertedSnapID("foo"), Revision: snap.R(99)}, @@ -1728,7 +1728,7 @@ "snap-setup-tasks": []interface{}{tSnapsup1.ID()}, }) tss.WaitFor(tSnapsup1) - // add the dummy task to the change + // add the test task to the change chg := s.state.NewChange("create-recovery-system", "create recovery system") chg.AddTask(tSnapsup1) chg.AddAll(tss) @@ -1873,7 +1873,7 @@ var triedSystemsAfter []string err = s.state.Get("tried-systems", &triedSystemsAfter) - c.Assert(err, Equals, state.ErrNoState) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) modeenvAfterFinalize, err := boot.ReadModeenv("") c.Assert(err, IsNil) diff -Nru snapd-2.55.5+20.04/overlord/devicestate/devicestate_test.go snapd-2.57.5+20.04/overlord/devicestate/devicestate_test.go --- snapd-2.55.5+20.04/overlord/devicestate/devicestate_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/devicestate_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -22,6 +22,7 @@ import ( "errors" "fmt" + "io/ioutil" "os" "path/filepath" "strings" @@ -59,7 +60,7 @@ "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/overlord/storecontext" "github.com/snapcore/snapd/release" - "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/snapdenv" @@ -245,6 +246,9 @@ s.AddCleanup(devicestate.MockTimeutilIsNTPSynchronized(func() (bool, error) { return true, nil })) + s.AddCleanup(devicestate.MockSecbootMarkSuccessful(func() error { + return nil + })) } func (s *deviceMgrBaseSuite) newStore(devBE storecontext.DeviceBackend) snapstate.StoreService { @@ -505,6 +509,13 @@ func (s *deviceMgrSuite) TestDeviceManagerEnsureBootOkBootloaderHappy(c *C) { s.setPCModelInState(c) + secbootMarkSuccessfulCalled := 0 + r := devicestate.MockSecbootMarkSuccessful(func() error { + secbootMarkSuccessfulCalled++ + return nil + }) + defer r() + s.bootloader.SetBootVars(map[string]string{ "snap_mode": boot.TryingStatus, "snap_try_core": "core_1.snap", @@ -524,6 +535,7 @@ err := devicestate.EnsureBootOk(s.mgr) s.state.Lock() c.Assert(err, IsNil) + c.Check(secbootMarkSuccessfulCalled, Equals, 1) m, err := s.bootloader.GetBootVars("snap_mode") c.Assert(err, IsNil) @@ -905,7 +917,7 @@ // third attempt ongoing, or done // fallback, try auto-refresh devicestate.IncEnsureOperationalAttempts(s.state) - // sanity + // validity c.Check(devicestate.EnsureOperationalAttempts(s.state), Equals, 3) c.Check(canAutoRefresh(), Equals, true) } @@ -1573,7 +1585,7 @@ }) st.Unlock() - mockKey := secboot.EncryptionKey{1, 2, 3, 4} + mockKey := keys.EncryptionKey{1, 2, 3, 4} var hookCalled []string hookInvoke := func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { @@ -1899,3 +1911,167 @@ c.Check(msgs, Matches, "(?sm).*fixing permissions of .*/var/lib/snapd/void to 0111") c.Check(strings.Split(msgs, "\n"), HasLen, 1) } + +func (s *deviceMgrSuite) TestDeviceManagerEnsurePostFactoryResetEncrypted(c *C) { + defer release.MockOnClassic(false) + + s.state.Lock() + s.state.Set("seeded", true) + s.state.Unlock() + devicestate.SetBootOkRan(s.mgr, false) + devicestate.SetSystemMode(s.mgr, "run") + + // encrypted system + mockSnapFDEFile(c, "marker", nil) + err := ioutil.WriteFile(filepath.Join(dirs.SnapFDEDir, "ubuntu-save.key"), + []byte("save-key"), 0644) + c.Assert(err, IsNil) + c.Assert(os.MkdirAll(boot.InitramfsSeedEncryptionKeyDir, 0755), IsNil) + err = ioutil.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + []byte("old"), 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"), + []byte("save"), 0644) + c.Assert(err, IsNil) + // matches the .factory key + factoryResetMarkercontent := []byte(`{"fallback-save-key-sha3-384":"d192153f0a50e826c6eb400c8711750ed0466571df1d151aaecc8c73095da7ec104318e7bf74d5e5ae2940827bf8402b"} +`) + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), factoryResetMarkercontent, 0644), IsNil) + + completeCalls := 0 + restore := devicestate.MockMarkFactoryResetComplete(func(encrypted bool) error { + completeCalls++ + c.Check(encrypted, Equals, true) + return nil + }) + defer restore() + transitionCalls := 0 + restore = devicestate.MockSecbootTransitionEncryptionKeyChange(func(mountpoint string, key keys.EncryptionKey) error { + transitionCalls++ + c.Check(mountpoint, Equals, boot.InitramfsUbuntuSaveDir) + c.Check(key, DeepEquals, keys.EncryptionKey([]byte("save-key"))) + return nil + }) + defer restore() + + err = s.mgr.Ensure() + c.Assert(err, IsNil) + + c.Check(completeCalls, Equals, 1) + c.Check(transitionCalls, Equals, 1) + // factory reset marker is gone, the key was verified successfully + c.Check(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), testutil.FileAbsent) + c.Check(filepath.Join(dirs.SnapFDEDir, "marker"), testutil.FilePresent) + + completeCalls = 0 + transitionCalls = 0 + // try again, no marker, nothing should happen + devicestate.SetPostFactoryResetRan(s.mgr, false) + err = s.mgr.Ensure() + c.Assert(err, IsNil) + // nothing was called + c.Check(completeCalls, Equals, 0) + c.Check(transitionCalls, Equals, 0) + + // have the marker, but migrate the key as if boot code would do it and + // try again, in this setup the marker hash matches the migrated key + c.Check(os.Rename(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"), + filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key")), + IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), factoryResetMarkercontent, 0644), IsNil) + + devicestate.SetPostFactoryResetRan(s.mgr, false) + err = s.mgr.Ensure() + c.Assert(err, IsNil) + c.Check(completeCalls, Equals, 1) + c.Check(transitionCalls, Equals, 1) + // the marker was again removed + c.Check(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), testutil.FileAbsent) +} + +func (s *deviceMgrSuite) TestDeviceManagerEnsurePostFactoryResetEncryptedError(c *C) { + defer release.MockOnClassic(false) + + s.state.Lock() + s.state.Set("seeded", true) + s.state.Unlock() + devicestate.SetBootOkRan(s.mgr, false) + devicestate.SetSystemMode(s.mgr, "run") + + // encrypted system + mockSnapFDEFile(c, "marker", nil) + c.Assert(os.MkdirAll(boot.InitramfsSeedEncryptionKeyDir, 0755), IsNil) + err := ioutil.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + []byte("old"), 0644) + c.Check(err, IsNil) + err = ioutil.WriteFile(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key.factory-reset"), + []byte("save"), 0644) + c.Check(err, IsNil) + // does not match the save key + factoryResetMarkercontent := []byte(`{"fallback-save-key-sha3-384":"uh-oh"} +`) + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), factoryResetMarkercontent, 0644), IsNil) + + completeCalls := 0 + restore := devicestate.MockMarkFactoryResetComplete(func(encrypted bool) error { + completeCalls++ + c.Check(encrypted, Equals, true) + return nil + }) + defer restore() + + err = s.mgr.Ensure() + c.Assert(err, ErrorMatches, "devicemgr: cannot verify factory reset marker: fallback sealed key digest mismatch, got d192153f0a50e826c6eb400c8711750ed0466571df1d151aaecc8c73095da7ec104318e7bf74d5e5ae2940827bf8402b expected uh-oh") + + c.Check(completeCalls, Equals, 0) + // factory reset marker is gone, the key was verified successfully + c.Check(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), testutil.FilePresent) + c.Check(filepath.Join(dirs.SnapFDEDir, "marker"), testutil.FilePresent) + + // try again, no marker, hit the same error + devicestate.SetPostFactoryResetRan(s.mgr, false) + err = s.mgr.Ensure() + c.Assert(err, ErrorMatches, "devicemgr: cannot verify factory reset marker: fallback sealed key digest mismatch, got d192153f0a50e826c6eb400c8711750ed0466571df1d151aaecc8c73095da7ec104318e7bf74d5e5ae2940827bf8402b expected uh-oh") + c.Check(completeCalls, Equals, 0) + + // and again, but not resetting the 'ran' check, so nothing is checked or called + err = s.mgr.Ensure() + c.Assert(err, IsNil) + c.Check(completeCalls, Equals, 0) +} + +func (s *deviceMgrSuite) TestDeviceManagerEnsurePostFactoryResetUnencrypted(c *C) { + defer release.MockOnClassic(false) + + s.state.Lock() + s.state.Set("seeded", true) + s.state.Unlock() + devicestate.SetBootOkRan(s.mgr, false) + devicestate.SetSystemMode(s.mgr, "run") + + // mock the factory reset marker of a system that isn't encrypted + c.Assert(os.MkdirAll(dirs.SnapDeviceDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), []byte("{}"), 0644), IsNil) + + completeCalls := 0 + restore := devicestate.MockMarkFactoryResetComplete(func(encrypted bool) error { + completeCalls++ + c.Check(encrypted, Equals, false) + return nil + }) + defer restore() + + err := s.mgr.Ensure() + c.Assert(err, IsNil) + + c.Check(completeCalls, Equals, 1) + // factory reset marker is gone + c.Check(filepath.Join(dirs.SnapDeviceDir, "factory-reset"), testutil.FileAbsent) + + // try again, no marker, nothing should happen + devicestate.SetPostFactoryResetRan(s.mgr, false) + err = s.mgr.Ensure() + c.Assert(err, IsNil) + // nothing was called + c.Check(completeCalls, Equals, 1) +} diff -Nru snapd-2.55.5+20.04/overlord/devicestate/export_test.go snapd-2.57.5+20.04/overlord/devicestate/export_test.go --- snapd-2.55.5+20.04/overlord/devicestate/export_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/export_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -34,8 +34,10 @@ "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/overlord/storecontext" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/sysconfig" + "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/timings" ) @@ -205,6 +207,10 @@ m.ensureTriedRecoverySystemRan = b } +func SetPostFactoryResetRan(m *DeviceManager, b bool) { + m.ensurePostFactoryResetRan = b +} + func StartTime() time.Time { return startTime } @@ -262,8 +268,22 @@ LogNewSystemSnapFile = logNewSystemSnapFile PurgeNewSystemSnapFiles = purgeNewSystemSnapFiles CreateRecoverySystemTasks = createRecoverySystemTasks + + MaybeApplyPreseededData = maybeApplyPreseededData ) +func MockMaybeApplyPreseededData(f func(st *state.State, ubuntuSeedDir, sysLabel, writableDir string) (bool, error)) (restore func()) { + r := testutil.Backup(&maybeApplyPreseededData) + maybeApplyPreseededData = f + return r +} + +func MockSeedOpen(f func(seedDir, label string) (seed.Seed, error)) (restore func()) { + r := testutil.Backup(&seedOpen) + seedOpen = f + return r +} + func MockGadgetUpdate(mock func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error) (restore func()) { old := gadgetUpdate gadgetUpdate = mock @@ -281,11 +301,15 @@ } func MockBootMakeSystemRunnable(f func(model *asserts.Model, bootWith *boot.BootableSet, seal *boot.TrustedAssetsInstallObserver) error) (restore func()) { - old := bootMakeRunnable + restore = testutil.Backup(&bootMakeRunnable) bootMakeRunnable = f - return func() { - bootMakeRunnable = old - } + return restore +} + +func MockBootMakeSystemRunnableAfterDataReset(f func(model *asserts.Model, bootWith *boot.BootableSet, seal *boot.TrustedAssetsInstallObserver) error) (restore func()) { + restore = testutil.Backup(&bootMakeRunnableAfterDataReset) + bootMakeRunnableAfterDataReset = f + return restore } func MockBootEnsureNextBootToRunMode(f func(systemLabel string) error) (restore func()) { @@ -328,6 +352,24 @@ } } +func MockInstallFactoryReset(f func(model gadget.Model, gadgetRoot, kernelRoot, device string, options install.Options, observer gadget.ContentObserver, perfTimings timings.Measurer) (*install.InstalledSystemSideData, error)) (restore func()) { + restore = testutil.Backup(&installFactoryReset) + installFactoryReset = f + return restore +} + +func MockSecbootStageEncryptionKeyChange(f func(node string, key keys.EncryptionKey) error) (restore func()) { + restore = testutil.Backup(&secbootStageEncryptionKeyChange) + secbootStageEncryptionKeyChange = f + return restore +} + +func MockSecbootTransitionEncryptionKeyChange(f func(mountpoint string, key keys.EncryptionKey) error) (restore func()) { + restore = testutil.Backup(&secbootTransitionEncryptionKeyChange) + secbootTransitionEncryptionKeyChange = f + return restore +} + func MockCloudInitStatus(f func() (sysconfig.CloudInitState, error)) (restore func()) { old := cloudInitStatus cloudInitStatus = f @@ -379,3 +421,27 @@ systemForPreseeding = old } } + +func MockSecbootEnsureRecoveryKey(f func(recoveryKeyFile string, rkeyDevs []secboot.RecoveryKeyDevice) (keys.RecoveryKey, error)) (restore func()) { + restore = testutil.Backup(&secbootEnsureRecoveryKey) + secbootEnsureRecoveryKey = f + return restore +} + +func MockSecbootRemoveRecoveryKeys(f func(rkeyDevToKey map[secboot.RecoveryKeyDevice]string) error) (restore func()) { + restore = testutil.Backup(&secbootRemoveRecoveryKeys) + secbootRemoveRecoveryKeys = f + return restore +} + +func MockMarkFactoryResetComplete(f func(encrypted bool) error) (restore func()) { + restore = testutil.Backup(&bootMarkFactoryResetComplete) + bootMarkFactoryResetComplete = f + return restore +} + +func MockSecbootMarkSuccessful(f func() error) (restore func()) { + r := testutil.Backup(&secbootMarkSuccessful) + secbootMarkSuccessful = f + return r +} diff -Nru snapd-2.55.5+20.04/overlord/devicestate/firstboot20_test.go snapd-2.57.5+20.04/overlord/devicestate/firstboot20_test.go --- snapd-2.55.5+20.04/overlord/devicestate/firstboot20_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/firstboot20_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -230,7 +230,7 @@ sysLabel := m.RecoverySystem model = s.setupCore20Seed(c, sysLabel, modelGrade, extraGadgetYaml, extraSnaps...) - // sanity check that our returned model has the expected grade + // validity check that our returned model has the expected grade c.Assert(model.Grade(), Equals, modelGrade) bloader = bootloadertest.Mock("mock", c.MkDir()).WithExtractedRunKernelImage() diff -Nru snapd-2.55.5+20.04/overlord/devicestate/firstboot.go snapd-2.57.5+20.04/overlord/devicestate/firstboot.go --- snapd-2.55.5+20.04/overlord/devicestate/firstboot.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/firstboot.go 2022-10-17 16:25:18.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2020 Canonical Ltd + * Copyright (C) 2014-2022 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 @@ import ( "errors" "fmt" + "runtime" "sort" "github.com/snapcore/snapd/asserts" @@ -39,6 +40,8 @@ var errNothingToDo = errors.New("nothing to do") +var runtimeNumCPU = runtime.NumCPU + func installSeedSnap(st *state.State, sn *seed.Snap, flags snapstate.Flags) (*state.TaskSet, *snap.Info, error) { if sn.Required { flags.Required = true @@ -105,7 +108,7 @@ // check that the state is empty var seeded bool err := st.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } if seeded { @@ -126,7 +129,7 @@ } timings.Run(tm, "load-verified-snap-metadata", "load verified snap metadata from seed", func(nested timings.Measurer) { - err = deviceSeed.LoadMeta(nested) + err = deviceSeed.LoadMeta(mode, nil, nested) }) if release.OnClassic && err == seed.ErrNoMeta { if preseed { @@ -395,6 +398,12 @@ return nil, err } + if runtimeNumCPU() > 1 { + // XXX set parallelism experimentally to 2 as I/O + // itself becomes a bottleneck ultimately + deviceSeed.SetParallelism(2) + } + // collect and // set device,model from the model assertion commitTo := func(batch *asserts.Batch) error { diff -Nru snapd-2.55.5+20.04/overlord/devicestate/firstboot_preseed_test.go snapd-2.57.5+20.04/overlord/devicestate/firstboot_preseed_test.go --- snapd-2.55.5+20.04/overlord/devicestate/firstboot_preseed_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/firstboot_preseed_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -24,13 +24,16 @@ "io/ioutil" "os" "path/filepath" + "time" . "gopkg.in/check.v1" + "gopkg.in/tomb.v2" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/hookstate" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -85,7 +88,7 @@ } } - // sanity: check that doneTasks is not declaring more tasks than + // validity: check that doneTasks is not declaring more tasks than // actually expected. c.Check(doneTasks, DeepEquals, seenDone) } @@ -253,7 +256,7 @@ restore := snapdenv.MockPreseeding(true) defer restore() - // sanity + // precondition c.Assert(release.OnClassic, Equals, true) coreFname, _, _ := s.makeCoreSnaps(c, "") @@ -337,14 +340,14 @@ // but we're not considered seeded var seeded bool err = diskState.Get("seeded", &seeded) - c.Assert(err, Equals, state.ErrNoState) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) } func (s *firstbootPreseedingClassic16Suite) TestPreseedClassicWithSnapdOnlyHappy(c *C) { restorePreseedMode := snapdenv.MockPreseeding(true) defer restorePreseedMode() - // sanity + // precondition c.Assert(release.OnClassic, Equals, true) core18Fname, snapdFname, _, _ := s.makeCore18Snaps(c, &core18SnapsOpts{ @@ -424,5 +427,219 @@ // but we're not considered seeded var seeded bool err = diskState.Get("seeded", &seeded) - c.Assert(err, Equals, state.ErrNoState) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) +} + +func (s *firstbootPreseedingClassic16Suite) TestPopulatePreseedWithConnectHook(c *C) { + restore := snapdenv.MockPreseeding(true) + defer restore() + + // precondition + c.Assert(release.OnClassic, Equals, true) + + hooksCalled := []*hookstate.Context{} + restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { + ctx.Lock() + defer ctx.Unlock() + + hooksCalled = append(hooksCalled, ctx) + return nil, nil + }) + defer restore() + + core18Fname, snapdFname, _, _ := s.makeCore18Snaps(c, &core18SnapsOpts{ + classic: true, + }) + + snapFilesWithHook := [][]string{ + {"bin/bar", ``}, + {"meta/hooks/connect-plug-network", ``}, + } + + // put a firstboot snap into the SnapBlobDir + snapYaml = `name: foo +base: core18 +version: 1.0 +plugs: + shared-data-plug: + interface: content + target: import + content: mylib +apps: + bar: + command: bin/bar + plugs: [network] +` + fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, snapFilesWithHook, snap.R(128), "developerid") + s.WriteAssertions("foo.asserts", s.devAcct, fooRev, fooDecl) + + // put a 2nd firstboot snap into the SnapBlobDir + snapYaml = `name: bar +base: core18 +version: 1.0 +slots: + shared-data-slot: + interface: content + content: mylib + read: + - / +apps: + bar: + command: bin/bar +` + snapFiles := [][]string{ + {"bin/bar", ``}, + } + barFname, barDecl, barRev := s.MakeAssertedSnap(c, snapYaml, snapFiles, snap.R(65), "developerid") + s.WriteAssertions("bar.asserts", s.devAcct, barDecl, barRev) + + // add a model assertion and its chain + assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil) + s.WriteAssertions("model.asserts", assertsChain...) + + // create a seed.yaml + content := []byte(fmt.Sprintf(` +snaps: + - name: snapd + file: %s + - name: core18 + file: %s + - name: foo + file: %s + - name: bar + file: %s +`, snapdFname, core18Fname, fooFname, barFname)) + err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644) + c.Assert(err, IsNil) + + // run the firstboot stuff + s.startOverlord(c) + st := s.overlord.State() + st.Lock() + defer st.Unlock() + + opts := &devicestate.PopulateStateFromSeedOptions{Preseed: true} + tsAll, err := devicestate.PopulateStateFromSeedImpl(st, opts, s.perfTimings) + c.Assert(err, IsNil) + // 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) + } + c.Assert(st.Changes(), HasLen, 1) + + checkPreseedOrder(c, tsAll, "snapd", "core18", "foo", "bar") + + st.Unlock() + err = s.overlord.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Err(), IsNil) + c.Check(chg.Status(), Equals, state.DoingStatus) + + // we are done with this instance of the overlord, stop it here. Otherwise + // it will interfere with our second overlord instance + c.Assert(s.overlord.Stop(), IsNil) + + // Verify state between the two change runs + r, err := os.Open(dirs.SnapStateFile) + c.Assert(err, IsNil) + diskState, err := state.ReadState(nil, r) + c.Assert(err, IsNil) + + diskState.Lock() + defer diskState.Unlock() + + // seeded snaps are installed + _, err = snapstate.CurrentInfo(diskState, "snapd") + c.Check(err, IsNil) + _, err = snapstate.CurrentInfo(diskState, "core18") + c.Check(err, IsNil) + _, err = snapstate.CurrentInfo(diskState, "foo") + c.Check(err, IsNil) + _, err = snapstate.CurrentInfo(diskState, "bar") + c.Check(err, IsNil) + + // but we're not considered seeded + var seeded bool + err = diskState.Get("seeded", &seeded) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) + + // For the next step of the test, we want to turn off pre-seeding so + // we can run the change all the way through, and also see the hooks go + // off. + restore = snapdenv.MockPreseeding(false) + defer restore() + + // Create a new overlord after turning pre-seeding off to run the + // change fully through, which we cannot do in pre-seed mode. To actually + // invoke the hooks we have to restart the overlord. + s.startOverlord(c) + st = s.overlord.State() + st.Lock() + defer st.Unlock() + + // avoid device reg + chg1 := st.NewChange("become-operational", "init device") + chg1.SetStatus(state.DoingStatus) + + st.Unlock() + err = s.overlord.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + restart.MockPending(st, restart.RestartUnset) + st.Unlock() + err = s.overlord.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(s.overlord.Stop(), IsNil) + c.Assert(err, IsNil) + + // Update the change pointer to the change in the new state + // otherwise we will be referring to the old one. + chg = nil + for _, c := range st.Changes() { + if c.Kind() == "seed" { + chg = c + break + } + } + c.Assert(chg, NotNil) + c.Assert(chg.Err(), IsNil) + c.Check(chg.Status(), Equals, state.DoneStatus) + + c.Assert(hooksCalled, HasLen, 1) + c.Check(hooksCalled[0].HookName(), Equals, "connect-plug-network") + + // and ensure state is now considered seeded + err = st.Get("seeded", &seeded) + c.Assert(err, IsNil) + c.Check(seeded, Equals, true) + + // check we set seed-time + var seedTime time.Time + err = st.Get("seed-time", &seedTime) + c.Assert(err, IsNil) + c.Check(seedTime.IsZero(), Equals, false) + + // verify that connections was made + var conns map[string]interface{} + c.Assert(st.Get("conns", &conns), IsNil) + c.Assert(conns, DeepEquals, map[string]interface{}{ + "foo:network core:network": map[string]interface{}{ + "auto": true, "interface": "network"}, + "foo:shared-data-plug bar:shared-data-slot": map[string]interface{}{ + "auto": true, "interface": "content", + "plug-static": map[string]interface{}{ + "content": "mylib", "target": "import", + }, + "slot-static": map[string]interface{}{ + "content": "mylib", + "read": []interface{}{ + "/", + }, + }, + }, + }) } diff -Nru snapd-2.55.5+20.04/overlord/devicestate/firstboot_test.go snapd-2.57.5+20.04/overlord/devicestate/firstboot_test.go --- snapd-2.55.5+20.04/overlord/devicestate/firstboot_test.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/firstboot_test.go 2022-10-17 16:25:18.000000000 +0000 @@ -141,6 +141,9 @@ t.overlord = ovld t.AddCleanup(func() { devicestate.EarlyConfig = nil + if t.overlord == nil { + return + } t.overlord.Stop() t.overlord = nil }) @@ -239,7 +242,7 @@ c.Assert(err, IsNil) _, _, err = devicestate.PreloadGadget(s.overlord.DeviceManager()) - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) tsAll, err := devicestate.PopulateStateFromSeedImpl(st, nil, s.perfTimings) c.Assert(err, IsNil) @@ -281,7 +284,7 @@ defer st.Unlock() _, _, err = devicestate.PreloadGadget(ovld.DeviceManager()) - c.Check(err, Equals, state.ErrNoState) + c.Check(err, testutil.ErrorIs, state.ErrNoState) tsAll, err := devicestate.PopulateStateFromSeedImpl(st, nil, s.perfTimings) c.Assert(err, IsNil) @@ -1340,7 +1343,7 @@ snapdSnapFiles := [][]string{ {"usr/lib/snapd/info", ` VERSION=2.54.3+git1.g479e745-dirty -SNAPD_APPARMOR_REEXEC=0 +SNAPD_APPARMOR_REEXEC=1 `}, } snapdFname, snapdDecl, snapdRev := s.MakeAssertedSnap(c, snapdYaml, snapdSnapFiles, snap.R(2), "canonical") @@ -2031,3 +2034,139 @@ _, _, _, err = devicestate.CriticalTaskEdges(ts) c.Assert(err, NotNil) } + +func (s *firstBoot16Suite) TestPopulateFromSeedWithConnectHook(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + hooksCalled := []*hookstate.Context{} + restore = hookstate.MockRunHook(func(ctx *hookstate.Context, tomb *tomb.Tomb) ([]byte, error) { + ctx.Lock() + defer ctx.Unlock() + + hooksCalled = append(hooksCalled, ctx) + return nil, nil + }) + defer restore() + + core18Fname, snapdFname, _, _ := s.makeCore18Snaps(c, &core18SnapsOpts{ + classic: true, + }) + + snapFilesWithHook := [][]string{ + {"bin/bar", ``}, + {"meta/hooks/connect-plug-network", ``}, + } + + // put a firstboot snap into the SnapBlobDir + snapYaml := `name: foo +base: core18 +version: 1.0 +plugs: + shared-data-plug: + interface: content + target: import + content: mylib +apps: + bar: + command: bin/bar + plugs: [network] +` + fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, snapFilesWithHook, snap.R(128), "developerid") + s.WriteAssertions("foo.asserts", s.devAcct, fooRev, fooDecl) + + // put a 2nd firstboot snap into the SnapBlobDir + snapYaml = `name: bar +base: core18 +version: 1.0 +slots: + shared-data-slot: + interface: content + content: mylib + read: + - / +apps: + bar: + command: bin/bar +` + snapFiles := [][]string{ + {"bin/bar", ``}, + } + barFname, barDecl, barRev := s.MakeAssertedSnap(c, snapYaml, snapFiles, snap.R(65), "developerid") + s.WriteAssertions("bar.asserts", s.devAcct, barDecl, barRev) + + // add a model assertion and its chain + assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil) + s.WriteAssertions("model.asserts", assertsChain...) + + // create a seed.yaml + content := []byte(fmt.Sprintf(` +snaps: + - name: snapd + file: %s + - name: core18 + file: %s + - name: foo + file: %s + - name: bar + file: %s +`, snapdFname, core18Fname, fooFname, barFname)) + err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644) + c.Assert(err, IsNil) + + // run the firstboot stuff + s.startOverlord(c) + st := s.overlord.State() + st.Lock() + defer st.Unlock() + + tsAll, err := devicestate.PopulateStateFromSeedImpl(st, nil, s.perfTimings) + c.Assert(err, IsNil) + // 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) + } + c.Assert(st.Changes(), HasLen, 1) + + checkOrder(c, tsAll, "snapd", "core18", "foo", "bar") + + st.Unlock() + err = s.overlord.Settle(settleTimeout) + st.Lock() + c.Check(err, IsNil) + c.Check(chg.Err(), IsNil) + + // at this point the system is "restarting", pretend the restart has + // happened + c.Assert(chg.Status(), Equals, state.DoingStatus) + restart.MockPending(st, restart.RestartUnset) + st.Unlock() + err = s.overlord.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%s", chg.Err())) + + c.Assert(hooksCalled, HasLen, 1) + c.Check(hooksCalled[0].HookName(), Equals, "connect-plug-network") + + // verify that connections was made + var conns map[string]interface{} + c.Assert(st.Get("conns", &conns), IsNil) + c.Assert(conns, DeepEquals, map[string]interface{}{ + "foo:network snapd:network": map[string]interface{}{ + "auto": true, "interface": "network"}, + "foo:shared-data-plug bar:shared-data-slot": map[string]interface{}{ + "auto": true, "interface": "content", + "plug-static": map[string]interface{}{ + "content": "mylib", "target": "import", + }, + "slot-static": map[string]interface{}{ + "content": "mylib", + "read": []interface{}{ + "/", + }, + }, + }, + }) +} diff -Nru snapd-2.55.5+20.04/overlord/devicestate/handlers_bootconfig.go snapd-2.57.5+20.04/overlord/devicestate/handlers_bootconfig.go --- snapd-2.55.5+20.04/overlord/devicestate/handlers_bootconfig.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/handlers_bootconfig.go 2022-10-17 16:25:18.000000000 +0000 @@ -19,6 +19,7 @@ package devicestate import ( + "errors" "fmt" "gopkg.in/tomb.v2" @@ -41,7 +42,7 @@ var seeded bool err := st.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } if !seeded { diff -Nru snapd-2.55.5+20.04/overlord/devicestate/handlers_gadget.go snapd-2.57.5+20.04/overlord/devicestate/handlers_gadget.go --- snapd-2.55.5+20.04/overlord/devicestate/handlers_gadget.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/handlers_gadget.go 2022-10-17 16:25:18.000000000 +0000 @@ -19,6 +19,7 @@ package devicestate import ( + "errors" "fmt" "os" "path/filepath" @@ -48,7 +49,7 @@ func currentGadgetInfo(st *state.State, curDeviceCtx snapstate.DeviceContext) (*gadget.GadgetData, error) { currentInfo, err := snapstate.GadgetInfo(st, curDeviceCtx) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return nil, err } if currentInfo == nil { @@ -231,6 +232,9 @@ if !isUndo { // when updating, command line comes from the new gadget gadgetData, err = pendingGadgetInfo(snapsup, devCtx) + if err != nil { + return false, err + } } else { // but when undoing, we use the current gadget which should have // been restored @@ -258,7 +262,7 @@ var seeded bool err := st.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } if !seeded { @@ -299,7 +303,7 @@ var seeded bool err := st.Get("seeded", &seeded) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } if !seeded { diff -Nru snapd-2.55.5+20.04/overlord/devicestate/handlers.go snapd-2.57.5+20.04/overlord/devicestate/handlers.go --- snapd-2.55.5+20.04/overlord/devicestate/handlers.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/handlers.go 2022-10-17 16:25:18.000000000 +0000 @@ -19,6 +19,7 @@ package devicestate import ( + "errors" "fmt" "os/exec" "time" @@ -54,7 +55,7 @@ // EnsureBefore(0) done somewhere else. // XXX: we should probably drop the flag from the task now that we have // one on the state. - if err := t.Get("preseeded", &preseeded); err != nil && err != state.ErrNoState { + if err := t.Get("preseeded", &preseeded); err != nil && !errors.Is(err, state.ErrNoState) { return err } if !preseeded { @@ -121,7 +122,7 @@ func (m *DeviceManager) recordSeededSystem(st *state.State, whatSeeded *seededSystem) error { var seeded []seededSystem - if err := st.Get("seeded-systems", &seeded); err != nil && err != state.ErrNoState { + if err := st.Get("seeded-systems", &seeded); err != nil && !errors.Is(err, state.ErrNoState) { return err } for _, sys := range seeded { @@ -170,7 +171,7 @@ now := time.Now() var whatSeeded *seededSystem - if err := t.Get("seed-system", &whatSeeded); err != nil && err != state.ErrNoState { + if err := t.Get("seed-system", &whatSeeded); err != nil && !errors.Is(err, state.ErrNoState) { return err } if whatSeeded != nil && deviceCtx.RunMode() { diff -Nru snapd-2.55.5+20.04/overlord/devicestate/handlers_install.go snapd-2.57.5+20.04/overlord/devicestate/handlers_install.go --- snapd-2.55.5+20.04/overlord/devicestate/handlers_install.go 2022-05-11 04:38:24.000000000 +0000 +++ snapd-2.57.5+20.04/overlord/devicestate/handlers_install.go 2022-10-17 16:25:18.000000000 +0000 @@ -20,34 +20,54 @@ package devicestate import ( + "bytes" "compress/gzip" + "crypto" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" "fmt" + "io" + "io/ioutil" "os" "os/exec" "path/filepath" + _ "golang.org/x/crypto/sha3" "gopkg.in/tomb.v2" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/gadget/install" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/randutil" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/secboot/keys" + "github.com/snapcore/snapd/seed" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/squashfs" "github.com/snapcore/snapd/sysconfig" "github.com/snapcore/snapd/timings" ) var ( - bootMakeRunnable = boot.MakeRunnableSystem - bootEnsureNextBootToRunMode = boot.EnsureNextBootToRunMode - installRun = install.Run + bootMakeRunnable = boot.MakeRunnableSystem + bootMakeRunnableAfterDataReset = boot.MakeRunnableSystemAfterDataReset + bootEnsureNextBootToRunMode = boot.EnsureNextBootToRunMode + installRun = install.Run + installFactoryReset = install.FactoryReset + secbootStageEncryptionKeyChange = secboot.StageEncryptionKeyChange + secbootTransitionEncryptionKeyChange = secboot.TransitionEncryptionKeyChange sysconfigConfigureTargetSystem = sysconfig.ConfigureTargetSystem ) @@ -91,13 +111,16 @@ return asserts.NewEncoder(f).Encode(model) } -func writeLogs(rootdir string) error { +func writeLogs(rootdir string, fromMode string) error { // XXX: would be great to use native journal format but it's tied // to machine-id, we could journal -o export but there // is no systemd-journal-remote on core{,18,20} // // XXX: or only log if persistent journal is enabled? logPath := filepath.Join(rootdir, "var/log/install-mode.log.gz") + if fromMode == "factory-reset" { + logPath = filepath.Join(rootdir, "var/log/factory-reset-mode.log.gz") + } if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { return err } @@ -148,8 +171,14 @@ return nil } -func writeTimings(st *state.State, rootdir string) error { +func writeTimings(st *state.State, rootdir, fromMode string) error { + changeKind := "install-system" logPath := filepath.Join(rootdir, "var/log/install-timings.txt.gz") + if fromMode == "factory-reset" { + changeKind = "factory-reset" + logPath = filepath.Join(rootdir, "var/log/factory-reset-timings.txt.gz") + } + if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { return err } @@ -165,7 +194,7 @@ var chgIDs []string for _, chg := range st.Changes() { - if chg.Kind() == "seed" || chg.Kind() == "install-system" { + if chg.Kind() == "seed" || chg.Kind() == changeKind { // this is captured via "--ensure=seed" and // "--ensure=install-system" below continue @@ -197,8 +226,8 @@ } fmt.Fprintf(gz, "\n") // then the install - fmt.Fprintf(gz, "---- Output of snap debug timings --ensure=install-system\n") - cmd = exec.Command("snap", "debug", "timings", "--ensure=install-system") + fmt.Fprintf(gz, "---- Output of snap debug timings --ensure=%v\n", changeKind) + cmd = exec.Command("snap", "debug", "timings", fmt.Sprintf("--ensure=%v", changeKind)) cmd.Stdout = gz if err := cmd.Run(); err != nil { return fmt.Errorf("cannot collect timings output: %v", err) @@ -311,31 +340,70 @@ } if trustedInstallObserver != nil { - // sanity check - if installedSystem.KeysForRoles == nil || installedSystem.KeysForRoles[gadget.SystemData] == nil || installedSystem.KeysForRoles[gadget.SystemSave] == nil { - return fmt.Errorf("internal error: system encryption keys are unset") + if err := prepareEncryptedSystemData(installedSystem.KeyForRole, trustedInstallObserver); err != nil { + return err } - dataKeySet := installedSystem.KeysForRoles[gadget.SystemData] - saveKeySet := installedSystem.KeysForRoles[gadget.SystemSave] + } - // make note of the encryption keys - trustedInstallObserver.ChosenEncryptionKeys(dataKeySet.Key, saveKeySet.Key) + if err := prepareRunSystemData(model, gadgetDir, perfTimings); err != nil { + return err + } - // keep track of recovery assets - if err := trustedInstallObserver.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir); err != nil { - return fmt.Errorf("cannot observe existing trusted recovery assets: err") - } - if err := saveKeys(installedSystem.KeysForRoles); err != nil { - return err - } - // write markers containing a secret to pair data and save - if err := writeMarkers(); err != nil { - return err - } + // make it bootable, which should be the final step in the process, as + // it effectively makes it possible to boot into run mode + logger.Noticef("make system runnable") + bootBaseInfo, err := snapstate.BootBaseInfo(st, deviceCtx) + if err != nil { + return fmt.Errorf("cannot get boot base info: %v", err) + } + recoverySystemDir := filepath.Join("/systems", modeEnv.RecoverySystem) + bootWith := &boot.BootableSet{ + Base: bootBaseInfo, + BasePath: bootBaseInfo.MountFile(), + Gadget: gadgetInfo, + GadgetPath: gadgetInfo.MountFile(), + Kernel: kernelInfo, + KernelPath: kernelInfo.MountFile(), + RecoverySystemDir: recoverySystemDir, + UnpackedGadgetDir: gadgetDir, } + timings.Run(perfTimings, "boot-make-runnable", "Make target system runnable", func(timings.Measurer) { + err = bootMakeRunnable(deviceCtx.Model(), bootWith, trustedInstallObserver) + }) + if err != nil { + return fmt.Errorf("cannot make system runnable: %v", err) + } + return nil +} + +func prepareEncryptedSystemData(keyForRole map[string]keys.EncryptionKey, trustedInstallObserver *boot.TrustedAssetsInstallObserver) error { + // validity check + if len(keyForRole) == 0 || keyForRole[gadget.SystemData] == nil || keyForRole[gadget.SystemSave] == nil { + return fmt.Errorf("internal error: system encryption keys are unset") + } + dataEncryptionKey := keyForRole[gadget.SystemData] + saveEncryptionKey := keyForRole[gadget.SystemSave] + + // make note of the encryption keys + trustedInstallObserver.ChosenEncryptionKeys(dataEncryptionKey, saveEncryptionKey) + + // keep track of recovery assets + if err := trustedInstallObserver.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir); err != nil { + return fmt.Errorf("cannot observe existing trusted recovery assets: err") + } + if err := saveKeys(keyForRole); err != nil { + return err + } + // write markers containing a secret to pair data and save + if err := writeMarkers(); err != nil { + return err + } + return nil +} +func prepareRunSystemData(model *asserts.Model, gadgetDir string, perfTimings timings.Measurer) error { // keep track of the model we installed - err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755) + err := os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755) if err != nil { return fmt.Errorf("cannot store the model: %v", err) } @@ -361,28 +429,6 @@ return err } - // make it bootable - logger.Noticef("make system runnable") - bootBaseInfo, err := snapstate.BootBaseInfo(st, deviceCtx) - if err != nil { - return fmt.Errorf("cannot get boot base info: %v", err) - } - recoverySystemDir := filepath.Join("/systems", modeEnv.RecoverySystem) - bootWith := &boot.BootableSet{ - Base: bootBaseInfo, - BasePath: bootBaseInfo.MountFile(), - Kernel: kernelInfo, - KernelPath: kernelInfo.MountFile(), - RecoverySystemDir: recoverySystemDir, - UnpackedGadgetDir: gadgetDir, - } - timings.Run(perfTimings, "boot-make-runnable", "Make target system runnable", func(timings.Measurer) { - err = bootMakeRunnable(deviceCtx.Model(), bootWith, trustedInstallObserver) - }) - if err != nil { - return fmt.Errorf("cannot make system runnable: %v", err) - } - // TODO: FIXME: this should go away after we have time to design a proper // solution // TODO: only run on specific models? @@ -395,7 +441,6 @@ if err := fixupWritableDefaultDirs(boot.InstallHostWritableDir); err != nil { return err } - return nil } @@ -414,7 +459,7 @@ // this restriction to let the device create one specific file, and then // we behind the scenes just create the directories for the device - for _, subDirToCreate := range []string{"/etc/udev/rules.d", "/etc/modprobe.d", "/etc/modules-load.d/"} { + for _, subDirToCreate := range []string{"/etc/udev/rules.d", "/etc/modprobe.d", "/etc/modules-load.d/", "/etc/systemd/network"} { dirToCreate := sysconfig.WritableDefaultsDir(systemDataDir, subDirToCreate) if err := os.MkdirAll(dirToCreate, 0755); err != nil { @@ -441,48 +486,22 @@ return fmt.Errorf("cannot create ubuntu-data/save marker secret: %v", err) } - dataMarker := filepath.Join(boot.InstallHostFDEDataDir, "marker") - if err := osutil.AtomicWriteFile(dataMarker, markerSecret, 0600, 0); err != nil { - return err - } - - saveMarker := filepath.Join(boot.InstallHostFDESaveDir, "marker") - if err := osutil.AtomicWriteFile(saveMarker, markerSecret, 0600, 0); err != nil { - return err - } - - return nil + return device.WriteEncryptionMarkers(boot.InstallHostFDEDataDir, boot.InstallHostFDESaveDir, markerSecret) } -func saveKeys(keysForRoles map[string]*install.EncryptionKeySet) error { - dataKeySet := keysForRoles[gadget.SystemData] - +func saveKeys(keyForRole map[string]keys.EncryptionKey) error { + saveEncryptionKey := keyForRole[gadget.SystemSave] + if saveEncryptionKey == nil { + // no system-save support + return nil + } // ensure directory for keys exists if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil { return err } - - // Write the recovery key - recoveryKeyFile := filepath.Join(boot.InstallHostFDEDataDir, "recovery.key") - if err := dataKeySet.RecoveryKey.Save(recoveryKeyFile); err != nil { - return fmt.Errorf("cannot store recovery key: %v", err) - } - - saveKeySet := keysForRoles[gadget.SystemSave] - if saveKeySet == nil { - // no system-save support - return nil - } - - saveKey := filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key") - reinstallSaveKey := filepath.Join(boot.InstallHostFDEDataDir, "reinstall.key") - - if err := saveKeySet.Key.Save(saveKey); err != nil { + if err := saveEncryptionKey.Save(device.SaveKeyUnder(boot.InstallHostFDEDataDir)); err != nil { return fmt.Errorf("cannot store system save key: %v", err) } - if err := saveKeySet.RecoveryKey.Save(reinstallSaveKey); err != nil { - return fmt.Errorf("cannot store reinstall key: %v", err) - } return nil } @@ -580,6 +599,17 @@ return fmt.Errorf("missing modeenv, cannot proceed") } + preseeded, err := maybeApplyPreseededData(st, boot.InitramfsUbuntuSeedDir, modeEnv.RecoverySystem, boot.InstallHostWritableDir) + if err != nil { + logger.Noticef("failed to apply preseed data: %v", err) + return err + } + if preseeded { + logger.Noticef("successfully preseeded the system") + } else { + logger.Noticef("preseed data not present, will do normal seeding") + } + // ensure the next boot goes into run mode if err := bootEnsureNextBootToRunMode(modeEnv.RecoverySystem); err != nil { return err @@ -587,16 +617,16 @@ var rebootOpts RebootOptions err = t.Get("reboot", &rebootOpts) - if err != nil && err != state.ErrNoState { + if err != nil && !errors.Is(err, state.ErrNoState) { return err } // write timing information - if err := writeTimings(st, boot.InstallHostWritableDir); err != nil { + if err := writeTimings(st, boot.InstallHostWritableDir, modeEnv.Mode); err != nil { logger.Noticef("cannot write timings: %v", err) } // store install-mode log into ubuntu-data partition - if err := writeLogs(boot.InstallHostWritableDir); err != nil { + if err := writeLogs(boot.InstallHostWritableDir, modeEnv.Mode); err != nil { logger.Noticef("cannot write installation log: %v", err) } @@ -618,3 +648,597 @@ return nil } + +func readPreseedAssertion(st *state.State, model *asserts.Model, ubuntuSeedDir, sysLabel string) (*asserts.Preseed, error) { + f, err := os.Open(filepath.Join(ubuntuSeedDir, "systems", sysLabel, "preseed")) + if err != nil { + return nil, fmt.Errorf("cannot read preseed assertion: %v", err) + } + + // main seed assertions are loaded in the assertion db of install mode; add preseed assertion from + // systems/